一篇文章带你学会Armadillo脱壳

0x0 前言

         最近在跟进一个APT组织的一次攻击,其中有一个样本使用了Arm加壳,所以花了差不多10多天的时间看看这方面的东西。并总结一下。

         这篇文章主要参考了FLY和刹那恍惚两位大佬的文章。和录制的视频。以及jcyhlh大侠在2008年写下的总结帖。那时候我估计还在玩泥巴呢。这是我写这篇文章的主要参考来源。前人栽树后人乘凉。此外还看了看雪的知识库。基本看了3.x和4.x所有师傅的文章。

         这篇文章的架构,文章架构主要参照了网上下载的视频教程的架构。并对此作出小小修改和注释以及归纳总结。更加方便我等小白学习成长。

         由于文章主要脱去的是3.x和4.x的Arm,可能有一些伪大佬又要说都发了几百遍了还在发。这篇文章适合我等小白,所以伪大佬勿扰。真大佬可以daidaiwo。对此我的处理意见是,把其直接挂在文章起始部分

         最后,加油吧,小伙伴们。

0x1 Armadillo

0x1.1 保护机制

         Armadillo,中文名穿山甲,本意为犰狳,就是下面那个有点可爱的家伙。

         Armadillo主要采用了Debug-Blocker,CopyMem-II, Enable Import Table Elimination,Enable Nanomites Processing,Enable Memory-Patching Protections保护手段。同时也有单双进程之分,造成了保护手段的多样性。

         Debug-Blocker,称为阻止调试器,所谓反调试,基本只要开插件都可以过,所以这也是为什么大家脱穿山甲的时候打开IsProcessDebug去反调试选项和忽略异常的原因。

         CopyMem-II:双进程保护,最常使用的是bp OpenMutexA,然后转到401000 patch代码。另外一种是修改相反跳转的方法。(脚本方法就是不说了)

         Enable Import Table Elimination:IAT保护,修改Magic_Jmp。

         Enable Nanomites Processing就是CC保护,也是Armadillo最强大的保护机制。原理就是就是将程序中的部分代码改写为int3或者向其中插入int3代码。

0x1.2 前期侦壳

         知己知彼百战不殆,在脱壳最重要的就是侦壳。这里需要使用到的工具主要有:PEID(不推荐),exepeinfo,ArmaFP,任务管理器。

         其中,exepeinfo是用于查壳的,任务管理器是用于判断是单进程还是双进程,如果是双进程就需要双转单。ArmaFP是用于判断其保护模式,是标准模式,还是全保护模式(专业模式)。

         不过关于壳的版本,exepeinfo容易误报,所以可以使用这个方法:OD载入程序,下HE OutputDebugStringA断点。shift+F9中断后,看堆栈如果出现如下的,就是4.0以上的壳。这是由于Arm在4.0利用Od在调式保护格式串的消息时会奔溃而新增的反调试技术。

1
2
3
4
5
//4.01
0012EC70 021B580F /CALL 到 OutputDebugStringA 来自 021B5809
0012EC74 0012F5E8 \String = "%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s"
/3.X
//此时程序运行起来。

0x2 Armadillo单进程脱壳

0x2.1 标准单进程Armadillo 3.78 - 4.xx 脱壳

         这是最简单的加密方法,只需要修改Magic_Jmp就可以了,因为这个版本单进程防护只是加密了IAT,(1)只需要绕过加密,(2)并让其解压压缩区段即可。

         绕过IAT加密的方法就是修改Magic_Jmp,这是脱穿山甲壳必须使用的方法。步骤如下:

  • step1:在GetModuleHandle下硬件断点,可以HE GetModuleHandle或者HE GetModuleHandle+5
  • Step2:然后Shift+F9,断下的时候,看堆栈窗口是否存在VirtualAlloc或者VirtualFree,只要出现这两个API函数,就表明快到了。

    1
    2
    3
    4
    5
    000C94C8 |00726DF3 返回到 00726DF3 来自 kernel32.GetModuleHandleA
    000C94CC |0073BC1C ASCII "kernel32.dll"
    000C94D0 |0073CEC4 ASCII "VirtualAlloc"
    000C94D4 |0073FA98
    000C94D8 |77E22270 ntdll.RtlLeaveCriticalSection
  • Step3:继续Shift+F9,只要堆栈出现kernel32.dll,但是不包含任何其他函数名称,表示到达了返回的时机。此时执行到返回ctrl+F9。如何判断之前执行到返回的时机是否正确呢,就是看是否存在LoadLibrary这个API函数。此时就是正确的。

    1
    2
    3
    4
    5
    000C9228 /000C94C8
    000C922C |00715CE1 返回到 00715CE1 来自 kernel32.GetModuleHandleA
    000C9230 |000C937C ASCII "kernel32.dll"
    000C9234 |000CEAB4
    000C9238 |3CBC24B7
1
2
3
4
5
6
7
8
9
10
00715CE1 8B0D AC407400 mov ecx, dword ptr [7440AC]
00715CE7 89040E mov dword ptr [esi+ecx], eax
00715CEA A1 AC407400 mov eax, dword ptr [7440AC]
00715CEF 391C06 cmp dword ptr [esi+eax], ebx
00715CF2 75 16 jnz short 00715D0A
00715CF4 8D85 B4FEFFFF lea eax, dword ptr [ebp-14C]
00715CFA 50 push eax
00715CFB FF15 BC627300 call dword ptr [7362BC] ; kernel32.LoadLibraryA
00715D01 8B0D AC407400 mov ecx, dword ptr [7440AC]
00715D07 89040E mov dword ptr [esi+ecx], eax
  • 此时就可以修改LoadLibrary函数下面的那个条件跳转(00715D12)为jmp,跳转到00425E5C,然后撤销之前的修改。
    1
    2
    3
    4
    5
    6
    7
    8
    00715CF4 8D85 B4FEFFFF lea eax, dword ptr [ebp-14C]
    00715CFA 50 push eax
    00715CFB FF15 BC627300 call dword ptr [7362BC] ; kernel32.LoadLibraryA
    00715D01 8B0D AC407400 mov ecx, dword ptr [7440AC]
    00715D07 89040E mov dword ptr [esi+ecx], eax
    00715D0A A1 AC407400 mov eax, dword ptr [7440AC]
    00715D0F 391C06 cmp dword ptr [esi+eax], ebx
    00715D12 - 0F84 2F010090 je 90715E47
