文章详细分析了Armadillo壳的脱壳过程,包括脱壳步骤、处理IAT表和子程序运行的流程。文章强调了逆向或破解他人软件的行为是不道德的,并呼吁尊重他人的劳动成果和权益。同时,提供了作者舒默哦的ID信息和一些相关推荐文章。
文章详细分析了Armadillo壳的脱壳过程,包括最低保护、仅标准保护、标准保护+检测调试器、双进程+检测调试器(最高保护)等不同情况下的处理。并介绍了IAT表的处理方法和子程序运行的流程。
文章强调逆向或破解他人软件的行为是不道德的,呼吁尊重他人的劳动成果和权益,并强调软件开发人员的艰辛与挑战。盗版软件的泛滥会严重打击软件开发和维护人员的信心,可能导致产品的流产。
文章最后提供了作者舒默哦的ID信息和一些相关推荐文章,包括往期推荐的精华文章。
写此帖的原因在于,许多论坛关于 Armadillo 脱壳的讨论往往只讲解脱壳步骤,而不涉及原理。即使有些帖子提到原理,内容也常常遵循安全人员的脱壳经验,这对初学者并不友好。Armadillo 壳属于强壳,但它是一款较老的壳,似乎已经很久没有更新了。之前只找到 9.64 的汉化版,而没有找到更高版本,因此我将分析这个版本。对于初学者,建议准备一个没有任何插件的 x32dbg,先尝试自己进行分析,然后再参考下面的分析过程。
Armadillo_9.64版本的汉化版界面如下:
01 最低保护(最兼容)
① IAT表加密
最低保护,只对IAT表进行了加密。可以与源文件对比,找到IAT表的首地址,下硬件写入断点。壳程序会在断点位置写入两次,第二次才是真正填写IAT表的地址。
接下来收集IAT表的信息,需要三个要素:
1.函数名称。
2.函数所在的dll地址(也可以称dll句柄)。
3.最后需要回填的地址。
经过多次调试,可以在add edx,4这条指令处下断,以收集IAT的信息。当断点停在这里时,eax寄存器中储存的就是IAT地址,函数名称位于堆栈的[esp-8]位置,而DLL句柄则储存在局部变量[ebp-0x2948]中。
其中,定位dll的地址,需要往前面看,dll地址的关键信息,在以下位置:
② 确定OEP入口
通过和源文件对比,来确定OEP的入口,然后在堆栈窗口往回溯,找到转跳到OEP的CALL(如下图),记录下图所选择的特征码(8B 55 F4 2B 55 DC FF D2 89 45 FC EB 48
)。
搜索的时机是在VirtualAlloc
函数下断,当遇到申请大小为0x200000时,返回到用户代码并运行到0x43F756这个位置。在0x3EA0000地址(即刚刚申请的0x200000大小的区域)中搜索 OEP 入口的特征码,就能够找到。
③ 修复IAT表
IAT表信息的收集,可以写插件来自动完成,通过插件来跟踪和保存收集到的IAT信息,然后运行到OEP后,再回填到IAT表,以完成修复。
因为是断点事件,所以插件的回调函数,其类型为断点回调(CB_BREAKPOINT)。开启跟踪后,收集的IAT信息会被存放到申请的缓存中,程序最终会停在OEP处。此时,查看IAT表,可以看到些地址加密了。
再点击回填IAT,以完成修复:
④ 脱壳
此时,可以使用x32dbg自带的插件Scylla来完成脱壳。需要注意的是,有些导入表地址是无效的,通过和源文件对比,这些地址是多余的,直接cut掉即可。
02 仅标准保护
勾选仅标准保护,调试的时候,会依次产生如下异常:
1.0xC0000005异常
push ebp
mov ebp,esp
push ecx
mov dword ptr ss:[ebp-4],0
mov eax,dword ptr ss:[ebp-4]
mov byte ptr ds:[eax],0 ; eax值为0,执行到这里会造成0xC0000005异常
mov esp,ebp
pop ebp
ret
2.0xC0000096异常
push ebp
mov ebp,esp
push ecx
push ebx
mov byte ptr ss:[ebp-1],0
mov eax,564D5868
mov ebx,0
mov ecx,A
mov edx,5658
in eax,dx ;in指令是特权指令,只能在0环使用。执行到这里会造成0xC0000096异常
cmp ebx,564D5868
jne 58BD6AA
mov byte ptr ss:[ebp-1],1
mov al,byte ptr ss:[ebp-1]
pop ebx
mov esp,ebp
pop ebp
ret
3.0xc000001d异常
004667F0 | 55 | push ebp |
004667F1 | 8BEC | mov ebp,esp |
004667F3 | 53 | push ebx |
004667F4 | B8 2D684600 | mov eax,huffmancoding.46682D |
004667F9 | 68 2D684600 | push huffmancoding.46682D |
004667FE | 64:FF35 00000000 | push dword ptr fs:[0] |
00466805 | 64:8925 00000000 | mov dword ptr fs:[0],esp |
0046680C | BB 00000000 | mov ebx,0 |
00466811 | B8 01000000 | mov eax,1 |
00466816 | 0F | ??? |;未知指令,造成了0xc000001d异常
00466817 | 3F | aas |
00466818 | 07 | pop es |
00466819 | 0B36 | or esi,dword ptr ds:[esi] |
0046681B | 8B0424 | mov eax,dword ptr ss:[esp] |
0046681E | 64:A3 00000000 | mov dword ptr fs:[0],eax |
00466824 | 83C4 08 | add esp,8 |
00466827 | 85DB | test ebx,ebx |
00466829 | 74 1A | je huffmancoding.466845 |
0046682B | EB 1C | jmp huffmancoding.466849 |
0046682D | 8B4C24 0C | mov ecx,dword ptr ss:[esp+C] |
00466831 | C781 A4000000 FFFFFFF | mov dword ptr ds:[ecx+A4],FFFFFFFF |
0046683B | 8381 B8000000 04 | add dword ptr ds:[ecx+B8],4 |
00466842 | 33C0 | xor eax,eax |
00466844 | C3 | ret |
直接nop掉产生第一个异常的指令(如下图),然后脱壳步骤和最低保护一样。
03 标准保护+检测调试器
标准保护加检测调试器,实际上是开启了双进程保护的。可以把仅标准保护的文件和此文件对比着一起调试,很容易发现关键指令je huffmancoding.44056F,跳过去就不会执行双进程策略。因此,在跳过去后,其脱壳步骤和仅标准保护一样。
关于检测调试器,有两个地方进行了相关检查:一个是直接调用IsDebuggerPresent
,另一个是通过FS
寄存器来判断是否存在调试器。然而,调试器的检测也包含在双进程保护策略中。如果前面的检查已经跳过,则无需再进行处理。
04 双进程+检测调试器(最高保护)
① 分析方法
根据上一节的方法,直接跳过je huffmancoding.44056F,无法有效阻止双进程保护的策略,后面会造成异常,导致程序崩溃。通过条件判断的来源,可以确认0x004F4838是一个全局变量,似乎是一个用于启用各种保护策略的结构体地址。值得注意的是,这些保护策略的启用或禁用并非简单的true或false,而是经过特定算法处理的开关。
正确的做法是不要跳过,而是直接分析创建子进程后的调试流程。因为被保护程序的代码是在子进程中运行的。可以逐步调试并跟踪,或在WaitForDebugEvent函数处下断,以找到关键函数sub_426A50。该函数可以通过 IDA 进行查看,如下所示:
sub_426A50函数执行的主要流程:
while(...)
{
if(WaitForDebugEvent(...))
{
EnterCriticalSection(...);
switch(...)
{
case 1:
{
if(异常码 == 0x80000001)
{
...
sub_428370(...)
...
}
else if(异常码 == 0xC0000005)
{
...
}
else if(异常码 == 0x80000003)
{
...
}else{
...
}
break;
}
case 2:
{
...
break;
}
case 4:
{
...
break;
}
case 5:
{
...
break;
}
case 8:
{
...
break;
}
default:
{
}
}
ContinueDebugEvent(...);
LeaveCriticalSection(...);
}
}
在x32dbg中,可以通过脚本来记录所有调试事件,获取的信息如下:
1.捕获创建进程信息(3) -> 恢复线程
2.捕获load-dynamic-link-library(DLL)调试事件(6) -> ReadProcessMemory,读取进程内存
3.捕获创建线程调试事件(不包括进程的main线程)(2) -> 获取线程句柄0x110
4.捕获创建线程调试事件(不包括进程的main线程)(2) -> 获取线程句柄0xFC
5.捕获创建线程调试事件(不包括进程的main线程)(2) -> 获取线程句柄0x2D8
---
多次捕获load-dynamic-link-library(DLL)事件(6) ->加载系统dll
---
6.捕获创建线程调试事件(不包括进程的main线程)(2) -> 获取线程句柄0x188
7.捕获创建线程调试事件(不包括进程的main线程)(2) -> 获取线程句柄0x308
8.捕获异常信息(1)->0x80000003,断点异常,填写一个未知数组的值
9.捕获退出线程事件(4)
10.捕获创建线程调试事件(不包括进程的main线程)(2) -> 获取线程句柄
---
多次捕获load-dynamic-link-library(DLL)事件(6) ->加载系统dll
---
---
多次捕获创建线程调试事件(不包括进程的main线程)(2) -> 获取线程句柄
---
11.捕获load-dynamic-link-library(DLL)事件(6) ->加载系统dll
12.捕获创建线程调试事件(不包括进程的main线程)(2) -> 获取线程句柄
13.捕获load-dynamic-link-library(DLL)事件(6) ->加载系统dll
14.捕获load-dynamic-link-library(DLL)事件(6) ->加载系统dll
15.捕获创建线程调试事件(不包括进程的main线程)(2) -> 获取线程句柄
16.捕获load-dynamic-link-library(DLL)事件(6) ->加载系统dll
17.捕获load-dynamic-link-library(DLL)事件(6) ->加载系统dll
18.捕获load-dynamic-link-library(DLL)事件(6) ->加载系统dll
19.捕获退出线程事件(4)
20.捕获output-debugging-string调试事件(8) -> ????
21.捕获output-debugging-string调试事件(8) -> ????
---
多次捕获load-dynamic-link-library(DLL)事件(6) ->加载系统dll
---
22.捕获创建线程调试事件(不包括进程的main线程)(2) -> 获取线程句柄
23.捕获退出线程事件(4)
24.捕获创建线程调试事件(不包括进程的main线程)(2) -> 获取线程句柄
25.捕获退出线程事件(4)
26.捕获异常信息(1)->0xc0000005,访问了未被允许的内存区域,造成异常的具体位置如下:
push ebp
mov ebp,esp
push ecx
mov dword ptr ss:[ebp-4],0
mov eax,dword ptr ss:[ebp-4]
mov byte ptr ds:[eax],0 ; eax值为0,执行到这里会造成0xC0000005异常
mov esp,ebp
pop ebp
ret
27.捕获异常信息(1)->0xc00000096,使用了非法指令,具体位置如下:
push ebp
mov ebp,esp
push ecx
push ebx
mov byte ptr ss:[ebp-1],0
mov eax,564D5868
mov ebx,0
mov ecx,A
mov edx,5658
in eax,dx ;in指令是特权指令,只能在0环使用
cmp ebx,564D5868
jne 58BD6AA
mov byte ptr ss:[ebp-1],1
mov al,byte ptr ss:[ebp-1]
pop ebx
mov esp,ebp
pop ebp
ret
28.捕获异常信息(1)->0xc000001d,使用了非法指令,具体位置如下:
004667F0 | 55 | push ebp |
004667F1 | 8BEC | mov ebp,esp |
004667F3 | 53 | push ebx |
004667F4 | B8 2D684600 | mov eax,huffmancoding.46682D |
004667F9 | 68 2D684600 | push huffmancoding.46682D |
004667FE | 64:FF35 00000000 | push dword ptr fs:[0] |
00466805 | 64:8925 00000000 | mov dword ptr fs:[0],esp |
0046680C | BB 00000000 | mov ebx,0 |
00466811 | B8 01000000 | mov eax,1 |
00466816 | 0F | ??? |;未知指令,造成了0xc000001d异常
00466817 | 3F | aas |
00466818 | 07 | pop es |
00466819 | 0B36 | or esi,dword ptr ds:[esi] |
0046681B | 8B0424 | mov eax,dword ptr ss:[esp] |
0046681E | 64:A3 00000000 | mov dword ptr fs:[0],eax |
00466824 | 83C4 08 | add esp,8 |
00466827 | 85DB | test ebx,ebx |
00466829 | 74 1A | je huffmancoding.466845 |
0046682B | EB 1C | jmp huffmancoding.466849 |
0046682D | 8B4C24 0C | mov ecx,dword ptr ss:[esp+C] |
00466831 | C781 A4000000 FFFFFFF | mov dword ptr ds:[ecx+A4],FFFFFFFF |
0046683B | 8381 B8000000 04 | add dword ptr ds:[ecx+B8],4 |
00466842 | 33C0 | xor eax,eax |
00466844 | C3 | ret |
---
多次捕获0xC0000005异常
---
---
多次捕获load-dynamic-link-library(DLL)事件(6) ->加载系统dll
---
---
多次捕获0xC0000005异常
---
---
多次捕获load-dynamic-link-library(DLL)事件(6) ->加载系统dll
---
29. 捕获unload-DLL调试事件(7)
30.捕获创建线程调试事件(不包括进程的main线程)(2) -> 获取线程句柄
31.捕获异常信息(1)->0x80000001, 访问标记了页保护的内存区域时,会触发此异常。异常位置:0x411023
32.捕获异常信息(1)->0x80000001, 访问标记了页保护的内存区域时,会触发此异常。异常位置:0x413190
33.捕获异常信息(1)->0x80000001, 访问标记了页保护的内存区域时,会触发此异常。异常位置:0x412DE0
34.捕获异常信息(1)->0x80000001, 访问标记了页保护的内存区域时,会触发此异常。异常位置:0x414740
35.捕获异常信息(1)->0x80000001, 访问标记了页保护的内存区域时,会触发此异常。异常位置:0x415C80
36.捕获异常信息(1)->0x80000001, 访问标记了页保护的内存区域时,会触发此异常。异常位置:0x416039
---
捕获其他事件
---
每次调试记录这些调试事件时,可能会有所不同,但这并不影响后续的分析。
可以先尝试在第一次捕获0xC0000005异常时脱离进程,然后附加子进程。这样做的原因是,这个异常是在填写 IAT 表之前产生的,因此可以记录 IAT 信息。附加子进程后,可以看到程序停在了0x5A38A7E处。
直接将指令mov byte ptr ds:[eax], 0替换为 NOP,然后恢复主线程。在经过0xC000001D和0xC00000096两个异常后,程序在0x411023处再次触发0x80000001页保护异常(如下图所示)。可以看到,这一页全是乱码,并不是正常的代码。这表明在0x411023触发页保护异常后,调试的主程序经过一些操作后将源代码拷贝到此处,并去掉了页保护,随后继续运行。
从上述脚本记录的异常事件中,可以看到捕获到的0x80000001异常包括以下内容:
31.捕获异常信息(1)->0x80000001, 访问标记了页保护的内存区域时,会触发此异常。异常位置:0x411023
32.捕获异常信息(1)->0x80000001, 访问标记了页保护的内存区域时,会触发此异常。异常位置:0x413190
33.捕获异常信息(1)->0x80000001, 访问标记了页保护的内存区域时,会触发此异常。异常位置:0x412DE0
34.捕获异常信息(1)->0x80000001, 访问标记了页保护的内存区域时,会触发此异常。异常位置:0x414740
35.捕获异常信息(1)->0x80000001, 访问标记了页保护的内存区域时,会触发此异常。异常位置:0x415C80
36.捕获异常信息(1)->0x80000001, 访问标记了页保护的内存区域时,会触发此异常。异常位置:0x416039
通过观察可以看到,第一次触发该异常时,就是OEP位置所在。
接下来,我们将分析代码解密的部分,重点关注关键函数 sub_428370。该函数调用了 sub_428620,而关键逻辑则位于 sub_428620 中。首先,该函数会拷贝子程序中触发异常位置的整个代码页。经过两次解密后,它最终调用 WriteProcessMemory 函数将解密后的代码写回去,去掉页保护,然后继续执行。
同理,后面产生的0x80000001页保护异常,也以同样方式处理。
② 脱壳步骤
通过以上分析,我们已经了解了子程序运行的整个流程,因此脱壳的过程并不复杂。
可以编写插件来实现脱壳,程序需要运行两遍。第一遍用于收集解密后的代码,并将数据存储到申请的缓存中;第二遍则在0xC0000005异常处断下,然后附加子程序以完成脱壳。具体步骤如下:
1.首先要过掉检测调试器。
2.其次,定位到在sub_426A50函数。在0x80000001异常处下断,断下后,在WriteProcessMemory处再次下断,收集解密后的代码数据,然后结束调试。
3.重新开始调试,在0xC0000005异常处下断,断下后,将子进程的所有线程挂起,然后脱离调试,附加子进程到当前调试器。
4.程序会停在mov byte ptr ds:[eax], 0这条指令处,使用 NOP 指令替换这条指令,并在 IAT 表的首地址设置硬件写入断点,然后恢复主线程。
5.停在 IAT 表第二次断下的地方(如下图所示),将解密后的代码拷贝到相应位置。如果停在 OEP 入口时再进行拷贝,可能会导致 OEP 入口的第一个字节无法拷贝,那么就需要手动修复了。
6.然后收集 IAT 表的信息,运行到 OEP,修复 IAT 表数据。
7.最后,使用 Scylla 插件进行 Dump 和 Fix Dump,完成脱壳过程。
05 高级保护
高级保护的所有选项,开启或者关闭,都没有作用,选项是失效的!因此,不做分析。
06 总结
此壳的最大难点在于双进程保护,只要妥善处理这一部分,其余的就相对简单了。其次,对于 IAT 表的处理,采用的是暴力跟踪的方法。在未找到关键点或加解密算法较为复杂的情况下,使用暴力跟踪来处理 IAT 表是一个有效的策略。
逆向或破解他人软件的行为是极其不道德的,这不仅缺乏对他人劳动成果的尊重,也损害了开发者的权益。作为一名软件开发人员,我深知软件开发过程中的艰辛与挑战。盗版软件的泛滥会严重打击软件开发和维护人员的信心,最终可能导致产品的流产。如果有任何侵权行为,请立即通知我,我将删除此帖。
看雪ID:舒默哦
https://bbs.kanxue.com/user-home-877885.htm
*本文为看雪论坛精华文章,由 舒默哦 原创,转载请注明来自看雪社区