大神论坛

找回密码
快速注册
查看: 50 | 回复: 0

[原创] 反逆向分析技术之模拟SysCall流程 附完整项目

主题

帖子

0

积分

初入江湖

UID
786
积分
0
精华
威望
0 点
违规
大神币
68 枚
注册时间
2024-01-20 19:29
发表于 2024-06-09 16:54
本帖最后由 52HB 于 2024-06-09 16:54 编辑

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,执行以下代码

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

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,方法大相径庭。

//获取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值匹配对应的函数。这样可以避免出现明文字符串

//国际知名算法---djb2,你也可以用其他的
DWORD64 djb2(PBYTE str)
{
DWORD64 dwHash = 0x52194628;//随意设定

int c;
while (c = *str++)
dwHash = ((dwHash << 0x5) + dwHash) + c;

return dwHash;//返回hash
}

P3:获取SysCall系统调用号,也就是先前的所称的”函数标号

//利用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

.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文件中声明两个函数

extern "C" VOID MySysCallWrapper(DWORD id);
extern "C" VOID MySysCall(...);

P5:调用测试,这里用的是NtCreateThreadEx函数,你可以换成任何函数

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,均无法定位并拦截我们的调用,极大的增添了破解者的分析难度分析成本

下方隐藏内容为本帖所有文件或源码下载链接:

游客你好,如果您要查看本帖隐藏链接需要登录才能查看, 请先登录

返回顶部