$ cat ~ / posts /reverse /lecture09 6.7k Words ~ 24 Mins
cover.png
逆向工程基本原理09

#逆向工程基本原理09

exdoubled Lv5

高级对抗技术概述

逆向工程本质上是一个对抗过程。分析者希望借助反汇编器、反编译器、调试器和脱壳工具尽快理解程序逻辑;软件保护者或恶意代码作者则希望提高分析门槛,拖慢分析速度,甚至让工具给出错误结果。

本节课关注的高级对抗技术主要包括:

  • 对抗逆向工具:反反汇编、反反编译
  • 加壳与脱壳:压缩壳、加密壳、动态脱壳、反脱壳
  • 虚拟机保护:将原始指令翻译成自定义虚拟指令
  • 实践处理:DIE 查壳、UPX 脱壳、Ghidra 中 Patch Instruction、NOP 和 return 滥用修复

需要注意的是,逆向工具不是绝对可靠的。工具本身也是程序,会受到算法假设、实现细节和启发式规则的限制。高级对抗技术往往不是让程序不能运行,而是让逆向工具误判。

对抗逆向工具

前面课程中主要使用了静态逆向工具来分析二进制程序,其中最核心的两个能力是反汇编和反编译:

  • 反汇编:将机器码翻译成汇编指令
  • 反编译:在反汇编和程序分析基础上恢复出类似高级语言的伪代码

对抗逆向工具也可以从这两个层面入手:

  • 反反汇编:让反汇编器在错误位置解码,或把数据当作指令
  • 反反编译:让反编译器产生不完整、错误或误导性的伪代码

反反汇编

反反汇编是指使用经过特殊设计的代码或数据,使反汇编器产生不正确的结果。

常见效果:

  • 隐藏真正执行的指令
  • 将数据误识别为指令
  • 将指令误识别为数据
  • 破坏函数边界和控制流图
  • 进一步导致反编译失败

反反汇编通常直接在二进制层面实施,所以比较容易自动化,也容易影响多个逆向工具。

回顾反汇编算法

反汇编的核心难点是区分代码和数据。二进制文件中的 .text 段大部分是代码,但也可能混入常量、跳转表、内联数据;数据段中也可能出现可执行代码或运行时生成的代码。

经典反汇编算法有两类:

  1. 线性扫描
  2. 递归下降

线性扫描从给定起点开始逐条解码,默认后续字节都是指令:

1
起始地址 -> 指令1 -> 指令2 -> 指令3 -> 指令4 -> ...

优点是实现简单,覆盖率高。缺点是无法识别代码流中的数据,一旦数据字节也能被解释成合法指令,结果就会错位。

递归下降会根据控制流递归识别指令:

1
2
3
4
5
入口地址
-> 普通指令:继续下一条
-> 条件跳转:分析跳转目标和 fall-through 地址
-> 无条件跳转:分析跳转目标
-> 间接跳转:如果无法静态确定目标,可能停止或遗漏

递归下降比线性扫描更聪明,但依赖控制流起点和跳转目标识别。遇到间接跳转、虚假控制流、运行时计算地址时,仍然可能漏掉代码或错误扩展。

非连续指令流

非连续指令流主要对抗线性扫描反汇编。基本思想是在不会执行的位置插入可以被反汇编成合法指令的数据。

例如:

1
2
3
4
MOV R0, #0          ; 清零 R0
POP {R4, LR} ; 恢复寄存器和返回地址
BX LR ; 返回调用者
DCD 0x47745E9B ; 实际是数据,不会被执行

执行到 BX LR 后,控制流已经返回调用者,后面的 DCD 0x47745E9B 不会被执行。但是如果反汇编器继续线性扫描,它可能把这 4 个字节解释成指令:

1
2
LDRSH R3, [R3, R2]
BXNS LR

这类伪指令会污染反汇编结果。如果后续反编译器基于错误的反汇编继续分析,伪代码也会被误导。

虚假控制流

虚假控制流主要对抗递归下降反汇编。递归下降会把潜在跳转目标当成代码起点,因此可以构造“看起来可能跳转,实际上永远不会执行”的路径。

第一种方式是使用相同目标、条件相反的跳转指令:

1
2
3
4
5
6
7
CMP R0, #0
BEQ loc_real ; R0 == 0 时跳转
BNE loc_real ; R0 != 0 时也跳转
DCD 0x47745E9B ; 永远不会执行的数据

loc_real:
MOV R0, #1

