软件安全实验-动态分析技术

软件安全实验-动态分析技术

【实验目的】

本实验使用x32dbg调试器,分析简单注册程序(TraceMe.exe),获得正确的序列号或者修改程序逻辑,以此学习调试Win32应用程序的相关技术。

【实验原理】

  • 调试器的基本工作机制: x32dbg 通过 Windows Debug API 附加到目标进程,或者直接启动被调试程序。调试器可以接管异常处理、控制程序暂停和继续执行,并支持单步跟踪、查看寄存器和观察内存变化。

  • 断点技术及其应用:

    • 消息断点: Windows 图形界面程序采用事件驱动模型。点击“Check“按钮后,系统会把鼠标消息传递到对应窗口过程。先在按钮消息上设置断点,通常可以更快找到后续的校验逻辑。

    • 内存断点: 程序从系统库回到自身代码时,执行流通常会落到可执行区段,如 .text。在这一区段设置访问断点,可以帮助定位到真正开始处理输入的用户代码。

  • 程序的暴力破解(Patch 修改控制流): 程序在比较序列号后,通常会通过 jejne 这类条件跳转决定进入成功还是失败分支。把关键跳转改成 nop,或者改成相反的跳转指令,就可以改变程序原本的执行路径,从而绕过验证。

【实验任务】

1 加载被调试程序

有两种方式加载被调试程序:

  • 打开x32dbg,通过“文件–>打开“菜单项打开被调试程序;

  • 运行被调试程序,打开x32dbg,通过“文件–>附加“菜单项附加被调试程序的进程。

从原理上看,这两种方式对应的是“调试器主导创建进程“和“调试器后置接管进程“两种调试入口。前者的优点是从程序入口点开始就能完整观察初始化流程,适合分析启动阶段的反调试或早期校验逻辑;后者更适合程序已经运行到某个稳定界面后的快速介入,能够减少重复启动的时间成本。在本实验中,目标是定位按钮触发后的校验路径,因此两种加载方式都可行,但为了保证上下文完整,通常优先采用直接打开方式。

2 输入用户名和序列号

按下F9快捷键运行程序,在程序窗口中输入用户名和序列号,待下断点后再单击“Check“按钮。这样做的目的不是立即验证结果,而是先构造一组可追踪的输入数据,让后续调试时能够在寄存器、栈和内存中识别“用户输入值“和“程序计算值“两条数据流。单击“Check“后程序会进入典型的事件驱动处理链,从窗口消息分发逐步过渡到用户代码中的校验函数,这正是后续断点定位的关键切入点。

随意输入用户名与序列号

3 下断点

(a)在x32dbg中打开“句柄“窗口,观察TraceMe程序中所有的窗口信息(首次打开句柄窗口可能需要通过“右键菜单–刷新“才能显示窗口信息),找到“Check“按钮后通过右键菜单设置消息断点,如图 2所示。该步骤利用了GUI程序“消息驱动“的执行模型:按钮点击并不是直接调用业务函数,而是先由系统消息机制分发事件,因此从消息入口下断点可以更稳定地抓住校验逻辑触发瞬间。之后切换到TraceMe程序,单击“Check“按钮,在x32dbg的“CPU“窗口可以看到被调试程序暂停在操作系统代码空间,如图 4所示。

设置消息断点

(b)接下来,打开x32dbg的“内存布局“窗口,找到TraceMe程序的“.text“区块,如图 3所示,通过右键菜单或者 F2 快捷键设置一次性的内存访问断点。之所以在.text区设置断点,是因为该区通常承载可执行机器指令;当程序执行流从系统模块返回目标程序自身代码时,会触发该断点,从而帮助我们完成“系统代码到用户代码“的边界切换定位。之后按下F9快捷键,被调试程序将暂停在自身的用户代码空间,如图 4所示。

在 TraceMe 的.text 区块设定内存访问断点

响应内存访问断点后暂停区域的指令

(c)接下来,通过或者“Ctrl+F9“跳出当前函数,或者 F8 快捷键单步调试,直至程序再次执行到操作系统代码区域中。该过程的核心是利用“函数层级跳出+指令级跟踪“两种粒度控制执行流:前者快速跨过无关细节,后者用于在关键分支附近精细观察寄存器和标志位变化。

