Anti Reverse Engineering Uncovered
简译:nbw
nbw.07cn.com
作者:Nicolas Brulez*
E-Mail: 0x90@Rstack.org
Received: 07. Mar. 2005, Accepted: 12. Mar. 2005, Published: 13. Mar. 2005
译注:作者写的不错。我翻译的很垃圾,也没翻译完,大家凑和着看,最好看原文更好。
Abstract
概述
相比在二进制形式分析文件,我更喜欢展示一些更具挑战性的技术,以及如何去实践他们。这篇文章站在软件保护的角度说明问题。你可学到一些关于保护/反保护数据的方法。很多保护方面的弱点都在这里被说明。
关键词:软件保护;逆向代码工程;Linx;反调试;反反调试
1. 介绍
本次的目标是通过逆向项工程,分析一个未知文件,并且改进一些从安全社区学来的方法,工具和曾经用到的分析过程。这看起来有点像SotM32,但是这些需要被分析的文件被处理过,以增加分析困难,防止被逆向分析。
技术水平:高级/专业水准
关于这个被分析的文件,目前可以说的是该文件在WInxp系统中建立,然后交给你去分析。你应该尽可能分析出来文件的信息和他的内部工作原理,以及该文件要完成的功能。而我的目标就是教会你去分析这些被加了很多保护的文件。这些技术也可以利用在其他方面。
2. 列举和解释一些反逆向和文件保护技术
很多技术被用来减慢或者中断逆向分析工具的工作。
修改PE头
很多文件的PE头被修改以欺扰分析工具。下面说一下一些比较重要的修改:
->Optional Header
Magic: 0x010B (HDR32_MAGIC)
MajorLinkerVersion: 0x02
MinorLinkerVersion: 0x19 -> 2.25
SizeOfCode: 0x00000200
SizeOfInitializedData: 0x00045400
SizeOfUninitializedData: 0x00000000
AddressOfEntryPoint: 0x00002000
BaseOfCode: 0x00001000
BaseOfData: 0x00002000
ImageBase: 0x00DE0000 <--- "Non Standard" ImageBase
SectionAlignment: 0x00001000
FileAlignment: 0x00001000
MajorOperatingSystemVersion: 0x0001
MinorOperatingSystemVersion: 0x0000 -> 1.00
MajorImageVersion: 0x0000
MinorImageVersion: 0x0000 -> 0.00
MajorSubsystemVersion: 0x0004
MinorSubsystemVersion: 0x0000 -> 4.00
Win32VersionValue: 0x00000000
SizeOfImage: 0x00049000
SizeOfHeaders: 0x00001000
CheckSum: 0x00000000
Subsystem: 0x0003 (WINDOWS_CUI)
DllCharacteristics: 0x0000
SizeOfStackReserve: 0x00100000
SizeOfStackCommit: 0x00002000
SizeOfHeapReserve: 0x00100000
SizeOfHeapCommit: 0x00001000
LoaderFlags: 0xABDBFFDE <--- Bogus Value
NumberOfRvaAndSizes: 0xDFFFDDDE <--- Bogus Value
标准的win32程序文件基址通常是400000,并且逆向工程也采用这个默认值来分析。虽然这并不是程序自身的保护,但是这个简单的修改的确可以混淆逆向分析。
反 OllyDbg:
修改LoaderFlags 和 NumberOfRvaAndSizes..我逆向分析了OllDBG和SoftICE,发现了一些可以减缓他们工作的技巧。更改了上面说的2个标记,可以导致OD认为文件基址错误,然后直接运行该文件而不是中断在入口点。有时候这是很糟糕的事情。
反Soft ICe:蓝屏死机
修改NumberOfRvaAndSizes 字段可以让现在版本的Soft ICE重起计算机。反汇编Soft ICE的PE加载器,我发现一个很严重的漏洞可以导致运行Soft ICE不报错而直接是系统崩溃。这个漏洞已经被报告给软件公司,并且下个版本将被修正。
这里是SoftICE的PE加载器的反汇编结果,可以从中找到为什么会重启系统:
.text:000A79FE
.text:000A79FE loc_A79FE: ; CODE XREF:
sub_A79B9+31j
.text:000A79FE ; sub_A79B9+3Cj
.text:000A79FE ; DATA XREF:
.text:00012F9Bo
.text:000A79FE sti
.text:000A79FF mov esi, ecx
.text:000A7A01 mov ax, [esi]
.text:000A7A04 cmp ax, 'ZM'
.text:000A7A08 jnz not_PE_file
.text:000A7A08
.text:000A7A0E mov edi, [esi+_IMAGE_DOS_HEADER.e_lfanew]
.text:000A7A11 add edi, esi
.text:000A7A13 mov ax, [edi]
.text:000A7A16 cmp ax, 'EP'
.text:000A7A1A jnz not_PE_file
.text:000A7A1A
.text:000A7A20 movzx ecx,
[edi+IMAGE_NT_HEADERS.FileHeader.NumberOfSections]
.text:000A7A24 or ecx, ecx
.text:000A7A26 jz not_PE_file
.text:000A7A26
.text:000A7A2C mov eax,
[edi+IMAGE_NT_HEADERS.OptionalHeader.NumberOfRvaAndSizes]
.text:000A7A2F lea edi,
[edi+eax*8+IMAGE_NT_HEADERS.OptionalHeader.DataDirectory]
.text:000A7A33 mov eax, ecx
.text:000A7A35 imul eax, 28h
.text:000A7A38 mov al, [eax+edi] ; 很严重的BUG,别人可以让EAX+EDI指向0,在Ring 0权限里面读取地址0处的内容可不是一件好事。;-)
.text:000A7A3B
.text:000A7A3B loc_A7A3B: ; DATA XREF:
.text:00012FA5o
.text:000A7A3B cli
.text:000A7A3C call sub_15C08
.text:000A7A3C
.text:000A7A41 mov byte_FA259, 0
.text:000A7A48 push eax ; 保存EAX
.text:000A7A49 mov eax, dword_16B56F ; 将一个保存的数据放入EAX
.text:000A7A4E mov dr7, eax ; dr7 = eax
.text:000A7A51 pop eax ; 恢复EAX
.text:000A7A52 mov dword_FC6CC, esp
.text:000A7A58 mov esp, offset unk_FBABC
.text:000A7A5D and esp, 0FFFFFFFCh
.text:000A7A60 xor al, al ; AL设定为0么?那么上面的 mov al, [eax+edi] 有什么用呢?我没找到其他合理解释
.text:000A7A62 call sub_4D2EB
.text:000A7A62
.text:000A7A67 call sub_36AC1
.text:000A7A67
.text:000A7A6C xor edx, edx
.text:000A7A6E
.text:000A7A6E loc_A7A6E: ; CODE XREF:
sub_A79B9+124j
.text:000A7A6E call sub_74916
.text:000A7A6E
根据上面的分析,可以让SoftICE读取内存地址0处的数据,或者修改PE结构,让它读取我们设定的地址。如果程序中对这个地方需要读取的正确内容进行验证,那么可以觉察到正在被分析。我不会说如何修改文件中的那个字段,因为这里毕竟只是说明原理,并且我不想别人利用这个做坏事。
你只需要修改一下PE头,就可以修正这个技巧。NumberOfRvaAndSizes 标准的数据为0x10。将这个字段修改正确,SoftICE就不会崩溃。对于OD,把上面说的LoaderFlags 和 NumberOfRvaAndSizes字段修改正确就可以,也可以将他们改成0。
修改区段以终止一些工具
->区段
1. item:
Name: CODE
VirtualSize: 0x00001000
VirtualAddress: 0x00001000
SizeOfRawData: 0x00001000
PointerToRawData: 0x00001000
PointerToRelocations: 0x00000000
PointerToLinenumbers: 0x00000000
NumberOfRelocations: 0x0000
NumberOfLinenumbers: 0x0000
Characteristics: 0xE0000020
(CODE, EXECUTE, READ, WRITE)
2. item:
Name: DATA
VirtualSize: 0x00045000
VirtualAddress: 0x00002000
SizeOfRawData: 0x00045000
PointerToRawData: 0x00002000
PointerToRelocations: 0x00000000
PointerToLinenumbers: 0x00000000
NumberOfRelocations: 0x0000
NumberOfLinenumbers: 0x0000
Characteristics: 0xC0000040
(INITIALIZED_DATA, READ, WRITE)
3. item:
Name: NicolasB
VirtualSize: 0x00001000
VirtualAddress: 0x00047000
SizeOfRawData: 0xEFEFADFF <--- 非常大的数据
PointerToRawData: 0x00047000
PointerToRelocations: 0x00000000
PointerToLinenumbers: 0x00000000
NumberOfRelocations: 0x0000
NumberOfLinenumbers: 0x0000
Characteristics: 0xC0000040
(INITIALIZED_DATA, READ, WRITE)
4. item:
Name: .idata
VirtualSize: 0x00001000
VirtualAddress: 0x00048000
SizeOfRawData: 0x00001000
PointerToRawData: 0x00047000
PointerToRelocations: 0x00000000
PointerToLinenumbers: 0x00000000
NumberOfRelocations: 0x0000
NumberOfLinenumbers: 0x0000
Characteristics: 0xC0000040
(INITIALIZED_DATA, READ, WRITE)
根据上面的数据,可以得出一些结论。首先,看起来文件没有被压缩,因为NicolasB 区段的文件地址和大小是相符合的。但是这个非常大的数据可以导致一些工具崩溃或者降低分析效率。
由于IDA认为这个区段有这么大,所以会试图申请一个这么大的内存,导致系统变的非常慢 -)。最终,他会加载文件,或者导致内存溢出,这些取决于你的电脑配置。
这种修改方法也会让其他一些工具带来分析麻烦,例如Objdump,PE editor,以及一些内存转存工具等等。这种手段也很容易被修复,你只需要把区段的大小更正过来。通过观察程序加载后区段的大小和这些字段的关系,可以发现有些区段是多余的,那么可以把那些区段的大小属性填充为0。
发现手段
在写程序时候,我知道有些人会更改我的文件的PE头,那么我就把这些PE头的字段作为文件保护的密钥,如果改动了这些字段,文件就无法运行。
另外我还修改了PE头的其他一些字段,但是并没有真正利用。(好像化妆品一样)
冗余代码
为了加强分析难度,我在在文件的真正的有用代码之间添加了一些垃圾代码。这些垃圾代码很长,但是实际上并没有真正做什么事情,只是干扰分析,尤其是静态分析。这些垃圾代码用一些工具生成,并且每段都不一样。
Here is how it looks inside a disassembler:
这里是一段垃圾代码的反汇编:
图1
这段垃圾代码开头是 pushad指令,用于将所有的寄存器状态保存到堆栈中,然后结束时候是popad,用于恢复这些寄存器状态,下面是代码结尾:
图2
对付这手段的方法:
这个保护错误并不完美,至少我上面所说的那种方法是这种情况。由于这种垃圾代码有明显的pushad/popad标记,因此很容易就可以发现。我也注意到了这一点,毕竟这只是一个演示而已。这种情况可以用IDA/OD脚本来处理。这些脚本很有趣,可以自己编写出来剔除垃圾代码的功能。如果你没写过,我建议你用一下。我写这些的时候,已经有了一个更加完善的垃圾代码生成器,他生成的代码不是以pushad/popad作为起始与结束。
SEH - 结构异常处理
SEH也可以被利用。它可以获取当前进程的内容结构,以此来读取或者修改调试寄存器。这些寄存器被用于硬件断点(BPM),如果可以设置他们,就可以取消硬件断点。
利用SEH检测时间
这里是我发现的一些检测调试器的方法。将SEH上下文结构和一些时间检测技术结合起来,可以检测到一些Ring 3级的调试器和跟踪器。具体方法就是采用RDTSC指令(CPU基本指令周期)读取CPU运行周期,然后根据产生的异常来判断是否被调试。
在异常处理时,我们可以在上下文结构中从eax读取RDTSC指令执行结果,从而获取TSC。然后再次使用一下RDTSC命令,获取当前TSC值。比较2次TSC值,就可以判断出来是否被调试或者跟踪。如果是被调试,这2个值的差距很大。发现这种情况,可以修改EIP值,指向上下文结构。程序会返回错误地址,然后就会崩溃。但是由于不同的系统中CPUID指令有差别,返回的值并非存放在eax,而是放在ecx。
这个"bug"导致这种方法有时候会被检测到,但是这个事情目前来说还不是太严重。很多人会问为什么在TDTSC指令前面用CPUID指令。因为在有些CPU中,比如P4,需要用这条指令来强制CPU执行顺序。该指令是指定CPU同步的指令。如果不告诉CPU不使用000执行,就无法确定CPU执行代码的顺序,导致跟程序中原来的意思发生不同。有时候甚至会无故导致程序崩溃。
下面是这种检测方法的代码:
pusha
call install_seh
mov ecx, [esp+40h+var_34]
add [ecx+context.eip], 2
xor eax, eax
mov [ecx+context.Dr0], eax
mov [ecx+context.Dr1], eax
mov [ecx+context.Dr2], eax
mov [ecx+context.Dr3], eax
mov [ecx+context.Dr4], eax
mov [ecx+context.Dr5], eax
mov [ecx+context.Dr6], eax
mov [ecx+context.Dr7], eax
mov eax, [ecx+context.eax]
push eax
cpuid ;保证CPU执行序
rdtsc ;获取Tsc
sub eax, [esp+44h+var_44]
add esp, 4
cmp eax, 0E0000h ;比较返回值
ja short bad_reverse_enginer
xor eax, eax
retn
bad_reverse_enginer:
add [ecx+context.eip], 63h
xor eax, eax
retn
install_seh proc near
xor eax, eax
push Dowrd ptr fs:[eax]
mov fs:[eax], esp
cpuid
rdtsc
xor ebx, ebx
pop dword ptr [ebx]
pop dword ptr fs:0
add esp, 0
popa
E0000h 是设定的最大检测标记。如果这里得到的数据大于这个数值,说明有可能处于被调试状态。
发现手段
我这里采用了一个固定的数据进行检测: E0000h。一般也可以采用一个随机数而不是常数,这样可以防止一些人通过搜索特征码来发现这些地方。我也可以采用一些手段让每处SEH指令都看来不同。不过这种方法最大的缺点是每次使用都无法避免使用一些相同的指令。当然,检测者也可以通过写一个内核模型驱动来捕获每次RDTS异常,从而发现我们做的手脚,具体可以参考Intel手册。
BPX检测
由于我们要使用API函数,所以必须防止这些函数被BPX断点拦截到。除了可以利用GetProceAddress函数获取API地址然后检测该处的int 3的机器码(0xCC)外,我还有另外一种方法。我可以直接读取Import表获取API函数的地址,然后检查该处是否有断点。
逆向人员都对int 3了如指掌。为了更有迷惑性,我检测断点的时候,利用"SHR"指令来产生检测值: 0x66 shr 3 = 0xCC。然后程序检测API函数入口处是否有int3断点。如果发现被段下,我用一种很巧妙的方法让程序崩溃:采用RDTSC指令产生一个随机数,把这个随机数入栈,利用RET指令,达到修改EIP到这个随机的地址。那么程序就会随机崩溃在任意一个地方,这个地方会离检测代码很远,并且Soft ICE的Fault on 指令无法捕获到。(译:真恶毒阿~~~)
对付这种anti的方法:
首先,导入函数是不被保护的,所以别人可以察看导入函数,看到printf, GetCommandLineA 和 ExitProcess函数都被使用。别人可以在这些函数上下断点,或者至少猜测到这些函数会被使用。然后别人会猜测到这里会传来一个command参数。对付这种人,可以通过手工加载Import表来解决。
手工加载Import表,可以利用一个自己写的GetProceAddress函数查找需要导入的函数所在的dll的导出表。由于Kernel32的地址总是被存放在堆栈中,我们可以获取他的ImageBase(或者用PEB,SEH链表等等。。。),然后获取LoadLibaray的地址,以便加载所有函数。这样,有了自己写的GetProceAddress,加上获得的LoadLibrary地址,就可以加载任何别的函数。这样就可以利用import表来引用别的库函数。
但实际上还需要做一点。对于win2k系统,必须给自己的文件做一个import表,里面至少有一个引入函数,哪怕从Kernel32导入也可以。因为Windows 2000的PE Loader和WindowsXP不一样,XP系统允许PE文件没有导入表,而Windows2000不允许。
自己做的小的Import表用来进行欺骗。真正的引入表被加密,并在运行时候才被解压缩。
然后我们只需要模拟操作系统自己加载Import表。我们需要把API地址放到Import表中,逆向者只有追踪引入表的解压代码时候才可以获取我们的函数地址。
这种BPX保护也有自己的弱点。如果这个API函数有很多指令,我只需要检查API入口的开始4个字节,就可以知道是否有断点。如果别人把断点下在这4个字节后面,我可以采用一个用于检测每条指令长度的反汇编引擎(LDE)来区分每条指令的长度,从而避免检查的是错误的地点,具体见下。
由于一般的正常指令也可能含有0xCC字节,比如: Mov eax, 0x4010CC。因为这条指令的机器码也含有0xCC,因此或许会误把这条指令检查成断点。为此,我们可以用一个LDE判断这条指令的长度(5字节),但是int 3指令只有1、2个字节(0xCC或者 0xCD 0x03)。这样就可以正确越过这条mov指令而不检查他。
一旦检测到有BPX断点,我们可以不抱错,而把断点转设在其他API函数中。这样或许会导致别的系统错误,但是却隐藏了我们自己。
未完无续
图1:
图2:
原文:
http://www.codebreakers-journal.com/include/getdoc.php?id=124&article=57&mode=pdf
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!