首页
社区
课程
招聘
ASProtect2.56脱壳分析及实例
发表于: 2025-3-29 23:33 5475

ASProtect2.56脱壳分析及实例

2025-3-29 23:33
5475

  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;
    }
}
 
 
 
// 不显示控制台窗口,可以使用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;
}
/*
* 程序流程:
*        以调试的方式创建子程序,并捕获子程序的调试信息,当子程序到达系统断点后,则注入相应的代码。
*/
 
 
#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;
    }
 

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2025-3-29 23:58 被舒默哦编辑 ,原因: 更正错误
上传的附件:
收藏
免费 3
支持
分享
最新回复 (1)
雪    币: 412
活跃值: (288)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
太牛了,如果能对细节更详细点就好了
8小时前
0
游客
登录 | 注册 方可回帖
返回