• 标 题:Armadillo 2.52加壳原理分析和改进的脱壳方法 (12千字)
  • 作 者:leo_cyl1
  • 时 间:2002-4-18 17:08:58
  • 链 接:http://bbs.pediy.com

目标软件: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;