
如果你在逆向分析一个复杂的 Windows 程序面对成千上万条汇编指令和不断变化的寄存器状态是否曾感到力不从心手动下断点、单步跟踪、记录内存值这些重复性操作不仅效率低下还极易出错。这正是许多逆向工程师从“手动挡”切换到“脚本驱动”自动化分析的关键痛点。今天要讨论的x64dbg 脚本编程就是解决这一痛点的利器。它远不止是 x64dbg 调试器的一个附属功能而是一个能够将你的逆向分析思路固化为可重复、可分享、可扩展自动化流程的“编程接口”。很多人以为脚本只是用来批量下断点但实际上它能实现从自动化脱壳、算法识别、漏洞挖掘到定制化分析报告生成等一系列高阶操作。本文将为你彻底拆解 x64dbg 脚本编程的核心。你会了解到为什么脚本编程能极大提升逆向效率而不仅仅是“锦上添花”。脚本语言的基础语法与核心 API如何用代码控制调试器的每一步。从零开始编写一个实用的自动化分析脚本包含完整代码和逐行解析。如何将脚本应用于真实逆向场景如寻找特定函数、追踪数据流、破解简单验证。调试脚本自身的技巧与最佳实践避免脚本编写中的常见陷阱。无论你是刚接触逆向的新手还是希望将重复工作自动化的资深分析师掌握 x64dbg 脚本都将是你工具箱中一次质的飞跃。我们直接从最核心的问题开始脚本到底改变了什么1. 手动逆向 vs. 脚本自动化效率的鸿沟在深入代码之前必须理解脚本带来的范式转变。假设你的任务是分析一个软件的网络心跳包生成算法。传统手动流程可能是对send或WSASend等函数下断点。断下后记录栈中缓冲区地址和长度。单步或执行到返回查看缓冲区内容。向上回溯调用栈寻找加密或组包函数。重复以上步骤数十次试图找出规律。这个过程枯燥、易错且难以复现。一旦程序更新所有手动记录的地址可能全部失效。脚本自动化流程则是编写一个脚本自动在发送函数处断点。断下后脚本自动读取并记录缓冲区数据、调用栈、甚至寄存器上下文。脚本可以自动向上回溯标记可能的算法函数入口。脚本将每次捕获的数据时间、内容、上下文格式化输出到日志文件。运行一次脚本即可获得完整的数据集供后续分析。脚本将你从重复的机械操作中解放出来让你更专注于逻辑分析和算法理解。x64dbg 内置的脚本引擎正是为此而生。2. x64dbg 脚本基础语法、变量与核心 APIx64dbg 脚本语言是一种类 C 的定制化语言语法简单但功能强大。它不需要复杂的编译环境直接在调试器的“脚本”窗口中编写和运行。2.1 基本语法结构脚本文件通常以.txt或.script为后缀。注释使用//或/* */。// 这是单行注释 /* 这是多行注释 用于说明脚本功能 */ // 脚本从这里开始执行 log 脚本开始运行...2.2 变量与数据类型脚本是弱类型语言主要数据类型包括整数十进制 (100)、十六进制 (0x64)。字符串用双引号包裹 (Hello)。地址通常用十六进制整数表示也可通过符号名获取。变量使用前无需声明直接赋值即可。counter 0; // 整数变量 baseAddress 0x00401000; // 地址变量 moduleName target.exe; // 字符串变量 result readByte(0x401000); // 函数返回值赋给变量2.3 控制流循环与条件判断脚本支持常见的控制流语句这是实现自动化的基础。// if-else 条件判断 eax reg.GetEAX(); if (eax 0x12345678) { log EAX 匹配目标值; } else { log EAX 不匹配当前值为: {eax}; } // while 循环 i 0; while (i 10) { addr 0x401000 i; byteVal readByte(addr); log 地址 {addr} 的值为: {byteVal}; i i 1; } // for 循环 (通过 while 模拟) for (i 0; i 10; i) { // 循环体 }2.4 核心 API 函数命令这是脚本与调试器交互的灵魂。以下是最常用的一些命令类别命令示例功能描述断点管理bp 0x401000,bpcnd 0x401000, eax1设置普通断点、条件断点内存访问readByte(addr),readWord(addr),readDword(addr),readString(addr)读取内存数据寄存器访问reg.GetEAX(),reg.SetECX(value)获取或设置寄存器值执行控制run,stepin,stepover,erun运行、单步步入、单步步过、运行到返回模块/符号mod.BaseFromName(moduleName),sym.FromName(symbolName)获取模块基址、符号地址日志输出log message,log Value: {value}输出信息到日志窗口用户交互msg 提示信息,ask 请输入弹出消息框、输入框掌握这些 API你就具备了控制调试器的基本能力。3. 环境准备配置你的脚本工作区在开始编写第一个复杂脚本前确保你的环境已就绪。安装 x64dbg从官方仓库x64dbg.org下载最新版。建议使用snapshot版本它集成了最新的插件和脚本功能。打开脚本窗口启动 x64dbg通过菜单View - Script或快捷键Ctrl R打开脚本窗口。加载目标程序打开你要分析的可执行文件例如一个crackme.exe。理解脚本窗口布局编辑区编写脚本代码。运行按钮(Run)执行整个脚本。步进按钮(Step)单步执行脚本用于调试脚本本身。停止按钮(Stop)停止脚本执行。日志输出脚本中log命令的输出会显示在这里。一个良好的习惯是在编写脚本前先手动调试目标程序熟悉其关键断点位置、数据结构和大致的控制流。这能为你的脚本提供明确的“攻击路径”。4. 实战编写一个自动化字符串引用查找脚本让我们通过一个实际案例来串联所有知识点。假设我们需要在一个程序中找出所有引用了特定字符串例如Registration failed的代码位置。4.1 脚本设计思路获取目标字符串的地址首先要在内存中找到这个字符串。枚举所有引用了该地址的代码查找所有push offset StringAddress、mov eax, StringAddress之类的指令。记录并输出结果将找到的代码地址记录到日志或文件中。自动化跳转可以自动在每个引用点设置断点方便后续分析。4.2 完整脚本代码与逐行解析将以下代码保存为find_string_refs.txt。// find_string_refs.txt - 查找字符串引用脚本 // 目标找到程序中所有引用特定字符串的代码位置 log 开始查找字符串引用 ; // 第一步定义我们要查找的字符串 targetString Registration failed; log 目标字符串: \{targetString}\; // 第二步在内存中搜索该字符串获取其地址 // findallmem 函数会搜索整个内存空间返回包含该字符串字节序列的地址数组 addrArray findallmem(targetString, 0, 0x7FFFFFFF); // 搜索范围0 - 0x7FFFFFFF // 检查是否找到 arraySize array.Size(addrArray); if (arraySize 0) { msg 未在内存中找到目标字符串; log 搜索失败脚本结束。; return; } log 在内存中找到 {arraySize} 个匹配项。; // 通常第一个或某个匹配项就是我们要找的字符串常量地址 // 这里我们取第一个地址进行后续分析 stringAddress addrArray[0]; log 目标字符串的基址确定为: {stringAddress}; // 第三步设置一个临时断点用于在每次找到引用时触发 // 我们定义一个标签函数当断点命中时执行 labelTempBreakpointHandler: log [断点命中] 在地址 {cip} 引用了字符串地址 {stringAddress}; // 这里可以添加更多分析逻辑例如记录调用栈 // backtrace; // 执行回溯命令 log ---; return; // 第四步遍历程序的代码段查找引用该地址的指令 // 首先获取主模块的代码段范围.text段 moduleBase mod.BaseFromName(target.exe); // 替换为你的目标模块名 codeStart mod.CodeBaseFromAddr(moduleBase); codeSize mod.CodeSizeFromAddr(moduleBase); codeEnd codeStart codeSize; log 开始扫描代码段: 0x{codeStart} - 0x{codeEnd}; currentAddr codeStart; refCount 0; while (currentAddr codeEnd) { // 反汇编当前地址的指令 disasmResult disasm(currentAddr); instr disasmResult[0]; // 指令文本 instrSize disasmResult[1]; // 指令长度 // 检查指令操作数中是否包含我们的字符串地址 // 这是一个简单的文本匹配对于复杂情况需要更精细的解析 instrStr instr; addrStr string.Format({stringAddress}); // 查找指令中是否出现地址如 0xXXXXXXXX 格式 // 注意需要处理地址可能以不同格式出现的情况如 401000h if (strstr(instrStr, addrStr) ! -1) { // 找到引用 refCount refCount 1; log 发现引用 #{refCount} 在地址: 0x{currentAddr}; log 指令: {instr}; // 可选在此地址设置一个条件断点触发我们定义的处理器 // bpcnd currentAddr, 1; // 设置一个始终触发的条件断点 // 将断点命令与我们的处理函数关联这里简化处理实际需更复杂的事件绑定 // 更实用的做法是直接记录地址稍后手动分析 log 已记录。; } // 移动到下一条指令 currentAddr currentAddr instrSize; } // 第五步输出总结报告 log 扫描完成 ; log 在代码段中共找到 {refCount} 处对字符串 \{targetString}\ 的引用。; log 所有引用地址已在上方列出。; log 脚本执行完毕。; // 提示用户 if (refCount 0) { summary string.Format(找到 {refCount} 个引用。请查看日志窗口获取详细信息。); msg summary; }4.3 关键代码段解析findallmem函数这是内存搜索的核心。它返回一个数组包含了所有匹配内存区域的起始地址。参数分别是要搜索的字节序列字符串会自动转换、起始地址、结束地址。disasm函数反汇编给定地址的指令返回一个数组第一个元素是指令文本第二个是指令长度。这是静态分析代码的关键。strstr函数在字符串中查找子串。我们用它来粗略判断指令文本中是否包含了目标地址。注意这是一种简易方法实际中可能因为地址格式如缺少0x前缀或间接引用如[eax404000]而漏检。生产级脚本需要更精确的反汇编操作数解析。循环与遍历while循环遍历代码段currentAddr currentAddr instrSize确保正确跳到下一条指令这是静态线性扫描的典型模式。5. 运行脚本与结果分析加载目标程序在 x64dbg 中打开一个包含Registration failed字符串的程序例如一个简单的注册机挑战程序。加载脚本在脚本窗口点击Load按钮选择find_string_refs.txt。运行脚本点击Run按钮。查看日志脚本将开始执行并在日志窗口输出类似以下信息 开始查找字符串引用 目标字符串: Registration failed 在内存中找到 1 个匹配项。 目标字符串的基址确定为: 0x404010 开始扫描代码段: 0x401000 - 0x402000 发现引用 #1 在地址: 0x401123 指令: push 0x404010 发现引用 #2 在地址: 0x401145 指令: cmp dword ptr [ebp-4], 0x404010 ... 扫描完成 在代码段中共找到 5 处对字符串 Registration failed 的引用。 所有引用地址已在上方列出。 脚本执行完毕。结果利用现在你得到了所有引用该字符串的代码地址。你可以双击日志中的地址在 CPU 窗口中直接跳转过去进行分析。你也可以修改脚本自动在这些地址设置断点 (bp 0x401123)然后运行程序每当执行到这些代码时就会中断方便你动态分析程序流。这个脚本虽然简单但已经具备了自动化分析的雏形。你可以在此基础上扩展例如同时搜索多个字符串。不仅找直接引用还找通过寄存器间接引用的代码。将找到的地址列表导出到文件。自动分析引用点的函数上下文。6. 进阶应用场景与脚本构思掌握了基础我们可以探索更复杂的应用这些才是脚本编程威力真正体现的地方。场景一自动化脱壳寻找 OEP许多加壳程序在运行时会在内存中解密原始代码并跳转到原始入口点OEP。手动寻找 OEP 需要跟踪大量的跳转。脚本可以自动化这个过程。脚本思路在程序入口点通常是加壳代码开始跟踪。单步执行但记录每个CALL和JMP指令的目标地址。监控内存属性变化例如.text段突然变为可写然后又被修改。当检测到一个跳转目标位于已解密的、看起来像正常编译器生成的代码区域时可通过统计指令模式判断很可能就是 OEP。脚本在此处断下并提示用户。场景二算法识别与数据追踪你需要分析一个加密函数输入一个字符串输出一个哈希值。脚本思路在已知的输入输出函数处设置断点例如scanf和printf。当输入函数断下时脚本记录输入缓冲区的内容和地址。当输出函数断下时脚本记录输出结果。在这两个断点之间脚本可以自动追踪输入数据是如何被传递、复制、修改的通过硬件断点监控内存访问。最终生成一份数据流图清晰地展示从输入到输出的变换路径。场景三漏洞挖掘辅助Fuzzing 反馈结合简单的 Fuzzing脚本可以帮助定位崩溃点。脚本思路外部 Fuzzer 向目标程序提供畸形输入。脚本监控目标程序等待异常发生如访问违例。一旦发生异常脚本立即暂停执行并记录完整的上下文寄存器状态、调用栈、触发异常的指令、以及导致异常的内存地址和内容。脚本自动生成一份详细的崩溃报告保存到文件。甚至可以尝试自动判断崩溃的可利用性如 EIP 是否被用户输入控制。7. 脚本调试与常见问题排查编写复杂的脚本时你也会遇到 Bug。调试脚本本身也是一项技能。7.1 脚本调试技巧使用log进行输出调试这是最基本也是最有效的方法。在关键分支、循环开始和变量改变的地方输出状态信息。log “进入循环i {i}, currentAddr 0x{currentAddr}”;使用msg进行交互式调试msg会弹出对话框暂停脚本执行直到你点击确定。这对于在特定点检查状态非常有用但要小心不要在循环中使用否则会点击到手软。单步执行脚本在脚本窗口中使用Step按钮或快捷键可以单步执行脚本命令就像调试程序一样。这能帮你精确定位脚本逻辑错误。检查 API 返回值许多 x64dbg 脚本函数在失败时返回0或空值。调用后检查返回值是良好习惯。addr findmem(“some pattern”, 0x400000); if (addr 0) { log “未找到模式”; return; }7.2 常见问题与解决方案问题现象可能原因排查方式解决方案脚本执行无任何输出立即结束。1. 脚本中存在语法错误如括号不匹配。2. 脚本开头有return或条件不满足直接退出。1. 检查脚本窗口是否有红色错误提示。2. 在脚本第一行后添加log “Start”测试。1. 修正语法错误。2. 使用Step单步调试查看执行流。findmem或findallmem找不到数据。1. 搜索范围不正确。2. 字符串编码问题如 Unicode vs ANSI。3. 目标数据尚未加载到内存。1. 使用meminfo命令查看内存区域。2. 尝试搜索十六进制字节序列。3. 确保程序已运行到相关代码加载数据。1. 扩大搜索范围或指定正确模块。2. 使用findallmem并遍历结果。3. 在合适的内存状态如下断点后执行脚本。设置的断点不触发。1. 断点地址错误代码未执行。2. 条件断点条件永远不满足。3. 断点被意外删除或禁用。1. 在断点地址手动jmp过去看代码是否存在。2. 将条件改为“1”始终触发测试。3. 查看断点列表AltB。1. 使用sym.FromName或mod.BaseFromNameoffset计算准确地址。2. 简化条件逐步调试。3. 在脚本中设置断点后用log输出确认。脚本运行导致调试器卡死或无响应。1. 脚本进入死循环。2. 脚本执行了耗时极长的操作如遍历所有内存。3. 与调试器事件冲突。1. 观察 CPU 使用率强制终止 x64dbg。2. 在循环中添加计数器超过阈值则退出。3. 检查脚本是否在不当位置调用了run。1. 在循环中添加安全退出条件。2. 将大任务分块并添加进度提示。3. 避免在可能递归触发的事件处理脚本中调用run。readString读出的字符串乱码。1. 读取的地址不是字符串起始地址。2. 字符串是宽字符Unicode。3. 内存已被破坏。1. 先用readByte查看内存内容确认。2. 检查字符间隔ASCII 单字节Unicode 双字节。1. 使用findmem确认地址。2. 使用readWString读取 Unicode 字符串。8. 最佳实践与工程化建议当你的脚本变得越来越复杂或者需要在团队中分享时遵循一些最佳实践至关重要。模块化与函数化将常用的功能封装成子例程使用label和call。label DumpRegisterState: log “EAX: {reg.GetEAX()} EBX: {reg.GetEBX()} ECX: {reg.GetECX()} EDX: {reg.GetEDX()}”; log “ESI: {reg.GetESI()} EDI: {reg.GetEDI()} EBP: {reg.GetEBP()} ESP: {reg.GetESP()}”; log “EIP: {cip}”; return; // 在需要的地方调用 call DumpRegisterState;配置文件与参数化将需要频繁修改的变量如目标字符串、搜索范围放在脚本开头作为配置项。甚至可以从外部文件读取配置。详细的日志与错误处理脚本应能记录其操作过程并在出错时给出清晰的提示而不是静默失败。资源清理如果脚本设置了临时断点、修改了内存或寄存器在脚本结束前应考虑恢复原状除非这是分析目的。使用bpd删除断点等命令。代码注释为复杂的逻辑添加注释说明意图。这在你几个月后回头修改脚本时能救命。版本管理像管理源代码一样管理你的脚本使用 Git 等工具。记录脚本的用途、依赖的目标程序版本。性能考虑遍历整个内存或代码段是昂贵的操作。如果可能尽量缩小搜索范围。避免在紧密循环中调用log输出大量信息这会极大拖慢速度。9. 总结从脚本使用者到创造者x64dbg 脚本编程的本质是将你对逆向工程的理解和策略转化为可执行的自动化方案。它不是一个孤立的语法学习而是与你对 Windows 系统、汇编语言、程序结构的理解深度绑定。学习的正确路径是先手动后自动彻底理解手动分析的过程才能写出有效的自动化脚本。从小工具开始不要一开始就想写一个万能自动化分析框架。从“自动记录所有CALL指令的目标地址”这样的小工具写起。积累代码片段将常用的功能如读内存、设断点、遍历模块封装成自己的代码库后续脚本直接组合调用。阅读他人脚本x64dbg 社区和网络上有很多优秀的脚本示例学习别人的思路和技巧。解决实际问题最好的学习动力来源于实际项目中的需求。遇到重复性劳动时思考“能不能用脚本搞定”最后记住脚本是辅助不是替代。它无法替代你对程序逻辑的深入思考。但它能帮你节省出大量时间让你将宝贵的精力集中在最需要创造力和洞察力的部分。将find_string_refs.txt脚本加载到你的 x64dbg 中针对一个实际程序运行一次这个过程中遇到的任何问题都是你迈向逆向自动化坚实的第一步。