-
-
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直播授课
赞赏
- [原创]VMProtect3.5.1脱壳临床指南 10396
- ASProtect2.56脱壳分析及实例 5476
- m_nasm项目简介 2063
- [原创]Armadillo(补充)及实例 1847
- [原创]Armadillo_9.64加壳脱壳分析 15468