三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

逆向工程实战:如何用OllyDbg动态分析程序中的浮点运算(以CrackMe为例)

逆向工程实战:如何用OllyDbg动态分析程序中的浮点运算(以CrackMe为例)

逆向工程实战:用OllyDbg动态解析浮点校验的CrackMe

在逆向分析领域,浮点运算就像加密算法中的盐值——它让简单的数值比较变得扑朔迷离。上周我遇到一个有趣的CrackMe,输入验证部分使用了七层浮点校验逻辑,常规的整数断点完全失效。这促使我深入研究了FPU寄存器的运作机制,最终发现调试器中的ST(0)到ST(7)寄存器栈才是破解的关键线索。

1. 搭建逆向分析实验环境

选择x64dbg作为主要调试工具(OllyDbg 1.x对现代PE文件支持有限),配合010 Editor进行二进制修补。测试目标是一个名为FloatGuard的CrackMe样本,其核心验证算法包含以下特征:

  • 使用FCOM指令链进行多级浮点校验
  • 通过FNSTSW将状态字存入AX寄存器
  • 采用SAHF转换标志位控制跳转

必备工具链配置:

# 调试器插件配置 x64dbg > Plugins > ScyllaHide > 勾选"HideDebugger" x64dbg > Options > Preferences > Engine > 启用"Memory breakpoints"

浮点运算的特殊性在于其精度处理。在调试过程中我发现,当CrackMe计算3.1415926535时,实际存储在内存中的可能是:

40 09 21 FB 54 44 2D 18 # IEEE 754标准的64位双精度表示

2. FPU寄存器栈的动态追踪技巧

在x86架构中,FPU采用栈式寄存器结构。当遇到FLD指令时,数据会被压入ST(0),原有数据依次下移。这个特性使得我们需要特别关注TOP指针的变化。

典型寄存器栈操作序列:

  1. FLD QWORD PTR [ebp-8]将变量压入ST(0)
  2. FADD ST(0), ST(1)栈顶与次栈顶相加
  3. FSTP QWORD PTR [ebp-10h]弹出结果到内存

在x64dbg中观察FPU寄存器的正确姿势:

  1. 右键CPU窗口 > FPU > 显示FPU寄存器
  2. 关键寄存器状态解读:
    • CW(控制字):精度控制位(8-9位)
    • SW(状态字):C0-C3条件码位置
    • TW(标记字):寄存器有效性标记

注意:当看到标记字显示"Empty"时,说明对应ST寄存器未初始化,直接访问会导致#IA异常

3. 浮点比较指令的逆向破解

CrackMe最狡猾的部分在于使用了三级浮点比较:

FCOM QWORD PTR [ebp-10h] ; 比较用户输入与预设值 FNSTSW AX ; 存储状态字 SAHF ; 导入EFLAGS JA @VALID ; 条件跳转

通过实战总结出浮点比较的破解路线图:

指令组合对应高级语言破解关键点
FCOM+FSTSWif(a > b)修改C0/C2/C3任一标志位
FCOMI+JNEif(a != b)在FCOMI后硬编码ZF值
FUCOMPP+FNSTSWwhile(a<=b)逆向修改TW标记字

动态修改技巧:在x64dbg中,当程序执行到SAHF指令时:

  1. 右键"FPU寄存器窗口" > 修改SW值为0x0000(清除所有标志)
  2. 或直接修补跳转指令:
JE -> JMP # 将74改为EB JNE -> NOP # 用90填充指令

4. 实战:破解多层浮点校验

以FloatGuard v1.2为例,其验证流程分为三个阶段:

  1. 初级校验(明码比较)
FLD QWORD PTR [403000h] ; 加载预设值3.1415926 FCOMP QWORD PTR [ebp-8] ; 与用户输入比较
  1. 中级校验(计算验证)
FILD DWORD PTR [ebp-0Ch] ; 加载整数参数 FYL2X ; ST(1)=ST(1)*log2(ST(0)) FSTP QWORD PTR [ebp-10h] ; 存储结果
  1. 终极校验(误差容忍)
FABS ; 取绝对值 FCOM REAL8 PTR [403008h] ; 与epsilon=1E-6比较

破解方案实施步骤:

  1. 在FCOMP指令下断点(bp 0040102A)
  2. 观察栈帧获取预设值:
    # IDA Python脚本提取常量 for addr in XrefsTo(0x0040102A, 0): print(GetFloat(PrevHead(addr.frm)))
  3. 修改FPU控制字降低精度:
    MOV EAX, 0x027F FLDCW WORD PTR [ESP-2]

5. 高级对抗技巧与防护方案

现代商业软件会采用更复杂的浮点混淆技术,例如:

时间轴混淆

// 将单次比较拆分为时序操作 double delta = fabs(input - secret); Sleep((int)(delta * 1000)); // 用延时隐藏比较结果

矩阵变换

FMUL ST(0), ST(0) ; 平方 FADD ST(0), ST(0) ; 乘2 FSUB QWORD PTR [ebx+8] ; 减特征值

防护建议:

  1. 在关键比较前插入垃圾指令:
    FSTENV [esp-28h] ; 保存环境 FRNDINT ; 随机干扰 FLDCW [esp-28h] ; 恢复环境
  2. 使用SSE2指令替代传统FPU:
    MOVSD XMM0, [secret] COMISD XMM0, [input]

经过三天的反复调试,当最后看到"Cracked!"弹窗时,那些在ST寄存器间跳动的十六进制值突然变得亲切起来。记得在修改FPU控制字时,把精度从64位降到32位会导致校验误差累积,这反而成了绕过验证的突破口——有时候不完美的解法恰恰是最有效的。

← 返回列表