Armadillo3.78带Key双进程手动脱壳
Passion
2010-10-01

拿到一HTTPS的暴力破解软件,带一个和机器有关的合法注册码,只能在别的某一台机器码为9534-57ED的PC上使用。PEID查壳为Armadillo 1.xx - 2.xx -> Silicon Realms Toolworks,找了几个自动脱壳机如dilloDIE等,均无效,卡在那儿没动作。于是尝试手动脱壳。
以前未研究过穿山甲壳,搜了许多资料,费了几天时间,按部就班地脱成功了,写下来记录一下。

工具:PEID、OD、ArmaAFP、LordPE、ImportRec

一、绕过Key

参考资料:
http://hi.baidu.com/csh0w/blog/item/ca8a8f83a9e462b46d811979.html

绕过Key的原理在于拦截程序获得机器码的部分,将其改为我们已知的机器码,这样和已知的注册码配合就能通过而运行。
首先,用ArmaAFP看到Armadillo的保护选项:

!- Protected Armadillo
Protection system (Professional)
!- <Protection Options>
Debug-Blocker
!- <Backup Key Options>
Fixed Backup Keys
!- <Compression Options>
Better/Slower Compression
!- <Other Options>
Use Digital River Edition Keys
Version 3.78 22Sep2004
 
可以看到是Armadillo 3.78版,里头有反调试器与Key选项,其实还有双进程保护,但没写出来。
程序名为https.exe,运行后有两个https.exe进程。原理大概是互相通过一个Mutex来通讯检测对方是否存在。OD中可以通过手动汇编的方式进行双变单,但我直接用了ArmaAFP的双变单功能,打印如下:

!- Child detach
Child process ID: 00001524
Entry point: 005A3243
Original bytes: 558B

它启动了一个进程号是1524的子进程,并通过将入口代码俩字节558B改成“JMP自身”的方式停在入口5A3243。双已经变单了。

下面启动OD附加1524这个进程。因为它带调试器检测,首先得隐藏OD:忽略所有异常,并在异常忽略范围内加入C000001D..C000001E。如果不干这个,程序一跑就会提示有调试器并退出。
设置绕过调试器检测并附加进程后,首先停在入口005A3243,将ArmaAFP改的死循环代码改回558B后这样:
005A3243    55            push    ebp

开始拦截机器码。下断点he GetDlgItem,Shitf+F9运行,中断再运行等出需要注册的提示框,点OK后中断,单步进行。

00CC4689    E8 253CFEFF     call    00CA82B3 在此F7进入

00CA82C2    E8 24D70000     call    00CB59EB 再F7进入

00CB59FF    C2 0800         retn    8

到这儿返回时,eax已经是根据我的机器生成的机器码6837-9958了。这儿手工将EAX改成953457ED,运行。出现注册对话框,里头的机器码果然变成了需要的9534-57ED,于是拿已知的KEY注册,注册成功,能跑了。

二、绕过IAT加密,寻找OEP

参考资料:看雪论坛精华中Armadillo相关文章。

绕过机器Key才是第一步,我们的目的是脱壳,于是开始寻找OEP。寻找OEP本身不难,但Armadillo会对IAT进行加密处理,导致后面dump出的内容无效,因此寻找OEP之前,需要绕过IAT的加密处理,以后dump后才能方便地用ImpRec修复。

重复上述ArmaAFP加OD加载https.exe的步骤,将首俩字节改回558B后,下断he GetModuleHandleA并反复Shift+F9运行(大概8次),直到OD堆栈窗口出现VirtualAlloc和VirtualFree后,寄存器窗口出现两个kernel32.dll字符串并且堆栈里也有个kernel32.dll时,Alt+F9返回。如下:

EAX 001293DC ASCII "kernel32.dll"
ECX 001293E8
EDX 001293DC ASCII "kernel32.dll"
EBX 00000000

0012928C   00CB5CE1  /CALL 到 GetModuleHandleA 来自 00CB5CDB
00129290   001293DC  \pModule = "kernel32.dll"
00129294   00000000

Alt+F9返回,出现Magic Jump,可以在此绕过IAT加密:

00CB5CDB    FF15 B860CD00   call    dword ptr [CD60B8]               ; kernel32.GetModuleHandleA
00CB5CE1    8B0D AC40CE00   mov     ecx, dword ptr [CE40AC]          <====== 返回到这儿
00CB5CE7    89040E          mov     dword ptr [esi+ecx], eax
00CB5CEA    A1 AC40CE00     mov     eax, dword ptr [CE40AC]
00CB5CEF    391C06          cmp     dword ptr [esi+eax], ebx
00CB5CF2    75 16           jnz     short 00CB5D0A
00CB5CF4    8D85 B4FEFFFF   lea     eax, dword ptr [ebp-14C]
00CB5CFA    50              push    eax
00CB5CFB    FF15 BC62CD00   call    dword ptr [CD62BC]               ; kernel32.LoadLibraryA
00CB5D01    8B0D AC40CE00   mov     ecx, dword ptr [CE40AC]
00CB5D07    89040E          mov     dword ptr [esi+ecx], eax
00CB5D0A    A1 AC40CE00     mov     eax, dword ptr [CE40AC]
00CB5D0F    391C06          cmp     dword ptr [esi+eax], ebx
00CB5D12    0F84 2F010000   je      00CB5E47                         <====== 这儿是绕过IAT处理的Magic Jump
00CB5D18    33C9            xor     ecx, ecx

在00CB5D12处手工汇编改为jmp      00CB5E47,然后往下滚,找到:

