【加壳脱壳-ASProtect2.56脱壳分析及实例】此文章归类为:加壳脱壳。
ASProtect 是远古四大猛壳之一。自 2010 年发布 ASProtect SKE 2.78 后便停止了更新。本文将分析其 2.56 版本。读者可以通过以下方式获取该软件:在看雪论坛的工具板块下载,也可以点击这里进行下载。Stolen code 和 IAT 加密是 ASProtect 壳的显著特点。接下来,让我们一起探讨和学习。
通过未加壳文件的 OEP 地址来定位加壳文件中的 OEP 位置,并在此处设置硬件执行断点。当程序停在原始 OEP 后,注意观察 esp-0x18 这个位置。
点击 0xC514F7 并按下 Enter 键进入汇编窗口(如下图)。其中 call 0xC5066C 是最终跳转到 OEP 的函数。因此,可以提取周围的特征码来定位到该位置。
注: 0xC514F2地址位于VirtualAlloc第二次申请大小为0x60000的内存区域,其相对位置为:0x414f2。
当程序运行到壳的入口时,在代码区的起始地址设置硬件写入断点。解码完成后,在代码区再次设置内存访问断点。
按 F9 运行,会断在如下位置:
接着进行回溯。在回溯过程中,程序可能会跑飞直接运行,记录下来哪个函数跑飞的,然后再次调试并进入该函数。通过这种反复的过程,无需回溯太远,最终停在地址 0xBF14F2(如下图)。注意观察 esp+0x14 这个位置,在0x3470000地址下硬件执行断点。
注: 0xBF14F2地址位于VirtualAlloc第二次申请大小为0x60000的内存区域,其相对位置为:0x414f2。
按 F9 继续运行。当进入地址 0x3470000 后,经过一系列解码操作,最终获取到真实 OEP 的确切地址。下方箭头所指的 jmp eax 指令即为跳转到真实 OEP 的指令。
在使用 ASProtect 进行程序保护时,即使没有勾选任何保护选项,该壳仍会默认对 IAT 进行加密处理。本节将对 ASProtect 的 IAT 加密机制做一个简要的分析,并给出一个有效的 IAT 表修复方案。
破坏原始IAT结构。ASProtect会抹去PE头中的Improtect Table目录项,以及导入表中DLL名称和函数名的字符串。
动态加载API。通过LoadLibrary和GetProcAddress来动态获取API地址,关键API的调用会被替换为跳板代码地址,伪代码示例如下:
1 2 3 4 5 6 7 | ; 原始调用:call [kernelbase.GetSystemTimeAsFileTime] ; 加密后变为: call ASProtect_Resolver ; 跳板代码: ASProtect_Resolver: push 0x035B0000 ; 真实API地址存储在壳分配的堆内存中 ret ; |
以上便是 IAT 加密的大致流程。此外,IAT 加密的另一种方式是动态解密 API 地址。即跳板地址并不直接跳转到 API 的地址,而是根据加密的 API 标识符跳转到壳的解析逻辑。解析逻辑获取真实的 API 地址后,将其存储在壳分配的堆内存地址中,然后将该地址回填到 ASProtect_Resolver。执行权随后返回到 call 指令处,像第一种方式一样调用 API。伪代码示例如下:
1 2 3 4 5 6 7 | ; 原始调用:call [user32.MessageBoxA] ; 加密后变为(ASProtect_Resolver需要回填): call ASProtect_Resolver ; 跳板代码: ASProtect_Resolver: push 0x12345678 ; 加密的API标识符 jmp ResolverFunction ; 跳转到壳的解析逻辑 |
要修复 IAT 表,首先需要找到加密的位置。在获取 OEP 入口后,定位到一个需要填写跳板地址的 call 指令(见下图),并设置硬件写入断点(注意要四字节对齐,否则断点会设置失败),然后重新开始调试。
在断点触发的位置,向上追踪执行流程,即可定位IAT加密的实现代码。只要确定 IAT 表的三个关键要素(函数名称、函数地址以及回填地址),并在关键位置设置硬件执行断点,收集到与 IAT 表相关的必要信息后,便可完成对 IAT 表的修复。以下展示的是 IAT 表两种加密方式的关键位置代码。
IAT加密的第一种方式:
IAT表加密的第二种方式:
收集到的 IAT 表数据可以回填到原 IAT 表的空余位置,从而完成 IAT 表的修复。
如何确定原IAT表中的空余位置?
首先,需要获取程序初始化时的 IAT 表数据。当程序运行到 OEP 时,再次获取当前 IAT 表数据,并与初始化时的数据进行对比,就能识别出 IAT 表中的空余位置。
寻找初始化 IAT 表数据的具体方法:只需确定哪个 DLL 的导出表函数地址最先被填入 IAT 表中,然后在相应的 IAT 表地址处设置硬件写入断点。再次开始调试,当断点触发后,向上追踪执行流程,便可定位到初始化IAT表数据的实现代码(见下图)。
勾选此选项,加壳程序会加密资源节内容,但在执行流转移至原始入口点(OEP)前会自动解密。如遇脱壳后资源显示问题,新建节区转移资源即可解决。
此选项作用似乎不大。
此壳有三种方式检测程序是否被调试,分别如下:
注:fs:[0x30] 指向线程环境块(TEB)的基址。在 TEB 中,偏移量 0x2(即 fs:[0x30] + 0x2)处的字节是一个标志,用于指示当前进程是否正在被调试。
校验保护主要包括内存校验和文件校验。内存校验会实时检测关键代码段是否被修改。对抗内存校验的最简单方法是尽量避免使用软件断点。文件校验则用于验证磁盘文件是否被篡改。
以下对文件校验做一个简单的流程分析:
壳程序首先调用 CreateFile 函数打开文件,然后通过 CreateFileMapping 创建映射对象,最后使用MapViewOfFile 将其映射到内存中。映射文件的所有字节会被分成多段,并通过 MD5 算法进行处理。整个处理过程实际上是在解码跳转到 OEP 的那段代码数据(如上文中寻找 OEP 入口的以 0x3470000 地址开始的代码数据)。文件中的每个字节都相当于一个密钥,因此,只要磁盘文件被修改,最终解出的那段代码数据必定会出错。
此选项勾选后没有效果,不知是否是破解版本的问题?
模拟标准系统函数,不知是何意?分析勾选了此选项的被保护程序时,并未遇到显著的障碍。
高级输入表保护与 IAT 加密的第二种方式类似,唯一的区别在于解析出的 API 地址存储在壳分配的堆内存中,而不会回填到跳板代码处,每次解析后直接运行。此外,解析 IAT 表有两条路径(见下图),因此需要在这两条路径的相应位置设置硬件断点,用来收集导入表函数的地址。
勾选了此选项后,运行被保护程序时,会弹窗以下的输入窗口:
可以随意输入一些字符串,然后调试观察。要绕过此保护,只需在GetDlgItemTextA函数下断,然后返回用户地址。可以看到,正如下图所示,该保护有哈希比较的验证机制,如果哈希值相等,则不会跳转。
除此此外,还有另外两个位置需要改变跳转流程才能绕过此保护。可以通过单步调试来找到这两个关键位置。
第一处,跳过:
第二处,不跳:
要使用此功能,请勾选“使用激活密钥”选项,然后在密钥栏中填写注册名,最后点击“创建”即可生成注册码。
注册码生成操作:
勾选了此选项并生成证书后,运行被保护程序时,会弹窗以下的输入窗口:
输入任意名称和注册码,如果输入有误,将会弹出一个错误窗口:
绕过这个注册窗口非常简单。只需在 MessageBoxA 函数处设置断点,然后回溯执行流程找到关键跳转并跳过它,即可成功绕过注册验证。
要启用激活密钥属性,必须选中“使用激活密钥”选项。此外,通过“帮助 -> 注册”可以获取密钥栏中的硬件ID。激活密钥属性是“使用激活密钥”保护策略的一部分,因此一旦绕过“使用激活密钥”选项,这个保护也将被绕过。
获取硬件 ID 的方法:
勾选此项后,若天数用完,程序启动时将显示如下提示并自动退出。
要绕过该时间验证机制,可以在 GetSystemTime 函数入口设置断点。在等待第二次调用时中断后,取消该断点,并返回用户地址。继续单步跟踪代码,定位并修改关键跳转指令(关键跳),即可绕过限制。
该选项的绕过方式与到期天数相同。如果两个选项都被勾选,将以到期天数为准。
要启用此功能,请确保未选中“此模式是已经注册状态吗?”选项,然后勾选“使用提醒”和“使用延迟”。
“使用提醒”选项会弹出一个未注册的提示框,但并不影响程序的正常运行。
“使用延迟”选项对应的参数单位为秒(如上例设置为1秒)。当壳程序完成代码解密后,将主动调用Sleep函数暂停执行1秒钟,随后才跳转到原始程序入口点。
如果使用的是未注册版,并勾选项了 "使用提醒”,程序启动时会弹出如下一个未注册的对话框:
Express Thumbnail Creator (ETC) 是一款经典的图像处理软件,用于快速创建和管理缩略图,至今仍在更新。它主要服务于需要批量处理图像的用户,如摄影师、设计师和网站管理员。软件提供多种功能,帮助用户轻松生成、编辑和优化图像缩略图。该软件主界面如下:
通过查壳工具检测发现,etc 程序受 ASProtect 2.x 版本的保护。
从主界面可以观察到,该软件提供 30 天的试用期。在窗口左上角点击 Help -> Enter Registration Code...,会弹出一个注册框。随意输入一个注册码后,程序会提示需要重启(见下图),这表明存在重启验证机制。
重启验证通常涉及到对注册表或 INI 文件的访问,可以通过设置相关的断点进行观察。通过调试分析可以发现,壳程序在解码过程中会访问注册表,其中 "Software\Neowise\Express Thumbnail Creator" 下保存了密钥和版本等信息,见下图:
注意:如果手头没有脱壳工具,可以尝试直接进行不脱壳分析。此壳对断点检测非常严格,尽量不要下软件断点。
在用x32dbg调试时,程序会触发两次异常,随后恢复正常运行。结合之前的分析可知,该程序的重启验证涉及注册表操作。因此,在第二次异常触发时,可以在相关位置设置好软件断点(见下图)。按下 Shift + F9 运行,断下后取消该断点。
接下来,在 RegOpenKeyExInternalA处设置硬件断点,再继续运行程序。如果查询到的注册表项是 "Software\Neowise\Express Thumbnail Creator",则返回到用户区域。
为什么不在RegOpenKeyExA函数下断?
这是由于 ASProtect 壳在运行时动态抽取了系统 API 的部分代码,并直接调用了 RegOpenKeyExInternalA。这一点需要注意。
调试找到关键跳转je 614475(见下图),跳过即可避免试用提示。但About窗口仍显示试用版信息,且Help菜单中的注册选项可见,说明还有其他验证机制未被绕过。
About窗口仍显示试用版信息:
向上查找不是很远,可以看到如下几行关键代码。只需将 sete dl
修改为 setne dl
,就可使 Help 菜单中不再显示注册选项。
从上面的几行代码可以看出,eax
的值来源于地址 0x6337A8
。因此,要去除 About 框中的试用版信息,0x6337A8
地址至关重要。可以尝试在该地址设置硬件访问断点。这样,当再次点击 About 窗口时,程序会在 0x5DC0D4
处中断。如果不进行跳转,程序将显示为注册版。
此时,About窗口的注册状态标识已正确显示:
通过以上分析可以看出,要实现 etc 程序的注册,只需修改两个地方:一是将 setz dl 改为 setnz dl,二是将 cmp dword ptr ds:[eax], 0 改为 cmp dword ptr ds:[eax], 1。然而,由于程序在运行时才会将代码解码,静态补丁的方法不可行。可以采用内存注入的方式来打补丁。考虑到程序在运行期间会调用系统函数 GetVersion ,因此可以HOOK此函数来实现修改。HOOK最佳时机是在程序到达系统入口点时。
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 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | / * * 程序流程: * 以调试的方式创建子程序,并捕获子程序的调试信息,当子程序到达系统断点后,则注入相应的代码。 * / #include <windows.h> #include <iostream> using namespace std; #define filePath "C:\\Users\\18573\\Desktop\\破解练习\\45-某ASProtect 2.x软件脱壳破解\\etc.exe" / / 读取内存数据 char * ReadMemory(LPCVOID address, SIZE_T size) { if (address = = NULL) { return NULL; } / / 存储读取的数据 BYTE * buffer = new BYTE[size]; / / 读取当前进程的内存 SIZE_T bytesRead; if (ReadProcessMemory(GetCurrentProcess(), address, buffer , size, &bytesRead)) { std::cout << "读取成功,地址: " << address << ", 内容: " ; for (SIZE_T i = 0 ; i < bytesRead; + + i) { std::cout << std:: hex << ( int ) buffer [i] << " " ; } std::cout << std::dec << std::endl; / / 恢复为十进制输出 } else { std::cerr << "无法读取内存,错误代码: " << GetLastError() << std::endl; } return (char * ) buffer ; } / / 获取API的地址 char * getApiAddr(const char * dllName, const char * funcName) { / / 加载 kernel32.dll HMODULE hModule = LoadLibrary( "kernelbase.dll" ); if (hModule = = NULL) { return NULL; } / / 获取 GetVersion 函数的地址 FARPROC pGetVersion = GetProcAddress(hModule, "GetVersion" ); if (pGetVersion = = NULL) { FreeLibrary(hModule); return NULL; } return (char * )pGetVersion; } / / Hook子程序 void HookSubProcess(HANDLE hProcess) { #define ByteNumb 9 char * pVersionAddr = getApiAddr( "kernelbase.dll" , "GetVersion" ); / / 获取GetVersion的地址 char * buff = ReadMemory((LPCVOID)pVersionAddr, ByteNumb); / / 读取前 9 个字节 if (buff = = NULL) { cout << "Hook 失败" << endl; return ; } char * backAddr = pVersionAddr + ByteNumb; / / 在 0x61BEEF 地址,写入以下的shellcode char modifyKeyChar[] = { 0xC6 , 0x05 , 0xB2 , 0x43 , 0x61 , 0x00 , 0x95 , / / mov byte ptr ds : [ 6143B2 ] , 95 0xC6 , 0x05 , 0xD3 , 0xC0 , 0x5D , 0x00 , 0x01 , / / mov byte ptr ds : [ 5DC0D3 ] , 1 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , / / GetVersion函数Hook后,需要回填的 9 个字节 0xB8 , 0x00 , 0x00 , 0x00 , 0x00 , / / mov eax, GetVersion + 9 0xFF , 0xE0 / / jmp eax }; for ( int i = 0 ; i < ByteNumb; i + + ) { modifyKeyChar[ 14 + i] = buff[i]; } * ( int * )(&modifyKeyChar[ 24 ]) = ( int )backAddr; / / 在Getversion函数开始位置,写入以下的shellcode char hookGetversion[] = { 0xB8 , 0xEF , 0xBE , 0x61 , 0x00 , / / mov eax, 0x61BEEF (源程序空白的代码区) 0xFF , 0xE0 / / jmp eax }; / / 把modifyKeyChar数据写入到子进程的 0x61BEEF 地址处 SIZE_T bytesWritten; int addr1 = 0x61BEEF ; if (!WriteProcessMemory(hProcess, (LPVOID)addr1, modifyKeyChar, sizeof(modifyKeyChar), &bytesWritten)) { std::cerr << "无法写入内存,错误代码: " << GetLastError() << std::endl; } else { std::cout << "成功写入 " << bytesWritten << " 字节到地址 " << addr1 << std::endl; } / / 把hookGetversion数据写入到Getversion函数开始位置 if (!WriteProcessMemory(hProcess, (LPVOID)pVersionAddr, hookGetversion, sizeof(hookGetversion), &bytesWritten)) { std::cerr << "无法写入内存,错误代码: " << GetLastError() << std::endl; } else { std::cout << "成功写入 " << bytesWritten << " 字节到地址 " << ( int )pVersionAddr << std::endl; } } / / 调试子进程 void DebugProcess(const char * processName) { PROCESS_INFORMATION processInfo; STARTUPINFO startupInfo; ZeroMemory(&startupInfo, sizeof(startupInfo)); startupInfo.cb = sizeof(startupInfo); ZeroMemory(&processInfo, sizeof(processInfo)); / / 启动目标进程 if (!CreateProcess(processName, NULL, NULL, NULL, FALSE, DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &startupInfo, &processInfo)) { std::cerr << "无法启动进程,错误代码: " << GetLastError() << std::endl; return ; } / / 处理调试事件 DEBUG_EVENT debugEvent; while (WaitForDebugEvent(&debugEvent, INFINITE)) { switch (debugEvent.dwDebugEventCode) { case EXCEPTION_DEBUG_EVENT: { / / int ExceptionCode = debugEvent.u.Exception.ExceptionRecord.ExceptionCode; / / 到达系统断点后,调用Hook函数 HookSubProcess(processInfo.hProcess); / / 脱离调试,并返回 DebugActiveProcessStop(processInfo.dwProcessId); CloseHandle(processInfo.hProcess); CloseHandle(processInfo.hThread); return ; } default: ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE); break ; } } return ; } / / 删除指定的注册表项 void DeleteRegistryKey(HKEY hRootKey, const char * subKey) { LONG result = RegDeleteKeyA(hRootKey, subKey); if (result = = ERROR_SUCCESS) { std::cout << "注册表项删除成功: " << subKey << std::endl; } else { std::cerr << "删除失败,错误码: " << result << std::endl; } } / / 不显示控制台窗口,可以使用WinMain int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, PSTR cmdline, int cmdshow) { const char * subKey = "Software\\ASProtect\\SpecData" ; DeleteRegistryKey(HKEY_CURRENT_USER, subKey); / / 尝试删除注册表项 DebugProcess(filePath); return 0 ; } |
通过动态注入内存补丁,成功修复了程序的执行流程。如下图所示,程序运行已符合正常标准:
针对加壳程序(特别是使用强壳的情况下),如果了解这个壳的特性但没有合适工具,或现有工具因壳版本更新而失效时,可考虑直接进行带壳分析,通过内存dump、API hooking等技术实现不脱壳对程序进行修改。
附件上传的是etc程序的安装压缩包。
更多【加壳脱壳-ASProtect2.56脱壳分析及实例】相关视频教程:www.yxfzedu.com