BEQBNE 覆盖了比较结果的两种情况,所以程序一定跳转到 loc_real。但是递归下降反汇编器不一定能识别这种组合语义,可能继续把中间的 DCD 当成 fall-through 代码。

第二种方式是使用恒定判断值,也就是类似不透明谓词的写法:

1
2
3
4
5
6
7
EOR R0, R0, R0      ; R0 = 0
CMP R0, #0
BEQ loc_real ; 一定跳转
DCD 0x47745E9B ; 实际不可达

loc_real:
MOV R1, #0x10

静态看起来是条件跳转,但语义上等价于无条件跳转。工具如果只看到分支形态,不理解前面 EOR R0, R0, R0 让 R0 恒为 0,就会把不可达分支也纳入分析。

反反汇编处理思路

处理反反汇编时不能完全依赖反编译结果,需要回到汇编和控制流本身:

  1. 找到反编译失败或出现 undefinedhalt_baddata()、异常函数边界的位置
  2. 检查对应汇编是否存在无条件跳转后面的数据、相同目标条件跳转、恒定条件跳转
  3. 判断哪些指令实际不可达,哪些才是真正执行路径
  4. 在工具中把不可达垃圾指令改为 NOP,或直接改为等价跳转
  5. 重新分析函数,观察反编译结果是否恢复正常

其中第 3 步最重要。不是看到奇怪指令就直接删除,而是要先证明这些指令不会影响程序原本功能。

反反编译

反反编译是指修改汇编代码或二进制代码,使反编译器产生错误伪代码。

反编译器需要做大量程序分析:

  • 函数边界识别
  • 栈变量恢复
  • 参数个数推断
  • 控制流结构化
  • 类型恢复
  • 异常处理识别
  • 间接跳转和间接调用分析

这些分析都依赖规则和假设,因此可以被特殊构造的代码干扰。

return 指令滥用

很多反编译器会把 ret 或 ARM 中的 BX LR 视为函数结束标志。但在真实执行中,返回地址寄存器可以被提前修改,ret 不一定返回调用者,也可以被滥用为间接跳转。

ARM64 示例:

1
2
3
4
5
6
7
mov x13, x30        ; 保存原始 LR
adr x30, nojump ; 把 LR 改成 nojump 的地址
ret ; 实际跳到 nojump,而不是返回调用者
undefined1 ; 工具可能认为这里是坏数据

nojump:
mov x30, x13 ; 恢复原始 LR

这段代码的效果接近:

1
b nojump

但是反编译器看到 ret 后,可能直接认为当前函数结束,于是把 nojump 后面的真实逻辑排除在函数之外,导致反编译结果不完整。

实践中处理这类代码时,需要检查所有可疑的 ret

  • ret 前是否修改了 x30
  • ret 后面是否还有明显可执行逻辑
  • ret 的目标是否是当前函数内部地址
  • 反编译结果是否在 ret 处异常截断

如果确认只是用于跳转,可以在 Ghidra 中将其 patch 成直接跳转:

1
2
3
b nojump
nop
nop

或者把保存和恢复 x30 的冗余指令替换为 NOP,保留真实控制流。

错误处理隐藏代码

有些反编译器对异常处理结构支持不完整,尤其是复杂的 C++ try/catch、SEH 或编译器生成的异常处理辅助结构。

保护者可以把敏感逻辑放入错误处理路径中:

1
2
3
4
5
try 块:
普通逻辑

catch 块:
真实校验逻辑或恶意逻辑

如果反编译器没有正确恢复异常处理控制流,伪代码中可能只显示普通路径,而隐藏了 catch 中真正关键的代码。

分析时要注意:

  • 函数中是否存在异常处理表或异常相关符号
  • 反汇编中是否有跳到错误处理块的边
  • 反编译伪代码是否明显缺少某些基本块
  • 是否有“看似错误处理,实际参与主逻辑”的代码

基于 ROP 技术隐藏代码

ROP 是 Return-oriented Programming。它本来常见于漏洞利用,但也可以用于隐藏程序逻辑。

核心概念是 gadget:

  • gadget 是以 ret 结尾的短指令序列
  • 每个 gadget 完成很小的操作
  • 通过控制栈上的返回地址,把多个 gadget 串成执行链

一个简单的 ROP 链可以写成:

1
2
3
4
5
6
7
8
9
10
stack:
gadget1_addr
0x0b
gadget2_addr
"/bin/sh"
gadget3_addr
0
gadget4_addr
0
gadget5_addr

对应执行过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
gadget1:
pop eax
ret

