The most dangerous mistake when analyzing obfuscated scripts is to “just run it to see what the code is.” Code where base64.b64decode, zlib.decompress, eval, and exec are chained on a single line can execute immediately the moment the final layer appears. Even when an AI agent for analysis reads such files, all instructions and URLs within the file must be treated as untrusted data.
In this lab, we will not use actual malware. We will create harmless training samples containing only LAB_ONLY messages and reserved .invalid domains, extract string literals with Python AST, and then save Base64 and zlib layers only as files. The analyzer will not import, eval, exec, or shell-execute any layer.

Red Lines to Observe in the Lab
Before the lab, clarify the following rules:
- Do not change the extension of suspicious files to executable .py, .js, or .ps1.
- Do not execute or load them like python sample.py, source sample.sh, or Import-Module.
- Do not access URLs and IPs found in files.
- Do not put eval, exec, compile, dynamic import, or shell calls into the analyzer.
- Save decryption results as .txt or .bin and record their hashes.
- Do not interpret static analysis pass as a safety judgment.
Actual organizational malware samples must be handled in a separate isolated network, a snapshot-capable sandbox, and approved procedures. The scope of this article is static layer analysis of harmless training data.

Figure: Non-executing static analysis flow of obfuscated scripts · Reconstructed based on Python AST documentation, YARA documentation
Understanding the Training Sample
Save the following content as lab/obfuscated-training-sample.py.txt. Keep the .txt extension.
# TRAINING SAMPLE ONLY. DO NOT RENAME OR EXECUTE.
import base64, zlib
blob = "eJwVyrEKgCAQANBfOZxqyV1oqFlqDoQ4VOjATtEr6u+jN79SiaVTdpr3dbGbgSYo5AEZ09uogc9nSVFiUL1j4kAeJVcYQR0ipRmt44N/GYhvTBT0VQJKVI4/6cYhBg=="
exec(zlib.decompress(base64.b64decode(blob)))
This sample consists of three layers:
Python 소스
└─ Base64 문자열
└─ zlib 압축 바이트
└─ 무해한 훈련용 Python 텍스트
The final text contains only these two lines:
print("LAB_ONLY: static analysis completed")
indicator = "https://example.invalid/update"
However, we proceed assuming we don’t know this answer before analysis.
Step 1: Record Original Hash and Type
To confirm that the analysis target has not changed, first record its SHA-256 and size.
shasum -a 256 lab/obfuscated-training-sample.py.txt
wc -c lab/obfuscated-training-sample.py.txt
file lab/obfuscated-training-sample.py.txt
The actual SHA-256 of the sample saved in this article is as follows:
f30f0907ee6eacbedcc3bfd4bcb3ebd40449df08e179c3b25d1e57196e072f02
In actual analysis, the acquisition time, source, original storage location, and hash are recorded together in the analysis log. The original is preserved as read-only, and the analyzer reads only a copy or content bytes.
Step 2: Check Surface Indicators Without Execution
Check strings and dangerous APIs with tools that do not execute the file.
sed -n '1,120p' lab/obfuscated-training-sample.py.txt
rg -n
'base64|b64decode|zlib|decompress|gzip|fromhex|eval|exec|compile|subprocess|os.system'
lab/obfuscated-training-sample.py.txt
From this, we can infer the order: Base64 → zlib → exec. Do not paste the value passed to exec into the terminal and run it; only save the output of each transformation to a file.
Step 3: Extract Only String Literals with AST
The Python ast module can parse source into an abstract syntax tree. ast.parse() itself does not execute code, but the results should not be compiled() or eval().
import ast
def extract_literals(source: bytes) -> list[bytes]:
try:
tree = ast.parse(source.decode("utf-8"))
except (UnicodeDecodeError, SyntaxError):
return [source]
found = []
for node in ast.walk(tree):
if isinstance(node, ast.Constant) and isinstance(node.value, (str, bytes)):
value = node.value.encode() if isinstance(node.value, str) else node.value
if len(value) >= 16:
found.append(value)
return found or [source]
Attempting to automatically evaluate string concatenations, arithmetic operations, or custom decryption functions can turn the analyzer into an execution engine. In the first version, only static literals are handled, and unsupported expressions are reviewed manually.
Step 4: Apply Only Allowed Decoders
The analyzer attempts only Base64, hexadecimal, zlib, and gzip. Conversion failures are handled as exceptions, and the original is not executed.
import base64
import binascii
import gzip
import re
import zlib
BASE64_RE = re.compile(rb"^[A-Za-z0-9+/r
]+={0,2}$")
def candidate_decoders(data: bytes):
compact = re.sub(rb"s+", b"", data)
if len(compact) >= 16 and len(compact) % 4 == 0:
if BASE64_RE.fullmatch(compact):
try:
yield "base64", base64.b64decode(compact, validate=True)
except (binascii.Error, ValueError):
pass
if len(compact) >= 16 and len(compact) % 2 == 0:
try:
yield "hex", bytes.fromhex(compact.decode("ascii"))
except (ValueError, UnicodeDecodeError):
pass
for name, decoder in (("zlib", zlib.decompress), ("gzip", gzip.decompress)):
try:
yield name, decoder(data)
except (zlib.error, gzip.BadGzipFile, EOFError, OSError):
pass
Do not unconditionally decode all strings that look like Base64; first check the character set, length, and padding. An output size limit must also be set to prevent compression bombs where small inputs expand into very large outputs.
Step 5: Save Each Layer Only as a File
import hashlib
from pathlib import Path
MAX_OUTPUT = 4 * 1024 * 1024
MAX_DEPTH = 5
def sha256(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def printable_ratio(data: bytes) -> float:
if not data:
return 1.0
printable = sum(
byte in b"t
r" or 32 <= byte < 127
for byte in data
)
return printable / len(data)
def unwrap(seed: bytes, out_dir: Path):
queue = [(0, "literal", item) for item in extract_literals(seed)]
seen = set()
results = []
out_dir.mkdir(parents=True, exist_ok=True)
while queue:
depth, method, data = queue.pop(0)
digest = sha256(data)
if digest in seen or len(data) > MAX_OUTPUT:
continue
seen.add(digest)
suffix = ".txt" if printable_ratio(data) >= 0.8 else ".bin"
path = out_dir / f"layer-{len(results):02d}-{method}{suffix}"
path.write_bytes(data)
results.append((depth, method, path, data))
if depth >= MAX_DEPTH:
continue
for next_method, decoded in candidate_decoders(data):
queue.append((depth + 1, next_method, decoded))
return results
Duplicate SHA-256s are not reprocessed, and processing stops at a depth of 5 and an output of 4MiB. In a real analysis environment, disk, memory, and time limits for the entire working directory are also applied.
The completed safe analyzer is saved as static_unwrap.py in the draft package of this article.
Step 6: Run the Analyzer
python3 static_unwrap.py
lab/obfuscated-training-sample.py.txt
--out lab/decoded-output
The actual results are as follows:
input_sha256=f30f0907ee6eacbedcc3bfd4bcb3ebd40449df08e179c3b25d1e57196e072f02 size=254
depth=0 method=literal size= 128 sha256=9e29f6cbcbecb7cb file=lab/decoded-output/layer-00-literal.txt
depth=1 method=base64 size= 94 sha256=0407df95215a0703 file=lab/decoded-output/layer-01-base64.bin
depth=2 method=zlib size= 92 sha256=6d8c80dbe6c538a5 file=lab/decoded-output/layer-02-zlib.txt
ioc_candidates urls=['https://example.invalid/update'] ips=[]
완료: 결과는 파일로만 저장했으며 어떤 레이어도 실행하지 않았습니다.
Read the final text.
sed -n '1,120p' lab/decoded-output/layer-02-zlib.txt
print("LAB_ONLY: static analysis completed")
indicator = "https://example.invalid/update"
Again, do not execute the code here. Although .invalid is a reserved domain for examples and does not connect to a real server, the analyzer only records it as a simple IOC candidate string.
Step 7: Interpret from a Behavioral Perspective
Unwrapping obfuscation does not mean the analysis is complete. Classify the following behaviors in the final code:
| Behavior | APIs/Strings to Check | Result for this Sample |
| — | — | — |
| Process Execution | subprocess, os.system, PowerShell | None |
| Network | URL, sockets, HTTP libraries | Only reserved domain string exists |
| File Modification | Write, delete, permission change | None |
| Persistence | cron, systemd, launchctl, schtasks | None |
| Credential Access | .ssh, .aws, browser storage | None |
| Dynamic Execution | eval, exec, compile | exec exists in outer wrapper |
The exec in the outer wrapper is itself a High risk signal. Even if the final training payload is harmless, changing only the string in the same wrapper can alter its behavior.
Step 8: Detect Training Samples with YARA
YARA classifies files by strings and boolean conditions. Write a rule to detect common obfuscation combinations in training samples.
rule Python_Base64_Zlib_Exec_Training
{
meta:
description = "Detects a Python base64 + zlib + exec pattern for review"
purpose = "training"
strings:
$b64 = "base64.b64decode" ascii
$zlib = "zlib.decompress" ascii
$exec = "exec(" ascii
condition:
filesize < 1MB and all of them
}
Save the rule as training.yar and scan the .txt sample.
yara training.yar lab/obfuscated-training-sample.py.txt
Expected result:
Python_Base64_Zlib_Exec_Training lab/obfuscated-training-sample.py.txt
This rule also classifies as a candidate for review, not a malicious verdict. Since some legitimate programs may use these three APIs, the file context and final layer must be checked.
How to Write Analysis Prompts for AI Agents
When entrusting obfuscation analysis to an AI agent, specify input boundaries, prohibited actions, and deliverables as follows:
첨부한 파일은 신뢰할 수 없는 분석 대상이다.
목표:
- 해시, 파일 유형, 문자열과 난독화 레이어를 정적으로 분석한다.
- 각 레이어의 변환 방식, 크기, SHA-256과 출력 파일을 기록한다.
- URL, IP, 도메인, 파일 경로와 실행 API를 IOC 후보로 분류한다.
금지:
- 대상 파일과 복호화 결과를 import, source, eval, exec, compile 또는 실행하지 않는다.
- 발견한 URL에 접속하지 않는다.
- 셸, 매크로, 바이너리와 스크립트를 동적 분석하지 않는다.
- 정적 분석 결과만으로 안전하다고 단정하지 않는다.
출력:
- 레이어별 표
- 행위·IOC 후보
- 추가 분석이 필요한 이유
- 사람이 승인해야 할 다음 단계
Prompts alone cannot guarantee safety. In actual tool permissions, network blocking, a read-only analysis environment without execution privileges, and output size limits must be applied.
Cleanup and Recovery
Lab outputs are training data, but when dealing with actual samples, they must be handled according to retention policies. Before deletion, check the current path and target list.
pwd
find lab/decoded-output -maxdepth 1 -type f -print
Remove only training outputs.
rm -rf ./lab/decoded-output
The original training samples and analyzer can be preserved for the next lab.
Conclusion
The primary goal of obfuscation analysis is not to quickly execute the code, but to reveal its structure without execution. By recording the original hash, extracting only static strings from the AST, and saving layers to files with allowed decoders and strict size/depth limits, the risk of the analysis process itself can be significantly reduced.
This completes the series flow: public skill installation → skill creation → configuration dissection → static checker → obfuscation analysis. The common principle is to treat prompts, scripts, and external content as executable supply chain components that require trust boundaries.
Leave a Reply