第一类是大量 IsDebuggerPresent 检查。

你看到很多函数都有:

if ( !&IsDebuggerPresent )
    exit(0);

但注意这个写法很怪:

!&IsDebuggerPresent- 

它检查的是导入函数地址是否为空,不是调用 IsDebuggerPresent() 的返回值。静态看像反调试,实际更多是导入解析/混淆。正常 IAT 有地址时就不会退出。

第二类是异常/SEH 反调试:

SetUnhandledExceptionFilter
UnhandledExceptionFilter
RaiseException
AddVectoredExceptionHandler

这里最有用的是:

AddVectoredExceptionHandler

第三类是 API 动态解析:

.SCY 段里有很多 API 名:

kernel32.dll
GetProcAddress
LoadLibraryW
VirtualProtect
AddVectoredExceptionHandler
RemoveVectoredExceptionHandler
IsDebuggerPresent
IsBadReadPtr
IsBadWritePtr

这说明它有个小 runtime/protector,把关键 API 和流程藏了一层。

实战绕法:

  1. 不要 patch 掉 AddVectoredExceptionHandler

因为这个 handler 是题目真实算法入口。你如果禁掉它,程序反而走不到正确逻辑。

  1. 可以 patch 掉真正的 IsDebuggerPresent() 返回

下面是SEH反调试的一个例题:

题目名称是:RC4

这个题真实是AES-128,RC4是不会执行的

  1. 不相信静态调用链
  2. 搜 .SCY / VEH / **SEH **/ VirtualProtect / RaiseException
  3. 看是否 patch 了某个 thunk
  4. 找异常 handler
  5. 看 handler 是否修改 ContextRecord->Rip
  6. 从 ContextRecord 还原真实参数
  7. 还原 handler 里的真实算法
  8. 逆算法求 flag

核心异常逻辑是这样:

sub_140017620()

它会注册 VEH:

VEH(向量化异常处理,Vectored Exception Handling)是 Windows 提供的一种异常处理机制。你可以通过 AddVectoredExceptionHandler 这个 API 来注册一个 VEH 处理函数,它在程序的异常处理流程中拥有很高的优先级

下图是VEH:

image

AddVectoredExceptionHandler(1, Handler);

然后把 sub_140011721 前 5 字节 patch 掉:

原来: jmp sub_1400122A0       // 跳到 RC4
改成: C6 04 25 00 00 ...     // 故意访问 0 地址,触发异常

所以 main 里看起来调用的是:

sub_140011479
  -> sub_14001B2B0
     -> sub_140011721
        -> sub_1400122A0  // 静态看像 RC4

但实际运行时不会进 RC4,而是:

调用 sub_140011721
↓
触发 0xC0000005 访问异常
↓
VEH Handler 接管
↓
Handler 自己加密输入
↓
修改 ContextRecord->Rip 到返回地址
↓
返回 EXCEPTION_CONTINUE_EXECUTION

handler 是:

Handler -> sub_140018670

它判断:

ExceptionCode == 0xC0000005
ContextRecord->Rip == sub_140011721

如果匹配,就从异常上下文里取原本函数参数:

Context.Rcx = key
Context.Rdx = input_data
Context.R8  = output_buffer

然后它不是调用 RC4,而是做两块 16 字节加密:

sub_140011735(key, block0, out0);
sub_14001133E();              // 修改 key
sub_140011735(key, block1, out1);

sub_14001133E() 实际跳到:

sub_14001BCA0

把 key 从:

Rc4_1s_very_S@f3

改成:

Rc4_n0t_5@f3_N0w(这里可以尝试自己动态调试)

所以真实算法是:

第 1 个 16 字节 block:用 Rc4_1s_very_S@f3 加密
第 2 个 16 字节 block:用 Rc4_n0t_5@f3_N0w 加密

sub_14001A930 是自定义 AES-128:

KeyExpansion
AddRoundKey
SubBytes
ShiftRows
MixColumns

其中 MixColumns 是标准 AES MixColumns 后再异或常量:

[DD, EE, AA, DD]

每列都加这个常量。

比较函数是:

sub_14001AD90

目标密文 32 字节:

target = bytes([
    0xB1, 0x82, 0x7B, 0x2E,
    0xA1, 0x12, 0xE2, 0x36,
    0xEF, 0x2C, 0x38, 0x8D,
    0x07, 0xFF, 0x41, 0x1F,
    0x22, 0x86, 0x53, 0x1F,
    0xA6, 0xF4, 0x2D, 0x11,
    0x20, 0x76, 0xDC, 0xE6,
    0x11, 0xA2, 0x19, 0x2A,
])

解法就是反过来:

decrypt block0 with key1 = Rc4_1s_very_S@f3
decrypt block1 with key2 = Rc4_n0t_5@f3_N0w
拼接 32 字节

得到:

b84d3ba0cq_3e286f44ab5_cCd591d25

补充:这个题当时是用来练upx手脱壳的,在ida中一直找running的函数直到跳到OEP后p冲定义函数就行
本人刚学逆向不久,有什么错误的地方请指出,感谢!