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在调式保护格式串的消息时会奔溃而新增的反调试技术。
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函数,就表明快到了。
12345000C94C8 |00726DF3 返回到 00726DF3 来自 kernel32.GetModuleHandleA000C94CC |0073BC1C ASCII "kernel32.dll"000C94D0 |0073CEC4 ASCII "VirtualAlloc"000C94D4 |0073FA98000C94D8 |77E22270 ntdll.RtlLeaveCriticalSectionStep3:继续Shift+F9,只要堆栈出现kernel32.dll,但是不包含任何其他函数名称,表示到达了返回的时机。此时执行到返回ctrl+F9。如何判断之前执行到返回的时机是否正确呢,就是看是否存在LoadLibrary这个API函数。此时就是正确的。
12345000C9228 /000C94C8000C922C |00715CE1 返回到 00715CE1 来自 kernel32.GetModuleHandleA000C9230 |000C937C ASCII "kernel32.dll"000C9234 |000CEAB4000C9238 |3CBC24B7
|
|
- 此时就可以修改LoadLibrary函数下面的那个条件跳转(00715D12)为jmp,跳转到00425E5C,然后撤销之前的修改。1234567800715CF4 8D85 B4FEFFFF lea eax, dword ptr [ebp-14C]00715CFA 50 push eax00715CFB FF15 BC627300 call dword ptr [7362BC] ; kernel32.LoadLibraryA00715D01 8B0D AC407400 mov ecx, dword ptr [7440AC]00715D07 89040E mov dword ptr [esi+ecx], eax00715D0A A1 AC407400 mov eax, dword ptr [7440AC]00715D0F 391C06 cmp dword ptr [esi+eax], ebx00715D12 - 0F84 2F010090 je 90715E47
|
|
- Step4:因为外壳肯定需要将存储在某一区段的数据解压到text段,需要对该段进行访问,所以,在内存窗口的程序的text段下访问断点。然后shift+F9。中断在43468F,然后单步,在此代码段的最后一个
call ecx
处步入就是OEP。
123450043F68F 8B12 mov edx, dword ptr [edx]0043F691 8955 DC mov dword ptr [ebp-24], edx0043F694 834D FC FF or dword ptr [ebp-4], FFFFFFFF0043F698 EB 11 jmp short 0043F6AB0043F69A 6A 01 push 1
|
|
0x2.2 单进程Armadillo v4.x脱壳
首先需要判断加壳版本是否是4.xxx。关于这点如何判断呢,主要下硬件断点 HE OutputDebugStringA
。在堆栈窗口出现%s%s%s%s的标志,说明这是4.X的壳。
关于Armadillo v4.x单进程脱壳把握两点,第一,使用Magic_Jmp避过IAT加密保护,对GetCurrentThreadId下断点找到OEP
关于第一条,就是上面2.1讲的原则,下面解释第二条。首先对GetCurrentThreadId下断。HE GetCurrentThreadId
。查看堆栈窗口,会出现如下结果.中间省略多个,查看关于GetCurrentThreadId都是来自其他模块的调用,但是最后一个是来自程序的调用。这就是程序返回的时机,所以,F8步过,根据之前说的规则,OEP在该程序段最后一个call ecx中。
|
|
2.3 加 PassWord单进程
这是就比2.2多了一个密码验证,我们直接绕过密码验证就好。首先Shift+F9运行,通过查看导入表,在GetDlgItem处下断bpx GetDlgItem
。然后在输入伪码按OK,程序中断在35359D0处.注意:先运行,在下断!在输入
接着修改魔法跳,可以使用bp GetModueHandle
或者HE GetModuleHandle
。这里发现了kernel32.dll就可以执行到返回查看了。
然后就是找OEP,这里还是可以使用2.2中对GetProcessId下断。这里介绍个新方法。**可以在内存窗口.text段按F2下断点。因为壳执行完肯定会执行代码段的内容。也就是说代码段是由外壳到源程序的一扇门。所以在此处下断必然成立。
总结一下,现在有两个方法可以找OEP。第一是对GetProcessId下断,第二个就是在.text下断。
0x2.4 包含Code Splicing和Import Table Elimination的修复
Armadillo使用Code Splicing和Import Table Elimination两项技术使得程序修复变得更加困难。幸好有大佬开发了ArmInline工具可以使得修复变得简单一些。注意:本节只将修复,不讲程序优化。
当我们寻找到OEP之后,就可以着手修复Code Splicing和Import Table Elimination了。
首先祭上大杀器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。观察堆栈
接着需要创建互斥体。转到401000处编写汇编代码,为什么需要401000,因为这是.text段,但是理论上在哪里修改都可以。然后将EIP修改到401000处,就可以在这里执行了,然后shitf+F9.
接着就是处理加密IAT和跳转OEP,DUMP的问题了。最后到达OEP如下:
|
|
0x3.2 CopyMem-II
去除CopyMem-ll 保护通常有两个方法。
方法1:首先寻找OEP,然后对WaitForDebugEvent下断点bp WaitForDebugEvent
,接着运行程序,看堆栈,当出现pDebugEvent
字符的时候,选择在数据窗口跟随,然后对WriteProcessMemory下断bp WriteProcessMemory
,中断后,在数据窗口发现OEP。
这里重点讲一下第二个方法:
bp WaitForDebugEvent,shift+F9运行起来,删除断点,然后执行到程序领空,大概停在0060F8BA
123456789101112130060F8BA ? 15 E0406400 adc eax,<&KERNEL32.WaitForDebugEvent>0060F8BF . 85C0 test eax,eax0060F8C1 . 0F84 2B270000 je MAGCT.00611FF20060F8C7 . 8B85 FCFDFFFF mov eax,dword ptr ss:[ebp-0x204]0060F8CD . 25 FF000000 and eax,0xFF0060F8D2 . 85C0 test eax,eax0060F8D4 . 74 13 je XMAGCT.0060F8E90060F8D6 . 8B0D 44AF6400 mov ecx,dword ptr ds:[0x64AF44]0060F8DC . 8379 20 00 cmp dword ptr ds:[ecx+0x20],0x00060F8E0 . 74 07 je XMAGCT.0060F8E90060F8E2 . C685 FCFDFFFF>mov byte ptr ss:[ebp-0x204],0x00060F8E9 > 68 38AE6400 push MAGCT.0064AE38 ; /pCriticalSection = MAGCT.0064AE380060F8EE . 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
|
|
接下来patch数据,我们向下看,找到
add eax,0xff
语句,在这里就可以patch了1234567891011121314151617181920//原始数据0060FF16 25 FF000000 and eax,0xFF ; patch0060FF1B 85C0 test eax,eax0060FF1D 0F84 D5010000 je MAGCT.006100F80060FF23 837D D8 00 cmp dword ptr ss:[ebp-0x28],0x00060FF27 75 27 jnz XMAGCT.0060FF500060FF29 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] ; patch0060FF1C C705 4CAF6400 010000>mov dword ptr ds:[0x64AF4C],0x10060FF26 ^ E9 18FFFFFF jmp MAGCT.0060FE430060FF2B 90 nop0060FF2C 90 nop0060FF2D 90 nop接着shift+F9,中断在006100F8处,就可以dump处子进程了
123456789006100EE > \C785 D8F5FFFF 020001>mov dword ptr ss:[ebp-0xA28],0x10002 ; UNICODE "::=::\"006100F8 > E9 D4100000 jmp MAGCT.006111D1006100FD > 8B0D B0436400 mov ecx,dword ptr ds:[0x6443B0]00610103 . 81F1 050000C0 xor ecx,0xC000000500610109 . 398D D4F5FFFF cmp dword ptr ss:[ebp-0xA2C],ecx0061010F . 0F85 92040000 jnz MAGCT.006105A700610115 . 70 07 jo XMAGCT.0061011E00610117 . 7C 03 jl XMAGCT.0061011C00610119 > EB 05 jmp XMAGCT
接下来就是还原IAT
- 首先对DebugActiveProcess下断点
BP DebugActiveProcess
这样是为了寻找子进程,在堆栈窗口发现子进程ID为D84(不定)。接着重新打开一个OD,附加子进程,然后F9+F12,中断在入口点120012BCBC 0060F71A /CALL 到 DebugActiveProcess 来自 MAGCT.0060F7140012BCC0 00000DB4 \ProcessId = DB4
|
|
将死跳转字节
EB FE
正常指令字节55 8B
,然后就可以执行我们上节讲的双变单了。在401000修改完双转单代码后,shift+F9跑起来,再次中断在OpenMutexA处。12340061F743 > 55 push ebp0061F744 8BEC mov ebp,esp0061F746 |. 6A FF push -0x10061F748 |. 68 209B6400 push MAGCT.00649B20然后对GetModuleHandle下硬件断点。
HE GetModuleHandle
,经过VirtualAlloc和VirtualFree到达,然后返回,修改Magic_JMp.在此之前关闭硬件断点12001265F4 00F9ACC1 /CALL 到 GetModuleHandleA 来自 00F9ACBB001265F8 00126738 \pModule = "kernel32.dll"初次以外还有一个时间校验,对GetTickCount下断,执行到返回
1234567891000E89116 FF15 AC22E900 call dword ptr ds:[E922AC] ; kernel32.GetTickCount00E8911C 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,3200E8912B 81C1 D0070000 add ecx,7D000E89131 3BC1 cmp eax,ecx00E89133 76 07 jbe short 00E8913C //修改为:JMP 00E8913C00E89135 C685 20C8FFFF 0>mov byte ptr ss:[ebp-37E0],100E8913C 83BD D0C6FFFF 0>cmp dword ptr ss:[ebp-3930],000E89143 0F85 8A000000 jnz 00E891D3然后用ImportRCE修复即可!
0x4 带KEY的Armadillo
0x4.1 单进程
带KEY的Armadillo相当于给软件多了一层保护,我们此时还不能通过爆破的方式解决这个KEY,原因有2,第一,OD对于这类情况不提供修改的选项,第二,就算爆破成功了,后期软件中还存在暗桩。所以可以逆向算法的方式得到一组合适的KEY。
首先shitf+F9运行起来,不要管出现的对话框,首先随便输入个Key,然后下HE GetDlgItem
断点即可。取消断点,ALT+F9执行返回。大概停在此处。
然后向上找,找到这个函数开始地方,也就是上一个ret的下个指令.然后下硬件执行断点。然后重新载入,shitf+F9
此时中断在之前下的执行断点处。单步走到021B4478处的第一个大跳转je 021B45F2
右键跟随。
跟随到021B45F2处,F2下断点,执行到此处,继续单步跟。
|
|
一直到021B4689处
在021982C2步入,执行到021A59FA处可以发现EAX就是硬件号3C6663B2
。接着在一个可以执行的代码段打补丁
ctrl+g,输入00401000然后输入如下内容
打好补丁后,在401000处F2下下断点,然后返回,剩下的使用上面讲的方法就可以脱去。
0x4.1 双进程
得鸽一下。
0x5 DLL脱壳
0x5.1 DLL脱壳
修改Magic_Jmp绕过IAT加密。
接下来就是和exe脱壳不一样的地方,处理重定位表。大佬这边的操作有点不明白,哪位师傅如果知道告知一下。首先对GetTickCount下硬件断点HE GetTickCount
,然后shitf+f9,观察堆栈是这个结果的话,删除断点,并返回.程序停在029AC3C8处。
|
|
然后ctrl+s,搜索,之后在找到的地址下断运行。就会出现如下黄色字体,记住标记的重定位RVA=6000和size=3B0.并将029ACFB8处跳转改为绝对跳转。
|
|
此处提供另外一种方法,首先在内存窗口,在PE文件头下访问断点,然后shift+F9中断到029A9BC7,这里是DLL文件的文件头区域
|
|
接着ctrl+s搜索如下指令
我们先来跟一下。这里是为了获取pdata,reloc
此时,当我们遍历到了reloc的时候,也就是eac为reloc的时候,在信息窗口显示的数据就是reloc的RVA=6000。size=3B0
绕过重定位表处理之后,直接在 EdrLib .text段上下F2断点,然后shitf+f9直达OEP,注意并不是在LoadDll.exe的text段,而是需要脱壳的DLL的text段下断点。
|
|
接下来就可以dump程序了,在LordPE目录中修改重定位信息。因为没有处理重定位表,所以只需要修复DLL原来的重定位表的RVA和大小就行了。
因为没有修改IAT表,所以IAT表的数据是正确的。所以直接Ctrl+M,选中.rdata处,双击,为了方便查看选择地址显示.可以判断起始地址为A24000,结束地址为A240C8,大小为C8
接下有有一个很秀的操作,将我们获得从A24000-A240C8的IAT数据,复制到新打开的notepad中的404000-4040C8处.这叫借鸡生蛋。然后在ImportRCE中IAT的RVA填写404000,大小填写C8就好了
0x6 非常规方法应对
0x6.1 应对IAT加密
这里使用了非常简单的Arm作为示范,只含有IAT加密,不涉及其他,第二常规方法下建议使用增强版OD。
通常规避IAT加密的方法就是Magic_Jmp,然后.text下断到OEP。这里前辈给出新方法。介绍一下。
首先须要了解到Arm并不是对所有的API函数进行加密,前辈这里的思路是先直接到达OEP,在根据里面一直的函数地址寻找IAT地址,然后寻找出IAT中被加密的地方,下硬件断点。重新运行之后到达被修改的的地址,然后分析加密IAT的过程,使用jmp或者nop规避即可。
在.text下断,然后F9运行,程序到达OEP,只是IAT被加密了。
大家应该都知道IID中所有函数应该是连续的,但是这些是不连续的,应该是被加密的。但是也说过其只是对IAT部分函数地址(有歧义自行理解)进行加密。但是IAT的RVA应该是一致的。所以将IAT的起始RVA=62E4,结束RVA=6524,大小应该为240,oep为4010cc记录下来。
我们在0040645C处下硬件断点,然后重新载入,接着按shitf+9,即可到达0218CF28处
接着往下跟,在0218CD6B处发现比较一个,可以发现第一轮他是和RegCreateKeyA比较
|
|
经过如下分析,我们可以知道0218CF1A处就是我们加密IAT的操作。同时也知道Arm只是针对部分IAT进行加密的。所以只需要修改之前在0218CD6B处的jnz short 0218CD88
,或者位于0218CF18处的jnb short 0218CF37
即可!