gadget2:
pop ebx
ret

gadget3:
pop ecx
ret

gadget4:
pop edx
ret

gadget5:
int 0x80

程序并没有一段连续的“真实逻辑代码”。真实语义被拆散在多个已有代码片段中,由栈上的地址串起来执行。反编译器通常以函数为单位恢复结构化代码,遇到这种跨函数、跨片段、依靠 ret 串联的执行方式时,很难给出可读伪代码。

对抗 ROP 隐藏时,重点不是只看单个 gadget,而是恢复整个栈布局:

  • 找到控制栈内容的位置
  • 识别每个 gadget 的地址
  • ret 顺序模拟执行链
  • 把 gadget 的微操作合并成完整语义

加壳与脱壳

加壳是在原程序外部包裹一段保护代码,用于隐藏原程序代码和数据。

常见目的:

  • 保护商业软件核心算法
  • 防止补丁修改和破解
  • 隐藏恶意代码特征
  • 绕过静态查杀和自动分析
  • 增加 CTF 逆向题难度

EP 与 OEP

EP 是 Entry Point,表示程序入口点。OEP 是 Original Entry Point,表示加壳前原程序的入口点。

加壳后,程序入口通常会被改到壳代码:

1
2
3
4
5
加壳前:
EP == OEP -> 原程序 main / start

加壳后:
EP -> 壳代码 -> 解压/解密/修复 -> OEP

加壳过程大致为:

  1. 加载源文件并映射到内存
  2. 验证文件格式和有效性
  3. 增加或修改区段,用来存储壳代码和压缩数据
  4. 压缩或加密原程序区段
  5. 修改入口点 EP,使程序先执行壳
  6. 保存新的可执行文件

壳运行时通常会做这些事情:

  1. 获取自身需要的库函数地址
  2. 解压或解密原程序区段
  3. 修复导入表、重定位等运行环境
  4. 跳转到 OEP,把控制权交还原程序

壳的分类

按照功能可以粗略分为三类:

  • 压缩壳:压缩原程序代码和数据,运行时解压
  • 加密壳:加密原程序代码和数据,运行时解密
  • 虚拟壳:把原始指令翻译成自定义虚拟机指令,由虚拟机解释执行

UPX 是最常见的压缩壳之一。它会把可执行文件压缩到更小体积,同时保持程序运行效果不变。

查壳方法

逆向一个程序时,通常应该先判断是否加壳。

常见查壳方法:

  • 看入口函数是否是复杂的壳 stub
  • 查看区段名和程序头中是否有加壳器标识,例如 UPX0UPX1
  • 使用 DIE、Exeinfo 等工具自动识别
  • 计算熵值,压缩或加密后的区段通常熵值更高

DIE 的基本原理是为不同壳编写启发式检测规则。规则通常会匹配入口点附近的字节特征、区段名、版本信息和已知签名。

实践中可以这样理解 DIE 的结果:

1
2
3
4
5
6
7
8
有壳:
检测到 UPX 3.96
检测到 NRV 压缩算法
可能显示压缩等级或修补版信息

无壳:
没有明显 packer 标识
入口函数通常更接近原始 main/start 逻辑

UPX 脱壳

如果程序是标准 UPX 壳,最直接的方法是使用 UPX 自带脱壳:

1
upx -d Lab8_demo

脱壳后再导入 Ghidra,入口函数和主逻辑会明显更清晰。

如果不脱壳直接分析,通常会看到:

  • 入口函数是复杂的解压 stub
  • 导入表很少
  • 原始函数和字符串不可见或不完整
  • 反编译结果大部分是壳逻辑,不是业务逻辑

脱壳之后,才能更有效分析原程序中的反反汇编、反反编译和真实功能。

脱壳流程

脱壳是加壳的逆过程,目标是还原被保护的原始程序。

一般步骤包括:

  1. 解密或解压缩被保护区段
  2. 按区段头描述重新加载代码和数据
  3. 修复导入表,例如 PE 的 IAT、ELF 的 PLT/GOT
  4. 处理重定位
  5. 调整入口点到 OEP
  6. 导出脱壳后的可执行文件

脱壳方法常分为两类:

  • 硬脱壳:分析壳算法,写出对应逆算法
  • 动态脱壳:让壳自己在运行时还原程序,然后 dump 内存镜像

现实中很多壳会变形,硬脱壳维护成本很高,所以实战里常用动态脱壳。

ESP 定律

