使用CodeReason进行原生程序语义分析:逆向工程与反混淆实战

本文介绍了CodeReason机器码分析框架在原生x86和ARM代码语义分析中的应用,通过实际案例展示了如何利用该工具解析混淆字符串、简化代码语义,并实现自动化反混淆功能,为逆向工程提供强大支持。

使用CodeReason进行原生程序语义分析

你是否曾想过对原生模式程序进行查询,了解哪些程序位置会将特定值写入寄存器?或者想要自动反混淆被混淆的字符串?

逆向工程原生程序涉及在低层次理解其语义,直到功能的高层次图景浮现。系统化理解原生模式程序的一个挑战是,这种理解必须扩展到程序使用的每一条指令。你的分析必须知道哪些指令对内存调用和寄存器有什么影响。

我们想介绍CodeReason,这是我们为DARPA Cyber Fast Track制作的机器码分析框架。CodeReason提供了一个分析原生x86和ARM代码语义的框架。我们喜欢CodeReason,因为它为我们提供了一个平台,可以查询原生代码对整体程序状态的影响。CodeReason通过深入理解原生指令的语义来实现这一点。

构建这种语义理解既耗时又昂贵。现有系统存在进入门槛高、无法精确满足需求,或者没有对其语义应用简化和优化的问题。我们想要这样做,因为这些简化可以将复杂的优化减少为易于理解的简单表达式。为了说明这一点,我们将给出一个使用CodeReason的示例。

简化Flame

当Flame恶意软件被曝光时,其部分二进制文件被发布到malware.lu。它们的整体方案是将混淆字符串存储在全局数据的结构中。该结构如下所示:

1
2
3
4
5
6
struct ObfuscatedString {
  char padding[7];
  char hasDeobfuscated;
  short stringLen;
  char string[];
};

每个结构末尾都有可变长度数据,其中7字节数据显然未使用。

这里有两个有趣的地方。首先,我使用CodeReason用C语言编写了一个字符串反混淆器。原始程序逻辑分三步执行字符串反混淆。

第一个函数检查hasDeobfuscated字段,如果为零,则返回字符串第一个元素的指针。如果该字段不为零,它将调用第二个函数,然后将hasDeobfuscated设置为零。

第二个函数将迭代’string’数组中的每个字符。对于每个字符,它将调用第三个函数,然后从字符串数组中的字符减去第三个函数返回的值,将结果写回数组。看起来像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void inplace_buffer_decrypt(unsigned char *buf, int len) {
  int counted = 0;
  while( counted < len ) {
    unsigned char *cur = buf + counted;
    unsigned char newChar = get_decrypt_modifier_f(counted);
    *cur -= newChar;
    ++counted;
  }
  return;
}

那么第三个函数’get_decrypt_modifier’呢?这个函数只有一个基本块,看起来像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
lea ecx, [eax+11h]
add eax, 0Bh
imul ecx, eax
mov edx, ecx
shr edx, 8
mov eax, edx
xor eax, ecx
shr eax, 10h
xor eax, edx
xor eax, ecx
retn

拥有原生代码语义理解系统的一个优势是,我可以捕获这个块并将其提供给CodeReason,让它告诉我’eax’的方程是什么样子。这将告诉我这个块向其调用者"返回"什么,并让我在我的反混淆器中捕获get_decrypt_modifier的语义。

也可以将这个片段反编译为C,但我真正关心的是代码对’eax’的影响,而不是像C反编译器视角中代码"看起来"那样的高层次内容。C反编译器也使用语义翻译器,但随后通过尝试翻译为C来代理该翻译的结果。CodeReason让我们跳过最后一步,只考虑语义,这有时可能更强大。

使用CodeReason

从CodeReason获取这个看起来像这样:

1
2
3
4
5
6
7
$ ./bin/VEEShell -a X86 -f ../tests/testSkyWipe.bin
blockLen: 28
r
...
EAX = Xor32[ Xor32[ Shr32[ Xor32[ Shr32[ Mul32[ Add32[ REGREAD(EAX), I:U32(0xb) ], Add32[ REGREAD(EAX), I:U32(0x11) ] ], I:U8(0x8) ], Mul32[ Add32[ REGREAD(EAX), I:U32(0xb) ], Add32[ REGREAD(EAX), I:U32(0x11) ] ] ], I:U8(0x10) ], Shr32[ Mul32[ Add32[ REGREAD(EAX), I:U32(0xb) ], Add32[ REGREAD(EAX), I:U32(0x11) ] ], I:U8(0x8) ] ], Mul32[ Add32[ REGREAD(EAX), I:U32(0xb) ], Add32[ REGREAD(EAX), I:U32(0x11) ] ] ]
...
EIP = REGREAD(ESP)

这很酷,因为如果我实现Xor32、Mul32、Add32和Shr32的函数,我在C语言中就有了这个函数,像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned char get_decrypt_modifier_f(unsigned int a) {
return Xor32(
Xor32(
Shr32(
Xor32(
Shr32(
Mul32(
Add32( a, 0xb),
Add32( a, 0x11) ),
0x8 ),
Mul32(
Add32( a, 0xb ),
Add32( a, 0x11 ) ) ),
0x10 ),
Shr32(
Mul32(
Add32( a, 0xb ),
Add32( a, 0x11 ) ),
0x8 ) ),
Mul32(
Add32( a, 0xb ),
Add32( a, 0x11 ) ) );
}

这也很酷,因为它有效:

1
2
C:\code\tmp>skywiper_string_decrypt.exe
CreateToolhelp32Snapshot

我们正在将CodeReason扩展为IDA插件,允许我们直接从IDA进行这些查询,这应该真的很酷!

第二个有趣的地方是,这个字符串反混淆器存在竞态条件。如果两个线程同时尝试反混淆同一个字符串,它们将永远损坏该字符串。如果你试图对混淆字符串做重要的事情,这可能会很糟糕,因为它会导致将错误数据传递给系统服务或其他东西,这可能产生非常糟糕的影响。

我使用CodeReason攻击了像这样实现的字符串混淆:

1
2
3
4
xor eax eax
push eax
sub eax, 0x21ece84
push eax

其中原生指令序列会将非字符串立即值转换为字符串值(通过巧妙使用二进制补码算术的语义),然后以正确的顺序将它们推入堆栈,从而在每次反混淆代码运行时动态构建字符串。CodeReason能够查看这一点,并使用一个非常简单的针孔优化器,将代码转换为字符串立即值的内存写入序列,如:

1
2
MEMWRITE[esp] = '.dll'
MEMWRITE[esp-4] = 'nlan'

结论

将机器码置于可以优化和理解的形式中可能非常强大!特别是当这可以从程序库中获得时。使用CodeReason,我们能够提取字符串混淆函数的语义,并自动实现字符串反混淆器。此外,我们能够将混淆代码简化为表达反混淆字符串值本身的形式。我们计划在未来的博客文章中介绍CodeReason的其他用途和能力。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计