|
本帖最后由 吃麻花麻花疼 于 2024-5-8 20:12 编辑
反逆向分析技术之模拟SysCall流程
Z0-导入:
当你尝试破解一个程序时,是否会关注其WindowsAPI调用?例如常见的CreateThread,OpenProcess,MessageBox等。
显然,如果可以定位到程序调用的API,可以极大降低我们的破解难度。反之,如果无法定位到程序调用的API,甚至是不知道调用了哪些API,岂不是难上加难?
Z1-SysCall(系统调用)流程:
Windows系统提供了两种处理器访问模式:用户模式(user mode)和内核模式(kernel mode)。通常情况下,应用程序运行在用户层,具有一段相对独立的虚拟内存,因此无法访问其他空间的内存;可是内核中包含了大量操作系统的内部数据结构,应用程序难免需要访问这些数据结构或调用内部Windows例程以执行特权操作,此时必须先从用户模式切换到内核模式,这里就涉及到SysCall(系统调用)。
举个简单的例子,OpenProcess调用流程:call OpenProcess-->kernel32.dll.OpenProcess-->ntoskrnl.exe.NtOpenProcess-->SysCall
当在程序代码段Call OpenProcess时,先跳转到kernel32.dll.OpenProcess,执行以下代码
[Asm] 纯文本查看 复制代码 mov edi,edi
push ebp
mov ebp,esp
sub esp,24
mov eax,dword ptr ss:[ebp+10]
xor ecx,ecx
mov dword ptr ss:[ebp-C],eax
mov eax,dword ptr ss:[ebp+C]
neg eax
mov dword ptr ss:[ebp-8],ecx
mov dword ptr ss:[ebp-24],18
sbb eax,eax
mov dword ptr ss:[ebp-20],ecx
and eax,2
mov dword ptr ss:[ebp-1C],ecx
mov dword ptr ss:[ebp-18],eax
lea eax,dword ptr ss:[ebp-C]
push eax
lea eax,dword ptr ss:[ebp-24]
mov dword ptr ss:[ebp-14],ecx
push eax
push dword ptr ss:[ebp+8]
lea eax,dword ptr ss:[ebp-4]
mov dword ptr ss:[ebp-10],ecx
push eax
call dword ptr ds:[<&NtOpenProcess>]
执行完毕后,会通过call dword ptr ds:[<&NtOpenProcess>]跳转到ntoskrnl.exe.NtOpenProcess,执行SysCall
[Asm] 纯文本查看 复制代码 mov eax,26
mov edx,ntdll.77938F70
syscall edx
ret 10
需要注意的是mov eax,26这条指令,其代表着需要调用的“函数标号”,不同函数具有不同的标号,只有为26时,才是调用NtOpenProcess
显而易见的是,API的调用流程十分繁琐与明显,导致极易定位乃至hook函数的调用,这也让逆向分析有了可乘之机。那么我们如何解决这个问题呢?
方法1:程序内复写WindowsAPI,不调用任何dll 弊端:呃?勇气可嘉,祝福你早日写完!
方法2:利用GetProcessAddress获取函数地址进行调用 弊端:GetProcessAddress过于敏感,下断即可拦截信息
看来以上方法不太行得通,难道没有解决方法了吗?
诶,似乎就算调用WindowsAPI,其底层也是通过SysCall实现的啊。那么,如果我们直接通过对应的”函数标号“进行SysCall,岂不妙哉?
Z2-C++模拟SysCall流程:
P1:动态获取所需dll的基址,此处以ntdll为例,如果你需要用到其他的dll,方法大相径庭。
[C++] 纯文本查看 复制代码 //获取ntdll基址[/font][/color]
[color=#acbac7][font=system-ui, -apple-system, BlinkMacSystemFont, "]void* GetNtDllBase()
{
//通过4次偏移从gs寄存器中取得ntdll基址
//用户模式时,gs指向TEB寄存器
ULONG64 peb = __readgsqword(0x60);//从TEB中获取PEB
ULONG64 ldr = *(ULONG64*)(peb + 0x18);//从PEB中获取LDR
PLIST_ENTRY modList = *(PLIST_ENTRY*)(ldr + 0x10);//从LDR中获取保存模块信息的链表
return *(void**)((ULONG64)modList->Flink+0x30);//从modList中获取ntdll基址
}
P2:实现自己的hash算法,你也可以用其他的算法。
这里用到hash算法的原因是,我们需要先将需要调用的API名称转为hash值,再通过hash值匹配对应的函数。这样可以避免出现明文字符串
[Java] 纯文本查看 复制代码 //国际知名算法---djb2,你也可以用其他的
DWORD64 djb2(PBYTE str)
{
DWORD64 dwHash = 0x52194628;//随意设定
int c;
while (c = *str++)
dwHash = ((dwHash << 0x5) + dwHash) + c;
return dwHash;//返回hash
}
P3:获取SysCall系统调用号,也就是先前的所称的”函数标号“
[C++] 纯文本查看 复制代码 //利用hash匹配系统调用号
int GetSystemCallIndex(DWORD64 hash)
{
//依旧是多次偏移获取目标
BYTE* ntdllBase = (BYTE*)GetNtDllBase();//获取ntdll基址
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)ntdllBase;//获取Dos头,直接强转就行了
PIMAGE_FILE_HEADER pFile = (PIMAGE_FILE_HEADER)(ntdllBase + pDos->e_lfanew + 4);//e_lfanew是Dos头末尾,+4跳过5045
PIMAGE_OPTIONAL_HEADER pOptional = (PIMAGE_OPTIONAL_HEADER)((BYTE*)pFile + IMAGE_SIZEOF_FILE_HEADER);//获取程序可选头
PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(ntdllBase + pOptional->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);//获取导出表
DWORD numberOfFunc = pExport->NumberOfFunctions;//获取函数个数
DWORD numberOfName = pExport->NumberOfNames;//获取函数名个数
DWORD* pEAT = (DWORD*)(ntdllBase + pExport->AddressOfFunctions);//获取导出地址表
DWORD* pENT = (DWORD*)(ntdllBase + pExport->AddressOfNames);//导出名称表
WORD* pEIT = (WORD*)(ntdllBase + pExport->AddressOfNameOrdinals);//导出序号表
//遍历ntdll内函数
for (size_t i = 0; i < numberOfFunc; i++)
{
for (size_t j = 0; j < numberOfName; j++)
{
if (i == pEIT[j])
{
BYTE* fnName = (BYTE*)(ntdllBase + pENT[j]);
if (hash == djb2(fnName))//如果有函数名称的hash和传入的目标API名称的hash相同
{
return *(DWORD*)(ntdllBase + pEAT + 4);//返回该函数的系统调用号
}
}
}
}
return -1;//如果未找到,返回-1
}
P4:ASM实现SysCall
[Asm] 纯文本查看 复制代码 .data
;全局变量 存放需要模拟的API的系统调用号
SysCallId dword 0h
.code
;将传入的Id赋值给SysCallId
MySysCallWrapper proc
mov SysCallId,0
mov SysCallId,ecx
ret
MySysCallWrapper endp
;模拟SysCall流程
MySysCall proc
mov r10,rcx
mov eax,SysCallId
syscall
ret
MySysCall endp
end
在cpp文件中声明两个函数
[C++] 纯文本查看 复制代码 extern "C" VOID MySysCallWrapper(DWORD id);
extern "C" VOID MySysCall(...);
P5:调用测试,这里用的是NtCreateThreadEx函数,你可以换成任何函数
[C++] 纯文本查看 复制代码 void MyThread()
{
cout << "OK!" << endl;
}
int main()
{
//此处以NtCreateThreadEx为例
//cout << hex << djb2((BYTE*)"NtCreateThreadEx") << endl;
//对应的hash为0xe4856a46f7313853
DWORD id = GetSystemCallIndex(0xe4856a46f7313853);//通过hash匹配对应函数标号
MySysCallWrapper(id);//将获取到的id传入
//调用SysCall
HANDLE hThread;
MySysCall(&hThread, PROCESS_ALL_ACCESS, 0, GetCurrentProcess(), MyThread,0,0,0,0,0,0);
system("pause");
return 0;
}
Z3-测试:
运行程序后,可以观察到线程已经成功创建,并且输出OK!
拖入调试工具中可以发现,函数堆栈调用中并没有关于NtCreateThreadEx的相关信息
导入表内也找不到NtCreateThreadEx,甚至没有ntdll的调用
这样,NtCreateThreadEx的调用就被我们成功模拟了。现在,无论是API断点还是APIHook,均无法定位并拦截我们的调用,极大的增添了破解者的分析难度与分析成本!
Z4-结语:
模拟SysCall流程的方法有许多,各种解决方案也非常精彩,本文采用的是Hell'sGate地狱之门技术。学习该技术期间,浏览了许多大神的帖子与视频,本想在结尾加上链接,但恐有引流之嫌。在此由衷感谢各位!初次发帖,难免漏洞百出,各位请多包涵!附上完整项目链接
|
评分
-
参与人数 4 | 威望 +1 |
HB +26 |
THX +4 |
收起
理由
|
诸葛村夫
| |
+ 2 |
+ 1 |
[吾爱汇编论坛52HB.COM]-吃水不忘打井人,给个评分懂感恩! |
龙龙飞凤舞
| |
+ 2 |
+ 1 |
|
Shark恒
| + 1 |
+ 20 |
+ 1 |
[吾爱汇编论坛52HB.COM]-学破解防破解,知进攻懂防守! |
boot
| |
+ 2 |
+ 1 |
|
查看全部评分
|