1
2
3
4
00425E53 395F FC cmp dword ptr [edi-4], ebx
00425E56 ^ 0F85 49FEFFFF jnz 00425CA5
00425E5C EB 03 jmp short 00425E61
00425E5E D6 salc
  • Step4:因为外壳肯定需要将存储在某一区段的数据解压到text段,需要对该段进行访问,所以,在内存窗口的程序的text段下访问断点。然后shift+F9。中断在43468F,然后单步,在此代码段的最后一个call ecx处步入就是OEP。
    1
    2
    3
    4
    5
    0043F68F 8B12 mov edx, dword ptr [edx]
    0043F691 8955 DC mov dword ptr [ebp-24], edx
    0043F694 834D FC FF or dword ptr [ebp-4], FFFFFFFF
    0043F698 EB 11 jmp short 0043F6AB
    0043F69A 6A 01 push 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0100739D 6A 70 push 70
0100739F 68 98180001 push 01001898
010073A4 E8 BF010000 call 01007568
010073A9 33DB xor ebx, ebx
010073AB 53 push ebx
010073AC 8B3D CC100001 mov edi, dword ptr [10010CC] ; kernel32.GetModuleHandleA
010073B2 FFD7 call edi
010073B4 66:8138 4D5A cmp word ptr [eax], 5A4D
010073B9 75 1F jnz short 010073DA
010073BB 8B48 3C mov ecx, dword ptr [eax+3C]
010073BE 03C8 add ecx, eax
010073C0 8139 50450000 cmp dword ptr [ecx], 4550
010073C6 75 12 jnz short 010073DA
010073C8 0FB741 18 movzx eax, word ptr [ecx+18]
010073CC 3D 0B010000 cmp eax, 10B
010073D1 74 1F je short 010073F2
010073D3 3D 0B020000 cmp eax, 20B
010073D8 74 05 je short 010073DF
010073DA 895D E4 mov dword ptr [ebp-1C], ebx
010073DD EB 27 jmp short 01007406
010073DF 83B9 84000000 0>cmp dword ptr [ecx+84], 0E

0x2.2 单进程Armadillo v4.x脱壳

         首先需要判断加壳版本是否是4.xxx。关于这点如何判断呢,主要下硬件断点 HE OutputDebugStringA 。在堆栈窗口出现%s%s%s%s的标志,说明这是4.X的壳。

1
2
0012EC70 021C580F /CALL 到 OutputDebugStringA 来自 021C5809
0012EC74 0012F5E8 \String = "%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s"

         关于Armadillo v4.x单进程脱壳把握两点,第一,使用Magic_Jmp避过IAT加密保护,对GetCurrentThreadId下断点找到OEP

         关于第一条,就是上面2.1讲的原则,下面解释第二条。首先对GetCurrentThreadId下断。HE GetCurrentThreadId。查看堆栈窗口,会出现如下结果.中间省略多个,查看关于GetCurrentThreadId都是来自其他模块的调用,但是最后一个是来自程序的调用。这就是程序返回的时机,所以,F8步过,根据之前说的规则,OEP在该程序段最后一个call ecx中。

1
2
3
4
5
6
7
0236FE50 7339352D /CALL 到 GetCurrentThreadId 来自 msvbvm60.73393527
//
0236FE30 76DE1434 /CALL 到 GetCurrentThreadId 来自 adsldpc.76DE142E
//
0236FE78 76F31298 /CALL 到 GetCurrentThreadId 来自 wldap32.76F31292
//....
0012F720 037560EC /CALL 到 GetCurrentThreadId 来自 037560E6

1
2
3
4
5
6
7
8
9
10
11
12
13
0376F70F 3350 40 xor edx,dword ptr ds:[eax+0x40]
0376F712 3350 04 xor edx,dword ptr ds:[eax+0x4]
0376F715 2BCA sub ecx,edx
0376F717 FFD1 call ecx ; NOTEPAD_.004010CC
0376F719 8945 E4 mov dword ptr ss:[ebp-0x1C],eax
0376F71C 8B45 E4 mov eax,dword ptr ss:[ebp-0x1C]
0376F71F 8B4D F0 mov ecx,dword ptr ss:[ebp-0x10]
0376F722 64:890D 0000000>mov dword ptr fs:[0],ecx
0376F729 5F pop edi
0376F72A 5E pop esi
0376F72B 5B pop ebx
0376F72C C9 leave
0376F72D C3 retn

2.3 加 PassWord单进程

         这是就比2.2多了一个密码验证,我们直接绕过密码验证就好。首先Shift+F9运行,通过查看导入表,在GetDlgItem处下断bpx GetDlgItem。然后在输入伪码按OK,程序中断在35359D0处.注意:先运行,在下断!在输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
035349D0 FF15 E8645403 call dword ptr [35464E8] ; USER32.GetDlgItem
035349D6 50 push eax
035349D7 FF15 E0645403 call dword ptr [35464E0] ; USER32.GetWindowTextA
035349DD 8D85 00FFFFFF lea eax, dword ptr [ebp-100]
035349E3 50 push eax
035349E4 E8 35060100 call 0354501E ; jmp 到 msvcrt.strlen
035349E9 85C0 test eax, eax
035349EB 59 pop ecx
035349EC 74 1A je short 03534A08
035349EE 8B0D E01E5503 mov ecx, dword ptr [3551EE0]
035349F4 8D85 00FFFFFF lea eax, dword ptr [ebp-100]
035349FA 50 push eax
035349FB E8 7D77FEFF call 0351C17D ; 比较原始密码
03534A00 84C0 test al, al
03534A02 74 04 je short 03534A08 ; 不是原码,则跳转(不跳)
03534A04 6A 01 push 1
03534A06 EB 4C jmp short 03534A54 ; 回到正常流程
03534A08 33F6 xor esi, esi

         接着修改魔法跳,可以使用bp GetModueHandle或者HE GetModuleHandle。这里发现了kernel32.dll就可以执行到返回查看了。

         然后就是找OEP,这里还是可以使用2.2中对GetProcessId下断。这里介绍个新方法。**可以在内存窗口.text段按F2下断点。因为壳执行完肯定会执行代码段的内容。也就是说代码段是由外壳到源程序的一扇门。所以在此处下断必然成立。

