-
-
ASProtect2.56脱壳分析及实例
-
发表于: 2025-3-29 23:33 7698
-
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的调用会被替换为跳板代码地址,伪代码示例如下:

以上便是 IAT 加密的大致流程。此外,IAT 加密的另一种方式是动态解密 API 地址。即跳板地址并不直接跳转到 API 的地址,而是根据加密的 API 标识符跳转到壳的解析逻辑。解析逻辑获取真实的 API 地址后,将其存储在壳分配的堆内存地址中,然后将该地址回填到 ASProtect_Resolver。执行权随后返回到 call 指令处,像第一种方式一样调用 API。伪代码示例如下:
要修复 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最佳时机是在程序到达系统入口点时。
通过动态注入内存补丁,成功修复了程序的执行流程。如下图所示,程序运行已符合正常标准:

针对加壳程序(特别是使用强壳的情况下),如果了解这个壳的特性但没有合适工具,或现有工具因壳版本更新而失效时,可考虑直接进行带壳分析,通过内存dump、API hooking等技术实现不脱壳对程序进行修改。
附件上传的是etc程序的安装压缩包。
; 原始调用:call [kernelbase.GetSystemTimeAsFileTime]; 加密后变为:call ASProtect_Resolver; 跳板代码:ASProtect_Resolver: push 0x035B0000 ; 真实API地址存储在壳分配的堆内存中 ret ; ; 原始调用:call [kernelbase.GetSystemTimeAsFileTime]; 加密后变为:call ASProtect_Resolver; 跳板代码:ASProtect_Resolver: push 0x035B0000 ; 真实API地址存储在壳分配的堆内存中 ret ; ; 原始调用:call [user32.MessageBoxA]; 加密后变为(ASProtect_Resolver需要回填):call ASProtect_Resolver ; 跳板代码:ASProtect_Resolver: push 0x12345678 ; 加密的API标识符 jmp ResolverFunction ; 跳转到壳的解析逻辑; 原始调用:call [user32.MessageBoxA]; 加密后变为(ASProtect_Resolver需要回填):call ASProtect_Resolver ; 跳板代码:ASProtect_Resolver: push 0x12345678 ; 加密的API标识符 jmp ResolverFunction ; 跳转到壳的解析逻辑/** 程序流程:* 以调试的方式创建子程序,并捕获子程序的调试信息,当子程序到达系统断点后,则注入相应的代码。*/#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; }}// 不显示控制台窗口,可以使用WinMainint 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;}/** 程序流程:* 以调试的方式创建子程序,并捕获子程序的调试信息,当子程序到达系统断点后,则注入相应的代码。*/#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; }[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!
赞赏
- [原创]反编译中复合条件问题的分析与处理 1308
- [原创]VMProtect3.5.1脱壳临床指南 18611
- ASProtect2.56脱壳分析及实例 7699
- m_nasm项目简介 2192
- [原创]Armadillo(补充)及实例 2062