aidbg
Health Warn
- No license — Repository has no license file
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Community trust — 18 GitHub stars
Code Pass
- Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
AI-first Windows x86/x64 命令行调试器 | JSONL output, MCP server, anti-anti-debug (ScyllaHide-style), snapshot rollback, IAT rebuild, PDB types
aidbg — 面向 AI 的 Windows x86/x64 命令行调试器
aidbg 是一个为 AI 辅助逆向分析 设计的命令行调试器。它的核心目标不是人类交互体验,而是
让程序(AI agent)能可靠地驱动调试会话并解析结果:
- 每条命令可输出 一行 JSON(JSONL),字段稳定、可直接解析
- 支持 批处理(
-c连续命令)与 脚本文件(--script) - 同一个 exe 自动识别并调试 x86 与 x64 目标(WOW64),无需换工具
- 所有地址/64 位值以 十六进制字符串 返回,避免 JSON 数字精度丢失
- 输出可控(
max=N、条目上限),不会撑爆上下文窗口 - 错误结构化返回,便于 agent 判断重试或换策略
技术栈:C++20 / MSVC、Zydis 4.1.1(反汇编,与 x64dbg 同款)、DbgHelp(PDB 符号)。
架构源于多个开源调试器:x64dbg 的命令层与引擎解耦、raddebugger 的模块化引擎、
GDB 的请求-响应模型、Z0BDbgCli 的条件断点与异常策略、Nanomite 的状态视图完备性。
1. 快速开始
:: 构建(需要 VS 18 / MSVC,脚本会自动调用 vcvars64)
build.bat Release
:: 产物
build\aidbg.exe
构建配置:/MT 静态链接 C++ 标准库 + VC-LTL(D:\vc\VC-LTL-Binary,ucrt 模式)把 C 运行时
重定向到系统 ucrtbase.dll。若 VC-LTL 不存在,CMakeLists.txt 会自动退回纯静态 CRT(/MT),
两者都能达到"零 VC 运行时依赖"的效果,区别只是体积与 CRT 更新方式。
产物只依赖系统自带的 KERNEL32.dll、dbghelp.dll、ucrtbase.dll,
不需要 Visual C++ Redistributable(无 MSVCP140.dll / VCRUNTIME140.dll / api-ms-win-crt-*),
拷贝到 Windows 10+ 的机器上即可直接运行。
一次典型的 AI 调用(每条命令输出一行 JSON):
aidbg --json ^
-c "file tests\target.exe" ^
-c "run" ^
-c "break accumulate" ^
-c "continue" ^
-c "regs" ^
-c "disasm 6" ^
-c "stack 5"
脚本模式(命令一行一条,# 开头为注释):
aidbg --script analysis.txt --json > result.jsonl
交互模式(人类使用):直接运行 aidbg.exe,help 查看命令,quit 退出。
1.1 作为 MCP 服务器使用
aidbg --mcp
此时 aidbg 通过 stdio 上的换行分隔 JSON-RPC 2.0(MCP 标准 stdio transport)提供工具,
直到 stdin 关闭才退出。调试会话在整个连接期间保持:一次 aidbg_session(action=run) 启动的目标,
后续任何 tool call 都还在同一个会话里。
客户端配置示例:
{
"mcpServers": {
"aidbg": {
"command": "D:\\crack\\debug\\code\\aidbg\\build\\aidbg.exe",
"args": ["--mcp"]
}
}
}
暴露 11 个工具,全部返回与命令行 完全相同 的 JSON 信封(见 §2):
| 工具 | 对应命令 | 用途 |
|---|---|---|
aidbg_session |
file / run / attach / detach / kill / restart / info |
会话管理 |
aidbg_execute |
continue / step / next / finish / until / stepuntil / rununtil / autostep |
执行控制与追踪(continue 可带 timeout_ms) |
aidbg_breakpoint |
break / hbreak / mbreak / cond / bl / be / bd / bc |
三类断点与条件 |
aidbg_registers |
regs / setreg |
寄存器读写 |
aidbg_memory |
read / mem / write / search / regions / alloc / free / protect |
内存 |
aidbg_disasm |
disasm |
反汇编 |
aidbg_cfg |
flow |
函数控制流图(基本块 + 边) |
aidbg_stack |
stack |
调用栈 |
aidbg_modules |
modules / threads / thread / symbols |
模块、线程、符号 |
aidbg_eval |
eval |
表达式求值 |
aidbg_exceptions |
exlist / exset / exdel / exall |
异常策略 |
aidbg_snapshot |
snapshot / stepback |
快照保存/恢复,回退执行 |
aidbg_command |
任意命令 | 原样透传,兜底所有未覆盖命令 |
协议行为:
- 成功/失败都通过
result.content[0].text里的 JSON 信封表达,失败时isError: true - 命令级错误(如未知寄存器)→ 信封
ok:false+error.code - 协议级错误 → JSON-RPC
error:-32601(未知方法)、-32602(未知工具)、-32700(解析失败) - 通知(无
id)不返回响应;stdout 只走协议,调试器自身输出不写 stdout
2. 输出契约(AI 必读)
2.1 JSON 信封
--json 下 每条命令恰好输出一行 JSON:
{"ok":true,"cmd":"regs","data":{...},"session":{...}}
{"ok":false,"cmd":"mem","error":{"code":"read_failed","message":"..."},"data":{},"session":{...}}
| 字段 | 说明 |
|---|---|
ok |
命令是否成功 |
cmd |
规范命令名(别名已归一化,如 b → break) |
data |
成功时的结构化结果 |
error |
失败时的 {code, message} |
session |
会话快照,每条响应都带,便于跟踪状态 |
session 字段:
{"active":true,"pid":123,"tid":456,"state":"paused",
"stop_reason":"breakpoint","rip":"0x7ff6...","arch":"x64",
"detail":"breakpoint 2 hit (accumulate)"}
state:idle(无目标)/paused(已停住,可读写状态)/running/exitedarch: 目标架构x86或x64,自动识别,决定寄存器名与反汇编模式stop_reason:entry/breakpoint/step/exception/memory_breakpoint/system_break/exit/timeout
异步事件(调试字符串等)单独一行:{"kind":"event","event":"debug_string","text":"..."}
2.2 数值编码
| 类型 | 编码 | 示例 |
|---|---|---|
| 地址 / 64 位寄存器 | 十六进制字符串 | "0x7ff674a47220" |
| 小整数(大小、计数、行号) | JSON 数字 | 64, 5 |
| 字节串 | 空格分隔大写十六进制 | "48 8B 5C 24 08" |
64 位值一律用字符串,因为 JSON number 只有 2^53 的安全整数范围。
2.3 退出码
0 全部成功;1 有命令失败;2 参数错误。批处理结束后会自动 detach,不会留下挂起的调试会话。
3. 地址与表达式
所有接受地址的地方都接受表达式:
| 形式 | 示例 | 含义 |
|---|---|---|
| 十六进制 | 0x7ff674a47220 |
字面量 |
| 十进制 | 1234 / 0n1234 |
字面量 |
| 寄存器 | rax, rsp, rip, eax, al, ah |
当前线程寄存器;x86 目标用 eax…edi/eip(rax 等 64 位名在 x86 下等价于低 32 位) |
| 符号 | main, kernel32!CreateFileW |
PDB/导出符号 |
| 模块相对 | target.exe+0x7220 |
模块基址 + 偏移 |
| 符号相对 | main+0x10, accumulate+4 |
符号地址 + 偏移 |
| 内置 | $entry, $ra |
入口点 / 返回地址 |
| 解引用 | [rsp], [rax+0x10] |
读 8 字节指针 |
| 运算 | rax+8, [rbp-0x20]&0xff, x<<2 |
+ - * / & | ^ ~ << >> |
| 比较/逻辑 | rcx == 14, rax && [rsp] > 0x1000 |
== != < <= > >= && || |
比较与逻辑运算返回 1/0,专供 条件断点 使用。
4. 命令参考
4.1 会话
| 命令 | 说明 |
|---|---|
file <path> [args...] |
设置待调试程序与参数 |
run [args...] |
启动并停在 入口点(自动跳过 ntdll 加载器断点) |
attach <pid> |
附加到已运行进程,停在系统断点 |
detach / kill |
干净分离 / 终止目标 |
restart |
结束并用相同参数重新启动 |
aslr [on|off] |
固定加载基址(默认关 = 固定),让动态地址与静态反汇编一致 |
processes|ps |
列出本次调试会话中的所有进程(含子进程) |
process|proc <pid> |
切换当前进程(后续命令作用于它) |
info / version |
会话状态 / 版本 |
help |
命令列表(--json 下返回机器可读目录) |
子进程默认会被调试(DEBUG_PROCESS):目标启动的任何进程都会进入同一个会话,
可以像主进程一样设断点、读寄存器、看调用栈。用 --no-children 关闭(退回单进程模式)。
每个进程的架构独立识别,所以一个会话里可以同时有 x64 和 x86 进程。
固定基址(动静态地址对齐)
默认关闭 ASLR,目标按 PE 头的 ImageBase 加载,这样动态地址与 IDA / Ghidra
等静态反汇编工具里的地址完全一致:
| 目标 | 基址 | 入口点 |
|---|---|---|
| x64 | 0x140000000 |
0x140002306 |
| x86 | 0x400000 |
0x4025cc |
modules 现在同时给出三个量,方便随时核对:
base image_base slide name path
0x7ff742810000 0x140000000 0x7ff602810000 target.exe ... ← ASLR on
0x140000000 0x140000000 0 target.exe ... ← ASLR off
base:实际加载地址image_base:PE 头里的ImageBase,也就是静态反汇编工具使用的基址slide:两者之差(重定位偏移)。固定基址下为0;开启 ASLR 时可用于手动换算
静态地址 → 运行时地址
注意
image_base必须从文件读取。Windows loader 会把已映射 PE 头中的ImageBase
字段改写成实际加载地址,所以读内存里的 PE 头永远只能得到运行时基址——这点很容易踩坑。
实现细节(重要):原始文件的字节绝不会被修改。决定是否重定位的是 PE 头里的DYNAMIC_BASE 标志,而内核是在 CreateProcess 建立映像节时读它的——所以标志必须在
创建进程之前就位。Windows 封死了所有纯内存写法(持有写句柄或可写映射视图都会让内核
自己打不开映像;映射完成之后又拒绝写、替换、删除运行中的映像文件),可行的是改名
交换:把原件整体改名到 <目标>.aidbg-orig(字节原封不动),补丁版复制到目标路径后
清掉 DYNAMIC_BASE;CreateProcess 读到补丁版,加载器自己按 ImageBase 映像;紧接着
补丁版改名到 .aidbg-patched、原件改名回目标路径。目标被调试事件门控着一行代码都没
执行过,它对自身文件做的任何自校验读到的都是原始字节(实测 self_check=PASS)。
运行中映像的文件只能等进程退出后删除,.aidbg-patched 残留会在下次固定基址启动时
自动清理;若中途断电,原件完好地留在 <目标>.aidbg-orig。
需要观察真实 ASLR 行为时用 aslr on(或 --aslr),此时 slide 会显示实际偏移。
x64dbg 的 "Disable ASLR" 走 TitanEngine 的 UE_ENGINE_DISABLE_ASLR,底层是HollowProcessWithoutASLR:进程镂空(create-suspended → 卸载随机映像 → 重映射到
首选基址 → 重新 attach)。这条路在实践中被公认为不可靠——第三方文档明确记录:
"ASLR disabling is unreliable (TitanEngine engine limitation) ... In this build that
path is unreliable and often fails withInitDebugW failed to create the target process."
"ASLR 开关无论 on/off 基址不变",即该特性从未真正生效。
进程镂空需要手工处理重定位、导入表、TLS、Load Config 等,任何一个环节出错都会导致
进程起不来。本项目也实测过同类路线(把映像节二次映射到首选基址):只要 CREATE_PROCESS
调试事件还挂着,任何映像视图操作都会死锁内核(进程变成不可杀状态),而DEBUG_PROCESS|CREATE_SUSPENDED 甚至根本不投递该事件——此路确认不可靠。相比之下,
清 DYNAMIC_BASE 让加载器自己按首选基址加载,不涉及任何手工映射,实测 100% 可靠,
且不改动用户文件的字节。
session.processes 字段给出当前会话的进程数(>1 时出现)。子进程自己的加载器断点
(ntdll / WOW64 层)会自动跳过,只有你设的断点才会停。
4.2 执行控制
| 命令 | 说明 |
|---|---|
continue|c|g [timeout_ms] |
继续执行;可设超时(超时返回 timeout 而非挂死) |
step|si [count] |
单步(步入) |
next|n|so [count] |
单步(步过 call/rep/loop) |
finish|gu |
执行到当前函数返回 |
until <addr|expr> |
执行到指定地址 |
stepuntil|tu <条件> [max=N] |
单步直到表达式为真,每步求值一次 |
rununtil|ru <条件> [max=N] |
继续执行直到表达式为真,每次停住时求值 |
autostep|astep <count> [delay_ms] |
连续单步并记录执行轨迹(每条指令的地址/字节/文本) |
追踪命令对 AI 探查特别有用:
break accumulate
continue
stepuntil eax == 3 max=200 # 一直单步,直到 eax 变成 3
→ condition 'eax == 3' true after 23 step(s)
rununtil ecx == 14 max=20 # 一直跑,直到 ecx 变成 14
→ condition 'ecx == 14' true after 4 stop(s)
autostep 14 # 记录接下来 14 条实际执行的指令
[ 0] 0x...220 89 4C 24 08 mov [rsp+0x08], ecx
[ 1] 0x...224 48 83 EC 18 sub rsp, 0x18
...
条件是任意表达式(比较、逻辑、[addr] 解引用、寄存器、符号都行)。stepuntil 逐步求值,rununtil 只在停住时求值(配合断点用)。max=N 是步数/停住次数上限,超了报condition_not_met 而不是无限跑。
注意:stepuntil/rununtil/autostep 期间自动快照是关闭的(否则一万步会产生一万份
10MB 快照),结束后恢复正常。
4.3 断点
| 命令 | 说明 |
|---|---|
break|b <addr|symbol> [condition] |
软件断点(INT3) |
hbreak <addr> [r|w|x] [size] |
硬件断点(DR0-DR3,最多 4 个;size = 1/2/4/8) |
mbreak <addr> <size> [r|w|x] |
内存断点(PAGE_GUARD,任意大小) |
cond <id> <expression> |
设置条件;条件为 0 时自动继续 |
bpcmd <id> [command|-] |
命中动作:每次命中先执行命令(如 eval rax / regs),再按条件决定停/走 |
bpload [pattern|del <pattern>|clear] |
模块加载断点:匹配的 DLL 加载时停下(bpload dbghelp.dll,支持 * 通配) |
bl / be <id> / bd <id> / bc <id|*> |
列表 / 启用 / 禁用 / 删除 |
断点可以在目标启动前设置,这是推荐工作流:
file sample.exe
break main # 此时还没启动,断点为 pending
hbreak some_var w 4
run # 启动后自动解析符号并植入全部断点
continue # 命中 main
bl 的 resolved 字段区分两者:false = 待定(符号还没解析出来),true = 已解析。
待定断点在模块加载后自动解析并植入,无需手动干预。
bl 的 data.breakpoints[] 每项含 id, kind, address, symbol, size, access, enabled, hit_count, condition, command, resolved。
4.4 状态读写
| 命令 | 说明 |
|---|---|
regs [name...] |
寄存器(无参数返回全部 + 标志位 + SSE);按目标架构输出 x64(rax…r15)或 x86(eax…edi)寄存器组,以及 mxcsr 与 xmm0-xmm15(x86 为 xmm0-xmm7) |
setreg <reg> <value> |
写寄存器;setreg xmm3 <1..32 hex> 写完整 128 位(字节序与 regs xmm3 显示一致) |
mem <addr> [size] |
十六进制 dump(默认 64 字节,上限 4096) |
read <addr> <size> |
读字节(上限 65536) |
write <addr> <hexbytes> |
写字节,如 write 0x1000 90 90 或 write 0x1000 488B??90 |
search <start> <end> <pattern> [max=N] |
字节搜索,?? 为通配符 |
regions [start] [end] |
内存区域(默认隐藏 free 区域) |
alloc <size> / free <addr> / protect <addr> <size> <r|w|x> |
内存管理 |
搜索示例:
search target.exe+0x0 target.exe+0xba100 61 69 64 62 67 max=16
也可用紧凑写法488B??90。max=N必须显式写成max=数字,以免与十六进制字节混淆。
4.5 分析
| 命令 | 说明 |
|---|---|
disasm|u [addr] [count] |
反汇编(默认从 RIP 起 8 条);含字节、符号注释、跳转目标 |
flow|cfg [addr] [max_blocks] |
控制流图:函数的基本块与块间边(fallthrough / branch / indirect) |
modules |
已加载模块(基址、大小、名称、完整路径) |
threads / thread <tid> |
线程列表 / 切换当前线程 |
stack|bt [max] |
调用栈(含符号、模块偏移、源文件:行号) |
teb [tid] |
线程环境块:栈范围、PEB 指针、LastError、ClientId |
peb |
进程环境块:BeingDebugged、ImageBaseAddress、NtGlobalFlag、堆指针 |
heap [max] |
枚举堆(读 PEB 的 ProcessHeaps 数组) |
handles [type] [max] |
枚举句柄:类型、访问掩码、对象名 |
symbols [mask] [max] |
枚举符号,如 symbols CreateFile* 20 |
eval <expression> |
求值,并标注所属模块与符号 |
exports [module] [mask] [max] |
解析模块导出表(直接读目标内存里的 PE 导出目录) |
imports <addr> [count] |
把一段指针解析成 module!function,即 IAT 分析 |
iat scan [start end] |
自动找导入表:在内存中定位连续的导入指针段 |
iat map <addr> [count] |
同 imports,但按模块分组输出(重建导入表用) |
iat refs <start> <end> |
扫描代码里的导入引用(call/jmp/mov/lea [rip+x]) |
iat fix <start> <end> [newva] |
重建导入表并改写引用——内存中自动修复 IAT |
iat trace <addr> [max_steps] |
追踪一次导入调用,看它最终落到哪个 API |
dump <file> [addr] [entry=<addr>] [compact] [keepbase] |
把内存镜像导出为可加载的 PE 文件 |
ptype <type> |
结构体/联合/枚举布局:字段名、偏移、类型、大小 |
whatis <symbol|expr> |
查看符号或表达式的声明类型 |
print <expression> |
带类型的求值,支持 . / -> / [] 成员访问,自动展开结构体字段 |
类型信息来自 PDB(DbgHelp),是 RadDbg Types.cpp/Values.cpp 那条路线的对应能力。
teb / peb / heap / handles:进程内部状态
Nanomite 那类"信息视图"。这几条直接读目标的内核结构,对反反调试和运行时分析很实用:
peb
PEB 0x317000 (x64)
BeingDebugged 1
ImageBaseAddress 0x140000000
NtGlobalFlag 0x70 [debugger flags set] ← 附加调试器的痕迹
ProcessHeap 0x510000
[ok] ImageBaseAddress matches the main module base
teb
TEB 0x318000 (x64)
pid/tid 37356 / 29224
stack base 0x160000
stack limit 0x15c000
last error 0
handles File 6
0x58 File 0x100020 \Device\HarddiskVolume4\crack\debug\code
0x60 File 0x12019F \Device\ConDrv
NtGlobalFlag 里的 0x70 是附加调试器时的典型标志(尾部检查/释放检查/堆校验),
反反调试检测常查这个值;BeingDebugged 同理。peb 会自校验 ImageBaseAddress
是否等于主模块基址,偏移写错会立刻暴露。
heap 走 PEB 的 ProcessHeaps 数组——toolhelp 的堆快照(TH32CS_SNAPHEAPLIST)在
被调试进程上会报 CRC 错误,PEB 方案既可靠又不受影响。handles 走系统句柄表
(SystemExtendedHandleInformation),按类型名过滤后再截断(先截断再过滤会漏掉匹配项)。
已知限制:x86 (WOW64) 目标的 teb.peb 与 peb 可能不一致——WOW64 线程同时有 32 位和
64 位两个 TEB,NtQueryInformationThread 返回的是哪一个取决于调用方位数。此时输出会
用 [!] 标出差异,核心字段(ImageBaseAddress/BeingDebugged/NtGlobalFlag)仍准确。典型用法:
ptype Node
Node (size 0x30 = 48 bytes)
offset type name
+0x0 int value
+0x4 Point pos
+0x20 Node* next
+0x28 double weight
print g_head.next->pos
g_head.next->pos = Point <Point> @ 0x7ff682c0d1f4
+0x0 int x 11 (0xb)
+0x4 int y 22 (0x16)
+0x8 char[16] label "second"
print 的表达式可以任意组合 .、->、[下标],例如 table[i].name、obj->next->next->value。
数组会按可打印字符串或十六进制两种方式展示,整数同时给出十进制与十六进制。
flow 输出函数的控制流结构,用于快速看清分支逻辑:
accumulate [0x7ff674a47220 - 0x7ff674a4726f) blocks=5 instructions=22
block 0 0x...220 [5 insn] -> 0x...241 branch # prologue + jmp
block 1 0x...239 [3 insn] -> 0x...241 fallthrough # i++
block 2 0x...241 [3 insn] -> 0x...266 branch, 0x...24a fallthrough # i < n
block 3 0x...24a [8 insn] -> 0x...239 branch # 循环体,跳回 i++
block 4 0x...266 [3 insn] -> (exit) [exit] # 返回
JSON 里每个块带完整指令列表和结构化边({"to":"0x...","type":"branch|fallthrough|indirect"}),external: true 表示跳到了函数外。函数边界取自符号表,无符号时会退化为"从当前位置扫到第一个 ret"。
disasm 传一个 ≤4096 的小整数 时按条数处理(disasm 6 = 6 条指令);传地址表达式则按起始地址处理。
4.6 快照与回退执行
| 命令 | 说明 |
|---|---|
snapshot|snap save [name] |
保存完整进程状态(内存 + 寄存器) |
snapshot restore [name|index] |
恢复到某个保存点 |
snapshot list |
列出快照(含自动捕获的) |
snapshot delete <name|index> / snapshot clear |
删除 / 全清 |
snapshot auto <on|off> |
开关"每次停住自动捕获"(默认开) |
snapshot file [path] |
打开/创建单文件快照库(不带参数则查看当前) |
snapshot history |
列出文件中的全部快照(含已不在内存里的) |
snapshot load <id> |
从文件恢复某个快照(可跨内存环限制) |
snapshot compact |
回收文件中已删除记录占用的空间 |
stepback|sbk|reverse [count] |
回到较早的停住点,可连续回退 |
这是 Z0BDbgCli snapshot/stepback 那条路线的对应能力。原理是把内存与寄存器整体
回写,属于可靠的"时间回溯"近似:
continue → g_counter = 87
continue → g_counter = 165
stepback 1 → g_counter = 87 ← 回到上一个停住点
stepback 1 → g_counter = 0 ← 继续往前
每次停住会自动捕获一份快照,所以 stepback 开箱即用,不需要手动 save。
存储:链式差分 + 压缩
快照采用只有首个全量、其余只存差异的结构(与 HDiffPatch 的思路一致,但用页粒度
脏页比较而非通用二进制 diff——捕获时目标是暂停的,必须快):
| raw | 压缩后 | |
|---|---|---|
| auto-1 full | 9924 KB | 4178 KB |
| auto-2 delta | 12 KB | 1 KB |
| auto-3 delta | 12 KB | 1 KB |
| auto-4 delta | 12 KB | 1 KB |
| auto-5 delta | 12 KB | 1 KB |
| 合计(5 份) | 9973 KB | 4184 KB |
- 差分:4KB 页粒度,只存与前一份不同的页。相邻停住点通常只有几页变化(局部变量、
栈顶、少量堆),所以增量只有 1KB 左右。旧的全量方案同样 5 份要 49.6MB,省 12 倍;
8 份的上限下约省 19 倍。 - 压缩:Windows 自带的 MS Compression(
cabinet.dll,XPRESS_HUFF 算法),无第三方
依赖,全量快照约 2.4 倍压缩。 - 全零页继续免费,两者叠加。
上限默认 8 份 / 256MB,超出自动丢最旧的,并把被丢弃的那份合并进下一份(保持链完整)。
单文件快照库
所有快照存在一个容器文件里,不会散落出一堆零散文件:
aidbg -c "snapshot file session.snap" -c "file a.exe" -c run ...
文件格式是自描述的追加式日志:一个 64 字节文件头 + 顺序追加的记录,每条记录带自己的
总长度、差分链引用(prev)、寄存器、名称和压缩后的页面负载。
| 场景 | 表现 |
|---|---|
| 单会话 5 次捕获 | 1 份 full(4178KB) + 4 份 delta(1KB) = 4.18 MB 一个文件 |
| 同样的 5 份散文件 | 5 个文件共 48.5 MB |
设计上几点值得注意:
- 追加式,不原地改写。删除标记为 dead,
snapshot compact才真正回收空间。 - 崩溃可恢复。每条记录自带长度,加载时直接扫描重建索引;文件头损坏或写入中断,
已提交的快照都不会丢。 - 差分链不跨会话。新进程内存布局不同,不能拿旧进程的页做差分,所以每个会话的第一份
必然是全量(history里kind=full、prev=-)。snapshot load也只会回溯到最近的
full 记录,正好是会话边界。 - id 跨会话连续,不会与文件里已有的记录冲突。
注意两点:
- 只回滚内存和寄存器。内核态对象(文件句柄、线程、套接字、已发出的系统调用)不会
撤销,所以回退后继续运行可能与真实的历史不同——适合"看看刚才那个值是什么/再走一遍
分支",不适合精确的确定性重放。 - 恢复是破坏性的:回到某个快照后,比它更新的快照会被丢弃(回到过去就意味着放弃
未来),这样差分链才能保持正确。 - 只读共享页(如
0x7FFE0000的 KUSER_SHARED_DATA)写不回去,会被自动跳过;
这些页不承载调试目标的状态。
| 命令 | 说明 |
|---|
4.7 异常策略
| 命令 | 说明 |
|---|---|
exlist |
显示策略(含各异常命中次数) |
exset <code> <break|ignore|pass> |
设置单个异常码的行为 |
exdel <code> |
删除规则 |
exall <on|off> |
全局忽略所有一次机会异常(单步噪音代码时用) |
异常码可写十六进制或别名:av(ACCESS_VIOLATION)、bp、ss、guard、cpp。
三种行为:
| 行为 | 一次机会异常 | 含义 |
|---|---|---|
break(默认) |
停下并报告 | 让 AI 看到异常 |
ignore |
不停,交给目标程序处理 | 程序自己会处理的异常 |
pass |
不停,当作已处理吞掉 | 明确要屏蔽的信号(如 OutputDebugString) |
二次机会异常 总是 停下(通常是致命错误)。
4.8 反反调试(hide / detect)
| 命令 | 说明 |
|---|---|
detect |
用常见反调试检测(al-khaser 清单)自查目标,报告哪些会命中 |
hide [status] |
显示当前隐藏配置与已布钩子 |
hide on [profile] / hide off |
开启/关闭隐藏(可同时指定 profile) |
hide profile <name> |
切换配置档:off basic standard aggressive vmp themida obsidium |
hide set <key> <0|1> |
逐项开关(键名与 scylla_hide.ini 一致) |
hide apply |
对所有存活进程重新应用补丁/钩子 |
hide hooks |
列出钩子位点(地址/命中次数/是否在位) |
hide profiles |
列出内置配置档及其开关组合 |
hide load <file> / hide save <file> |
读写 ini(直接兼容 ScyllaHide 的 scylla_hide.ini) |
启动参数:--hide <profile|off>(默认 standard)。
工作原理(两层,均不向目标注入代码):
- L1 内存补丁(调试器侧写入,零副作用):
PEB.BeingDebugged、NtGlobalFlag的
0x70 调试位、堆Flags/ForceFlags的调试位、RTL_USER_PROCESS_PARAMETERS
的启动信息泄漏、KUSER_SHARED_DATA.KdDebuggerEnabled。WOW64 目标会同时修补
32 位与 64 位两套 PEB。 - L2 API 虚拟化(在目标 ntdll 导出上布 INT3,由调试器代答):
NtQueryInformationProcess(DebugPort/DebugObjectHandle/DebugFlags/
BreakOnTermination/HandleTracing)、NtQuerySystemInformation(内核调试器信息类)、NtSetInformationThread(吞掉 ThreadHideFromDebugger)、NtSetInformationProcess、NtQueryObject(隐藏 DebugObject 计数)、NtClose(非法句柄不再触发异常)、NtYieldExecution、NtGet/SetContextThread(隐藏调试寄存器)。无法代答的调用
会在单步下回放原始指令(目标无感知)。
detect 的报告同时给出真实内核状态和目标实际会读到的答案(钩子生效时后者
已被伪装),并标注每个检测项由哪个开关修复。
诚实的能力边界(重要):
- L2 的 INT3 留在目标 ntdll 里,对 ntdll 代码完整性扫描可见(
detect的ntdll.integrity项会如实标注)。ScyllaHide 的跳板 hook 同样可被扫描到,这是
用户态方案的共有限制。 - 直接 syscall(绕过 ntdll 发
syscall指令)不受 L2 影响;只清 PEB 的 L1 仍然有效。 - 计时类检测(RDTSC / GetTickCount 循环)、TrapFlag、int 2D、WriteWatch、PageGuard
等需要在目标内执行代码的检测未做规避,detect中标注为 skipped。 - 句柄表/进程列表过滤(SystemHandleInformation、SystemProcessInformation)未实现:
结果缓冲含跨进程指针,转发过滤容易破坏结构,宁可透传。 - NtQueryObject 的 DebugObject 计数过滤在 WOW64 目标上不生效(结构布局随位数变化,
32 位路径走透传)。 - 驱动级隐藏(HyperHide/TitanHide 路线)不做,aidbg 保持纯用户态单文件。
hide off、detach、进程退出都会把 INT3 还原,不会在目标里留下断点字节。
4.9 用户笔记(label / comment)
| 命令 | 说明 |
|---|---|
label <addr> [name|-] |
设置/查看/删除标签;标签可在表达式中直接使用(break my_loop、until decrypt) |
comment <addr> [text|-] |
设置/查看/删除注释;disasm 输出会带上 |
notes [list|save <file>|load <file>|clear] |
列表 / 持久化到 JSON / 读回 / 清空 |
标签和注释锚定在 模块+RVA(借鉴 Syser 的符号/注释容器):ASLR 滑动、目标重启后依然
有效。notes save/load 的 JSON 格式({"format":"aidbg-notes-1","notes":[{"module","rva", "label","comment"}]})适合 AI 把一轮分析的结论带到下一轮会话:
label target.exe+0x2306 entry_point
comment target.exe+0x2306 program starts here
break entry_point
notes save session1.json
disasm 的每条指令会附带 label/note 字段(JSON)和 ; label: comment 注释(文本)。
5. 典型 AI 工作流
5.1 摸清一个陌生二进制
aidbg --json ^
-c "file sample.exe" ^
-c "run" ^
-c "modules" ^
-c "disasm 16" ^
-c "stack 8" ^
-c "symbols main* 16"
5.2 先设断点再启动(推荐)
aidbg --json ^
-c "file sample.exe" ^
-c "break CreateFileW" ^
-c "break main" ^
-c "run" ^
-c "continue" ^
-c "regs" ^
-c "stack 5"
5.3 追踪某个函数的调用
aidbg --json ^
-c "file sample.exe" ^
-c "run" ^
-c "break CreateFileW" ^
-c "continue" ^
-c "regs rcx rdx r8 r9" ^
-c "read rdx 64" ^
-c "stack 6"
5.4 找到写某变量的位置
aidbg --json ^
-c "file sample.exe" ^
-c "run" ^
-c "hbreak <var_addr> w 4" ^
-c "continue" ^
-c "disasm 4" ^
-c "stack 4"
5.5 条件断点:只在特定参数时停下
aidbg --json ^
-c "file sample.exe" ^
-c "run" ^
-c "break SomeFunc" ^
-c "cond 1 rcx == 0x2a" ^
-c "continue"
5.6 脱壳:定位并解析导入表
加密壳会在运行时解密代码并动态填充导入表,脱壳后需要修复 IAT 才能独立运行。aidbg 可以在调试过程中自动完成导入表的识别与解析(相当于把 Scylla 的分析
那一步自动化):
file packed.exe
run
until <OEP> # 跑到壳解密完成后的原始入口点
iat scan # 自动在内存里找导入表
0x1400b5000 0x2b8 87 slots 87 exports 3 modules ← 目标自己的 IAT
imports 0x1400b5000 20 # 逐槽位解析
0x1400b5000 0x7ffa4f153480 kernel32.dll!QueryPerformanceCounter
0x1400b5008 0x7ffa4f152bb0 kernel32.dll!GetCurrentProcessId
0x1400b5020 0x7ffa4ff3c980 ntdll.dll!RtlInitializeSListHead
iat map 0x1400b5000 87 # 按模块分组,直接可用于重建导入描述符
kernel32.dll (29 个)
QueryPerformanceCounter, GetCurrentProcessId, ...
ntdll.dll (8 个)
RtlInitializeSListHead, ...
实现要点:
- 导出表直接从目标内存解析(PE 导出目录),不依赖符号文件,所以对第三方 DLL、
加过壳的模块都有效 - 转发导出能识别(
CreateFileMappingFromApp -> api-ms-win-core-memory-l1-1-1.*) - 导入跳转桩会自动跟踪(
jmp [rip+x]/jmp rel32等),不会停在 stub 上 - IAT 自动定位按"连续指针段"扫描(与 Scylla 同思路),而不是整块内存区——
真正的 IAT 往往只占.rdata的一小段,按整区统计会被稀释 - 解析不出来时退化为
module+0xoff或最近导出name+0xdisp,不会假装成功
iat refs / iat fix:内存中自动修复导入表
这对应 Wannabe Universal Import Fixer 的算法,解决 VMProtect/Themida 一类的场景:
壳解密后导入引用散落在代码各处,指向各自临时的槽位,而不是集中的导入表。
iat refs target.exe+0x7e00 target.exe+0x8000
instruction mnem slot api module!function
0x140007e92 call 0x1400b5018 0x7ffa4f160e70 kernel32.dll!GetSystemTimeAsFileTime
0x140007ea2 call 0x1400b5010 0x7ffa4f148620 kernel32.dll!GetCurrentThreadId
...
iat fix target.exe+0x7e00 target.exe+0x8000
import table written at 0x13fff0000
unique imports: 4
patched instructions: 8
iat fix 做三件事:
- 去重:把所有引用到的 API 汇总成一张连续表(8 条引用 → 4 个唯一 API)
- 写表:把真实 API 指针写进新表(
newva省略时自动在代码 ±2GB 内找空闲区,
否则 RIP 相对位移放不下) - 改写位移:把每条指令的 disp32 指向新槽位。重复引用复用同一槽位
验证方式:打补丁后程序仍正常退出(退出码 0),与未打补丁的对照一致——
说明改写后的指令通过新表正确调用了 API。
iat refs 只记录指针落在已加载模块内的引用(lea/mov 读普通数据会产生假阳性,
这与 wannabeuif 的过滤一致)。
iat trace:追踪动态解析的导入
VMProtect/Themida 一类的壳不靠静态引用调用 API——代码里是 call [rsp+0x30]
这类间接调用,目标地址运行时才算出来,反汇编里完全看不出指向哪个函数。这类只能执行观察:
iat trace 0x14000726b
0x14000726b -> kernel32.dll!GetTickCount (1 step(s))
0x14000726b 是一条 call [rsp+0x30],静态分析无解;追踪器让它执行一步,
RIP 落到 kernel32!GetTickCount 的导出起点,就认出了真实 API。
原理:在调用点单步执行,每步检查 RIP 是否落在某个模块的导出起点上——
无论中间经过多少层哈希查表、多少次跳转,最终总要跳到真实 API。
两个细节:
- 追踪不污染现场:追踪期间关闭自动快照(否则每步存 10MB),结束后用命名快照把
内存与寄存器还原,追踪过程的副作用全部撤销。RIP 回到调用点,可以直接继续执行。 - 判定条件是"落在导出起点",所以普通的函数调用(如
printf)追踪下去会停在它
调用的第一个 API 上,而不是调用点本身——对导入解析场景是对的,但要知道这个语义。
dump:导出内存镜像
脱壳的另一步——把运行时已解密的镜像写回成 PE 文件:
run
until <OEP> # 等壳解密完
dump unpacked.exe # 导出内存镜像
dump unpacked.exe entry=<OEP> # 同时把入口点改成 OEP
导出的文件是可直接运行的(实测导出后程序退出码与原始程序一致)。几个细节:
- 文件布局镜像内存布局(
PointerToRawData = VirtualAddress),所以壳新加的节、
运行时变大的节都原样保留;compact可改为紧凑布局 ImageBase写成实际加载地址并清除DYNAMIC_BASE(keepbase可关闭),这样
导出文件在内存镜像原址加载,地址与调试时完全一致- 节间自动补齐对齐——
PointerToRawData是声明式偏移,顺序写入时若不补空隙,
文件会比自己的节表声称的短,加载器直接拒绝 - 读不到的内存(守卫页等)以零填充,不会中断导出
典型脱壳流程串起来就是:
file packed.exe
run
until <OEP>
iat refs <code_start> <code_end> # 看导入引用散落在哪
iat fix <code_start> <code_end> # 收拢成干净的导入表
dump unpacked.exe entry=<OEP> # 导出
5.7 长时间运行的脚本
把命令写进 analysis.txt,用 --script 执行并落盘:
aidbg --script analysis.txt --json > result.jsonl
之后让 AI 读 result.jsonl 分析即可;每行是一条命令的完整结果。
6. 给 AI agent 的使用建议
- 始终用
--json,不要解析文本模式输出。 - 每次响应都看
session.state与stop_reason:只有state="paused"时才能读寄存器/内存。 - 先看
session.arch:x86用eax/eip/esp,x64用rax/rip/rsp。64 位寄存器名在 x86 目标下也能用(取低 32 位),但输出寄存器组以arch为准。 - 不要假设地址固定:开了 ASLR 后每次运行的模块基址都变,用
modules拿基址,或直接用module+0xoff/ 符号。x86 目标地址通常在 32 位范围(如0x447190)。 - 控制输出量:
mem默认 64 字节、disasm默认 8 条、search默认 64 个命中。需要更多时显式加大,但注意上下文预算。 - 失败要读
error.code:not_paused表示需要先continue/step;read_failed常表示地址不可读(先用regions确认)。 - 脚本跑完会自动 detach,不会把目标进程挂住;但用交互/管道模式时记得
detach或kill。 continue建议带超时(如continue 5000),避免目标进入死循环时 agent 永久阻塞。
7. 已知限制(当前版本)
- 仅支持 Windows 用户态 调试:x64 与 WOW64 (x86) 目标均可,内核态、远程调试未实现
step/next不支持反向执行;rep前缀指令按单步处理- 符号依赖 DbgHelp,非 ASCII 路径的 PDB 可能解析失败
- 硬件断点最多 4 个;内存断点基于 PAGE_GUARD,会捕获整个区域的访问(按地址范围过滤)
- 条件断点每次命中都会求值,高频路径下会有开销
- 反反调试为用户态方案:L2 的 INT3 可被 ntdll 完整性扫描发现,直接 syscall 不受影响,
计时类/驱动级检测未覆盖,详见 4.8 的"诚实的能力边界" - Python 绑定尚未提供(命令行 + JSON 与 MCP 已覆盖同等能力)
关于 x86 / x64 自动识别
调试器本身始终是 64 位进程。对 WOW64 (x86) 目标,寄存器通过Wow64Get/SetThreadContext 读写 32 位上下文,反汇编切到 Zydis 的LONG_COMPAT_32 模式,调用栈用 IMAGE_FILE_MACHINE_I386 回溯,指针宽度为 4 字节。session.arch 与 regs/disasm 的 arch 字段会告诉你是哪种目标,AI 可据此选择寄存器名。
8. 项目结构
aidbg/
build.bat 构建脚本(自动调用 MSVC 环境)
CMakeLists.txt
src/
main.cpp CLI 入口:-c / --script / 交互 / --mcp 四种模式
cmd/commands.cpp 命令注册、解析与分发(70 个命令(含子命令))
mcp/mcp_server.cpp MCP (Model Context Protocol) 服务器,11 个工具
output/output.cpp JSON/文本双格式输出层
engine/
engine.cpp 调试事件循环、进程/线程/模块管理、执行控制
breakpoints.cpp 软件/硬件/内存断点 + 条件 + 命中命令
memory.cpp 读写、搜索、区域、保护
registers.cpp 寄存器表(GPR 8/16/32 位视图 + mxcsr/xmm)
symbols.cpp DbgHelp PDB 符号
expr.cpp 表达式求值器
exceptions.cpp 异常策略
hide.cpp 反反调试(PEB 补丁 + ntdll API 虚拟化 + detect)
notes.cpp 用户标签/注释(模块+RVA 锚定,JSON 持久化)
disasm.cpp Zydis 封装
util/ JSON 构造/解析、字符串与模式解析
third_party/zydis/ Zydis 4.1.1 + Zycore
tests/ 测试目标程序(x64 的 target.exe 与 x86 的 target32.exe)与冒烟脚本
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found