ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

x64dbg 脚本命令完全指南:控制流、日志、交互与脚本调度(Script Commands)

x64dbg 脚本命令完全指南:控制流、日志、交互与脚本调度(Script Commands) x64dbg 脚本命令完全指南控制流、日志、交互与脚本调度Script Commands【免费下载链接】x64dbgAn open-source user mode debugger for Windows. Optimized for reverse engineering and malware analysis.项目地址: https://gitcode.com/gh_mirrors/x6/x64dbg导读本文是 x64dbgWindows 用户态调试器面向逆向工程与恶意代码分析脚本系统的命令参考指南。脚本命令Script Commands是指仅在脚本上下文中使用或可用的命令它们支撑起 x64dbg 自动化脚本的完整能力分支跳转、函数调用、日志输出、用户交互、脚本的加载/运行/中止。读完本文你将掌握每一条脚本命令的参数、结果变量与执行语义并能结合源码理解其底层调度机制单脚本线程、脚本栈、30 秒超时保护从而编写出可靠、可复用的调试自动化脚本。本文内容以 docs/commands/script/index.rst 及其下属命令文档为主体结合 src/dbg/simplescript.cpp、src/dbg/commands/cmd-script.cpp 与 src/dbg/x64dbg.cpp 的源码实现进行佐证与扩充。脚本命令概览x64dbg 的命令系统中脚本命令与普通命令共用一套命令处理器但脚本命令只在脚本上下文中使用或可用且同时存在于非脚本上下文的同名命令具有更高优先级见 docs/commands/script/index.rst。也就是说当你在命令行手动输入pause、log这类命令时可能命中调试器普通命令而非脚本语义而scriptcmd、scriptrun等命令则明确面向脚本运行场景。在源码层面这些命令统一在 registercommands() 中注册脚本相关注册代码位于//script注释段src/dbg/x64dbg.cpp#L430-L442命令注册名称回调函数加载脚本scriptloadcbScriptLoad消息框msgcbScriptMsg是/否消息框msgyncbScriptMsgyn日志输出logcbInstrLog执行脚本 DLLscriptdll,dllscriptcbScriptDll在脚本上下文中执行命令scriptcmdcbScriptCmd运行当前脚本scriptruncbScriptRun加载并运行脚本scriptexeccbScriptExec除此之外call、invalid、error、Jxx、pause、printstack、ret等属于脚本解析器的内部命令在 src/dbg/simplescript.cpp 的scriptInternalCmdExec中直接处理src/dbg/simplescript.cpp#L476-L581无需经过命令注册表。控制流命令跳转、调用与返回无条件跳转与函数调用callcall与无条件分支完全等价区别在于它会把自身的地址压入脚本栈script stack从而支持调用-返回语义见 docs/commands/script/call.md。参数arg1要跳转到的标签label。结果变量不设置任何结果变量。对应源码中分支类型由scriptGetBranchType识别src/dbg/simplescript.cpp#L138-L159当执行call label时脚本引擎执行if(branchtype scriptcall) //calls have a special meaning scriptStack.push_back({scriptIp, state}); scriptIp scriptNextIp(labelIp); //go to the first command after the label见 src/dbg/simplescript.cpp#L549-L555由此可见call压入栈的不仅是指令指针scriptIp还包含当时的运行状态state这一点在后面讲解ret的行为时非常关键。条件分支Jxx/IFxxJxx/IFxx是一系列可响应cmp比较命令所设置标志位的分支指令见 docs/commands/script/Jxx.md。完整的分支族如下分支语义跳转条件命令别名无条件分支总是跳转jmp/goto不等于相等标志为 0jne/ifne(q)/jnz/ifnz等于相等标志为 1je/ife(q)/jz/ifz小于低于且不相等jb/ifb/jl/ifl大于高于且不相等ja/ifa/jg/ifg小于等于低于或相等jbe/ifbe(q)/jle/ifle(q)大于等于高于或相等jae/ifae(q)/jge/ifge(q)参数arg1要跳转到的标签。结果变量不设置任何结果变量。在底层分支的是否跳转由scriptBranchTaken根据两个内部变量$_EZ_FLAG相等标志与$_BS_FLAG大小/符号标志判定src/dbg/simplescript.cpp#L448-L474case scriptjnejnz: //$_EZ_FLAG0 return !ezflag; case scriptjejz: //$_EZ_FLAG1 return ezflag; case scriptjbjl: //$_BS_FLAG0 and $_EZ_FLAG0 //below, not equal return !bsflag !ezflag; case scriptjajg: //$_BS_FLAG1 and $_EZ_FLAG0 //above, not equal return bsflag !ezflag; case scriptjbejle: //$_BS_FLAG0 or $_EZ_FLAG1 return !bsflag || ezflag; case scriptjaejge: //$_BS_FLAG1 or $_EZ_FLAG1 return bsflag || ezflag;这两个标志位由cmp等指令设置cmp命令注册于 src/dbg/x64dbg.cpp#L133实现在 src/dbg/commands/cmd-general-purpose.cpp#L379。分支目标解析依赖脚本中的标签若分支标签不存在脚本引擎会报错Invalid branch label ... detected on line N!并终止src/dbg/simplescript.cpp#L541-L548。一个典型用法示例mov $flag, 0 cmp $flag, 0 jne error_label ; 若 $flag ! 0 则跳转 log flag is zero ret error_label: log flag is not zero返回retret的行为取决于脚本栈中是否有call压入的帧见 docs/commands/script/ret.md栈为空结束整个脚本并把脚本指令指针script IP重置回第一行栈非空之前执行过call从该call处返回脚本 IP 恢复为call之后的下一条指令。对应源码src/dbg/simplescript.cpp#L478-L513if(scriptIsInternalCommand(cmd, ret)) //script finished { if(scriptStack.empty()) //nothing on the stack { scriptIp scriptNextIp(0); ... return STATUS_EXIT; } auto frame scriptStack.back(); scriptIp frame.ip; scriptStack.pop_back(); ... return frame.state ! SCRIPT_RUNNING ? STATUS_PAUSE : STATUS_CONTINUE_BRANCH; }注意这里返回时还会根据栈帧保存的状态决定是否暂停若call是在脚本步进stepping状态下发起的ret返回后会暂停方便单步调试脚本本身这是源码注释中明确描述的场景脚本断点命中后通过scriptcmd call mylabel进入标签、跨过ret时保持暂停。错误与中断控制invalid、error、pauseinvalidinvalid是一个刻意设计的无效命令用于触发错误消息并终止脚本执行见 docs/commands/script/invalid.md。参数无。结果变量不设置。源码中它直接返回STATUS_ERRORsrc/dbg/simplescript.cpp#L524-L525随后由主循环统一处理scriptIp回退到出错行并输出Error executing command!脚本停止src/dbg/simplescript.cpp#L600-L603。它主要用于测试脚本引擎的错误处理路径。errorerror显示一条错误消息并终止脚本见 docs/commands/script/error.md。参数arg1要显示的错误消息文本。结果变量不设置。源码实现src/dbg/simplescript.cpp#L515-L523会重置脚本 IP 到下一行、去掉消息首尾的空格与引号然后通过scriptError上报if(scriptIsInternalCommand(cmd, error)) //show an error and end the script { scriptIp scriptNextIp(0); GuiScriptSetIp(scriptIp); auto message StringUtils::Trim(cmd strlen(error), \); scriptError(scriptIpOld, message.c_str(), gui); return STATUS_EXIT; }典型用法是在脚本的校验分支中主动中止error Invalid input parameter detected, aborting.pausepause暂停脚本执行用户之后可以手动恢复运行见 docs/commands/script/pause.md。参数无。结果变量不设置。源码中将当前状态切换为STATUS_PAUSEsrc/dbg/simplescript.cpp#L528-L529并调用GuiScriptSetIp让 GUI 高亮当前行src/dbg/simplescript.cpp#L606-L608。适合在脚本的关键节点停下来让分析人员确认现场。日志与信息输出log、printstackloglog将信息写入 x64dbg 的日志窗口见 docs/commands/script/log.rst。参数[arg1]可选格式化字符串格式语法见 docs/introduction/Formatting.md。未指定时输出一个换行。使用格式化字符串时强烈建议加引号避免与命令分隔符;产生歧义例如log {mem;8rax}。x64dbg 的字符串格式化基本语法为{?:expression}其中?是可选类型标识。常用类型包括类型含义示例输出d有符号十进制-3u无符号十进制57329171p零填充指针0000000410007683x十六进制整数默认3C28Aa地址信息00401010 module.EntryPointi指令文本jmp 0x77ac3c87f/F单/双精度浮点指针或 XMM/YMM/ZMM 寄存器{f:XMM0}mem;sizeaddress从地址读取指定字节数的十六进制{mem;8rax}utf8address/utf16address/asciiaddress读取字符串{utf164*ecx0x402000}disasmaddress反汇编一行等价{i:address}winerrorcode/ntstatuscodeWindows 错误码/NTSTATUS 名称与描述—完整表格见 docs/introduction/Formatting.md若需输出字面{或}需转义为{{或}}。源码层面log由cbGenericLog处理src/dbg/commands/cmd-script.cpp#L62-L90它区分三种形态无参数仅输出换行单参数内联格式化log format {rax}调用stringformatinline多参数log format {0} string, arg1, arg2, argN调用stringformat并依次填充位置参数。输出会同时进入日志窗口dputs_untranslated与脚本日志锁ScriptLogLocked保证与脚本执行同步。printstack/logstackprintstack别名logstack将调用栈回溯打印到日志见 docs/commands/script/printstack.md。参数无。结果变量不设置。实现在 src/dbg/commands/cmd-script.cpp#L101-L122通过GetContextDataEx(hActiveThread, UE_CSP)取当前活动线程的栈指针调用stackgetcallstack收集调用栈帧若栈为空则输出No call stack.否则先打印帧数与寄存器快照x64 下为RIP/RSP/RBPx86 下为EIP/ESP/EBP再逐帧输出地址与注释符号信息。示例输出5 call stack frames (RIP 00007FF... , RSP 000000... , RBP 000000... ): 0000000140001000 module.funcA ...用户交互msg、msgynmsgmsg弹出一个消息框见 docs/commands/script/msg.md。参数arg1消息框文本支持字符串格式化。结果变量不设置。源码实现非常简单src/dbg/commands/cmd-script.cpp#L19-L25bool cbScriptMsg(int argc, char* argv[]) { if(IsArgumentsLessThan(argc, 2)) return false; GuiScriptMessage(stringformatinline(argv[1]).c_str()); return true; }即对参数做内联格式化后交给 GUI 层弹出对话框。msgynmsgyn弹出询问是/否的消息框用户回答会写入结果变量见 docs/commands/script/msgyn.md。参数arg1消息框文本支持字符串格式化。结果变量$result—— 用户回答是时置为1否则置为0。对应源码src/dbg/commands/cmd-script.cpp#L27-L33bool cbScriptMsgyn(int argc, char* argv[]) { if(IsArgumentsLessThan(argc, 2)) return false; varset($RESULT, GuiScriptMsgyn(stringformatinline(argv[1]).c_str()), false); return true; }典型用法是在脚本中实现确认式交互msgyn Continue with the analysis? cmp $result, 1 jne abort_script脚本的加载与调度scriptload、scriptrun、scriptexec、scriptcmd这四条命令构成脚本调度的核心。它们都基于 simplescript.cpp 中的脚本队列script queue机制脚本执行由单条专用线程负责保证同时只有一个脚本在运行命令按提交顺序执行互不干扰。scriptloadscriptload加载一个脚本文件见 docs/commands/script/scriptload.md。参数arg1要加载的脚本文件路径。结果变量不设置。回调函数cbScriptLoad将路径交给ScriptLoadAwaitsrc/dbg/commands/cmd-script.cpp#L11-L17 → src/dbg/simplescript.cpp#L716-L722。加载成功后脚本被逐行解析进scriptLineMap脚本 IP 指向第一条可执行指令scriptIp scriptNextIp(0)并同步到 GUI 的 Script 标签页。加载过程会清空旧的断点列表与脚本栈src/dbg/simplescript.cpp#L700-L713。注意scriptload只是装载不会自动执行随后需要配合scriptrun。scriptrunscriptrun从当前脚本 IP 位置运行已加载的脚本见 docs/commands/script/scriptrun.md。参数arg1可选停止执行的行号。不提供则运行到脚本结束。结果变量不设置。前置条件必须先通过scriptload加载脚本。执行语义阻塞直至脚本完成、到达指定停止行、出错或被手动中止同一时刻只能运行一个脚本执行从当前脚本指令指针开始指针由引擎自动管理可用scriptabort停止正在运行的脚本关于脚本中止入口参见 ScriptRunAsync 相关的打断机制。回调cbScriptRun会解析可选行号参数并调用ScriptRunAwaitsrc/dbg/commands/cmd-script.cpp#L47-L53。运行前有两个重要前置校验src/dbg/simplescript.cpp#L624-L651调试器必须处于暂停状态否则报错Debugger must be paused to run a script!通过原子比较交换compare_exchange_strong确保脚本状态从SCRIPT_PAUSED切换为SCRIPT_RUNNING防止重复运行。若指定了停止行号引擎会在该行设置一个内部静默断点scriptInternalBpToggle见 src/dbg/simplescript.cpp#L419-L446运行到该行即暂停。scriptexecscriptexec将加载 运行合并为一步操作见 docs/commands/script/scriptexec.md。参数arg1要加载并执行的脚本文件路径。结果变量不设置。执行行为由ScriptExecAwait实现src/dbg/simplescript.cpp#L947加载指定脚本文件从头开始执行脚本仅当执行成功完成时自动卸载脚本若执行失败或被中止脚本保持加载状态便于排查。其他注意点不能在正在运行的脚本内部调用scriptrun/scriptexec嵌套运行会被拒绝见 src/dbg/simplescript.cpp#L526-L527执行始终从脚本第一行开始这是运行独立脚本最便捷的方式可通过 Script 标签页的上下文菜单中止运行中的脚本。scriptcmdscriptcmd在运行中的脚本上下文里执行一条命令见 docs/commands/script/scriptcmd.md。参数特殊——它会将scriptcmd之后的所有内容原样转发给命令处理器例如scriptcmd add rax, 0x1245实际执行add rax, 0x1234。结果变量不设置。源码实现src/dbg/commands/cmd-script.cpp#L35-L45找到argv[0]中第一个空格之后的部分交给ScriptCmdExecAwait并检查返回结果不是Abort才算成功auto scriptcmd strchr(argv[0], ); if(scriptcmd nullptr) return false; while(isspace(*scriptcmd)) scriptcmd; return ScriptCmdExecAwait(scriptcmd, false, nullptr) ! ScriptCommandOutcome::Abort;执行行为阻塞直到被转发命令执行完毕。脚本执行由单一专用线程处理保证命令按提交顺序执行。实战示例在断点回调中运行脚本官方文档示例与 SetBreakpointCommand 联用fn_addr module.dll:$0x1234 // module.dll RVA 0x1234 bp fn_addr SetBreakpointCommand fn_addr, scriptcmd call mycallback // TODO: make sure the script is not unloaded (using run) mycallback: log fn({arg.get(0)}, {arg.get(1)}) ret该示例展示了完整闭环用模块 RVA 表达式定位函数地址 → 下断点 → 为断点绑定scriptcmd call mycallback→ 命中断点时脚本引擎以call方式进入回调标签用格式化日志打印参数最后ret返回断点前的执行流。脚本执行引擎的调度与超时保护从 scriptRun 的实现可以看到脚本运行主循环的完整面貌进入循环前禁用 GUI 更新若尚未禁用避免调试界面高频刷新影响执行每执行一条内部命令后检查脚本断点scriptInternalBpGet命中即停止提供30 秒超时保护若脚本持续运行超过 30 秒可通过设置Engine/NoScriptTimeout关闭会弹出确认框询问是否终止避免死循环脚本卡死调试会话循环中通过scriptHandleInterrupt响应外部的暂停/中止请求src/dbg/simplescript.cpp#L118-L136。脚本 DLLscriptdll/dllscriptscriptdll别名dllscript用于执行脚本 DLL见 docs/commands/script/scriptdll.md。参数arg1脚本 DLL 的文件名与路径若未提供完整路径x64dbg 会在scripts目录下查找该 DLL。结果变量不设置但脚本 DLL 可以自行设置任何变量。注册dbgcmdnew(scriptdll,dllscript, cbScriptDll, false)src/dbg/x64dbg.cpp#L436回调入口见 src/dbg/x64dbg.cpp#L93-L98。脚本 DLL 的契约官方文档明确说明是一个导出AsyncStart()或Start()函数的 DLL若导出AsyncStart()x64dbg 在独立线程上调用它脚本引擎不阻塞可继续处理后续命令若导出Start()x64dbg 在当前线程上调用它阻塞后续命令执行直到该函数返回若同时导出两个函数只执行AsyncStart()AsyncStart()/Start()的返回值不被 x64dbg 使用函数执行完毕后脚本 DLL 会从进程中被卸载。这为需要原生代码逻辑如复杂算法、与外部库交互的自动化场景提供了比纯脚本更强大的扩展手段。综合实战一个完整的脚本示例将以上命令组合起来一个典型的 x64dbg 脚本工作流如下; sample.txt —— 通过 scriptload 加载再用 scriptrun 运行 scriptload sample.txt ; 命令行加载脚本 scriptrun 42 ; 命令行运行到第 42 行暂停 ; 脚本内部逻辑 start: log analysis started, rax{rax} cmp rax, 0 je zero_case jne nonzero_case zero_case: msgyn rax is zero, continue? cmp $result, 1 jne abort_script call do_work ret nonzero_case: log {mem;8rax} abort_script: error unexpected value, aborting do_work: printstack pause ret要点回顾log使用带引号的格式化字符串{mem;8rax}不会与;分隔符冲突cmpje/jne依据$_EZ_FLAG分派分支标签名必须存在call do_work压栈后ret返回call下一条指令printstack打印调用栈便于现场分析pause让脚本在关键点停留等待人工介入msgyn的答案写入$result用于驱动后续分支error主动终止并留下错误信息。小结x64dbg 的脚本命令体系覆盖了控制流call/Jxx/ret、错误与中断invalid/error/pause、输出与交互log/printstack/msg/msgyn以及脚本调度scriptload/scriptrun/scriptexec/scriptcmd/scriptdll四个维度。理解其底层实现——单一脚本线程的队列调度、脚本栈保存的 IP 与状态帧、$_EZ_FLAG/$_BS_FLAG分支判定、30 秒超时保护——有助于写出行为可预期、可断点、可中止的健壮自动化脚本。配合断点命令回调SetBreakpointCommandscriptcmd call脚本系统可以在调试事件驱动的场景下无缝嵌入自定义逻辑是 x64dbg 自动化能力的关键组成部分。【免费下载链接】x64dbgAn open-source user mode debugger for Windows. Optimized for reverse engineering and malware analysis.项目地址: https://gitcode.com/gh_mirrors/x6/x64dbg创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进