1
2
3
4
5
004010CC 55 push ebp
004010CD 8BEC mov ebp, esp
004010CF 83EC 44 sub esp, 44
004010D2 56 push esi
004010D3 FF15 E4634000 call dword ptr [4063E4] ; kernel32.GetCommandLineA

         总结一下,现在有两个方法可以找OEP。第一是对GetProcessId下断,第二个就是在.text下断。

0x2.4 包含Code Splicing和Import Table Elimination的修复

         Armadillo使用Code Splicing和Import Table Elimination两项技术使得程序修复变得更加困难。幸好有大佬开发了ArmInline工具可以使得修复变得简单一些。注意:本节只将修复,不讲程序优化。

         当我们寻找到OEP之后,就可以着手修复Code Splicing和Import Table Elimination了。

1
2
3
4
5
6
7
8
0040C434 55 push ebp
0040C435 8BEC mov ebp,esp
0040C437 6A FF push -0x1
0040C439 68 28334100 push fraps.00413328
0040C43E 68 30E94000 push fraps.0040E930
0040C443 64:A1 00000000 mov eax,dword ptr fs:[0]
0040C449 50 push eax
0040C44A 64:8925 0000000>mov dword ptr fs:[0],esp

         首先祭上大杀器ArmInline,欲要善其事,必先利其器。需要我们填写的就是上述三个区域,不过我这个版本可以自动填写修复的数据,只需要知道我们需要修复的进程,如图,目标进程ID为FC4,选中后依次删除拼接代码和巡回IAT基址。

         然后按照常规的方法dump和修复IAT就可以了。注意的是使用PELord一定要勾选从磁盘粘贴文件头(一般默认勾选上了)

         如果你的ArmInline不能自己修复(反正牛逼的师傅都是自己修复的,我不牛逼所以都是软件自动修复的),关于Code Splicing的修复可以这样,Alt+M到内存窗口,在fraps模块之后有一段内存没有被其他模块映射(不知道这样说对不对,反正对于Kernel32这样的dll来说肯定是对的。大家理解就好)。在最后一块内存处,就是拼接代码起点,这个值不是一个定值。(这个只是经验之谈,需要大佬解释一波的)

         接着修正IAT乱序,首先随便找个函数调用,在信息窗口点数据窗口跟随地址,然后向上拖动窗口(你最好改成显示地址)。找到IAT起始地址,然后找到结束地址,两者相减。计算大小即可。关于填充地址。可以考虑在一块没有读写的空白区域就好。不过大佬给的建议是在程序加壳前原来IAT的相近地方。可以这样寻找。Alt+M到内存窗口,因为IAT早rdata区域,又因为IAT肯定保存了一些IID成员,其中有个Name成员,也就是DllName。我们通过全局搜索确定

0x3 Armadillo双进程脱壳

         这一章节主要讲穿山甲的双进程保护手段。所以双进程保护,简单的来说就是创建两个进程,一个进程是另外一个进程的调试进程,又由于在R3下面一个进程只能被一个调试器附加。这样可以有效避免程序被调试。

0x3.1 标准保护

         接下来简单讲解一下关于双进程保护的原理,主要可以利用互斥体来判断进程列表是否存在相同的进程(即多开)。首先是利用CreateMutex创建一个互斥体。然后在利用OpenMutex打开那个互斥体,如果OpenMutex成功返回互斥体句柄,说明已经存在一个进程。如果不存在则在CreateProcess一个进程。而对于穿山甲壳双转单也是如此。如果提前创建了一个即将被打开的互斥体。那么程序就不会去创建新的进程。如下的脱壳方法就是基于这点考虑。

         首先对openMutex下断点(HE,bp皆可)。HE openMutexA,然后shift+F9。观察堆栈

1
2
3
4
0012F798 00434DB8 /CALL 到 OpenMutexA 来自 NOTEPAD_.00434DB2
0012F79C 001F0001 |Access = 1F0001
0012F7A0 00000000 |Inheritable = FALSE
0012F7A4 0012FDD8 \MutexName = "8A4::DABDC997F2" ;这是是重点,标记了互斥体名称。记住堆栈地址0012FDD8,以后要用

         接着需要创建互斥体。转到401000处编写汇编代码,为什么需要401000,因为这是.text段,但是理论上在哪里修改都可以。然后将EIP修改到401000处,就可以在这里执行了,然后shitf+F9.

1
2
3
4
5
6
7
8
9
10
00401000 60 pushad ;保存所有寄存器
00401001 9C pushfd ;保存标志寄存器
00401002 68 F8FB1200 push 0012FDD8 ;堆栈里看到的值MutexName
00401007 33C0 xor eax,eax
00401009 50 push eax ;参数2
0040100A 50 push eax ;参数1
0040100B E8 B5A6A577 call kernel32.CreateMutexA ;创建互斥体
00401010 9D popfd
00401011 61 popad ;恢复
00401012 - E9 7A13A677 jmp kernel32.OpenMutexA ;打开互斥体

         接着就是处理加密IAT和跳转OEP,DUMP的问题了。最后到达OEP如下:

1
2
3
4
5
6
004010CC 55 push ebp
004010CD 8BEC mov ebp,esp
004010CF 83EC 44 sub esp,0x44
004010D2 56 push esi
004010D3 FF15 E4634000 call dword ptr ds:[0x4063E4] ; kernel32.GetCommandLineA
004010D9 8BF0 mov esi,eax