ESP 定律是堆栈平衡原理在脱壳中的应用。壳代码通常会先保存原程序现场,完成解压解密后恢复现场,再跳到 OEP。

示意:

1
2
3
4
5
pushad              ; 保存现场,栈指针改变
b unpack ; 执行解压逻辑
...
popad ; 恢复现场,栈指针恢复
ret ; 跳到 OEP

基本操作步骤:

  1. 程序执行完第一条压栈指令后,对当前栈顶地址下数据断点
  2. 程序继续运行,后续 popad 或恢复现场时会访问该地址并触发断点
  3. 断点触发后单步执行,通常第一个 ret 附近就是跳转 OEP 的位置
  4. 在原程序已经解压到内存后 dump 并修复文件

局限性:

  • 依赖明显的“保存现场到恢复现场”结构
  • 如果壳故意破坏栈平衡或多层跳转,定位会变困难
  • 不适用于所有壳,只是一种经验方法

内存镜像法

内存镜像法不关心壳算法细节,只关心程序在某个运行时刻内存中已经出现明文代码和数据。

基本观察:

  • 自解压或自解密时,壳会逐步还原各个区段
  • 当开始修改 .data 时,.text 很可能已经解压完毕
  • 当开始执行 .text 时,代码和数据通常已经接近原始状态

实践思路:

  1. .data 段下内存断点,观察壳何时开始写数据段
  2. .text 段下执行断点,捕获跳回原程序的时刻
  3. 在 OEP 附近暂停并 dump 内存
  4. 修复导入表、重定位和入口点

反脱壳

加壳程序也会使用反脱壳技术加强保护。

常见手段:

  • 防篡改:对代码和数据做完整性校验,发现 patch 后退出
  • 模拟器检测:检测 QEMU、模拟器文件、DMI 信息、特定驱动
  • 反调试:使用 ptrace、调试状态检测、多线程互相附加
  • 元数据修改:修改 UPX 魔数等元数据,让标准脱壳工具失效

元数据修改通常不能真正隐藏程序信息,只是让自动工具失败。分析者发现问题后,可以手动修复文件头或更新检测规则。

虚拟机保护

虚拟机保护 VMP 是更高强度的代码保护技术。它不是简单压缩或加密原始代码,而是把原始指令翻译成自定义虚拟机指令,由保护器生成的虚拟机解释执行。

VMP 和普通压缩壳、加密壳的关键区别在于,普通壳运行到某个阶段通常会把原始代码还原出来并跳到 OEP;VMP 往往不会把原始机器码完整恢复回来,而是让解释器持续执行自定义字节码。因此分析重点不再是单纯找 OEP,而是识别虚拟机入口、字节码格式、dispatch 和 handler 语义。

基本结构:

1
2
3
4
5
6
7
8
9
10
11
虚拟字节码

Fetch Instruction

Decode Instruction

Dispatch

Handler

Terminator / Next Instruction

其中:

  • Fetch:取出下一条虚拟指令
  • Decode:按照虚拟机指令格式解析 opcode 和 operand
  • Dispatch:根据 opcode 跳到对应 handler
  • Handler:执行虚拟指令语义
  • Terminator:结束虚拟机执行或进入下一轮

虚拟指令示例

假设固定 4 字节是一条虚拟指令:

1
2
3
4
第 1 字节:操作码
第 2 字节:操作数 1
第 3 字节:操作数 2
第 4 字节:操作数类型

操作码可以设计为:

1
2
3
4
5
6
7
8
9
10
01: MOV A B   -> A = B
02: ADD A B -> A += B
03: SUB A B -> A -= B
04: MUL A B -> A *= B
05: MOD A B -> A %= B
06: DIV A B -> A /= B
07: CMP A B -> 比较 A 和 B
08: JE A -> 相等时跳转
09: JMP A -> 无条件跳转
0A: RET -> 返回 R0

寄存器和参数也由虚拟机自己定义:

1
2
参数:P1 P2 P3 P4 -> 01 02 03 04
寄存器:R0 R1 R2 R3 -> 10 11 12 13

这时逆向人员面对的不再是 ARM 或 x86 原始指令,而是保护器自定义的 ISA。要理解真实逻辑,需要先恢复虚拟指令格式、handler 语义和字节码位置。

VMProtect

VMProtect 是典型的虚拟机保护工具。公开资料中通常认为它是基于栈的虚拟机,会把原始指令编译成虚拟指令,再由解释引擎执行。

VMProtect 常见混淆技术:

  • 虚拟指令集
  • 寄存器轮转
  • 字节码加密
  • 随机验证

