开此帖的原因在于,许多论坛关于 Armadillo 脱壳的讨论往往只讲解脱壳步骤,而不涉及原理。即使有些帖子提到原理,内容也常常遵循安全人员的脱壳经验,这对初学者并不友好。因此,我决定开设此帖。Armadillo 壳属于强壳,但它是一款较老的壳,似乎已经很久没有更新了。我在吾爱破解论坛上只找到 9.64 的汉化版,而没有找到更高版本,因此我将分析这个版本。对于初学者,建议准备一个没有任何插件的 x32dbg,先尝试自己进行分析,然后再参考下面的分析过程。
Armadillo_9.64版本的汉化版界面如下:
最低保护,只对IAT表进行了加密。可以与源文件对比,找到IAT表的首地址,下硬件写入断点。壳程序会在断点位置写入两次,第二次才是真正填写IAT表的地址。
接下来收集IAT表的信息,需要三个要素:
1.函数名称。
2.函数所在的dll地址(也可以称dll句柄)。
3.最后需要回填的地址。
经过多次调试,可以在add edx,4这条指令处下断,以收集IAT的信息。当断点停在这里时,eax寄存器中储存的就是IAT地址,函数名称位于堆栈的[esp-8]位置,而DLL句柄则储存在局部变量[ebp-0x2948]中。
其中,定位dll的地址,需要往前面看,dll地址的关键信息,在以下位置:
通过和源文件对比,来确定OEP的入口,然后在堆栈窗口往回溯,找到转跳到OEP的CALL(如下图),记录下图所选择的特征码(8B 55 F4 2B 55 DC FF D2 89 45 FC EB 48
)。
搜索的时机是在 VirtualAlloc
函数下断,当遇到申请大小为 0x200000 时,返回到用户代码并运行到 0x43F756 这个位置。在 0x3EA0000 地址(即刚刚申请的 0x200000 大小的区域)中搜索 OEP 入口的特征码,就能够找到。
IAT表信息的收集,可以写插件来自动完成,通过插件来跟踪和保存收集到的IAT信息,然后运行到OEP后,再回填到IAT表,以完成修复。
因为是断点事件,所以插件的回调函数,其类型为断点回调(CB_BREAKPOINT)。开启跟踪后,收集的IAT信息会被存放到申请的缓存中,程序最终会停在OEP处。此时,查看IAT表,可以看到些地址加密了。
再点击回填IAT,以完成修复:
此时,可以使用x32dbg自带的插件Scylla来完成脱壳。需要注意的是,有些导入表地址是无效的,通过和源文件对比,这些地址是多余的,直接cut掉即可。
勾选仅标准保护,调试的时候,会依次产生如下异常:
1.0xC0000005异常
2.0xC0000096异常
3.0xc000001d异常
直接nop掉产生第一个异常的指令(如下图),然后脱壳步骤和最低保护一样。
标准保护加检测调试器,实际上是开启了双进程保护的。可以把仅标准保护的文件和此文件对比着一起调试,很容易发现关键指令 je huffmancoding.44056F,跳过去就不会执行双进程策略。因此,在跳过去后,其脱壳步骤和仅标准保护一样。
关于检测调试器,有两个地方进行了相关检查:一个是直接调用 IsDebuggerPresent
,另一个是通过 FS
寄存器来判断是否存在调试器。然而,调试器的检测也包含在双进程保护策略中。如果前面的检查已经跳过,则无需再进行处理。
根据上一节的方法,直接跳过je huffmancoding.44056F,无法有效阻止双进程保护的策略,后面会造成异常,导致程序崩溃。通过条件判断的来源,可以确认0x004F4838是一个全局变量,似乎是一个用于启用各种保护策略的结构体地址。值得注意的是,这些保护策略的启用或禁用并非简单的true或false,而是经过特定算法处理的开关。
正确的做法是不要跳过,而是直接分析创建子进程后的调试流程。因为被保护程序的代码是在子进程中运行的。可以逐步调试并跟踪,或在 WaitForDebugEvent 函数处下断,以找到关键函数 sub_426A50。该函数可以通过 IDA 进行查看,如下所示:
在x32dbg中,可以通过脚本来记录所有调试事件,获取的信息如下:
每次调试记录这些调试事件时,可能会有所不同,但这并不影响后续的分析。
可以先尝试在第一次捕获 0xC0000005 异常时脱离进程,然后附加子进程。这样做的原因是,这个异常是在填写 IAT 表之前产生的,因此可以记录 IAT 信息。附加子进程后,可以看到程序停在了 0x5A38A7E处。
直接将指令 mov byte ptr ds:[eax], 0 替换为 NOP,然后恢复主线程。在经过 0xC000001D 和 0xC00000096 两个异常后,程序在 0x411023 处再次触发 0x80000001 页保护异常(如下图所示)。可以看到,这一页全是乱码,并不是正常的代码。这表明在 0x411023 触发页保护异常后,调试的主程序经过一些操作后将源代码拷贝到此处,并去掉了页保护,随后继续运行。
从上述脚本记录的异常事件中,可以看到捕获到的 0x80000001 异常包括以下内容:
通过观察可以看到,第一次触发该异常时,就是OEP位置所在。
接下来,我们将分析代码解密的部分,重点关注关键函数 sub_428370。该函数调用了 sub_428620,而关键逻辑则位于 sub_428620 中。首先,该函数会拷贝子程序中触发异常位置的整个代码页。经过两次解密后,它最终调用 WriteProcessMemory 函数将解密后的代码写回去,去掉页保护,然后继续执行。
同理,后面产生的0x80000001页保护异常,也以同样方式处理。
通过以上分析,我们已经了解了子程序运行的整个流程,因此脱壳的过程并不复杂。
可以编写插件来实现脱壳,程序需要运行两遍。第一遍用于收集解密后的代码,并将数据存储到申请的缓存中;第二遍则在 0xC0000005 异常处断下,然后附加子程序以完成脱壳。具体步骤如下:
高级保护的所有选项,开启或者关闭,都没有作用,选项是失效的!因此,不做分析。
此壳的最大难点在于双进程保护,只要妥善处理这一部分,其余的就相对简单了。其次,对于 IAT 表的处理,采用的是暴力跟踪的方法。在未找到关键点或加解密算法较为复杂的情况下,使用暴力跟踪来处理 IAT 表是一个有效的策略。
逆向或破解他人软件的行为是极其不道德的,这不仅缺乏对他人劳动成果的尊重,也损害了开发者的权益。作为一名软件开发人员,我深知软件开发过程中的艰辛与挑战。盗版软件的泛滥会严重打击软件开发和维护人员的信心,最终可能导致产品的流产。如果有任何侵权行为,请立即通知我,我将删除此帖。
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
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
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
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
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 |
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 |
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:
{
/
/
这里面处理了调试事件类型的代码为
3
和
6
的事件,
/
/
3
是创建进程调试事件,
6
是加载dll调试事件。
}
}
ContinueDebugEvent(...);
/
/
继续调试事件
LeaveCriticalSection(...);
/
/
离开临界区
}
}
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
:
/
/
处理退出进程调试事件
{
...
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2024-11-23 13:27
被舒默哦编辑
,原因: 更正确定OEP入口的错误。