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

本文介绍了CodeReason框架如何通过深度语义分析原生x86和ARM代码,实现自动反混淆和程序行为查询。通过Flame恶意软件实例,展示了从指令级语义提取到C代码转换的全过程,并演示了优化混淆代码为可读字符串的方法。

语义分析原生程序与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 设计