标 题:Armadillo 2.52加壳原理分析和改进的脱壳方法
(12千字)
发信人:leo_cyl1
时 间:2002-4-18
17:08:58
详细信息:
目标软件:The Armadillo Software Protection System Version2.52 build 1164
目标文件:Armadillo.exe
加壳方式:Armadillo 2.52
使用工具:WinDbg或trw2000, peditor, WinHex 10.2 SR-2,m$的win32 sdk文档,<winnt.h>
URL: http://www.siliconrealms.com/
本文作者:leo_cyl
Armadillo 的保护方式主要有以下特点:修改pe头,使WinDbg等调试器无法“attach”,使procdump无法dump;anti-debug代码(我以前的贴子有讨论);利用调试器捕获页面保护异常,分段解码。
(一)捕获页面保护异常、找OEP
和以前版本一样,Armadillo 外壳产生另外一个进程(被加壳的程序)。所以下断点“bpx createprocessa”
004C39E4 call ds:CreateProcessA
004C39EA test eax, eax
004C39EC jnz 4C39F5
……
……
0187:004C3A0F 51 PUSH ECX
0187:004C3A10 68E6314C00 PUSH DWORD 004C31E6
0187:004C3A15 6A00 PUSH BYTE +00
0187:004C3A17 6A00 PUSH BYTE +00
0187:004C3A19 FF1554A04C00 CALL `KERNEL32!CreateThread`
0187:004C3A1F 6A00 PUSH BYTE +00
0187:004C3A21 FF1530A14C00 CALL `KERNEL32!GetModuleHandleA`
……
……
……
0187:004C3A42 6A00 PUSH BYTE +00
0187:004C3A44 FF1530A14C00 CALL `KERNEL32!GetModuleHandleA`
……
……
0187:004C3A68 E80C0C0000 CALL 004C4679
0187:004C3A6D 83C40C ADD ESP,BYTE +0C
0187:004C3A70 8B8D68FEFFFF MOV ECX,[EBP+FFFFFE68]
0187:004C3A76 51 PUSH ECX
0187:004C3A77 8B157CE54C00 MOV EDX,[004CE57C]
0187:004C3A7D 52 PUSH EDX
0187:004C3A7E E8BA0C0000 CALL 004C473D
以上代码在被加壳的程序第一条指令处写入“eb fe”代码,即“jmp eip” 使被加壳程序“挂起”
0187:004C3A83 83C408 ADD ESP,BYTE +08
0187:004C3A86 A17CE54C00 MOV EAX,[004CE57C]
0187:004C3A8B 8B4804 MOV ECX,[EAX+04]
0187:004C3A8E 51 PUSH ECX
0187:004C3A8F FF158CA04C00 CALL `KERNEL32!ResumeThread`
0187:004C3A95 8B157CE54C00 MOV EDX,[004CE57C]
0187:004C3A9B 8B4208 MOV EAX,[EDX+08]
0187:004C3A9E 50 PUSH EAX
0187:004C3A9F FF1588A04C00 CALL `KERNEL32!DebugActiveProcess`
0187:004C3AA5 8B0D7CE54C00 MOV ECX,[004CE57C]
0187:004C3AAB 8B5104 MOV EDX,[ECX+04]
0187:004C3AAE 52 PUSH EDX
0187:004C3AAF FF1584A04C00 CALL `KERNEL32!SuspendThread`
以上代码通过调用“DebugActiveProcess”开始调试被加壳的程序。
……
……
……
0187:004C3AFA 68E8030000 PUSH DWORD 03E8 《===== dwMilliseconds
0187:004C3AFF 8B9574FEFFFF MOV EDX,[EBP+FFFFFE74]
0187:004C3B05 52 PUSH EDX 〈==== lpDebugEvent
0187:004C3B06 FF1580A04C00 CALL `KERNEL32!WaitForDebugEvent`
0187:004C3B0C 85C0 TEST EAX,EAX
0187:004C3B0E 0F8414060000 JZ NEAR 004C4128
0187:004C3B14 C78570FDFFFF68B4+MOV DWORD [EBP+FFFFFD70],004CB468
0187:004C3B1E 33C0 XOR EAX,EAX
0187:004C3B20 A06EE54C00 MOV AL,[004CE56E]
0187:004C3B25 85C0 TEST EAX,EAX
0187:004C3B27 7565 JNZ 004C3B8E
0187:004C3B29 8B4DE4 MOV ECX,[EBP-1C]
0187:004C3B2C 81E1FF000000 AND ECX,FF
0187:004C3B32 85C9 TEST ECX,ECX
0187:004C3B34 7458 JZ 004C3B8E 〈==跳转
0187:004C3B36 C645E400 MOV BYTE [EBP-1C],00
0187:004C3B3A C745FC00000000 MOV DWORD [EBP-04],00
0187:004C3B41 669C PUSHFW +
0187:004C3B43 6658 POP AX |
0187:004C3B45 66056200 ADD AX,62 |设置单步调试标志
0187:004C3B49 66059E00 ADD AX,9E |anti-debug???
0187:004C3B4D 6650 PUSH AX |
0187:004C3B4F 669D POPFW +
0187:004C3B51 66050100 ADD AX,01
0187:004C3B55 C745FCFFFFFFFF MOV DWORD [EBP-04],FFFFFFFF
0187:004C3B5C EB30 JMP SHORT 004C3B8E
以上进入调试循环,根据DebugEvent结构中的debugging event code 做相应处理。其中最重要的是EXCEPTION_DEBUG_EVENT(0x00000001)。请参考MSDN中WaitForDebugEvent的描述。
……
……
0187:004C3BA5 MOV DWORD [EBP+FFFFFB60],80010001
0187:004C3BAF MOV EDX,[EBP+FFFFFE74] 〈====注意!edx = lpDebugEvent
0187:004C3BB5 CMP DWORD [EDX],BYTE +01 //EXCEPTION_DEBUG_EVENT
0187:004C3BB8 JNZ NEAR 004C3ED7
0187:004C3BBE MOV DWORD [EBP+FFFFFD70],004CB470
0187:004C3BC8 XOR EAX,EAX
0187:004C3BCA MOV AL,[004CE56F]
0187:004C3BCF TEST EAX,EAX
0187:004C3BD1 JZ NEAR 004C3CB7
0187:004C3BD7 MOV ECX,[EBP+FFFFFE74]
0187:004C3BDD CMP DWORD [ECX+0C],80000001 //STATUS_GUARD_PAGE_VIOLATION
0187:004C3BE4 JNZ NEAR 004C3CB7
0187:004C3BEA MOV DWORD [EBP+FFFFFD70],004CB488
0187:004C3BF4 MOV EDX,[EBP+FFFFFE74] 〈====注意!edx = lpDebugEvent
0187:004C3BFA MOV EAX,[EDX+24]
……
……
0187:004C3BB5 处比较是否是EXCEPTION_DEBUG_EVENT。(包括页面异常)
注意在NT下,页面异常的代码是0x80000001(STATUS_GUARD_PAGE_VIOLATION)而9x下是0xc0000005(STATUS_ACCESS_VIOLATION)所以在9x下会来到这里:
0187:004C3CBD CMP DWORD [ECX+0C],C0000005 //STATUS_ACCESS_VIOLATION
0187:004C3CC4 JNZ NEAR 004C3E89
0187:004C3CCA MOV EDX,[EBP+FFFFFE74] 〈====注意!edx = lpDebugEvent
0187:004C3CD0 CMP DWORD [EDX+5C],BYTE +00
0187:004C3CD4 JZ 004C3CE2
0187:004C3CD6 MOV DWORD [EBP+FFFFFD70],004CB4E8
0187:004C3CE0 JMP SHORT 004C3CEC
当被加壳的程序需要解码时,会触发页面异常,来到上述代码,如果你参考MSDN中有关DebugEvent结构的描述,很容易找到产生页面异常的地址,是在lpDebugEvent+0x18处。(即edx+18)。
这有什么用呢?呵呵……这是找OEP的关键!!!假如在0187:004C3CD0下断点,当程序第一次在这里中断时,查看edx+18h的内容“dd edx+18”即可看到OEP。(41EF80H)
继续跟踪……
0187:004C3D52 8B8D50FBFFFF MOV ECX,[EBP+FFFFFB50]
0187:004C3D58 83E901 SUB ECX,BYTE +01
0187:004C3D5B 85C9 TEST ECX,ECX
0187:004C3D5D 7C14 JL 004C3D73
0187:004C3D5F 6A01 PUSH BYTE +01
0187:004C3D61 8B9550FBFFFF MOV EDX,[EBP+FFFFFB50]
0187:004C3D67 83EA01 SUB EDX,BYTE +01
0187:004C3D6A 52 PUSH EDX
0187:004C3D6B E8D0030000 CALL 004C4140 //关键,进入
0187:004C3D70 83C408 ADD ESP,BYTE +08
0187:004C3D73 8B8550FBFFFF MOV EAX,[EBP+FFFFFB50]
0187:004C3D79 83C001 ADD EAX,BYTE +01
0187:004C3D7C 3B0588E54C00 CMP EAX,[004CE588]
0187:004C3D82 7D14 JNL 004C3D98
以上代码将产生页面异常的地址进行页对齐,并CALL 004C4140解码。
进入函数004C4140后,来到这里:
0187:004C4271 PUSH BYTE +00
0187:004C4273 MOV ECX,[EBP+08]
0187:004C4276 PUSH ECX
0187:004C4277 CALL 004C432A 〈==这里对异常页解码
0187:004C427C ADD ESP,BYTE +08
0187:004C427F AND EAX,FF
0187:004C4284 TEST EAX,EAX
0187:004C4286 JNZ 004C428F〈===解码成功?
0187:004C4288 XOR AL,AL
0187:004C428A JMP 004C4326
0187:004C428F MOV EDX,[004CE58C] 〈==注意!!!
0187:004C4295 ADD EDX,BYTE +01
0187:004C4298 MOV [004CE58C],EDX
0187:004C429E MOV EAX,[004CE588]
0187:004C42A3 LEA ECX,[EAX*4+FFFFFFFC]
注意0187:004C428F处,[004CE58C]是一个记数器,解码成功后加一,有什么用呢?看后面……
……
……
0187:004C42D1 25FF000000 AND EAX,FF
0187:004C42D6 85C0 TEST EAX,EAX
0187:004C42D8 754A JNZ 004C4324
0187:004C42DA 8B0D8CE54C00 MOV ECX,[004CE58C]
0187:004C42E0 3B0D2CB44C00 CMP ECX,[004CB42C]
0187:004C42E6 7E3C JNG 004C4324
0187:004C42E8 6A01 PUSH BYTE +01
0187:004C42EA 8B158CE54C00 MOV EDX,[004CE58C]
当解码一定数量的页面后,将以前解码的页面重新加密!在这里的数量是0x13页。为了以后脱壳方便。
我用peditor计算出0187:004C428F的实际偏移,然后用WinHex,将ADD EDX,BYTE +01改为ADD EDX,BYTE +00;只改了一个byte。
简要分析0187:004C4277处的解码函数“ CALL 004C432A”。以下是它的流程:
1。调用VirtualProtectEx将异常页面属性改为PAGE_READWRITE
2。调用ReadProcessMemory,将异常页面读入
3。解码。
4。调用WriteProcessMemory,写入正确代码。
5。调用VirtualProtectEx将页面属性改为PAGE_EXECUTE_READ
(二)修改PE头
被加壳的程序运行后,用procdump没法dump出,会非法操作。用ring3的调试器没法“attach”,用peditor dump出的文件是无效的exe。为什么呢?通过比较dump出的PE头,发现偏移0x3c处被修改。重新装入Armadillo,下断点“bpm 400003c w”,执行……
0187:00A8EF54 MOV EAX,[EBP-0C] 〈== EAX=400000H
0187:00A8EF57 LEA ECX,[EBP-04]
0187:00A8EF5A SUB EBX,EDI 〈=== EBX = 4E7119
0187:00A8EF5C PUSH ECX
0187:00A8EF5D ADD [EAX+3C],EBX 〈== 修改PE头,[EAX+3C] = 40003C
0187:00A8EF60 PUSH DWORD [EBP-04]
0187:00A8EF63 PUSH BYTE +40
……
……
……
0187:00A8EF89 AND EAX,BYTE +03
0187:00A8EF8C LEA ECX,[EBP-08]
0187:00A8EF8F INC EAX
0187:00A8EF90 ADD [EDI+06],AX 〈==修改PE头,[EDI+06] = 4000FE
0187:00A8EF94 CALL 00A88597
0187:00A8EF99 MOV EBX,EAX
共修改3C和FE两处,参考有关PE头的资料,可知偏移3C的DWORD是PE表头的偏移(原值是F8)。偏移FE的WORD是sections 的个数(原值是8)。Armadillo故意将PE头改错,难怪没法dump出。
(三)dump出加壳的程序
好了,原理分析清楚了。要dump出加壳程序就很容易了。
事先用peditor查看,得知size of image 是104000;
首先找到OEP是41EF80,另外修改004C428F,将ADD EDX,BYTE +01改为ADD EDX,BYTE +00。
在41EF80下断点。中断后,将PE头改回,40003C处是000000F8,4000FE处是0008,注意高位在前!!!
在程序中找一个没用到的地址。不要选代码段,因为解码后会覆盖掉我们写的代码。我选.Data1段的最后256个byte即4CF000处,将eip改为4CF000,输入以下代码:
push esi
push ecx
push eax
mov esi,401000
mov ecx,103000 //size of image 减 1000h
rep lodsb
pop eax
pop ecx
pop esi
int3
注意我没有使用hying的代码,因为要申请内存和查找api地址,而且内存不够的话也麻烦,其实只要扫描一遍内存就足以触发调试器了,修改004C428F的原因就是防止外壳把解码过的页面从新加密。
f5执行,在int3处停下(注意打开 i3here on),将eip改回41EF80,可看到已经解码了,挂起进程,回到window桌面,这时用什么工具都可以dump出了!(任我鱼肉了!!)。
另外,还有一个地方要注意,最好在OEP处挂起进程再dump,因为这时程序的初始化变量还没有被修改。
大家可对比一下,当程序运行后,用procdump dump出的文件和在OEP处dump出的有何不同。(会非法操作!!因为初始化变量[438110]已经不同了。)
(四)修复IAT
没什么方便的方法!:(
我的方法是用peditor查看,得知IAT在426000处,下断点“bpm 426000 w” 在这停下:
0187:00A8E4BB 8A06 MOV AL,[ESI]
0187:00A8E4BD 3AC3 CMP AL,BL
0187:00A8E4BF 7468 JZ 00A8E529
0187:00A8E4C1 3CFF CMP AL,FF
0187:00A8E4C3 7537 JNZ 00A8E4FC
0187:00A8E4C5 668B7E01 MOV DI,[ESI+01]
0187:00A8E4C9 46 INC ESI
0187:00A8E4CA 0FB7C7 MOVZX EAX,DI
0187:00A8E4CD 50 PUSH EAX
0187:00A8E4CE 46 INC ESI
0187:00A8E4CF FF75F4 PUSH DWORD [EBP-0C]
0187:00A8E4D2 46 INC ESI
0187:00A8E4D3 E84075FFFF CALL 00A85A18
然后把[ESI]前后的内容保存下来,以后手工修复IAT时参考。
(五)小结
无论用什么方法脱壳,原理和hying的是一样的。
但为什么用procdump 不会触发调试器呢?我的解释是这样:无论procdump 或 WinHex 在读取其他进程的内容时,是通过WriteProcessMemory来完成的,而WriteProcessMemory最终调用VMM完成读取,VMM是在ring0上,所以不会触发ring3上的调试器。我能想到的方法是:在ring3下、在自身进程空间内扫描整个内存,以触发调试器。
附录:
附上部分DEBUG_EVENT结构的声明:(注释是我加的)
typedef struct _DEBUG_EVENT {
DWORD dwDebugEventCode; //偏移0x0
DWORD dwProcessId; //偏移0x4
DWORD dwThreadId; //偏移0x8
union {
EXCEPTION_DEBUG_INFO Exception; //偏移0xC
CREATE_THREAD_DEBUG_INFO CreateThread;
CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
EXIT_THREAD_DEBUG_INFO ExitThread;
EXIT_PROCESS_DEBUG_INFO ExitProcess;
LOAD_DLL_DEBUG_INFO LoadDll;
UNLOAD_DLL_DEBUG_INFO UnloadDll;
OUTPUT_DEBUG_STRING_INFO DebugString;
RIP_INFO RipInfo;
} u;
} DEBUG_EVENT, *LPDEBUG_EVENT;
typedef struct _EXCEPTION_DEBUG_INFO { //即异常代码是0x00000001时的结构
EXCEPTION_RECORD ExceptionRecord; //偏移0xC;0x80000001为STATUS_GUARD_PAGE_VIOLATION(NT)
//0xC0000005为STATUS_ACCESS_VIOLATION(WIN (win 9x)
DWORD dwFirstChance;
} EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode; //偏移0xc
DWORD ExceptionFlags; //偏移0x10
struct _EXCEPTION_RECORD *ExceptionRecord; //偏移0x14
PVOID ExceptionAddress; //偏移0x18
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD, *PEXCEPTION_RECORD;
| | |||
|