0x3.2 CopyMem-II

         去除CopyMem-ll 保护通常有两个方法。

         方法1:首先寻找OEP,然后对WaitForDebugEvent下断点bp WaitForDebugEvent,接着运行程序,看堆栈,当出现pDebugEvent字符的时候,选择在数据窗口跟随,然后对WriteProcessMemory下断bp WriteProcessMemory,中断后,在数据窗口发现OEP。

         这里重点讲一下第二个方法:

  • bp WaitForDebugEvent,shift+F9运行起来,删除断点,然后执行到程序领空,大概停在0060F8BA

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    0060F8BA ? 15 E0406400 adc eax,<&KERNEL32.WaitForDebugEvent>
    0060F8BF . 85C0 test eax,eax
    0060F8C1 . 0F84 2B270000 je MAGCT.00611FF2
    0060F8C7 . 8B85 FCFDFFFF mov eax,dword ptr ss:[ebp-0x204]
    0060F8CD . 25 FF000000 and eax,0xFF
    0060F8D2 . 85C0 test eax,eax
    0060F8D4 . 74 13 je XMAGCT.0060F8E9
    0060F8D6 . 8B0D 44AF6400 mov ecx,dword ptr ds:[0x64AF44]
    0060F8DC . 8379 20 00 cmp dword ptr ds:[ecx+0x20],0x0
    0060F8E0 . 74 07 je XMAGCT.0060F8E9
    0060F8E2 . C685 FCFDFFFF>mov byte ptr ss:[ebp-0x204],0x0
    0060F8E9 > 68 38AE6400 push MAGCT.0064AE38 ; /pCriticalSection = MAGCT.0064AE38
    0060F8EE . FF15 A4416400 call dword ptr ds:[<&KERNEL32.EnterCriti>; \EnterCriticalSection
  • 然后Ctrl+F搜索命令:or eax,0FFFFFFF8,想上看有两个比较,一个是cmp dword ptr ss:[ebp-0xA34],另外一个是cmp ecx,dword ptr ds:[0x64AF48],然后对第一个cmp下断点,F9运行。这一步你需要记住以下内容,等下patch的时候需要用到内容,第一:第一个cmp的地址0060FE43,第二:第一个cmp【】内的值ebp-0xA34,第三:第二个cmp【】的值:0x64AF48.除此以外,需要将次一个cmp栈里面的数据清0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0060FE43 > \83BD CCF5FFFF 00 cmp dword ptr ss:[ebp-0xA34],0x0 ; 3
0060FE4A . 0F8C A8020000 jl MAGCT.006100F8 ;在目的地址006100F8下断点
0060FE50 . 8B8D CCF5FFFF mov ecx,dword ptr ss:[ebp-0xA34]
0060FE56 . 3B0D 48AF6400 cmp ecx,dword ptr ds:[0x64AF48] ; 2
0060FE5C . 0F8D 96020000 jge MAGCT.006100F8
0060FE62 . 8B95 40F6FFFF mov edx,dword ptr ss:[ebp-0x9C0]
0060FE68 . 81E2 FF000000 and edx,0xFF
0060FE6E . 85D2 test edx,edx
0060FE70 . 0F84 AD000000 je MAGCT.0060FF23
0060FE76 . 6A 00 push 0x0
0060FE78 . 8BB5 CCF5FFFF mov esi,dword ptr ss:[ebp-0xA34]
0060FE7E . C1E6 04 shl esi,0x4
0060FE81 . 8B85 CCF5FFFF mov eax,dword ptr ss:[ebp-0xA34]
0060FE87 . 25 07000080 and eax,0x80000007
0060FE8C . 79 05 jns XMAGCT.0060FE93
0060FE8E . 48 dec eax
0060FE8F . 83C8 F8 or eax,0xFFFFFFF8 ; 1
0060FE92 . 40 inc eax
  • 接下来patch数据,我们向下看,找到add eax,0xff语句,在这里就可以patch了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //原始数据
    0060FF16 25 FF000000 and eax,0xFF ; patch
    0060FF1B 85C0 test eax,eax
    0060FF1D 0F84 D5010000 je MAGCT.006100F8
    0060FF23 837D D8 00 cmp dword ptr ss:[ebp-0x28],0x0
    0060FF27 75 27 jnz XMAGCT.0060FF50
    0060FF29 8B15 D0436400 mov edx,dword ptr ds:[0x6443D0]
    //
    //patch模块
    inc dword ptr ds:[] //第一个CMP内的值
    mov dword ptr ds:[XXXX+4],1 //XXXX为第二个CMP[]内的值
    jmp XXXX //第一个CMP前的地址
    //
    //patch后的数据
    0060FF16 FF85 CCF5FFFF inc dword ptr ss:[ebp-0xA34] ; patch
    0060FF1C C705 4CAF6400 010000>mov dword ptr ds:[0x64AF4C],0x1
    0060FF26 ^ E9 18FFFFFF jmp MAGCT.0060FE43
    0060FF2B 90 nop
    0060FF2C 90 nop
    0060FF2D 90 nop
  • 接着shift+F9,中断在006100F8处,就可以dump处子进程了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    006100EE > \C785 D8F5FFFF 020001>mov dword ptr ss:[ebp-0xA28],0x10002 ; UNICODE "::=::\"
    006100F8 > E9 D4100000 jmp MAGCT.006111D1
    006100FD > 8B0D B0436400 mov ecx,dword ptr ds:[0x6443B0]
    00610103 . 81F1 050000C0 xor ecx,0xC0000005
    00610109 . 398D D4F5FFFF cmp dword ptr ss:[ebp-0xA2C],ecx
    0061010F . 0F85 92040000 jnz MAGCT.006105A7
    00610115 . 70 07 jo XMAGCT.0061011E
    00610117 . 7C 03 jl XMAGCT.0061011C
    00610119 > EB 05 jmp XMAGCT.00610120

         接下来就是还原IAT

  • 首先对DebugActiveProcess下断点BP DebugActiveProcess这样是为了寻找子进程,在堆栈窗口发现子进程ID为D84(不定)。接着重新打开一个OD,附加子进程,然后F9+F12,中断在入口点
    1
    2
    0012BCBC 0060F71A /CALL 到 DebugActiveProcess 来自 MAGCT.0060F714
    0012BCC0 00000DB4 \ProcessId = DB4
1
2
3
4
5
6
7
0061F743 >/$- EB FE jmp XMAGCT.<ModuleEntryPoint>
0061F745 |? EC in al,dx
0061F746 |. 6A FF push -0x1
0061F748 |. 68 209B6400 push MAGCT.00649B20
0061F74D |. 68 80F46100 push MAGCT.0061F480 ; SE 处理程序安装
0061F752 |. 64:A1 0000000>mov eax,dword ptr fs:[0]
0061F758 |. 50 push eax
  • 将死跳转字节EB FE正常指令字节55 8B,然后就可以执行我们上节讲的双变单了。在401000修改完双转单代码后,shift+F9跑起来,再次中断在OpenMutexA处。

    1
    2
    3
    4
    0061F743 > 55 push ebp
    0061F744 8BEC mov ebp,esp
    0061F746 |. 6A FF push -0x1
    0061F748 |. 68 209B6400 push MAGCT.00649B20
  • 然后对GetModuleHandle下硬件断点。HE GetModuleHandle,经过VirtualAlloc和VirtualFree到达,然后返回,修改Magic_JMp.在此之前关闭硬件断点

    1
    2
    001265F4 00F9ACC1 /CALL 到 GetModuleHandleA 来自 00F9ACBB
    001265F8 00126738 \pModule = "kernel32.dll"
  • 初次以外还有一个时间校验,对GetTickCount下断,执行到返回

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    00E89116 FF15 AC22E900 call dword ptr ds:[E922AC] ; kernel32.GetTickCount
    00E8911C 2B85 8CC3FFFF sub eax,dword ptr ss:[ebp-3C74]
    00E89122 8B8D 90C3FFFF mov ecx,dword ptr ss:[ebp-3C70]
    00E89128 6BC9 32 imul ecx,ecx,32
    00E8912B 81C1 D0070000 add ecx,7D0
    00E89131 3BC1 cmp eax,ecx
    00E89133 76 07 jbe short 00E8913C //修改为:JMP 00E8913C
    00E89135 C685 20C8FFFF 0>mov byte ptr ss:[ebp-37E0],1
    00E8913C 83BD D0C6FFFF 0>cmp dword ptr ss:[ebp-3930],0
    00E89143 0F85 8A000000 jnz 00E891D3
  • 然后用ImportRCE修复即可!

0x4 带KEY的Armadillo

0x4.1 单进程

         带KEY的Armadillo相当于给软件多了一层保护,我们此时还不能通过爆破的方式解决这个KEY,原因有2,第一,OD对于这类情况不提供修改的选项,第二,就算爆破成功了,后期软件中还存在暗桩。所以可以逆向算法的方式得到一组合适的KEY。

         首先shitf+F9运行起来,不要管出现的对话框,首先随便输入个Key,然后下HE GetDlgItem断点即可。取消断点,ALT+F9执行返回。大概停在此处。

1
2
3
4
5
6
7
8
9
10
11
12
13
021B44FC 85C0 test eax,eax
021B44FE 74 33 je X021B4533
021B4500 BB 00010000 mov ebx,0x100
021B4505 8D85 00FFFFFF lea eax,dword ptr ss:[ebp-0x100]
021B450B 53 push ebx
021B450C 50 push eax
021B450D 57 push edi
021B450E FF75 08 push dword ptr ss:[ebp+0x8]
021B4511 FFD6 call esi
021B4513 8B3D E0641C02 mov edi,dword ptr ds:[0x21C64E0] ; USER32.GetWindowTextA
021B4519 50 push eax
021B451A FFD7 call edi
021B451C 8D85 00FEFFFF lea eax,dword ptr ss:[ebp-0x200]

         然后向上找,找到这个函数开始地方,也就是上一个ret的下个指令.然后下硬件执行断点。然后重新载入,shitf+F9

1
2
3
4
5
6
7
8
9
10
021B4462 40 inc eax
021B4463 C3 retn
021B4464 55 push ebp
021B4465 8BEC mov ebp,esp
021B4467 81EC 00040000 sub esp,0x400
021B446D 8B45 0C mov eax,dword ptr ss:[ebp+0xC]
021B4470 53 push ebx
021B4471 56 push esi
021B4472 2D 10010000 sub eax,0x110
021B4477 57 push edi

         此时中断在之前下的执行断点处。单步走到021B4478处的第一个大跳转je 021B45F2右键跟随。

1
2
3
4
5
6
7
8
9
10
11
021B4464 55 push ebp
021B4465 8BEC mov ebp,esp
021B4467 81EC 00040000 sub esp,0x400
021B446D 8B45 0C mov eax,dword ptr ss:[ebp+0xC]
021B4470 53 push ebx
021B4471 56 push esi
021B4472 2D 10010000 sub eax,0x110
021B4477 57 push edi
021B4478 0F84 74010000 je 021B45F2
021B447E 48 dec eax
021B447F 74 07 je X021B4488

         跟随到021B45F2处,F2下断点,执行到此处,继续单步跟。

1
2
3
4
5
6
7
8
9
10
11
12
021B45F2 F645 17 80 test byte ptr ss:[ebp+0x17],0x80
021B45F6 8B7D 08 mov edi,dword ptr ss:[ebp+0x8]
021B45F9 74 12 je X021B460D
021B45FB 8065 17 7F and byte ptr ss:[ebp+0x17],0x7F
021B45FF 6A 01 push 0x1
021B4601 68 5CC91C02 push 0x21CC95C
021B4606 57 push edi
021B4607 FF15 C0641C02 call dword ptr ds:[0x21C64C0] ; USER32.SetPropA
021B460D 8B35 E8641C02 mov esi,dword ptr ds:[0x21C64E8] ; USER32.GetDlgItem
021B4613 6A 01 push 0x1
021B4615 57 push edi
021B4616 FFD6 call esi

         一直到021B4689处

1
2
3
4
5
6
7
8
9
10
11
021B467D /0F84 B7000000 je 021B473A
021B4683 |53 push ebx
021B4684 |B9 98FA1C02 mov ecx,0x21CFA98
021B4689 |E8 253CFEFF call 021982B3 ; 跟入
021B468E |53 push ebx
021B468F |B9 98FA1C02 mov ecx,0x21CFA98
021B4694 |8945 08 mov dword ptr ss:[ebp+0x8],eax
021B4697 |E8 353CFEFF call 021982D1
021B469C |837D 14 01 cmp dword ptr ss:[ebp+0x14],0x1
021B46A0 |75 27 jnz X021B46C9
021B46A2 |8B45 08 mov eax,dword ptr ss:[ebp+0x8]

         在021982C2步入,执行到021A59FA处可以发现EAX就是硬件号3C6663B2。接着在一个可以执行的代码段打补丁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
021982B3 56 push esi
021982B4 8BF1 mov esi,ecx
021982B6 FF7424 08 push dword ptr ss:[esp+0x8]
021982BA 8B8E 5C060000 mov ecx,dword ptr ds:[esi+0x65C]
021982C0 6A 00 push 0x0
021982C2 E8 24D70000 call 021A59EB ;步入
021982C7 3386 5C200000 xor eax,dword ptr ds:[esi+0x205C]
021982CD 5E pop esi
//
021A59EB 8B4424 04 mov eax,dword ptr ss:[esp+0x4]
021A59EF C1E0 06 shl eax,0x6
021A59F2 034424 08 add eax,dword ptr ss:[esp+0x8]
021A59F6 8B4481 18 mov eax,dword ptr ds:[ecx+eax*4+0x18]
021A59FA 35 8AC0E665 xor eax,0x65E6C08A
//

         ctrl+g,输入00401000然后输入如下内容

1
2
3
4
5
xor eax, 65E6C08A
cmp eax, 5980A338 ; 判断 EAX 是否是我的机器码( )
jnz 00401011 ;不是则返回
mov eax, 5F48DD41 ;如果是,则修改为5F48DD41
retn 8

         打好补丁后,在401000处F2下下断点,然后返回,剩下的使用上面讲的方法就可以脱去。

0x4.1 双进程

         得鸽一下。

0x5 DLL脱壳

0x5.1 DLL脱壳

         修改Magic_Jmp绕过IAT加密。

1
2
3
4
02995E53 395F FC cmp dword ptr ds:[edi-0x4],ebx
02995E56 ^ 0F85 49FEFFFF jnz 02995CA5
02995E5C EB 03 jmp X02995E61
02995E5E D6 salc

         接下来就是和exe脱壳不一样的地方,处理重定位表。大佬这边的操作有点不明白,哪位师傅如果知道告知一下。首先对GetTickCount下硬件断点HE GetTickCount,然后shitf+f9,观察堆栈是这个结果的话,删除断点,并返回.程序停在029AC3C8处。

1
00129220 029AC3C8 /CALL 到 GetTickCount 来自 029AC3C2

1
2
3
4
5
6
7
8
029AC3C8 2B85 A4D4FFFF sub eax,dword ptr ss:[ebp-0x2B5C]
029AC3CE 8B8D A8D4FFFF mov ecx,dword ptr ss:[ebp-0x2B58]
029AC3D4 6BC9 32 imul ecx,ecx,0x32
029AC3D7 81C1 D0070000 add ecx,0x7D0
029AC3DD 3BC1 cmp eax,ecx
029AC3DF 76 07 jbe X029AC3E8
029AC3E1 C685 34D9FFFF 0>mov byte ptr ss:[ebp-0x26CC],0x1
029AC3E8 83BD E4D7FFFF 0>cmp dword ptr ss:[ebp-0x281C],0x0

         然后ctrl+s,搜索,之后在找到的地址下断运行。就会出现如下黄色字体,记住标记的重定位RVA=6000和size=3B0.并将029ACFB8处跳转改为绝对跳转

1
2
3
4
PUSH EAX
XCHG CX,CX
POP EAX
STC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
029ACF59 C705 E0C09B02 6>mov dword ptr ds:[0x29BC0E0],0x29BCB60 ; 重定位表RVA为6000
029ACF63 A1 E49F9C02 mov eax,dword ptr ds:[0x29C9FE4]
029ACF68 8B00 mov eax,dword ptr ds:[eax]
029ACF6A 8985 3CD9FFFF mov dword ptr ss:[ebp-0x26C4],eax
029ACF70 A1 E49F9C02 mov eax,dword ptr ds:[0x29C9FE4]
029ACF75 83C0 04 add eax,0x4
029ACF78 A3 E49F9C02 mov dword ptr ds:[0x29C9FE4],eax
029ACF7D A1 E49F9C02 mov eax,dword ptr ds:[0x29C9FE4]
029ACF82 8B00 mov eax,dword ptr ds:[eax] ; 重定位表的大小为03B0
029ACF84 8985 78D9FFFF mov dword ptr ss:[ebp-0x2688],eax
029ACF8A A1 E49F9C02 mov eax,dword ptr ds:[0x29C9FE4]
029ACF8F 83C0 04 add eax,0x4
029ACF92 A3 E49F9C02 mov dword ptr ds:[0x29C9FE4],eax
029ACF97 83BD 3CD9FFFF 0>cmp dword ptr ss:[ebp-0x26C4],0x0 ; 重定位表为0??
029ACF9E 74 6F je X029AD00F
029ACFA0 83BD 78D9FFFF 0>cmp dword ptr ss:[ebp-0x2688],0x0
029ACFA7 74 66 je X029AD00F
029ACFA9 8B85 FCD7FFFF mov eax,dword ptr ss:[ebp-0x2804]
029ACFAF 8B8D 0CD8FFFF mov ecx,dword ptr ss:[ebp-0x27F4]
029ACFB5 3B48 34 cmp ecx,dword ptr ds:[eax+0x34]
029ACFB8 74 55 je X029AD00F ; 重定位处理,此处需要跳过
029ACFBA FFB5 78D9FFFF push dword ptr ss:[ebp-0x2688]
029ACFC0 8B85 0CD8FFFF mov eax,dword ptr ss:[ebp-0x27F4]
029ACFC6 0385 3CD9FFFF add eax,dword ptr ss:[ebp-0x26C4]
029ACFCC 50 push eax
029ACFCD 8B85 FCD7FFFF mov eax,dword ptr ss:[ebp-0x2804]
029ACFD3 FF70 34 push dword ptr ds:[eax+0x34]
029ACFD6 FFB5 0CD8FFFF push dword ptr ss:[ebp-0x27F4]
029ACFDC E8 3C150000 call 029AE51D ; 处理重定位
029ACFE1 83C4 10 add esp,0x10
029ACFE4 0FB6C0 movzx eax,al
029ACFE7 85C0 test eax,eax
029ACFE9 75 24 jnz X029AD00F
029ACFEB 8B45 08 mov eax,dword ptr ss:[ebp+0x8]
029ACFEE 8B00 mov eax,dword ptr ds:[eax]
029ACFF0 C700 07000000 mov dword ptr ds:[eax],0x7
029ACFF6 68 50CB9B02 push 0x29BCB50 ; ASCII "Location CPG"
029ACFFB 8B45 08 mov eax,dword ptr ss:[ebp+0x8]
029ACFFE FF70 04 push dword ptr ds:[eax+0x4]
029AD001 E8 24800000 call 029B502A ; jmp 到 msvcrt.strcpy

         此处提供另外一种方法,首先在内存窗口,在PE文件头下访问断点,然后shift+F9中断到029A9BC7,这里是DLL文件的文件头区域

1
2
3
4
5
6
7
8
9
Memory map, 条目 35
地址=00A20000
大小=00001000 (4096.)
属主=EdrLib 00A20000 (自身)
区段=
包含=PE 文件头
类型=Imag 01001002
访问=R
初始访问=RWE

1
2
3
4
5
6
7
8
029A9BC7 0348 3C add ecx,dword ptr ds:[eax+0x3C]
029A9BCA 898D FCD7FFFF mov dword ptr ss:[ebp-0x2804],ecx ; EdrLib.00A200D8
029A9BD0 A1 FC009C02 mov eax,dword ptr ds:[0x29C00FC]
029A9BD5 8985 B0AAFFFF mov dword ptr ss:[ebp+0xFFFFAAB0],eax
029A9BDB 8B85 B0AAFFFF mov eax,dword ptr ss:[ebp+0xFFFFAAB0]
029A9BE1 8985 0CD8FFFF mov dword ptr ss:[ebp-0x27F4],eax
029A9BE7 8B85 FCD7FFFF mov eax,dword ptr ss:[ebp-0x2804]
029A9BED 8B40 50 mov eax,dword ptr ds:[eax+0x50]

         接着ctrl+s搜索如下指令

1
2
mov edx,dword ptr ds:[ecx+C]
add edx,dword ptr ds:[ecx+8]

         我们先来跟一下。这里是为了获取pdata,reloc

1
2
3
4
5
6
7
8
9
10
029AFABD 3BCE cmp ecx,esi ; 和pdata比较
029AFABF 73 11 jnb X029AFAD2
029AFAC1 8B51 0C mov edx,dword ptr ds:[ecx+0xC]
029AFAC4 0351 08 add edx,dword ptr ds:[ecx+0x8]
029AFAC7 3BD0 cmp edx,eax
029AFAC9 76 02 jbe X029AFACD
029AFACB 8BC2 mov eax,edx
029AFACD 83C1 28 add ecx,0x28 ; next dir
029AFAD0 ^ EB EB jmp X029AFABD
029AFAD2 5E pop esi

         此时,当我们遍历到了reloc的时候,也就是eac为reloc的时候,在信息窗口显示的数据就是reloc的RVA=6000。size=3B0

1
ds:[00A20254]=00006000 edx=000058E0

         绕过重定位表处理之后,直接在 EdrLib .text段上下F2断点,然后shitf+f9直达OEP,注意并不是在LoadDll.exe的text段,而是需要脱壳的DLL的text段下断点。

1
2
3
4
5
6
7
8
Memory map, 条目 36
地址=00A21000
大小=00003000 (12288.)
属主=EdrLib 00A20000
区段=.text
类型=Imag 01001002
访问=R
初始访问=RWE

1
2
3
4
5
6
7
8
9
00A211C9 55 push ebp
00A211CA 8BEC mov ebp,esp
00A211CC 53 push ebx
00A211CD 8B5D 08 mov ebx,dword ptr ss:[ebp+0x8]
00A211D0 56 push esi
00A211D1 8B75 0C mov esi,dword ptr ss:[ebp+0xC]
00A211D4 57 push edi
00A211D5 8B7D 10 mov edi,dword ptr ss:[ebp+0x10]
00A211D8 85F6 test esi,esi

         接下来就可以dump程序了,在LordPE目录中修改重定位信息。因为没有处理重定位表,所以只需要修复DLL原来的重定位表的RVA和大小就行了。

1
2
RVA=6000
SIZE=3B0

         因为没有修改IAT表,所以IAT表的数据是正确的。所以直接Ctrl+M,选中.rdata处,双击,为了方便查看选择地址显示.可以判断起始地址为A24000,结束地址为A240C8,大小为C8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
00A24000 77EF7F9D GDI32.GetTextExtentPoint32W
00A24004 77EF7EAC GDI32.TextOutW
00A24008 77EFBA3F GDI32.TextOutA
00A2400C 77F0C63D GDI32.GetTextExtentPoint32A
00A24010 029970A0
00A24014 7C809A99 kernel32.lstrlenW
00A24018 7C814B77 kernel32.FreeEnvironmentStringsW
00A2401C 7C812FAD kernel32.GetCommandLineA
00A24020 7C81126A kernel32.GetVersion
00A24024 7C81CAFA kernel32.ExitProcess
00A24028 7C801E1A kernel32.TerminateProcess
00A2402C 7C80DE85 kernel32.GetCurrentProcess
00A24030 7C8097B8 kernel32.GetCurrentThreadId
00A24034 7C809C55 kernel32.TlsSetValue
00A24038 7C812E2F kernel32.TlsAlloc
00A2403C 7C813767 kernel32.TlsFree

         接下有有一个很秀的操作,将我们获得从A24000-A240C8的IAT数据,复制到新打开的notepad中的404000-4040C8处.这叫借鸡生蛋。然后在ImportRCE中IAT的RVA填写404000,大小填写C8就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
00404020 6A 12 81 7C FA CA 81 7C 1A 1E 80 7C 85 DE 80 7C j亅亅€|呣€|
00404030 B8 97 80 7C 55 9C 80 7C 2F 2E 81 7C 67 37 81 7C 笚€|U渶|/.亅g7亅
00404040 D0 97 80 7C 27 CD 80 7C C9 2F 81 7C E1 0E 81 7C 袟€|'蛝|?亅?亅
00404050 F2 1E 80 7C 5A 13 93 7C 5F B5 80 7C D7 D6 81 7C ?€|Z搢_祤|字亅
00404060 46 BE 80 7C 64 A1 80 7C 7B CC 81 7C 98 2F 81 7C F線|d|{虂|?亅
00404070 88 0F 81 7C 46 2C 81 7C 74 9B 80 7C 0D FF 92 7C ?亅F,亅t泙|.抾
00404080 17 0E 81 7C 81 9F 80 7C 00 10 92 7C E0 10 92 7C 亅仧€|.抾?抾
00404090 A4 00 93 7C 06 2F 81 7C A5 99 80 7C 37 28 81 7C ?搢/亅€|7(亅
004040A0 E1 9A 80 7C 80 9B 93 7C 30 AE 80 7C 7B 1D 80 7C 釟€|€洆|0畝|{€|
004040B0 88 9C 80 7C 00 8E 83 7C 38 CD 80 7C 24 8A 83 7C 垳€|.巸|8蛝|$妰|
004040C0 20 A5 80 7C A5 AB 94 7C 3B C6 7D 1B FF 15 CC 63 |カ攟;苶蘡
004040D0 40 00 83 F8 3F 0F 85 A2 03 00 00 C7 45 FC FD FF @.凐?參..荅
004040E0 FF FF E9 96 03 00 00 33 F6 A1 00 50 40 00 56 50 闁..3觥.P@.VP

0x6 非常规方法应对

0x6.1 应对IAT加密

         这里使用了非常简单的Arm作为示范,只含有IAT加密,不涉及其他,第二常规方法下建议使用增强版OD。

         通常规避IAT加密的方法就是Magic_Jmp,然后.text下断到OEP。这里前辈给出新方法。介绍一下。

         首先须要了解到Arm并不是对所有的API函数进行加密,前辈这里的思路是先直接到达OEP,在根据里面一直的函数地址寻找IAT地址,然后寻找出IAT中被加密的地方,下硬件断点。重新运行之后到达被修改的的地址,然后分析加密IAT的过程,使用jmp或者nop规避即可。

         在.text下断,然后F9运行,程序到达OEP,只是IAT被加密了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
004010CC 55 push ebp
004010CD 8BEC mov ebp, esp
004010CF 83EC 44 sub esp, 44
004010D2 56 push esi
004010D3 FF15 E4634000 call dword ptr [4063E4]
004010D9 8BF0 mov esi, eax
004010DB 8A00 mov al, byte ptr [eax]
004010DD 3C 22 cmp al, 22
004010DF 75 1B jnz short 004010FC
004010E1 56 push esi
004010E2 FF15 F4644000 call dword ptr [4064F4] ; USER32.CharNextA
004010E8 8BF0 mov esi, eax
004010EA 8A00 mov al, byte ptr [eax]
004010EC 84C0 test al, al

         大家应该都知道IID中所有函数应该是连续的,但是这些是不连续的,应该是被加密的。但是也说过其只是对IAT部分函数地址(有歧义自行理解)进行加密。但是IAT的RVA应该是一致的。所以将IAT的起始RVA=62E4,结束RVA=6524,大小应该为240,oep为4010cc记录下来。

1
2
3
4
5
6
7
0040645C 77D3C972 USER32.SetDlgItemTextA
00406460 77D5A5E5 USER32.TabbedTextOutA
00406464 021AB517
00406468 77D29849 USER32.EnableWindow
0040646C 021AA871
00406470 77D3C2E7 USER32.SendDlgItemMessageA
00406474 77D2AF1B USER32.GetDlgCtrlID

         我们在0040645C处下硬件断点,然后重新载入,接着按shitf+9,即可到达0218CF28处

1
2
3
4
5
6
7
0218CF28 8B85 10D9FFFF mov eax, dword ptr [ebp-26F0] ; PackEd.004062EC
0218CF2E 83C0 04 add eax, 4
0218CF31 8985 10D9FFFF mov dword ptr [ebp-26F0], eax
0218CF37 ^ E9 4DFCFFFF jmp 0218CB89
0218CF3C FF15 84721902 call dword ptr [2197284] ; kernel32.GetTickCount
0218CF42 2B85 A4D4FFFF sub eax, dword ptr [ebp-2B5C]
0218CF48 8B8D A8D4FFFF mov ecx, dword ptr [ebp-2B58]

         接着往下跟,在0218CD6B处发现比较一个,可以发现第一轮他是和RegCreateKeyA比较

1
2
3
4
5
6
7
8
0218CD63 50 push eax
0218CD64 8D85 68C2FFFF lea eax, dword ptr [ebp-3D98]
0218CD6A 50 push eax
0218CD6B FF15 78731902 call dword ptr [2197378] ; msvcrt._stricmp
0218CD71 59 pop ecx
0218CD72 59 pop ecx
0218CD73 85C0 test eax, eax
0218CD75 75 11 jnz short 0218CD88

1
2
3
001294D4 0012AE7C |s1 = "RegOpenKeyA"
001294D8 0012AD6C \s2 = "RegCreateKeyA"
001294DC 021A0F88

         经过如下分析,我们可以知道0218CF1A处就是我们加密IAT的操作。同时也知道Arm只是针对部分IAT进行加密的。所以只需要修改之前在0218CD6B处的jnz short 0218CD88,或者位于0218CF18处的jnb short 0218CF37即可!

1
2
3
4
5
6
7
0218CF1A 8B85 10D9FFFF mov eax, dword ptr [ebp-26F0] ; eax为IAT地址
0218CF20 8B8D 68CAFFFF mov ecx, dword ptr [ebp-3598] ; ecx是加密的值
0218CF26 8908 mov dword ptr [eax], ecx ; 将值写入IAT中
0218CF28 8B85 10D9FFFF mov eax, dword ptr [ebp-26F0] ; 定位IAT
0218CF2E 83C0 04 add eax, 4 ; 指向下一个地址
0218CF31 8985 10D9FFFF mov dword ptr [ebp-26F0], eax ; 写入
0218CF37 ^ E9 4DFCFFFF jmp 0218CB89 ; 便利下一个