【加壳脱壳-Armadillo_9.64加壳脱壳分析】此文章归类为:加壳脱壳。
开此帖的原因在于,许多论坛关于 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
)。搜索的时机,就是在IAT二次断下的时候,在x32dbg中右键->搜索->当前区域->特征匹配,然后填上特征码,就可以找到以下位置:
IAT表信息的收集,可以写插件来自动完成,通过插件来跟踪和保存收集到的IAT信息,然后运行到OEP后,再回填到IAT表,以完成修复。
因为是断点事件,所以插件的回调函数,其类型为断点回调(CB_BREAKPOINT)。开启跟踪后,收集的IAT信息会被存放到申请的缓存中,程序最终会停在OEP处。此时,查看IAT表,可以看到些地址加密了。
再点击回填IAT,以完成修复:
此时,可以使用x32dbg自带的插件Scylla来完成脱壳。需要注意的是,有些导入表地址是无效的,通过和源文件对比,这些地址是多余的,直接cut掉即可。
勾选仅标准保护,调试的时候,会依次产生如下异常:
1.0xC0000005异常
1 2 3 4 5 6 7 8 9 | 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异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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掉产生第一个异常的指令(如下图),然后脱壳步骤和最低保护一样。
标准保护加检测调试器,实际上是开启了双进程保护的。可以把仅标准保护的文件和此文件对比着一起调试,很容易发现关键指令 je huffmancoding.44056F,跳过去就不会执行双进程策略。因此,在跳过去后,其脱壳步骤和仅标准保护一样。
关于检测调试器,有两个地方进行了相关检查:一个是直接调用 IsDebuggerPresent
,另一个是通过 FS
寄存器来判断是否存在调试器。然而,调试器的检测也包含在双进程保护策略中。如果前面的检查已经跳过,则无需再进行处理。
根据上一节的方法,直接跳过je huffmancoding.44056F,无法有效阻止双进程保护的策略,后面会造成异常,导致程序崩溃。通过条件判断的来源,可以确认0x004F4838是一个全局变量,似乎是一个用于启用各种保护策略的结构体地址。值得注意的是,这些保护策略的启用或禁用并非简单的true或false,而是经过特定算法处理的开关。
正确的做法是不要跳过,而是直接分析创建子进程后的调试流程。因为被保护程序的代码是在子进程中运行的。可以逐步调试并跟踪,或在 WaitForDebugEvent 函数处下断,以找到关键函数 sub_426A50。该函数可以通过 IDA 进行查看,如下所示:
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | 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(...); / / 离开临界区 } } |
在x32dbg中,可以通过脚本来记录所有调试事件,获取的信息如下:
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | 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 异常包括以下内容:
1 2 3 4 5 6 | 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 异常处断下,然后附加子程序以完成脱壳。具体步骤如下:
高级保护的所有选项,开启或者关闭,都没有作用,选项是失效的!因此,不做分析。
此壳的最大难点在于双进程保护,只要妥善处理这一部分,其余的就相对简单了。其次,对于 IAT 表的处理,采用的是暴力跟踪的方法。在未找到关键点或加解密算法较为复杂的情况下,使用暴力跟踪来处理 IAT 表是一个有效的策略。
逆向或破解他人软件的行为是极其不道德的,这不仅缺乏对他人劳动成果的尊重,也损害了开发者的权益。作为一名软件开发人员,我深知软件开发过程中的艰辛与挑战。盗版软件的泛滥会严重打击软件开发和维护人员的信心,最终可能导致产品的流产。如果有任何侵权行为,请立即通知我,我将删除此帖。
更多【加壳脱壳-Armadillo_9.64加壳脱壳分析】相关视频教程:www.yxfzedu.com