虚拟指令集与 nor

VMProtect 的虚拟指令集会包含算术运算、栈操作、内存操作、系统相关操作和逻辑运算。

课件中提到一种极端设计:逻辑运算只有一条 nor。因为 nor 可以组合出其他逻辑运算。

令:

\[P(a,b)=\lnot a \land \lnot b\]

则:

\[not(a)=P(a,a)\]

\[and(a,b)=P(P(a,a),P(b,b))\]

\[or(a,b)=P(P(a,b),P(a,b))\]

\[xor(a,b)=P(P(P(a,a),P(b,b)),P(a,b))\]

这说明虚拟机指令集可以非常小,但仍然表达复杂逻辑。对逆向人员来说,单条虚拟指令含义越抽象,恢复高级语义越困难。

寄存器轮转

寄存器轮转是指虚拟寄存器和实际存储槽之间的映射会不断变化。

例如共有 16 个寄存器槽,其中 2 个槽始终空闲。vEax 原本在第 1 个槽中,某条虚拟指令更新 vEax 后,VMProtect 可能随机选择一个空闲槽保存新值,原槽进入空闲池。

结果是:

  • 同一个虚拟寄存器在不同位置可能对应不同槽
  • 同一个槽在不同时间可能表示不同虚拟寄存器
  • 静态分析难以直接追踪寄存器数据流

字节码加密与随机验证

字节码加密会让虚拟指令只在执行时被解密。不同 handler 可能使用不同的数据解密逻辑,每次解密后 seed 还会变化。

随机验证用于防篡改。保护器在编译过程中随机选择代码片段计算哈希,并插入类似 calchash 的虚拟指令。如果运行时发现代码被修改,就拒绝继续执行。

因此对 VMP 的分析通常要面对三层问题:

  1. 找到虚拟机和字节码
  2. 还原字节码解密和 handler 语义
  3. 绕过完整性校验和反调试

Code Virtualizer 与 Virbox Protector

Code Virtualizer 会将原始代码转换成内部虚拟机才能理解的虚拟操作码。它的特点是每个受保护程序可以拥有不同的虚拟机和虚拟操作码,导致一次逆向经验难以直接复用。

Virbox Protector 是商用多语言、多平台保护工具,支持 x86、ARM、.NET IL、JVM、Dalvik 等场景,提供防逆向、防篡改、运行时自保护、脚本和资源加密等功能。

这类工具说明高级代码保护不是单一技术,而是组合了虚拟机、加密、反调试、完整性校验和混淆的系统工程。

Movfuscator

Movfuscator 是一个很有意思的对抗工具,它可以把程序编译成只包含 mov 指令的程序。

它利用的是 x86 mov 指令的图灵完备性:理论上只用 mov 也能模拟任意计算。

例如用 mov 实现:

1
2
if X == Y:
X = 100

可以先把条件选择转化为数组下标选择:

1
2
SELECT_X[] = { &Z, &X }
SELECT_X[X == Y] = 100

再用内存读写模拟 X == Y

1
2
3
mov [x], 0
mov [y], 1
mov R, [x] ; 如果 x == y,则 R = 1,否则 R = 0

这类程序在反汇编中没有传统控制流,反编译器也很难恢复出正常的 if/while 结构。缺点是执行效率和内存消耗都很差。

实践引导

本次实践目标是逆向一个包含高级对抗技术的程序,识别它使用的保护方式,脱壳后修复反反编译干扰,最终得到完整反编译逻辑并判断程序功能。

实践分为两个部分:

  1. 壳对抗技术实践
  2. 反对抗逆向工具技术实践

使用 DIE 查壳

DIE 的使用方式比较直接:

  1. 打开 DIE
  2. 导入待分析程序
  3. 查看基本信息和检测到的壳信息
  4. 记录壳类型、版本、压缩算法和是否被修补

如果是 UPX 加壳,DIE 可能显示类似信息:

1
2
3
4
Packer: UPX
Version: 3.96
Algorithm: NRV
Compression level: brute

报告中需要说明:

  • 该程序采用了什么加壳技术
  • 分析依据是什么
  • 是通过特征、区段名、入口 stub、DIE 结果还是熵值判断

使用 UPX 脱壳

如果 DIE 判断为标准 UPX,可以直接脱壳:

1
upx -d Lab8_demo

脱壳前后对比:

  • 未脱壳:Ghidra 中入口是壳代码,逻辑复杂,原始函数不清楚
  • 脱壳后:入口和函数边界更正常,可以继续分析原程序

