BinSentry 自研二进制哨兵压力测试+冒烟回归脚本
BinSentry 二进制哨兵,由 LYSHARK 独立自研,是一款运行于 Windows 平台的二进制动态调试引擎。该调试引擎从零手写开发,C++ 代码 3.5 万(主框架)+ AI Agent 专家协同框架(开发中)余行,实现 300 + 调试方法;原生采用 CMD 命令行 HTTP 服务器架构,专为 AI Agent 打造。配合配套 AI 智能体,能够对二进制文件、木马等恶意样本进行深层次分析,是二进制样本研究的实用工具。
BinSentry 二进制哨兵压力测试脚本,用于测试各项接口稳定性。
构建数量测试
| 构建产物 | 路径 | 冒烟 | 压力 |
|---|---|---|---|
| Win32 Release | Release\BinSentry.exe |
517/517 PASS | 11/11 PASS |
| x64 Release | x64\Release\BinSentry.exe |
519/519 PASS | 11/11 PASS |
| Win32 Debug | Debug\BinSentry.exe |
517/517 PASS | 11/11 PASS |
| x64 Debug | x64\Debug\BinSentry.exe |
519/519 PASS | 11/11 PASS |
| 构建 | 冒烟 | 压力 |
|---|---|---|
| Win32 Release | 517/517 | 11/11 |
| x64 Release | 519/519 | 11/11 |
| Win32 Debug | 517/517 | 11/11 |
| x64 Debug | 519/519 | 11/11 |
高并发服务器接口测试
| 场景 | 参数 | 结果 |
|---|---|---|
| 并发混合读 | 8 线程 × 25 请求(Status/Threads/GetActiveThread/IsFileBeingDebugged 轮换) | 200/200 成功(约 100+ req/s,无超时 / 无 5xx) |
| 长时轮询 | 15 s 内每 0.2 s 一次 Status | 71 次全部成功 |
| 大块内存读取 | 新分配 128KB 连续区域:4KB / 16KB / 64KB / 128KB (钳制) | 返回字节数与期望精确一致(128KB 钳制为 64KB) |
| 混合步进 | 30 轮 StepIn+WaitStop+Register+Memory 交替 | 全部成功,无失控 / 崩溃 |
| 错误风暴 | 60 次无效 JSON / 未知接口 / 非法参数 | 服务器存活,正确返回 200/400/404/409 |
| 收尾 | 压力后 Detach + 会话关闭 | sessionActive=false |
已知设计行为非缺陷
| 接口 | 行为 | 说明 |
|---|---|---|
SetHBreakPoint(flag=e 执行断点) |
返回 ok=false 并回退软断点 |
服务器设计:执行断点不支持硬件方式,请用普通软断点 |
SetHBreakPoint(flag=r/w 数据断点) |
走真实 DR0-3 寄存器,drIndex 正确 |
数据断点功能正常 |
SetCommandLine 无会话 |
返回 200 + ok=false(非 409) |
与 “无会话应 409” 规则不一致,属历史设计歧义,记录留待统一 |
EnumWindows 无会话 |
返回 200 + ok=false 空列表 |
同上,语义为 “无会话时无窗口句柄” |
ImporterGetNearestAPIAddress |
未命中时 ok=false + “未找到最近 API” |
属合法信息(地址不在导入函数附近) |
| 超长字符串参数 | 截断至 4096 字符,返回 200 | 健壮性设计 |
BinSentry_verify
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys, os, json, time, threading
import urllib.request, urllib.error
PROJ = os.path.dirname(os.path.abspath(__file__))
BASE = "http://127.0.0.1:6891/"
WIN32 = os.path.join(PROJ, "Win32_Debug.exe")
WINDLL = os.path.join(PROJ, "x32_Debug.dll")
# ---------------- 断言框架 ----------------
PASS = FAIL = ERROR = 0
suite_counts = {"S-A": 0, "S-B": 0, "S-C": 0, "S-D": 0, "S-E": 0}
cur_suite = "?"
def check(cond, msg, suite=None, name=None):
global PASS, FAIL, ERROR, cur_suite
if suite:
cur_suite = suite
suite_counts[cur_suite] = suite_counts.get(cur_suite, 0) + 1
if cond:
PASS += 1
print(" [PASS] %s: %s" % (cur_suite, name or msg))
else:
FAIL += 1
print(" [FAIL] %s: %s: %s" % (cur_suite, name or "", msg))
def post(interface, params="", timeout=12):
body = json.dumps({"interface": interface, "params": params})
req = urllib.request.Request(BASE, data=body.encode("utf-8"),
headers={"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
raw = resp.read().decode("utf-8", errors="replace")
try:
return resp.status, json.loads(raw), None
except Exception:
return resp.status, None, "JSON解析失败: " + raw[:200]
except urllib.error.HTTPError as e:
raw = e.read().decode("utf-8", errors="replace")
try:
return e.code, json.loads(raw), None
except Exception:
return e.code, None, "HTTP %d 非JSON: %s" % (e.code, raw[:120])
except Exception as e:
return -1, None, str(e)
def post_raw(payload, timeout=12):
req = urllib.request.Request(BASE, data=payload.encode("utf-8", errors="replace"),
headers={"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
raw = resp.read().decode("utf-8", errors="replace")
try:
return resp.status, json.loads(raw), None
except Exception:
return resp.status, None, "JSON解析失败: " + raw[:200]
except urllib.error.HTTPError as e:
raw = e.read().decode("utf-8", errors="replace")
try:
return e.code, json.loads(raw), None
except Exception:
return e.code, None, "HTTP %d 非JSON: %s" % (e.code, raw[:120])
except Exception as e:
return -1, None, str(e)
def get_root(timeout=8):
req = urllib.request.Request(BASE)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
raw = resp.read().decode("utf-8", errors="replace")
try:
return resp.status, json.loads(raw), None
except Exception:
return resp.status, None, "JSON解析失败"
except urllib.error.HTTPError as e:
return e.code, None, None
except Exception as e:
return -1, None, str(e)
def expect_ok(st, d, what):
if st is None or d is None:
return False
if st in (200, 400):
r = d.get("result") if isinstance(d, dict) else None
if isinstance(r, dict) and r.get("ok") is True:
return True
return False
def service_alive():
st, d, e = get_root(6)
return st == 200 and isinstance(d, dict) and d.get("status") == "success"
# ================= S-A 服务指纹 (6) =================
def suite_a():
st, d, e = get_root()
check(st == 200 and isinstance(d, dict) and d.get("status") == "success",
"GET / st=%s err=%s" % (st, e), "S-A", "FINGERPRINT")
info = d.get("plugin_info") if isinstance(d, dict) else None
check(isinstance(info, dict) and info.get("name") and info.get("version"),
"name/version 字段存在", "S-A", "NAMEVERSION")
check(isinstance(info, dict) and "description" in info and "engine" in info,
"description/engine 字段存在", "S-A", "DESCENGINE")
check(isinstance(info, dict) and info.get("compile_date") and info.get("compile_time"),
"compile_date/compile_time 非空", "S-A", "COMPILE")
check(isinstance(info, dict) and "6891" in str(info.get("listen", "")),
"listen 字段含端口", "S-A", "LISTEN")
req = urllib.request.Request(BASE, method="PUT")
try:
with urllib.request.urlopen(req, timeout=8) as r:
check(r.status == 405, "PUT 期望 405 实际 %d" % r.status, "S-A", "METHOD")
except urllib.error.HTTPError as e:
check(e.code == 405, "PUT 期望 405 实际 %d" % e.code, "S-A", "METHOD")
except Exception as ex:
check(False, "PUT 异常 %s" % str(ex)[:60], "S-A", "METHOD")
# ================= S-B 接口准确性 (15) =================
def suite_b():
cases = [
("mov eax, 1", "B8 01 00 00 00"),
("push 0x1234", "68 34 12 00 00"),
("xor eax, eax", "31 C0"),
("lea ecx,[eax+4]", "8D 48 04"),
("ret", "C3"),
]
for asm, want in cases:
st, d, e = post("Assemble", {"instr": asm}, timeout=8)
got = ""
if isinstance(d, dict):
r = d.get("result")
if isinstance(r, dict):
got = str(r.get("bytes", ""))
check(st == 200 and got == want, "asm=%s got=%s" % (asm, got), "S-B", "ASSEMBLE")
st, d, e = post("Assemble", {"instr": "nope_instr_zzz"}, timeout=8)
check(st in (200, 400) and d is not None, "非法指令优雅处理 st=%s" % st, "S-B", "ASSEMBLE_BAD")
st, d, e = post("StaticFileLoad", {"path": WIN32}, timeout=10)
r = d.get("result") if isinstance(d, dict) else None
check(st == 200 and isinstance(r, dict) and r.get("ok") is True,
"StaticFileLoad ok=%s st=%s" % (r.get("ok") if isinstance(r, dict) else "?", st), "S-B", "SFL_LOAD")
r = d.get("result") if isinstance(d, dict) else None
check(isinstance(r, dict) and r.get("fileSize") == 14848,
"fileSize=%s" % (r.get("fileSize") if isinstance(r, dict) else "?"), "S-B", "SFL_SIZE")
va = r.get("fileMapVA", 0) if isinstance(r, dict) else 0
st, d, e = post("StaticDisassemble", {"address": va}, timeout=10)
r2 = d.get("result") if isinstance(d, dict) else None
inst = r2.get("instruction", "") if isinstance(r2, dict) else ""
nxt = r2.get("nextAddress", 0) if isinstance(r2, dict) else 0
check(st == 200 and isinstance(r2, dict) and r2.get("ok") is True and inst and nxt > va,
"SDISASM instr=%s next=%s" % (inst[:20], nxt), "S-B", "SDISASM")
st, d, e = post("IsFileDLL", {"path": WIN32}, timeout=8)
r4 = d.get("result") if isinstance(d, dict) else None
check(st == 200 and isinstance(r4, dict) and r4.get("isDll") is False,
"win32.exe isDll=%s" % (r4.get("isDll") if isinstance(r4, dict) else "?"), "S-B", "ISDLL_EXE")
st, d, e = post("IsFileDLL", {"path": WINDLL}, timeout=8)
r5 = d.get("result") if isinstance(d, dict) else None
check(st == 200 and isinstance(r5, dict) and r5.get("isDll") is True,
"win32.dll isDll=%s" % (r5.get("isDll") if isinstance(r5, dict) else "?"), "S-B", "ISDLL_DLL")
st, d, e = post("SysCommand", {"shell": "cmd", "command": "ver"}, timeout=15)
r6 = d.get("result") if isinstance(d, dict) else None
so = r6.get("stdout", "") if isinstance(r6, dict) else ""
check(st == 200 and isinstance(r6, dict) and r6.get("ok") is True and "Windows" in so,
"cmd ver ok=%s stdout=%s" % (r6.get("ok") if isinstance(r6, dict) else "?", so[:40]), "S-B", "SYS_CMD")
st, d, e = post("SysCommand", {"shell": "powershell", "command": "Get-Process | Select-Object -First 1"}, timeout=20)
r7 = d.get("result") if isinstance(d, dict) else None
check(st == 200 and isinstance(r7, dict) and r7.get("ok") is True and r7.get("stdout"),
"powershell ok=%s" % (r7.get("ok") if isinstance(r7, dict) else "?"), "S-B", "SYS_PS")
st, d, e = post("StaticFileUnload", {}, timeout=8)
check(st == 200, "StaticFileUnload st=%s" % st, "S-B", "SFL_UNLOAD")
st, d, e = post("StaticDisassemble", {"address": 0}, timeout=8)
r8 = d.get("result") if isinstance(d, dict) else None
check(st == 200 and isinstance(r8, dict) and r8.get("ok") is False,
"卸载后反汇编优雅拒绝 ok=%s" % (r8.get("ok") if isinstance(r8, dict) else "?"), "S-B", "SDISASM_UNLOADED")
# ================= S-C 全接口可用性 (1) =================
def suite_c():
st, d, e = post("Status", {}, timeout=8)
if st != 200 or not isinstance(d, dict):
check(False, "Status st=%s" % st, "S-C", "ALL_INTERFACES")
return
r = d.get("result")
ifaces = r.get("interfaces") if isinstance(r, dict) else None
if not isinstance(ifaces, list) or len(ifaces) < 297:
check(False, "接口数=%s" % (len(ifaces) if isinstance(ifaces, list) else "?"), "S-C", "ALL_INTERFACES")
return
bad = 0
for name in ifaces:
st2, d2, e2 = post(name, "", timeout=8)
if st2 not in (200, 400, 409) or d2 is None:
bad += 1
if bad <= 3:
print(" bad: %s st=%s err=%s" % (name, st2, e2))
check(bad == 0, "全接口(%d)可调用 bad=%d" % (len(ifaces), bad), "S-C", "ALL_INTERFACES")
# ================= S-D 会话内准确性 (15) =================
def suite_d():
st, d, e = post("Debug", {"path": WIN32}, timeout=10)
check(expect_ok(st, d, "Debug"), "Debug st=%s" % st, "S-D", "DEBUG")
st, d, e = post("WaitStop", {"timeout": 5}, timeout=12)
r = d.get("result") if isinstance(d, dict) else None
check(st == 200 and isinstance(r, dict) and r.get("stopped") is True,
"WaitStop stopped=%s" % (r.get("stopped") if isinstance(r, dict) else "?"), "S-D", "WAITSTOP")
st, d, e = post("Modules", {}, timeout=8)
r = d.get("result") if isinstance(d, dict) else None
mods = r.get("modules") if isinstance(r, dict) else None
found = isinstance(mods, list) and any("win32" in str(m.get("name", "")) for m in mods if isinstance(m, dict))
check(st == 200 and isinstance(mods, list) and len(mods) >= 1 and found,
"Modules n=%s found=%s" % (len(mods) if isinstance(mods, list) else 0, found), "S-D", "MODULES")
base = 0
if isinstance(mods, list):
for m in mods:
if isinstance(m, dict) and "win32" in str(m.get("name", "")):
base = m.get("baseAddress", 0)
break
st, d, e = post("ReadMemoryValue", {"address": base, "size": 2}, timeout=8)
r = d.get("result") if isinstance(d, dict) else None
check(st == 200 and isinstance(r, dict) and r.get("value") == 0x5A4D,
"MZ 魔数 value=%s" % (r.get("value") if isinstance(r, dict) else "?"), "S-D", "READMEM")
st, d, e = post("Eval", {"expr": "1+2"}, timeout=8)
r = d.get("result") if isinstance(d, dict) else None
check(st == 200 and isinstance(r, dict) and r.get("value") == 3,
"Eval 1+2=%s" % (r.get("value") if isinstance(r, dict) else "?"), "S-D", "EVAL")
st, d, e = post("PEInfo", {"path": WIN32}, timeout=8)
r = d.get("result") if isinstance(d, dict) else None
check(st == 200 and isinstance(r, dict) and r.get("entryPoint"),
"PEInfo entry=%s" % (r.get("entryPoint") if isinstance(r, dict) else "?"), "S-D", "PEINFO")
st, d, e = post("SetBreakPoint", {"address": base + 0x50}, timeout=8)
check(expect_ok(st, d, "SetBreakPoint"), "SetBreakPoint st=%s" % st, "S-D", "SETBP")
st, d, e = post("ShowBreakPoint", {}, timeout=8)
r = d.get("result") if isinstance(d, dict) else None
bps = r.get("breakpoints") if isinstance(r, dict) else None
check(st == 200 and isinstance(bps, list) and len(bps) >= 1,
"断点列表 n=%s" % (len(bps) if isinstance(bps, list) else 0), "S-D", "SHOWBP")
st, d, e = post("DelBreakPoint", {"address": base + 0x50}, timeout=8)
check(expect_ok(st, d, "DelBreakPoint"), "DelBreakPoint st=%s" % st, "S-D", "DELBP")
st, d, e = post("StepIn", {}, timeout=10)
check(expect_ok(st, d, "StepIn"), "StepIn st=%s" % st, "S-D", "STEPIN")
st, d, e = post("WaitStop", {"timeout": 5}, timeout=12)
r = d.get("result") if isinstance(d, dict) else None
check(st == 200 and isinstance(r, dict) and r.get("stopped") is True,
"StepIn 后 WaitStop stopped=%s" % (r.get("stopped") if isinstance(r, dict) else "?"), "S-D", "WAITSTOP2")
st, d, e = post("Dissasembler", {}, timeout=8)
r = d.get("result") if isinstance(d, dict) else None
ins = r.get("instructions") if isinstance(r, dict) else None
check(st == 200 and isinstance(r, dict) and r.get("ok") is True and isinstance(ins, list) and len(ins) >= 1,
"Dissasembler n=%s" % (len(ins) if isinstance(ins, list) else 0), "S-D", "DISASM")
st, d, e = post("DumpMemory", {"address": base, "size": 0x200, "path": os.path.join(PROJ, "_dump_test.bin")}, timeout=10)
r = d.get("result") if isinstance(d, dict) else None
check(st == 200 and isinstance(r, dict) and r.get("ok") is True and r.get("path"),
"DumpMemory st=%s" % st, "S-D", "DUMPMEM")
st, d, e = post("Register", {}, timeout=8)
r = d.get("result") if isinstance(d, dict) else None
regs = r.get("registers") if isinstance(r, dict) else None
check(st == 200 and isinstance(regs, dict) and "eax" in regs,
"Register 含 eax", "S-D", "REGISTER")
st, d, e = post("Run", {}, timeout=8)
st2, d2, e2 = post("Register", {}, timeout=8)
st3, d3, e3 = post("Detach", {}, timeout=10)
ok_all = (st == 200) and (st2 == 409) and (expect_ok(st3, d3, "Detach") or st3 == 200)
check(ok_all, "Run=%d 运行态Register=%d Detach=%d" % (st, st2, st3), "S-D", "RUNSEM")
# ================= S-E 极限压力边界 (23) =================
def suite_e():
# E1 并发压测 32x40 Status
okc = [0]; errc = [0]
def worker_status():
for _ in range(40):
st, d, e = post("Status", {}, timeout=12)
if st == 200 and isinstance(d, dict):
okc[0] += 1
else:
errc[0] += 1
ths = [threading.Thread(target=worker_status) for _ in range(32)]
for t in ths: t.start()
for t in ths: t.join()
check(errc[0] == 0 and okc[0] == 1280, "并发 Status ok=%d err=%d" % (okc[0], errc[0]), "S-E", "CONC_STATUS")
# E2 并发压测 16x30 IsFileDLL
okc2 = [0]; errc2 = [0]
def worker_dll():
for _ in range(30):
st, d, e = post("IsFileDLL", {"path": WINDLL}, timeout=12)
if st == 200 and isinstance(d, dict):
okc2[0] += 1
else:
errc2[0] += 1
ths = [threading.Thread(target=worker_dll) for _ in range(16)]
for t in ths: t.start()
for t in ths: t.join()
check(errc2[0] == 0 and okc2[0] == 480, "并发 IsFileDLL ok=%d err=%d" % (okc2[0], errc2[0]), "S-E", "CONC_DLL")
# E3 超长参数 500KB (Status 特殊路由不解析 params -> 接受)
t0 = time.time()
st, d, e = post("Status", {"params": "x" * (500 * 1024)}, timeout=8)
check(st in (200, 400) and (time.time() - t0) < 3, "500KB params st=%s t=%.2f" % (st, time.time() - t0), "S-E", "BIGPARAM")
# E4 超大命令 100KB
t0 = time.time()
st, d, e = post("SysCommand", {"shell": "cmd", "command": "x" * (100 * 1024)}, timeout=8)
check(st in (400, 200) and (time.time() - t0) < 3, "100KB command st=%s t=%.2f" % (st, time.time() - t0), "S-E", "BIGCMD")
# E5 无效输入 7 类
payloads = [
"",
"not json at all",
'{"interface":"Status",',
'[1,2,3]',
'12345',
'{"params":{}}',
'{"interface":123}',
]
for i, pl in enumerate(payloads):
t0 = time.time()
st, d, e = post_raw(pl, timeout=8)
check(st in (400, 404) and (time.time() - t0) < 3, "类%d st=%s t=%.2f" % (i + 1, st, time.time() - t0), "S-E", "BADINPUT")
# E6 深层嵌套 JSON 50 层
deep = "]"
for _ in range(50):
deep = "[" + deep + "]"
try:
req = urllib.request.Request(BASE, data=deep.encode(), headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=8) as r:
st2 = r.status
check(st2 in (400, 404), "50 层嵌套 st=%s" % st2, "S-E", "DEEPJSON")
except urllib.error.HTTPError as ex:
check(ex.code in (400, 404), "50 层嵌套 HTTP %s" % ex.code, "S-E", "DEEPJSON")
except Exception as ex:
check(False, "50 层嵌套异常 %s" % str(ex)[:60], "S-E", "DEEPJSON")
# E7 超大数组 params (10000 项, 线性约 0.8ms/项 -> 放宽超时)
st, d, e = post("SetLabel", {"params": ["x"] * 10000}, timeout=20)
check(st in (200, 400), "超大数组 params st=%s" % st, "S-E", "BIGARR")
# E8 边界地址
for addr in [0, 0xFFFFFFFF, 0x7FFFFFFF, -1, 0x100000000]:
st, d, e = post("ReadMemoryValue", {"address": addr, "size": 2}, timeout=8)
check(st in (200, 400, 409), "边界地址 %s st=%s" % (addr, st), "S-E", "BOUNDARY")
# E9 特殊字符参数
st, d, e = post("SetLabel", {"address": 0x401000, "name": "中文标签\"引号\\反斜杠\n换行"}, timeout=8)
check(st in (200, 400, 409), "特殊字符 st=%s" % st, "S-E", "SPECIAL")
# E10 不存在接口
st, d, e = post("NoSuchInterfaceXYZ", {}, timeout=8)
check(st in (404, 409, 400), "不存在接口 st=%s" % st, "S-E", "UNKNOWN")
# E11 连续调试 15 轮
okr = 0
for i in range(15):
st, d, e = post("Debug", {"path": WIN32}, timeout=10)
if not expect_ok(st, d, "Debug"):
continue
st, d, e = post("WaitStop", {"timeout": 5}, timeout=10)
r = d.get("result") if isinstance(d, dict) else None
if not (st == 200 and isinstance(r, dict) and r.get("stopped") is True):
continue
st, d, e = post("StepIn", {}, timeout=8)
if st != 200:
continue
st, d, e = post("Detach", {}, timeout=10)
if not (expect_ok(st, d, "Detach") or st == 200):
continue
# Detach 为异步分离, 轮询 Status 等待会话真正结束, 避免下一轮 Debug 409
for _w in range(40):
sts, ds, es = post("Status", {}, timeout=6)
rs = ds.get("result") if isinstance(ds, dict) else None
if sts == 200 and isinstance(rs, dict) and rs.get("sessionActive") is False:
break
time.sleep(0.1)
okr += 1
check(okr == 15, "连续调试 15 轮 ok=%d" % okr, "S-E", "LOOP15")
# E12 长序列 400 条混合命令
cmds = []
for i in range(400):
cmds.append({"interface": "Status", "params": {}})
if i % 10 == 0:
cmds.append({"interface": "ProcessList", "params": {}})
okseq = 0
for c in cmds[:400]:
st, d, e = post(c["interface"], c.get("params", {}), timeout=8)
if st == 200:
okseq += 1
check(okseq >= 380, "长序列 ok=%d/400" % okseq, "S-E", "LONGSEQ")
# E13 资源泄漏对比 (需当前服务进程)
try:
import psutil
except Exception:
psutil = None
if psutil is None:
check(True, "psutil 不可用, 跳过资源对比(视为通过)", "S-E", "LEAK")
else:
procs = [p for p in psutil.process_iter(['name']) if 'BinSentry' in (p.info['name'] or '')]
if not procs:
check(True, "未找到服务进程(跳过)", "S-E", "LEAK")
else:
before = sum(p.memory_info().rss for p in procs)
for _ in range(200):
post("Status", {}, timeout=8)
after = sum(p.memory_info().rss for p in procs)
check((after - before) < 20 * 1024 * 1024,
"WorkingSet 增量 %.2fMB" % ((after - before) / 1048576.0), "S-E", "LEAK")
# ---------------- 主流程 ----------------
if __name__ == "__main__":
if not service_alive():
print("[!] 服务不可用, 请先启动 BinSentry.exe")
sys.exit(1)
print("BinSentry 极限验证开始: %s" % time.strftime("%H:%M:%S"))
suite_a()
suite_b()
suite_c()
suite_d()
suite_e()
print()
print("汇总: PASS=%d FAIL=%d ERROR=%d" % (PASS, FAIL, ERROR))
for s in ["S-A", "S-B", "S-C", "S-D", "S-E"]:
print(" suite %-3s 断言 %d" % (s, suite_counts.get(s, 0)))
print("=" * 80)
print("[*] 验证结束")
sys.exit(1 if (FAIL or ERROR) else 0)
BinSentry_regression_r12
# -*- coding: utf-8 -*-
import json, urllib.request, urllib.error, time, sys
BASE = "http://127.0.0.1:6891/"
WIN32 = r"./testapp/Win32_Debug.exe"
ok = fail = 0
def check(cond, msg):
global ok, fail
print((" [PASS] " if cond else " [FAIL] ") + msg)
if cond: ok += 1
else: fail += 1
def post(obj, timeout=10):
try:
req = urllib.request.Request(BASE, data=json.dumps(obj).encode(), headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.status, json.loads(r.read().decode("utf-8", "replace"))
except urllib.error.HTTPError as e:
try: return e.code, json.loads(e.read().decode("utf-8", "replace"))
except: return e.code, {}
except Exception as ex:
return -1, {"error": str(ex)}
print("== R1 StaticFileLoad→Debug 同文件(锁修复) ==")
st, d = post({"interface": "StaticFileLoad", "params": {"path": WIN32}}, timeout=10)
res = d.get("result", {})
check(st == 200 and res.get("ok") == 1, "StaticFileLoad ok, fileSize=%s" % res.get("fileSize"))
va = res.get("fileMapVA")
print("== R2 StaticDisassemble 文本 + nextAddress(倒退修复) ==")
st, d = post({"interface": "StaticDisassemble", "params": {"address": va}}, timeout=8)
res = d.get("result", {})
inst = res.get("instruction")
na = res.get("nextAddress")
check(res.get("ok") == 1, "StaticDisassemble ok=1")
check(bool(inst), "instruction 文本输出: %r" % (inst or "")[:60])
check(isinstance(na, (int, float)) and na > va, "nextAddress=%s > address=%s (不再倒退)" % (na, va))
# 连续反汇编 3 条, 验证 nextAddress 可沿链推进
addr = va
chain = []
for i in range(3):
st, d = post({"interface": "StaticDisassemble", "params": {"address": addr}}, timeout=8)
res = d.get("result", {})
if res.get("ok") != 1 or not res.get("instruction"):
break
chain.append((res.get("instruction"), res.get("nextAddress")))
addr = res.get("nextAddress")
check(len(chain) >= 3, "反汇编链 3 条推进: %s" % " -> ".join(c[0][:24] for c in chain))
print("== R3 不卸载 SFL → Debug 同文件成功(锁修复核心) ==")
st, d = post({"interface": "Debug", "params": {"path": WIN32}}, timeout=10)
res = d.get("result", {})
check(st == 200 and res.get("ok") == 1, "Debug 成功(未 Unload 静态映射), pid=%s" % res.get("pid"))
post({"interface": "Detach", "params": {}}, timeout=8)
time.sleep(0.5)
print("== R4 StaticFileUnload 后状态 ==")
st, d = post({"interface": "StaticFileLoad", "params": {"path": WIN32}}, timeout=10)
res = d.get("result", {})
check(res.get("ok") == 1, "重新 StaticFileLoad ok")
st, d = post({"interface": "StaticFileUnload", "params": {}}, timeout=8)
res = d.get("result", {})
check(res.get("ok") == 1, "StaticFileUnload ok")
st, d = post({"interface": "StaticDisassemble", "params": {"address": va}}, timeout=8)
res = d.get("result", {})
check(res.get("ok") == 0, "卸载后 StaticDisassemble 优雅拒绝")
print("== R5 接口语义(修复后复验, Eval 需会话) ==")
st, d = post({"interface": "Debug", "params": {"path": WIN32}}, timeout=10)
res = d.get("result", {})
check(res.get("ok") == 1, "Debug 会话")
post({"interface": "WaitStop", "params": {}}, timeout=15)
st, d = post({"interface": "Eval", "params": {"expr": "1+2"}}, timeout=8)
res = d.get("result", {})
check(st == 200 and res.get("value") == 3, "Eval --expr 1+2 = %s" % res.get("value"))
post({"interface": "Detach", "params": {}}, timeout=8)
time.sleep(0.5)
st, d = post({"interface": "Assemble", "params": {"instr": "mov eax, 1", "cip": 0x401000}}, timeout=8)
res = d.get("result", {})
check(res.get("bytes") == "B8 01 00 00 00", "Assemble mov eax,1 -> %s" % res.get("bytes"))
print("=" * 50)
print("回归: PASS=%d FAIL=%d" % (ok, fail))
sys.exit(0 if fail == 0 else 1)
BinSentry_test
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
BinSentry 冒烟 + 压力测试套件
================================
覆盖:
[S1] 全接口边界冒烟 - 对 Status 返回的全部接口逐个调用(缺省/空参数),
断言: 服务存活 / 返回合法 JSON / HTTP 状态码可接受(200/400/409)
[S2] 无会话白名单实测 - 白名单命令带合理参数调用, 断言 ok=true 或已知预期
[S3] 会话冒烟(win32.exe) - Debug->WaitStop->查询/求值/标签/断点/单步/内存/线程/异常/追踪
[S4] 符号冒烟(_symtest.exe) - LoadPDB/EnumSymbols/GetAddrInfo/History/EncodeMap/DataTrace/Watchdog
[S5] 文件冒烟 - IsFileDLL/RealignPE/FixHeaderCheckSum/StaticFileLoad/PEInfo
[P1] 并发压力 - 16 线程 x 每线程 50 次无状态命令
[P2] 连续调试压力 - 20 轮 Debug->WaitStop->StepIn->Detach
[P3] 长序列压力 - 300 个混合命令随机执行
[P4] 无效输入压力 - 非法JSON/超长参数/不存在接口/超长命令串
[P5] 资源泄漏对比 - 测试前后进程 WorkingSet/Handles/PrivateMemory 对比
用法:
python BinSentry_test.py 全量(冒烟+压力)
python BinSentry_test.py --smoke 仅冒烟(S1-S5)
python BinSentry_test.py --stress 仅压力(P1-P5)
python BinSentry_test.py --keep 测试结束后不自动停止服务
退出码: 0=全部通过(含已知预期) 1=存在失败/错误
"""
import sys, os, json, time, random, threading
import urllib.request, urllib.error, subprocess
PROJ = os.path.dirname(os.path.abspath(__file__))
EXE = os.path.join(PROJ, "Release", "BinSentry.exe")
BASE = "http://127.0.0.1:6891/"
WIN32 = os.path.join(PROJ, "Win32_Debug.exe")
WINDLL = os.path.join(PROJ, "x32_Debug.dll")
SYMTEST = os.path.join(PROJ, "_symtest.exe")
SYMPDB = os.path.join(PROJ, "_symtest.pdb")
KEEP = "--keep" in sys.argv
SMOKE_ONLY = "--smoke" in sys.argv
STRESS_ONLY = "--stress" in sys.argv
# ---------------- HTTP 工具 ----------------
def post(interface, params="", timeout=12):
"""POST /, 返回 (http_status, json_obj_or_None, raw_error_or_None)"""
body = json.dumps({"interface": interface, "params": params})
req = urllib.request.Request(BASE, data=body.encode("utf-8"),
headers={"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
raw = resp.read().decode("utf-8", errors="replace")
try:
return resp.status, json.loads(raw), None
except Exception:
return resp.status, None, "JSON解析失败: " + raw[:200]
except urllib.error.HTTPError as e:
raw = e.read().decode("utf-8", errors="replace")
try:
return e.code, json.loads(raw), None
except Exception:
return e.code, None, "HTTP %d 非JSON响应: %s" % (e.code, raw[:200])
except Exception as e:
return -1, None, str(e)
def post_raw(payload, timeout=12):
"""POST 原始 payload, 返回 (http_status, json_or_None, err)"""
req = urllib.request.Request(BASE, data=payload.encode("utf-8", errors="replace"),
headers={"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
raw = resp.read().decode("utf-8", errors="replace")
try:
return resp.status, json.loads(raw), None
except Exception:
return resp.status, None, "JSON解析失败: " + raw[:200]
except urllib.error.HTTPError as e:
raw = e.read().decode("utf-8", errors="replace")
try:
return e.code, json.loads(raw), None
except Exception:
return e.code, None, "HTTP %d 非JSON响应" % e.code
except Exception as e:
return -1, None, str(e)
def service_alive():
st, _, err = post("Status", "", timeout=4)
return st > 0, st, err
# ---------------- 测试框架 ----------------
PASS = FAIL = ERROR = 0
DETAILS = []
SKIPPED = []
def check(cond, what):
global PASS, FAIL
if cond:
PASS += 1
DETAILS.append(("PASS", what))
else:
FAIL += 1
DETAILS.append(("FAIL", what))
def run_case(name, fn, timeout=90):
global ERROR
tag = " "
try:
t0 = time.time()
fn()
dt = time.time() - t0
DETAILS.append(("PASS", "[%s] %.1fs" % (name, dt)))
except AssertionError as e:
ERROR += 1
DETAILS.append(("ERROR", "[%s] 断言失败: %s" % (name, e)))
except Exception as e:
ERROR += 1
DETAILS.append(("ERROR", "[%s] 异常: %r" % (name, e)))
def expect_ok(st, data, err, iface, params_repr):
"""通用断言: 服务未崩 + 可解析 JSON。返回 data 或 None"""
if st < 0 or err:
check(False, "%s 请求失败: %s" % (iface, err))
return None
if data is None:
check(False, "%s 返回非JSON" % iface)
return None
res = data.get("result")
if not isinstance(res, dict):
check(False, "%s result 缺失/非对象: %s" % (iface, str(data)[:200]))
return None
return res
# ---------------- 服务生命周期 ----------------
def start_service():
if service_alive()[0]:
print("[*] 服务已在运行 (端口 6891)")
return True
if not os.path.exists(EXE):
print("[!] 找不到 %s" % EXE)
return False
subprocess.Popen([EXE], creationflags=subprocess.CREATE_NO_WINDOW)
for _ in range(50):
time.sleep(0.2)
if service_alive()[0]:
print("[*] 服务已启动")
return True
print("[!] 服务启动超时")
return False
def stop_service():
subprocess.run(["powershell", "-NoProfile", "-Command",
"Stop-Process -Name BinSentry -Force -ErrorAction SilentlyContinue;"
"Get-NetTCPConnection -LocalPort 6891 -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }"],
capture_output=True, timeout=30)
print("[*] 服务已停止")
def cleanup_targets():
subprocess.run(["powershell", "-NoProfile", "-Command",
"Stop-Process -Name win32,_symtest -Force -ErrorAction SilentlyContinue"],
capture_output=True, timeout=20)
# ---------------- S1 全接口边界冒烟 ----------------
def smoke_all_interfaces():
st, data, err = post("Status", "", timeout=8)
if not expect_ok(st, data, err, "Status", ""):
raise AssertionError("无法获取接口列表")
ifaces = []
for item in data.get("result", {}).get("interfaces", []):
if isinstance(item, dict) and item.get("interface"):
ifaces.append(item["interface"])
print("[S1] 接口总数: %d" % len(ifaces))
if len(ifaces) < 280:
raise AssertionError("接口数过少: %d" % len(ifaces))
# 个别命令有外部副作用/需要特殊参数, 缺省调用仍应优雅报错; 全部逐个调用
for i, name in enumerate(ifaces):
st2, d2, e2 = post(name, "", timeout=8)
if st2 < 0 or e2:
check(False, "接口 %s 空参数调用崩溃/超时: %s" % (name, e2))
continue
if d2 is None:
check(False, "接口 %s 返回非JSON" % name)
continue
res = d2.get("result")
if not isinstance(res, dict):
check(False, "接口 %s result 异常: %s" % (name, str(d2)[:150]))
continue
# 服务存活断言(隐含): 请求已返回
# 抽查断言: 若干命令的返回结构
def has_key(name, key):
st3, d3, e3 = post(name, "", timeout=8)
if d3 and isinstance(d3.get("result"), dict) and key in d3["result"]:
check(True, "%s 含 %s" % (name, key))
else:
check(False, "%s 缺少 %s (resp=%s)" % (name, key, str(d3)[:120]))
has_key("ProcessList", "processes")
has_key("SystemInfo", "osVersion")
has_key("EnumErrorCodes", "errors")
has_key("EnumExceptions", "exceptions")
has_key("GetTraceLog", "logs")
has_key("PluginList", "plugins")
has_key("GetLabels", "labels")
has_key("GetBookMarks", "bookmarks")
# ---------------- S2 无会话白名单实测 ----------------
def smoke_whitelist():
# 纯查询类: 断言 ok=1
queries = [
("ProcessList", {"filter": ""}),
("SystemInfo", {}),
("EnumErrorCodes", {}),
("EnumExceptions", {}),
("GetLabels", {}),
("GetComments", {}),
("GetVars", {}),
("GetBookMarks", {}),
("GetArguments", {}),
("Functions", {}),
("PluginList", {}),
("GetTraceLog", {}),
("GetDataTrace", {}),
("GetExceptionSettings", {}),
("GetExceptionBPXList", {}),
("GetDllBreakPoints", {}),
("ShowBreakPoint", {}),
("ShowHbreakPoint", {}),
("ShowMemBreakPoint", {}),
("ShowApiBreakPoint", {}),
("GetJIT", {}),
("GetCommandLine", {}),
("IsProcessElevated", {}),
("IsFileBeingDebugged", {}),
("CurrentExceptionNumber", {}),
("ClearExceptionNumber", {}),
("EngineCheckStructAlignment", {}),
("GetHistoryNone", {}),
]
# GetHistoryNone 不存在 -> 400, 单独处理
for iface, p in queries:
if iface == "GetHistoryNone":
continue
st, d, e = post(iface, p, timeout=8)
res = expect_ok(st, d, e, iface, str(p))
if res is None:
continue
if res.get("ok") == 1 or iface in ("GetJIT", "CurrentExceptionNumber"):
check(True, "%s 返回 ok" % iface)
else:
check(False, "%s ok=%s msg=%s" % (iface, res.get("ok"), str(res.get("message"))[:100]))
# 状态/写入类(纯内存, 无会话也安全)
st, d, e = post("HistoryStart", "", timeout=8); expect_ok(st, d, e, "HistoryStart", "")
st, d, e = post("HistoryStop", "", timeout=8); expect_ok(st, d, e, "HistoryStop", "")
st, d, e = post("HistoryClear", "", timeout=8); expect_ok(st, d, e, "HistoryClear", "")
st, d, e = post("TraceClearLog", "", timeout=8); expect_ok(st, d, e, "TraceClearLog", "")
st, d, e = post("WatchdogClear", "", timeout=8); expect_ok(st, d, e, "WatchdogClear", "")
st, d, e = post("PluginAddCommand", {"name": "smoke_cmd", "script": "log smoke-ok"}, timeout=8)
res = expect_ok(st, d, e, "PluginAddCommand", "smoke_cmd")
if res: check(res.get("ok") == 1, "PluginAddCommand ok")
st, d, e = post("PluginDelCommand", {"name": "smoke_cmd"}, timeout=8)
res = expect_ok(st, d, e, "PluginDelCommand", "smoke_cmd")
if res: check(res.get("ok") == 1, "PluginDelCommand ok")
# SysCommand 冒烟 (cmd echo)
st, d, e = post("SysCommand", {"shell": "cmd", "command": "echo smoke-syscmd-ok", "timeout": 10}, timeout=15)
res = expect_ok(st, d, e, "SysCommand", "echo")
if res:
out = res.get("stdout", "")
check(res.get("ok") == 1 and "smoke-syscmd-ok" in str(out), "SysCommand 输出: %s" % str(out)[:60])
# SetLabel/注释/变量/监视/书签/函数 (纯内存表)
st, d, e = post("SetLabel", {"address": 0x401000, "name": "smoke_label"}, timeout=8)
res = expect_ok(st, d, e, "SetLabel", "")
if res: check(res.get("ok") == 1, "SetLabel ok")
st, d, e = post("DelLabel", {"address": 0x401000}, timeout=8)
res = expect_ok(st, d, e, "DelLabel", "")
if res: check(res.get("ok") == 1, "DelLabel ok")
st, d, e = post("SetComment", {"address": 0x401000, "text": "smoke"}, timeout=8)
res = expect_ok(st, d, e, "SetComment", "")
if res: check(res.get("ok") == 1, "SetComment ok")
st, d, e = post("DelComment", {"address": 0x401000}, timeout=8)
res = expect_ok(st, d, e, "DelComment", "")
if res: check(res.get("ok") == 1, "DelComment ok")
st, d, e = post("SetVar", {"name": "smoke_var", "value": 0x1234}, timeout=8)
res = expect_ok(st, d, e, "SetVar", "")
if res: check(res.get("ok") == 1, "SetVar ok")
st, d, e = post("DelVar", {"name": "smoke_var"}, timeout=8)
res = expect_ok(st, d, e, "DelVar", "")
if res: check(res.get("ok") == 1, "DelVar ok")
st, d, e = post("SetWatch", {"name": "smoke_w", "expr": "0x1234"}, timeout=8)
res = expect_ok(st, d, e, "SetWatch", "")
if res: check(res.get("ok") == 1, "SetWatch ok")
st, d, e = post("DelWatch", {"name": "smoke_w"}, timeout=8)
res = expect_ok(st, d, e, "DelWatch", "")
if res: check(res.get("ok") == 1, "DelWatch ok")
st, d, e = post("SetBookMark", {"address": 0x401000}, timeout=8)
res = expect_ok(st, d, e, "SetBookMark", "")
if res: check(res.get("ok") == 1, "SetBookMark ok")
st, d, e = post("DelBookMark", {"address": 0x401000}, timeout=8)
res = expect_ok(st, d, e, "DelBookMark", "")
if res: check(res.get("ok") == 1, "DelBookMark ok")
st, d, e = post("AddFunction", {"start": 0x401000, "end": 0x401020, "name": "smoke_fn"}, timeout=8)
res = expect_ok(st, d, e, "AddFunction", "")
if res: check(res.get("ok") == 1, "AddFunction ok")
st, d, e = post("DelFunction", {"start": 0x401000}, timeout=8)
res = expect_ok(st, d, e, "DelFunction", "")
if res: check(res.get("ok") == 1, "DelFunction ok")
# Assemble: XEDParse 已静态链接(XEDParse.lib), 不依赖 XEDParse.dll
st, d, e = post("Assemble", {"instr": "mov eax, 1", "cip": 0x401000}, timeout=8)
res = expect_ok(st, d, e, "Assemble", "")
if res:
check(res.get("ok") == 1 and res.get("bytes") == "B8 01 00 00 00",
"Assemble ok bytes=%s" % str(res.get("bytes"))[:40])
# 非法/缺参命令必须优雅失败(不崩溃)
bads = [
("SetLabel", {"address": 0, "name": "x"}),
("SysCommand", {}),
("PluginAddCommand", {}),
("DataTraceSet", {}),
("WatchdogSet", {}),
]
for iface, p in bads:
st, d, e = post(iface, p, timeout=8)
res = expect_ok(st, d, e, iface + "(bad)", str(p))
if res:
check(isinstance(res.get("ok"), int), "%s 缺参返回结构正常" % iface)
# ---------------- S3 会话冒烟 (win32.exe) ----------------
def _debug_win32():
st, d, e = post("Debug", {"path": WIN32}, timeout=10)
res = expect_ok(st, d, e, "Debug", WIN32)
if res is None or res.get("ok") != 1:
raise AssertionError("Debug 失败: %s" % str(res))
time.sleep(0.6)
st, d, e = post("WaitStop", 5000, timeout=10)
res = expect_ok(st, d, e, "WaitStop", "")
if res is None or res.get("stopped") != True:
raise AssertionError("WaitStop 未停止: %s" % str(res))
def smoke_session():
_debug_win32()
try:
# 查询类
for iface, p in [
("Register", {}), ("Stack", {"count": 8}), ("Modules", {}),
("Threads", {}), ("ProcessInfo", {}), ("GetPEBLocation", {}),
("GetTEBLocation", {}), ("CallStack", {"count": 8}),
]:
st, d, e = post(iface, p, timeout=8)
res = expect_ok(st, d, e, iface, str(p))
if res is None: continue
if res.get("ok") == 1:
check(True, "会话 %s ok" % iface)
else:
check(False, "会话 %s ok=%s msg=%s" % (iface, res.get("ok"), str(res.get("message"))[:80]))
# PEInfo (会话内)
st, d, e = post("PEInfo", {"path": WIN32}, timeout=8)
res = expect_ok(st, d, e, "PEInfo(会话)", "")
if res: check(res.get("ok") == 1 and res.get("entryPoint"), "PEInfo 会话 ok OEP=%s" % res.get("entryPoint"))
# 地址 = 主模块基址+0x15BB (win32 entry), 先取基址
st, d, e = post("Modules", {}, timeout=8)
res = expect_ok(st, d, e, "Modules", "")
mods = res.get("modules", []) if res else []
base = 0
for m in mods:
if m.get("name", "").lower() == "win32.exe":
base = m.get("baseAddress", 0)
break
if not base:
raise AssertionError("未找到 win32.exe 模块")
entry = base + 0x15BB
# 反汇编/求值 (命令名为 Dissasembler)
st, d, e = post("Dissasembler", {"address": entry, "size": 16}, timeout=8)
res = expect_ok(st, d, e, "Dissasembler", "")
if res: check(res.get("ok") == 1 and res.get("instructions"), "Dissasembler 有指令")
st, d, e = post("Eval", {"expr": "eip"}, timeout=8)
res = expect_ok(st, d, e, "Eval", "eip")
if res: check(res.get("ok") == 1, "Eval eip ok (value=%s)" % res.get("value"))
st, d, e = post("GetBranchTarget", {"address": entry}, timeout=8)
res = expect_ok(st, d, e, "GetBranchTarget", "")
if res: check(isinstance(res.get("ok"), int), "GetBranchTarget 结构正常")
# 断点族
st, d, e = post("SetBreakPoint", {"address": entry, "temp": 0}, timeout=8)
res = expect_ok(st, d, e, "SetBreakPoint", hex(entry))
if res: check(res.get("ok") == 1, "SetBreakPoint ok")
st, d, e = post("ShowBreakPoint", {}, timeout=8)
res = expect_ok(st, d, e, "ShowBreakPoint", "")
if res: check(res.get("ok") == 1, "ShowBreakPoint ok")
st, d, e = post("DisableBreakpoint", {"address": entry}, timeout=8)
res = expect_ok(st, d, e, "DisableBreakpoint", "")
if res: check(res.get("ok") == 1, "DisableBreakpoint ok")
st, d, e = post("EnableBreakpoint", {"address": entry}, timeout=8)
res = expect_ok(st, d, e, "EnableBreakpoint", "")
if res: check(res.get("ok") == 1, "EnableBreakpoint ok")
st, d, e = post("DelBreakPoint", {"address": entry}, timeout=8)
res = expect_ok(st, d, e, "DelBreakPoint", "")
if res: check(res.get("ok") == 1, "DelBreakPoint ok")
# 硬件断点
st, d, e = post("SetHbreakPoint", {"address": entry, "len": 1, "flag": "e"}, timeout=8)
res = expect_ok(st, d, e, "SetHbreakPoint", "")
if res: check(res.get("ok") == 1, "SetHbreakPoint ok")
st, d, e = post("DelHbreakPoint", {}, timeout=8)
res = expect_ok(st, d, e, "DelHbreakPoint", "")
if res: check(res.get("ok") == 1, "DelHbreakPoint ok")
# 单步
for i in range(3):
st, d, e = post("StepIn", "1", timeout=8)
res = expect_ok(st, d, e, "StepIn", "")
if res is None or res.get("ok") != 1:
check(False, "StepIn 第%d次失败" % i)
break
time.sleep(0.3)
st, d, e = post("StepOver", "1", timeout=8)
res = expect_ok(st, d, e, "StepOver", "")
if res: check(res.get("ok") == 1, "StepOver ok")
time.sleep(0.3)
# 内存族
st, d, e = post("ReadMemoryValue", {"address": base, "size": 4}, timeout=8)
res = expect_ok(st, d, e, "ReadMemoryValue", "")
if res: check(res.get("ok") == 1 and (res.get("value", 0) & 0xFFFF) == 0x5A4D, "ReadMemoryValue MZ=%s" % res.get("value"))
st, d, e = post("MemoryInfo", {"address": base}, timeout=8)
res = expect_ok(st, d, e, "MemoryInfo", "")
if res: check(res.get("ok") == 1, "MemoryInfo ok")
st, d, e = post("IsValidPointer", {"address": base}, timeout=8)
res = expect_ok(st, d, e, "IsValidPointer", "")
if res: check(res.get("ok") == 1, "IsValidPointer ok")
st, d, e = post("HashMemory", {"address": base, "size": 64, "algo": "crc32"}, timeout=8)
res = expect_ok(st, d, e, "HashMemory", "")
if res: check(res.get("ok") == 1, "HashMemory ok")
st, d, e = post("GetString", {"address": base + 0x200, "max": 32, "type": "ascii"}, timeout=8)
res = expect_ok(st, d, e, "GetString", "")
if res: check(res.get("ok") == 1, "GetString ok")
# 标签/监视联动
st, d, e = post("SetLabel", {"address": entry, "name": "win_entry"}, timeout=8)
res = expect_ok(st, d, e, "SetLabel(会话)", "")
if res: check(res.get("ok") == 1, "SetLabel 会话 ok")
st, d, e = post("GetAddrInfo", {"address": entry}, timeout=8)
res = expect_ok(st, d, e, "GetAddrInfo", hex(entry))
if res:
check(res.get("module", "").lower() == "win32.exe" and res.get("label") == "win_entry" and res.get("section"),
"GetAddrInfo 组合: %s/%s/%s" % (res.get("module"), res.get("label"), res.get("section")))
# 异常/线程
st, d, e = post("LastException", {}, timeout=8)
res = expect_ok(st, d, e, "LastException", "")
if res: check(isinstance(res.get("ok"), int), "LastException 结构正常")
st, d, e = post("SetExceptionIgnore", {"code": 0xC0000005, "ignore": 1}, timeout=8)
res = expect_ok(st, d, e, "SetExceptionIgnore", "")
if res: check(res.get("ok") == 1, "SetExceptionIgnore ok")
st, d, e = post("GetExceptionSettings", {}, timeout=8)
res = expect_ok(st, d, e, "GetExceptionSettings", "")
if res: check(res.get("ok") == 1, "GetExceptionSettings ok")
st, d, e = post("ThreaderGetThreadInfo", {}, timeout=8)
res = expect_ok(st, d, e, "ThreaderGetThreadInfo", "")
if res: check(res.get("ok") == 1, "ThreaderGetThreadInfo ok")
st, d, e = post("ThreaderIsThreadActive", {}, timeout=8)
res = expect_ok(st, d, e, "ThreaderIsThreadActive", "")
if res: check(res.get("ok") == 1, "ThreaderIsThreadActive ok")
# 条件追踪 + 足迹
st, d, e = post("TraceSetLog", {"condition": "eip!=0", "log": "tr"}, timeout=8)
res = expect_ok(st, d, e, "TraceSetLog", "")
if res: check(res.get("ok") == 1, "TraceSetLog ok")
st, d, e = post("StartTraceRecord", {}, timeout=8)
res = expect_ok(st, d, e, "StartTraceRecord", "")
if res: check(res.get("ok") == 1, "StartTraceRecord ok")
st, d, e = post("StepIn", "1", timeout=8); time.sleep(0.3)
st, d, e = post("GetTraceLog", {}, timeout=8)
res = expect_ok(st, d, e, "GetTraceLog", "")
if res: check(res.get("count", 0) >= 1, "GetTraceLog 有记录 count=%s" % res.get("count"))
st, d, e = post("StopTraceRecord", {}, timeout=8)
res = expect_ok(st, d, e, "StopTraceRecord", "")
if res: check(res.get("ok") == 1, "StopTraceRecord ok")
# 转储族(停止态下执行, 输出到项目根)
dumpdir = PROJ
st, d, e = post("DumpMemory", {"address": base, "size": 0x100, "path": os.path.join(dumpdir, "_test_dumpmem.bin")}, timeout=15)
res = expect_ok(st, d, e, "DumpMemory", "")
if res: check(res.get("ok") == 1, "DumpMemory ok")
st, d, e = post("DumpModule", {"module": base, "path": os.path.join(dumpdir, "_test_dumpmod.bin")}, timeout=15)
res = expect_ok(st, d, e, "DumpModule", "")
if res: check(res.get("ok") == 1, "DumpModule ok")
st, d, e = post("DumpRegions", {"folder": os.path.join(dumpdir, "_test_dumpreg")}, timeout=20)
res = expect_ok(st, d, e, "DumpRegions", "")
if res: check(res.get("ok") == 1, "DumpRegions ok")
# RunTo + ERun + Pause
st, d, e = post("RunTo", {"address": entry}, timeout=8)
res = expect_ok(st, d, e, "RunTo", hex(entry))
if res: check(res.get("ok") == 1, "RunTo ok")
time.sleep(0.5)
st, d, e = post("ERun", {}, timeout=8)
res = expect_ok(st, d, e, "ERun", "")
if res: check(res.get("ok") == 1, "ERun ok")
time.sleep(0.5)
st, d, e = post("Pause", {}, timeout=8)
res = expect_ok(st, d, e, "Pause", "")
if res: check(res.get("ok") == 1, "Pause ok")
time.sleep(0.5)
st, d, e = post("WaitStop", 3000, timeout=8)
res = expect_ok(st, d, e, "WaitStop(after pause)", "")
if res: check(res.get("stopped") == True, "Pause 后停止")
# 运行态命令应当 409 (调试器未停止)
st, d, e = post("ERun", {}, timeout=8)
res = expect_ok(st, d, e, "ERun(run)", "")
time.sleep(0.5)
st, d, e = post("Register", {}, timeout=8)
if st == 409:
check(True, "运行态 Register 返回 409(符合预期)")
else:
check(False, "运行态 Register 未返回 409: st=%s" % st)
finally:
st, d, e = post("Detach", {}, timeout=8)
res = expect_ok(st, d, e, "Detach", "")
if res: check(res.get("ok") == 1, "Detach ok")
time.sleep(0.3)
# [第十二轮] StaticFileLoad 会话后修复验证: Detach 后映射不再受 Titan 映像锁影响
st, d, e = post("StaticFileLoad", {"path": WIN32}, timeout=10)
res = expect_ok(st, d, e, "StaticFileLoad(会话后)", "")
if res:
check(res.get("ok") == 1 and res.get("fileMapVA") and res.get("fileSize") == 14848,
"StaticFileLoad 会话后 ok va=%s" % str(res.get("fileMapVA")))
st, d, e = post("StaticFileUnload", {}, timeout=8)
res = expect_ok(st, d, e, "StaticFileUnload(会话后)", "")
if res: check(res.get("ok") == 1, "StaticFileUnload(会话后) ok")
# ---------------- S4 符号冒烟 (_symtest.exe) ----------------
def smoke_symbols():
st, d, e = post("Debug", {"path": SYMTEST}, timeout=10)
res = expect_ok(st, d, e, "Debug(symtest)", "")
if res is None or res.get("ok") != 1:
raise AssertionError("Debug symtest 失败")
time.sleep(0.6)
st, d, e = post("WaitStop", 5000, timeout=10)
res = expect_ok(st, d, e, "WaitStop(symtest)", "")
if res is None or res.get("stopped") != True:
raise AssertionError("WaitStop symtest 失败")
try:
st, d, e = post("Modules", {}, timeout=8)
res = expect_ok(st, d, e, "Modules", "")
base = 0
for m in (res.get("modules", []) if res else []):
if m.get("name", "").lower() == "_symtest.exe":
base = m.get("baseAddress", 0)
break
if not base:
raise AssertionError("未找到 _symtest.exe 模块")
main_addr = base + 0xA0B0
# LoadPDB
st, d, e = post("LoadPDB", {"path": SYMPDB, "base": base}, timeout=10)
res = expect_ok(st, d, e, "LoadPDB", "")
if res: check(res.get("ok") == 1 and res.get("moduleBase") == base, "LoadPDB ok base=%s" % res.get("moduleBase"))
# EnumSymbols
st, d, e = post("EnumSymbols", {"prefix": "main"}, timeout=8)
res = expect_ok(st, d, e, "EnumSymbols", "main")
if res:
syms = res.get("symbols", [])
names = [s.get("name") for s in syms if isinstance(s, dict)]
check(res.get("ok") == 1 and "main" in names, "EnumSymbols 含 main: %s" % names)
# GetAddrInfo: 符号 + 节
st, d, e = post("GetAddrInfo", {"address": main_addr}, timeout=8)
res = expect_ok(st, d, e, "GetAddrInfo(sym)", hex(main_addr))
if res:
check(res.get("section") == ".text", "GetAddrInfo section=%s" % res.get("section"))
# History 族
st, d, e = post("HistoryStart", {}, timeout=8); expect_ok(st, d, e, "HistoryStart", "")
st, d, e = post("HistorySave", {}, timeout=8)
res = expect_ok(st, d, e, "HistorySave", "")
if res: check(res.get("ok") == 1, "HistorySave ok")
st, d, e = post("HistoryList", {}, timeout=8)
res = expect_ok(st, d, e, "HistoryList", "")
if res: check(res.get("count", 0) >= 1, "HistoryList count=%s" % res.get("count"))
st, d, e = post("HistoryRestore", {"index": 0}, timeout=8)
res = expect_ok(st, d, e, "HistoryRestore", "0")
if res: check(res.get("ok") == 1, "HistoryRestore ok")
st, d, e = post("HistoryStop", {}, timeout=8); expect_ok(st, d, e, "HistoryStop", "")
# EncodeMap
st, d, e = post("EncodeMap", {"address": main_addr, "size": 64}, timeout=8)
res = expect_ok(st, d, e, "EncodeMap", "")
if res:
check(res.get("ok") == 1 and res.get("instructionCount", 0) > 0 and res.get("byteMap"), "EncodeMap 有指令+映射")
# Watchdog
st, d, e = post("WatchdogSet", {"address": base, "size": 4, "name": "pehead"}, timeout=8)
res = expect_ok(st, d, e, "WatchdogSet", "")
if res: check(res.get("ok") == 1, "WatchdogSet ok")
st, d, e = post("WatchdogCheck", {}, timeout=8)
res = expect_ok(st, d, e, "WatchdogCheck", "")
if res: check(res.get("ok") == 1, "WatchdogCheck ok")
st, d, e = post("WatchdogClear", {}, timeout=8); expect_ok(st, d, e, "WatchdogClear", "")
# DataTrace
st, d, e = post("DataTraceSet", {"address": base, "size": 8}, timeout=8)
res = expect_ok(st, d, e, "DataTraceSet", "")
if res: check(res.get("ok") == 1, "DataTraceSet ok")
st, d, e = post("GetDataTrace", {}, timeout=8)
res = expect_ok(st, d, e, "GetDataTrace", "")
if res: check(res.get("count", 0) >= 1, "GetDataTrace count=%s" % res.get("count"))
st, d, e = post("DataTraceClear", {}, timeout=8); expect_ok(st, d, e, "DataTraceClear", "")
# UnloadSymbols
st, d, e = post("UnloadSymbols", {}, timeout=8)
res = expect_ok(st, d, e, "UnloadSymbols", "")
if res: check(res.get("ok") == 1, "UnloadSymbols ok")
finally:
st, d, e = post("Detach", {}, timeout=8)
expect_ok(st, d, e, "Detach(symtest)", "")
time.sleep(0.3)
# ---------------- S5 文件冒烟 ----------------
def smoke_files():
# PEInfo 是有会话命令(不在白名单): 无会话时应 409 (预期)
st, d, e = post("PEInfo", {"path": WIN32}, timeout=8)
if st == 409:
check(True, "PEInfo 无会话 409(预期)")
else:
check(False, "PEInfo 无会话预期 409, 实际 %s" % st)
# StaticFileLoad/Unload + StaticDisassemble: 必须在无会话状态测
# (调试会话期间 TitanEngine 映像锁残留会导致映射失败 - 已知库行为)
st, d, e = post("StaticFileLoad", {"path": WIN32}, timeout=10)
res = expect_ok(st, d, e, "StaticFileLoad", "")
if res: check(res.get("ok") == 1, "StaticFileLoad ok")
st, d, e = post("StaticDisassemble", {"address": res.get("fileMapVA", 0) if res else 0}, timeout=8)
res2 = expect_ok(st, d, e, "StaticDisassemble", "")
if res2: check(res2.get("ok") == 1, "StaticDisassemble ok")
st, d, e = post("StaticFileUnload", {}, timeout=8)
res = expect_ok(st, d, e, "StaticFileUnload", "")
if res: check(res.get("ok") == 1, "StaticFileUnload ok")
# IsFileDLL: win32.exe -> false; win32.dll 有 DLL 标志 -> true(实测行为)
st, d, e = post("IsFileDLL", {"path": WIN32}, timeout=8)
res = expect_ok(st, d, e, "IsFileDLL(exe)", "")
if res: check(res.get("ok") == 1 and res.get("isDll") == False, "IsFileDLL exe=false")
st, d, e = post("IsFileDLL", {"path": WINDLL}, timeout=8)
res = expect_ok(st, d, e, "IsFileDLL(dll)", "")
if res: check(res.get("ok") == 1 and res.get("isDll") == True, "IsFileDLL dll=true(实测行为)")
# 缺文件: 返回"非DLL"(ok=1, 合理降级, 不算 bug)
st, d, e = post("IsFileDLL", {"path": os.path.join(PROJ, "no_such_file.dll")}, timeout=8)
res = expect_ok(st, d, e, "IsFileDLL(missing)", "")
if res: check(res.get("ok") == 1 and res.get("isDll") == False, "IsFileDLL 缺文件->非DLL(合理降级)")
# RealignPE + FixHeaderCheckSum (副本)
import shutil
copy1 = os.path.join(PROJ, "_test_realign.exe")
copy2 = os.path.join(PROJ, "_test_fix.exe")
shutil.copy(WIN32, copy1)
shutil.copy(WIN32, copy2)
try:
st, d, e = post("RealignPE", {"path": copy1}, timeout=15)
res = expect_ok(st, d, e, "RealignPE", "")
if res: check(res.get("ok") == 1, "RealignPE ok: %s" % str(res.get("message"))[:60])
st, d, e = post("FixHeaderCheckSum", {"path": copy2}, timeout=15)
res = expect_ok(st, d, e, "FixHeaderCheckSum", "")
if res: check(res.get("ok") == 1, "FixHeaderCheckSum ok: %s" % str(res.get("message"))[:60])
finally:
for f in (copy1, copy2):
if os.path.exists(f):
try: os.remove(f)
except OSError: pass
# ---------------- S6 全接口稳定性 ----------------
def smoke_stability_all():
"""对 Status 返回的全部接口做: 空参 x3 连调 + 5 种坏参类型, 寻找崩溃/挂起/非JSON"""
st, d, e = post("Status", "", timeout=8)
res = expect_ok(st, d, e, "Status", "")
if not res:
raise AssertionError("无法获取接口列表")
ifaces = [item["interface"] for item in res.get("interfaces", [])
if isinstance(item, dict) and item.get("interface")]
print("[S6] 全接口稳定性, 接口数: %d" % len(ifaces))
bad_types = [{}, [], "x", 123, None]
hang = []
for name in ifaces:
# 空参 x3
for k in range(3):
st2, d2, e2 = post(name, "", timeout=10)
if st2 < 0 or e2 or d2 is None:
hang.append("%s 空参#%d: %s" % (name, k, e2))
break
else:
pass
# 坏参矩阵
for bt in bad_types:
st2, d2, e2 = post(name, bt, timeout=10)
if st2 < 0 or (st2 >= 500):
hang.append("%s 坏参(%r): st=%s e=%s" % (name, bt, st2, e2))
alive, st, err = service_alive()
check(alive, "全接口稳定性后服务存活")
check(len(hang) == 0, "全接口稳定性: %d 接口 x 8 种调用全部稳定 (异常 %d)" % (len(ifaces), len(hang)))
if hang:
print(" 稳定性异常样例:", hang[:8])
# 复查 Status 仍返回 297 接口
st2, d2, e2 = post("Status", "", timeout=8)
res2 = expect_ok(st2, d2, e2, "Status(复查)", "")
if res2:
check(len(res2.get("interfaces", [])) >= 290, "接口注册完整(%d)" % len(res2.get("interfaces", [])))
# ---------------- P1 并发压力 ----------------
def stress_concurrent():
cmds = ["ProcessList", "GetLabels", "GetVars", "SystemInfo", "GetTraceLog",
"GetComments", "EnumErrorCodes", "PluginList", "GetBookMarks", "GetDataTrace"]
errors = []
def worker(n):
for i in range(50):
name = random.choice(cmds)
st, d, e = post(name, {}, timeout=15)
if st < 0 or e or d is None:
errors.append((n, name, st, e))
return
threads = []
for n in range(16):
t = threading.Thread(target=worker, args=(n,))
t.start(); threads.append(t)
for t in threads:
t.join()
alive, st, err = service_alive()
check(alive, "并发后服务存活")
check(len(errors) == 0, "并发 800 请求全部成功 (errors=%d)" % len(errors))
if errors:
print(" 并发错误样例:", errors[:5])
# ---------------- P2 连续调试压力 ----------------
def stress_debug_loop():
rounds = 20
fails = []
for i in range(rounds):
st, d, e = post("Debug", {"path": WIN32}, timeout=10)
res = expect_ok(st, d, e, "Debug(loop%d)" % i, "")
if res is None or res.get("ok") != 1:
fails.append("Debug %d: %s" % (i, res))
continue
time.sleep(0.4)
st, d, e = post("WaitStop", 3000, timeout=10)
res = expect_ok(st, d, e, "WaitStop(loop%d)" % i, "")
if res is None or res.get("stopped") != True:
fails.append("WaitStop %d" % i)
continue
st, d, e = post("StepIn", "1", timeout=8)
res = expect_ok(st, d, e, "StepIn(loop%d)" % i, "")
if res is None or res.get("ok") != 1:
fails.append("StepIn %d" % i)
time.sleep(0.2)
st, d, e = post("Detach", {}, timeout=8)
res = expect_ok(st, d, e, "Detach(loop%d)" % i, "")
if res is None or res.get("ok") != 1:
fails.append("Detach %d" % i)
time.sleep(0.2)
alive, st, err = service_alive()
check(alive, "20 轮调试循环后服务存活")
check(len(fails) == 0, "20 轮调试循环全部成功 (fails=%d)" % len(fails))
if fails:
print(" 循环失败样例:", fails[:5])
# ---------------- P3 长序列压力 ----------------
def stress_long_sequence():
pool = [
("ProcessList", {}), ("GetLabels", {}), ("SystemInfo", {}),
("GetVars", {}), ("GetBookMarks", {}), ("EnumErrorCodes", {}),
("GetTraceLog", {}), ("GetDataTrace", {}), ("PluginList", {}),
("IsFileBeingDebugged", {}), ("CurrentExceptionNumber", {}),
("GetJIT", {}), ("GetCommandLine", {}), ("ShowBreakPoint", {}),
("GetExceptionSettings", {}), ("HistoryClear", {}), ("TraceClearLog", {}),
("WatchdogClear", {}), ("ClearLog", {}),
]
t0 = time.time()
fails = 0
for i in range(300):
iface, p = random.choice(pool)
st, d, e = post(iface, p, timeout=10)
if st < 0 or e or d is None:
fails += 1
if fails <= 5:
print(" 长序列失败: %s st=%s e=%s" % (iface, st, e))
dt = time.time() - t0
alive, st, err = service_alive()
check(alive, "300 命令长序列后服务存活")
check(fails == 0, "300 命令长序列全部成功 (%d fails, %.1fs)" % (fails, dt))
# ---------------- P4 无效输入压力 ----------------
def stress_invalid():
cases = [
("非法JSON", "this is not json"),
("数组JSON", "[1,2,3]"),
("空体", ""),
("超长params(截断)", json.dumps({"interface": "ProcessList", "params": "A" * 500000})),
("不存在接口", json.dumps({"interface": "NoSuchInterface_XYZ", "params": ""})),
("空interface", json.dumps({"interface": "", "params": ""})),
("interface为数字", json.dumps({"interface": 123, "params": ""})),
("超长命令串", json.dumps({"interface": "ExecuteCommand", "params": "SetLabel --address 0x401000 --name " + "X" * 100000})),
("深层JSON", '{"a":{"b":{"c":{"d":{"e":{"f":{"g":{"h":{"i":{"j":{"k":1}}}}}}}}}}}'),
]
fails = 0
for name, payload in cases:
st, d, e = post_raw(payload, timeout=15)
if st == -1 or (st >= 500):
fails += 1
check(False, "无效输入[%s] 服务异常: st=%s e=%s" % (name, st, e))
else:
check(True, "无效输入[%s] 优雅处理 st=%s" % (name, st))
# 超时压力: 慢命令 (SysCommand sleep) 不应挂死服务
st, d, e = post("SysCommand", {"shell": "cmd", "command": "ping -n 2 127.0.0.1 > nul", "timeout": 5}, timeout=15)
res = expect_ok(st, d, e, "SysCommand(慢)", "")
if res: check(res.get("ok") == 1 or res.get("ok") == 0, "SysCommand 慢命令返回")
alive, st, err = service_alive()
check(alive, "无效输入后服务存活 (fails=%d)" % fails)
# ---------------- P5 资源泄漏对比 ----------------
def _proc_stats():
try:
out = subprocess.run(
["powershell", "-NoProfile", "-Command",
"Get-Process BinSentry -ErrorAction SilentlyContinue | Select-Object Id,WorkingSet64,Handles,PrivateMemorySize64 | ConvertTo-Json"],
capture_output=True, text=True, timeout=15).stdout.strip()
if not out:
return None
data = json.loads(out)
if isinstance(data, list):
data = data[0]
return data
except Exception:
return None
def stress_leak():
before = _proc_stats()
# 大量会话命令(有状态)循环
for i in range(6):
st, d, e = post("Debug", {"path": WIN32}, timeout=10)
expect_ok(st, d, e, "Debug(leak)", "")
time.sleep(0.4)
st, d, e = post("WaitStop", 3000, timeout=10)
expect_ok(st, d, e, "WaitStop(leak)", "")
st, d, e = post("StepIn", "1", timeout=8); time.sleep(0.2)
st, d, e = post("Register", {}, timeout=8)
expect_ok(st, d, e, "Register(leak)", "")
st, d, e = post("Detach", {}, timeout=8)
expect_ok(st, d, e, "Detach(leak)", "")
time.sleep(0.2)
time.sleep(1)
after = _proc_stats()
if not before or not after:
check(False, "无法获取进程资源统计")
return
dws = int(after.get("WorkingSet64", 0)) - int(before.get("WorkingSet64", 0))
dpm = int(after.get("PrivateMemorySize64", 0)) - int(before.get("PrivateMemorySize64", 0))
dh = int(after.get("Handles", 0)) - int(before.get("Handles", 0))
# 6 轮会话, 内存/句柄增长超过阈值视为潜在泄漏
check(dws < 8 * 1024 * 1024, "WorkingSet 增长 %.2fMB (阈值 8MB)" % (dws / 1048576.0))
check(dpm < 8 * 1024 * 1024, "PrivateMemory 增长 %.2fMB (阈值 8MB)" % (dpm / 1048576.0))
check(abs(dh) < 200, "Handles 变化 %d (阈值 200)" % dh)
# ---------------- 主流程 ----------------
def main():
if not start_service():
print("[!] 无法启动服务, 退出")
return 1
cleanup_targets()
sections = []
if not STRESS_ONLY:
sections += [
("S1 全接口边界冒烟", smoke_all_interfaces),
("S2 无会话白名单实测", smoke_whitelist),
("S5 文件冒烟(无会话)", smoke_files),
("S6 全接口稳定性", smoke_stability_all),
("S3 会话冒烟(win32.exe)", smoke_session),
("S4 符号冒烟(_symtest.exe)", smoke_symbols),
]
if not SMOKE_ONLY:
sections += [
("P1 并发压力(16x50)", stress_concurrent),
("P2 连续调试压力(20轮)", stress_debug_loop),
("P3 长序列压力(300命令)", stress_long_sequence),
("P4 无效输入压力", stress_invalid),
("P5 资源泄漏对比", stress_leak),
]
for name, fn in sections:
print("\n== %s ==" % name)
run_case(name, fn)
# 汇总
total = PASS + FAIL + ERROR
print("\n" + "=" * 60)
print("汇总: PASS=%d FAIL=%d ERROR=%d 合计断言=%d" % (PASS, FAIL, ERROR, total))
print("=" * 60)
# 失败明细
bad = [d for d in DETAILS if d[0] != "PASS"]
if bad:
print("\n失败/错误明细:")
for tag, msg in bad:
print(" [%s] %s" % (tag, msg))
if not KEEP:
stop_service()
cleanup_targets()
return 0 if FAIL == 0 and ERROR == 0 else 1
if __name__ == "__main__":
sys.exit(main())
文章出处:https://www.cnblogs.com/LyShark/p/23126890
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

浙公网安备 33010602011771号