(d)重复步骤(b)一次,之后采用单步调试定位到用户代码空间中获取输入的指令区域,如图 5所示。重复设置边界断点的目的是在复杂调用链中重新建立定位锚点,避免在系统函数内部迷失;当进入读取输入相关指令后,就可以围绕API调用参数继续追踪用户名与序列号的流向。

4 分析

找到紧跟在两次调用GetDlgItemTextA函数之后的调用用户自定义函数的指令,按下F4快捷键运行到该处;或者以F8单步跟踪的方式,运行到该处,如图 5所示的标记为1的指令。这里的分析依据是:GetDlgItemTextA负责把界面输入搬运到缓冲区,随后出现的用户自定义函数通常就是核心运算或格式转换逻辑,因此该位置在控制流上非常接近“生成正确序列号“的关键路径。

单击 Check 按钮后的代码逻辑

F7进入该函数内部,然后运行至调用lstrcmpA函数处,如图 6所示,观察栈窗口中压入栈中的两个参数,这两个值分别为“114514“和“6572“,表示的对象为输入的序列号和真正的序列号。其原理在于lstrcmpA按调用约定接收两个待比较字符串地址,因此在比较指令执行前,栈上的实参就是最直接、最可信的运行时证据。由此可知,此次输入用户名计算得到的正确序列号为:6572。停止调试再次输入用户名和正确的序列号,观察验证结果。

验证序列号是否正确的代码和当前栈中数据

序列号正确

5 暴力破解

调试程序运行至在如图 5中标记2或者3的指令时,可以通过修改指令的方式改变程序运行逻辑,通过验证。该修改的本质是破坏“比较结果驱动分支跳转“的控制流约束:原本程序依赖标志位决定是否进入失败分支,替换为nop后,条件跳转失效,执行流将按顺序落入后续路径,从而达到绕过校验的效果。具体操作如下:

  • 选择判断指令,在“汇编于xxxx“窗口中将现有指令替换为nop。

  • 打开“补丁“窗口,选中全部补丁后单击“修补文件“按钮。

  • 在弹出的保存文件窗口中输入文件名保存修改后的文件。

修改文件,覆盖指令

破解成功

【思考与总结】

(1)两种下断点的方式在分析过程中有何优缺点?

第一种方式在 GetDlgItemTextA函数入口地址直接下断点。

优点:操作简单快捷,能直接拦截程序获取输入的动作,适合快速定位输入验证相关代码。

缺点:容易被反调试技术检测;此外,如果程序多次调用该函数,可能需要频繁点击运行才能到达目标逻辑位置。

第二种方式通过“句柄“窗口找到“Check“按钮并设置消息断点,再结合内存访问断点定位用户代码。

优点:隐蔽性较强,能有效避开一些简单的API扫描。它能更精准地定位到用户点击按钮后的处理代码段。通过内存访问断点,可以从系统空间顺利返回到用户编写的逻辑代码中。

缺点:操作复杂,需多次切换窗口、设置多种断点,对调试者熟练度要求较高。

(2)使用其他指令修改方式改变程序逻辑。

有如下几种方法:

**强制跳转:**将条件跳转指令修改为无条件跳转指令 jmp,或者修改为相反的条件跳转指令。

**修改寄存器值:**在跳转指令执行前,通过调试器手动修改标志寄存器(如将 ZF 标志位手动置1或清0),从而改变跳转结果。

**修改函数返回值:**通常函数的验证结果存在 eax 寄存器中。可以在验证函数返回后,强行将 mov eax, 1 写入,以此欺骗程序认为验证已通过 。

(3)思考如何在不修改程序的情况下,得到正确的序列号?

通过调试运行至调用 lstrcmpA函数处,观察栈窗口中压入的两个参数。其中一参数为程序根据输入用户名计算得到的正确序列号。直接记录该序列号即可实现不修改程序通过验证。在调试中定位到如图 6所示位置,从栈中读取参数值(即正确序列号),停止调试后重新运行程序,输入该序列号即可通过验证。

或者根据逆向得到的算法,编写keygen工具,自动生成正确的序列号。

破解成功


软件安全实验-动态分析技术
https://eleco.top/2026/04/23/软件安全实验-动态分析技术/
作者
Eleco
发布于
2026年4月23日
许可协议