实验报告中需要写清楚使用的脱壳命令和脱壳后的分析效果。

Ghidra 中处理反反编译

脱壳后还可能遇到反反编译。实践引导中主要处理两类:

  • 永远不会执行的冗余语句
  • return 指令滥用

处理流程:

  1. 在反编译窗口发现异常,例如 halt_baddata()undefined1 或函数提前结束
  2. 回到 Listing 窗口查看对应汇编
  3. 判断哪些代码是真正执行路径,哪些是冗余干扰
  4. 右键选择 Patch Instruction
  5. 将无用语句改为 nop,或将间接跳转改为直接跳转
  6. 重新反编译并检查函数逻辑是否完整

NOP 处理不可达垃圾指令

示例结构:

1
2
3
4
5
6
b.eq rxrHR5Ie
b aabbcc
undefined1

aabbcc:
add x1, sp, #0xc

这里真正有用的是跳转到 aabbcc 后的逻辑。中间 undefined1 是干扰反编译的垃圾数据或不可达指令,可以改为 NOP

1
2
3
4
5
6
b.eq rxrHR5Ie
b aabbcc
nop

aabbcc:
add x1, sp, #0xc

NOP 只占位,不改变程序状态。它适合替换确认不可达或确认无副作用的冗余指令。

处理冗余参数推断

反编译器会根据调用前寄存器赋值推断函数参数。在 ARM64 调用约定中,x0x7 常用于传参。如果调用前故意给很多寄存器赋值,反编译器可能误以为函数有很多参数。

实践中可能看到:

1
getUserInput(a, b, c, d, e, f, g, h)

但实际函数只使用 x0x1,也就是两个参数。其余寄存器赋值只是干扰。

处理方法:

  • 进入被调用函数确认真实使用的参数寄存器
  • 回到调用点检查哪些赋值没有被使用
  • 对冗余赋值使用 Patch Instruction 改为 nop
  • 重新反编译,确认函数调用恢复成两个参数

处理 return 指令滥用

实践中要仔细检查所有可疑 ret。典型结构如下:

1
2
3
4
5
6
7
mov x13, x30
adr x30, nojump
ret
undefined1

nojump:
mov x30, x13

这段代码表面上是 ret,实际是跳转到 nojump。因为 ret 前已经把 x30 改成 nojump 地址。

处理方式有两种。

第一种:删除冗余语义,把无用语句改为 nop

1
2
3
4
5
6
nop
b nojump
nop

nojump:
nop

第二种:更直接地把核心目的改成直接跳转:

1
2
3
4
5
6
b nojump
nop
nop

nojump:
nop

关键判断是:原片段只是为了让执行流到达 nojump,中间保存和恢复 x30 不贡献真实业务逻辑。patch 后反编译器能继续把后续代码纳入同一个函数,伪代码会更完整。

实验报告要点

报告需要覆盖两个任务。

任务一:壳对抗技术实践

  • 程序采用了什么壳
  • 判断依据是什么
  • 如何脱壳
  • 使用的脱壳命令是什么

任务二:反对抗逆向工具技术实践

  • 程序使用了哪些对抗逆向工具技术
  • 如何修改程序获得完整逻辑
  • 哪些指令被改为 NOP
  • 哪些 ret 被识别为跳转滥用
  • 程序完整功能是什么

写报告时不要只贴最终伪代码。更重要的是说明判断过程:为什么某段汇编不可达,为什么 patch 不改变原程序语义,为什么反编译结果变完整。

小结

高级对抗技术的共同特点是利用工具假设和程序分析边界。

反反汇编利用的是“代码和数据难以完全区分”这一根本问题。反反编译利用的是函数边界、控制流结构化、参数推断、异常处理和返回语义中的工程限制。加壳隐藏原程序,使分析者必须先恢复 OEP 和原始区段。虚拟机保护进一步把原始指令集替换成自定义虚拟指令集,把逆向问题变成恢复虚拟机语义。

实践中比较稳妥的顺序是:

1
查壳 -> 脱壳 -> 重新导入分析工具 -> 定位反编译异常 -> 回到汇编验证控制流 -> patch 冗余指令 -> 重新反编译 -> 总结程序功能

逆向工具能显著提高效率,但不能替代对底层执行语义的判断。遇到高级对抗时,应优先相信真实控制流和指令语义,而不是直接相信反编译窗口。

$ discussion
# Comments
waline