00CB5E50    83C6 04         add     esi, 4
00CB5E53    395F FC         cmp     dword ptr [edi-4], ebx
00CB5E56  ^ 0F85 49FEFFFF   jnz     00CB5CA5
00CB5E5C    EB 03           jmp     short 00CB5E61                   <====== 在这儿下断
00CB5E5E    D6              salc
00CB5E5F    D6              salc
00CB5E60    8F              ???                                      ; 未知命令

00CB5E5C处的jmp与salc处是IAT加密结束标志,在jmp处下断,Shift+F9运行,断在这儿。回到上面,把jmp的改动恢复成je。至此IAT加密被绕过了。

下面寻找OEP,清除GetModuleHandleA断点后下断he CreateThread,Shift+F9运行一次,断后Alt+F9返回。

00CBC23B    57              push    edi
00CBC23C    FF15 5C61CD00   call    dword ptr [CD615C]               ; kernel32.CreateThread
00CBC242    50              push    eax                              <======= 返回到这儿
00CBC243    FF15 4C62CD00   call    dword ptr [CD624C]               ; kernel32.CloseHandle
00CBC249    5F              pop     edi
00CBC24A    5E              pop     esi
00CBC24B    C9              leave
00CBC24C    C3              retn

再F8从retn返回,到上层:

00CCF5F9    E8 95CBFEFF     call    00CBC193
00CCF5FE    59              pop     ecx                              <======= 返回到这儿
00CCF5FF    BE 98FACD00     mov     esi, 0CDFA98
00CCF604    8BCE            mov     ecx, esi
00CCF606    E8 3393FDFF     call    00CA893E
00CCF60B    84C0            test    al, al
00CCF60D    75 09           jnz     short 00CCF618

单步F8下去几十步,一直到一个CALL ECX的地方:

00CCF709    8B90 90000000   mov     edx, dword ptr [eax+90]
00CCF70F    3350 40         xor     edx, dword ptr [eax+40]
00CCF712    3350 04         xor     edx, dword ptr [eax+4]
00CCF715    2BCA            sub     ecx, edx
00CCF717    FFD1            call    ecx                              ; https.00401000 这儿!!!F7进去!!!
00CCF719    8945 E4         mov     dword ptr [ebp-1C], eax
00CCF71C    8B45 E4         mov     eax, dword ptr [ebp-1C]

00CCF717进去,里头就是OEP,401000。

00401000   /EB 10           jmp     short 00401012                   <======= 程序入口点
00401002   |66:623A         bound   di, dword ptr [edx]              <======= 这几句不是代码,是字符串C++HOOK
00401005   |43              inc     ebx
00401006   |2B2B            sub     ebp, dword ptr [ebx]
00401008   |48              dec     eax
00401009   |4F              dec     edi
0040100A   |4F              dec     edi
0040100B   |4B              dec     ebx
0040100C   |90              nop
0040100D  -|E9 78545000     jmp     0090648A
00401012   \A1 6B545000     mov     eax, dword ptr [50546B]

后来分析出这是C++Builder写的,入口点是一个JMP,后面紧跟ASCII字符串C++HOOK,这是可以用来辨认C++Builder程序的入口点的特征。

三、Dump以及Fix IAT

现在可以DUMP了,用LoadPE找到1524号进程,Full dump之,并在PE Editor中修改其入口点为1000,也就是401000-ImageBase 400000=1000。
照理现在可以直接用ImpRec来修复输入表了,不过启动ImpRec,载入1524号进程后,GetImport不完全。修复出来的没法用。于是继续在OD中手工找IAT的开始和结束地址。

有篇教程说在这种情况下可以往下Ctrl+B查找汇编代码FF25,也就是JMP DWORD PTR[],这种代码通常是API的调用。我们只要找到一个API调用,看它的地址在哪儿,就能找到IAT的所在。掌握这种思想就行。

刚上面断在OEP处,往下翻一两页,看到个API的调用:

0040109B    6A 08           push    8
0040109D    E8 CE2B1000     call    00503C70
004010A2    50              push    eax
004010A3    E8 522C1000     call    00503CFA                         ; jmp 到 ntdll.RtlAllocateHeap
004010A8    0BC0            or      eax, eax
004010AA    75 0A           jnz     short 004010B6

从004010A3处有个CALL,Ctrl+G跑到00503CFA,一看是一堆API的JMP DWORD PTR调用。

00503CE8  - FF25 0CC45300   jmp     dword ptr [53C40C]               ; kernel32.GlobalReAlloc
00503CEE  - FF25 10C45300   jmp     dword ptr [53C410]               ; kernel32.GlobalSize
00503CF4  - FF25 14C45300   jmp     dword ptr [53C414]
00503CFA  - FF25 18C45300   jmp     dword ptr [53C418]               ; ntdll.RtlAllocateHeap
00503D00  - FF25 1CC45300   jmp     dword ptr [53C41C]               ; ntdll.RtlFreeHeap
00503D06  - FF25 20C45300   jmp     dword ptr [53C420]               ; kernel32.InitializeCriticalSection

后面的53C414等就是IAT表的内容!
按长型,地址在数据窗口显示内容如下:

0053C410  7C834DD1  kernel32.GlobalSize
0053C414  00CBA413
0053C418  7C9300C4  ntdll.RtlAllocateHeap
0053C41C  7C92FF2D  ntdll.RtlFreeHeap
0053C420  7C809F91  kernel32.InitializeCriticalSection

往上下翻,可以得到IAT表的起始地址和大小。起始是53C100,结束是53CED4。长度一减得到DD4。
现在启动ImpRec,选这个进程,输入其OEP 1000,IAT起始地址13C100(已经减去了ImageBase 400000),长度DD4,点GetImports,得到一堆Valid NO的。
Show Invalid后把十几个无效指针挨个CUT掉,再Fix dump刚才dump出的文件。保存后双击,正常运行。脱壳成功。