一、前言
最近翻找以前的代码,发现了这个竟然还处于弃坑状态,正好这两天比较闲,回坑补充一下,毕竟远古的技术啦,现在写来应该问题不大。
二、原理分析
1、wow64的调用流程
众所周知,64位系统的内核是64位的,所以32位程序跑的时候,进内核就需要转换一下,wow64就是做这个工作的。
简单的调用流程
32位ntdll.Zw/Ntxxxx
--> Wow64Transition
--> 64位wow64处理 --> 64位ntdll.Zw/Ntxxxx
-->进入内核
具体分析可参看oxygen1a1
大佬的帖子
Wow64系统调用分析以及ServiceTableHook
https://www.52pojie.cn/thread-1711323-1-1.html
(出处: 吾爱破解论坛)
2、hook点位查找
根据上面的调用流程分析,可以知道,在32位的终点是通过Wow64Transition
处的天堂之门
即jmp far 0033:xxxxxxx
进入64位运行。
所以找到Wow64Transition
的位置便是关键。
通过逆向分析,可以看到,在windows10上,ntdll
经过多次跳转才来到了Wow64Transition
。
win10 ntdll.ZwAccessCheck:
77DE2FB0 B8 00000000 mov eax,0x0
77DE2FB5 BA 408FDF77 mov edx,ntdll.77DF8F40
77DE2FBA FFD2 call edx
77DE2FBC C2 2000 retn 0x20
77DF8F40 - FF25 2892E977 jmp dword ptr ds:[Wow64Transition]
77D67000 EA 0970D677 330>jmp far 0033:77D67009 ;Wow64Transition
而windows7就比较简单了,通过fs:[0xC0]
既可以获取地址。
win7 ntdll.ZwAccessCheck:
7DE90218 B861000000 mov eax, 00000061h
7DE9021D 33C9 xor ecx, ecx
7DE9021F 8D542404 lea edx, dword ptr ss:[esp+04h]
7DE90223 64FF15C call dword ptr fs:[000000C0h]
7DE9022A 83C404 add esp, 04h
7DE9022D C22000 retn 0020h
经过测试,win10也是可以通过fs:[0xC0]
获得地址。
那么,问题就简单了,一句__readfsdword(0xC0)
就可以解决。
三、代码编写
1、SSDT和Shadow SSDT序号对应的函数名
通过查看上面的反汇编可以知道,序号在经过hook点时,是存放在eax
中的。在写hook函数时,可以获取eax
的值来判断调用的是哪个API。
那么,通过遍历ntdll
的导出表,将Zw
或者Nt
开头的函数挑出来,读取mov eax,xxx
中的xxx
即可将函数名和序号一一对应起来。
void listNtdllExport()
{
char* funcName;
DWORD addr, nIndex;
// 获取导出表地址
IMAGE_DOS_HEADER* dosHeader = (IMAGE_DOS_HEADER*)GetModuleHandle(L"ntdll.dll");
IMAGE_NT_HEADERS* ntHeaders = (IMAGE_NT_HEADERS*)((BYTE*)dosHeader + dosHeader->e_lfanew);
IMAGE_EXPORT_DIRECTORY* exportDir = (IMAGE_EXPORT_DIRECTORY*)((BYTE*)dosHeader +
ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
// 获取导出表中函数名称和地址
DWORD* nameRVAs = (DWORD*)((BYTE*)dosHeader + exportDir->AddressOfNames);
DWORD* funcRVAs = (DWORD*)((BYTE*)dosHeader + exportDir->AddressOfFunctions);
WORD* ordinals = (WORD*)((BYTE*)dosHeader + exportDir->AddressOfNameOrdinals);
for (DWORD i = 0; i < exportDir->NumberOfNames; i++)
{
funcName = (char*)((BYTE*)dosHeader + nameRVAs[i]);
addr = (DWORD)((BYTE*)dosHeader + funcRVAs[ordinals[i]]);
//确保地址不是0,并且只检查Zw开头的函数
if (addr != 0 && *funcName == 'Z')
{
//77DE3260 > B8 29000000 mov eax,0x29
if (*(BYTE*)addr == 0xB8)
{
nIndex = *(DWORD*)(addr + 1);
if (nIndex < 0xFFFFFF)
{
nIndex &= 0xFFF;
strcpy_s(ntList[nIndex], funcName);
}
}
}
}
}
在Ntdll
中,Nt
开头和Zw
开头是同一个函数,函数地址相同,这里使用Z开头判断,是为了和Shadow SSDT
函数做区分。
同理,在win10中,Shadow SSDT
函数在win32u.dll
中做跳转,根据上方代码做少量修改即可。
需要注意的是,Ntdll
序号为0x0XXX
,Win32u
中序号为0x1XXX
,部分序号高16位还有数据,需要加以剔除。
例如
77DE3660 ntdll.ZwAddAtomEx B8 69001100 mov eax,0x110069
77DE3665 BA 408FDF77 mov edx,ntdll.77DF8F40
2、hook代码编写
由于需要保存一个eax
,并且最后需要jmp到Wow64Transition
,所以需要一个中间函数。
__declspec(naked) void hookProc()
{
__asm
{
pushad
mov dwEax,eax
call hookProc2
popad
jmp tWow64Transition
}
}
这里使用一个裸函数,call一个hook处理函数即可保证只记录处理数据,不修改相关寄存器环境。
//处理函数
void WINAPI hookProc2()
{
DWORD dwCount;
DWORD dwId;
//跳过,防止输出信息时重入
if (bFlag)
{
dwCount++;
return;
}
bFlag = TRUE;
dwCount = dwCount;
dwCount = 0;
//取eax低16位,win32u中序号为1xxx,ntdll中为0xxx
dwId = dwEax & 0xFFFF;
if ((dwId&0x1000)==0x1000)
{
printf("跳过%d id:%d %s\r\n", dwCount, dwId&0xFFF, w32List[dwId & 0xFFF]);
}
else
{
printf("跳过%d id:%d %s\r\n", dwCount, dwId & 0xFFF, ntList[dwId & 0xFFF]);
}
bFlag = FALSE;
return;
}
在上面处理函数中只输出了函数名等信息,但需要注意,即便使用printf,也是需要进入内核的,所以,会引发hook重入,导致堆栈耗尽程序崩溃。本文使用了较为简单的全局标记法,这种方法在多线程时,会漏掉不少调用(通过下方输出信息也可以看出来,标准printf会跳过2条记录,但有时会跳5条或者更多)。至于优化方法,不在本文探讨范围之内。
输出信息
跳过2 id:24 ZwAllocateVirtualMemory
跳过2 id:340 ZwQueryLicenseValue
跳过2 id:5 NtUserCallNoParam
跳过2 id:347 ZwQuerySecurityAttributesToken
跳过2 id:347 ZwQuerySecurityAttributesToken
跳过2 id:34 NtUserGetProcessWindowStation
跳过2 id:106 NtUserGetObjectInformation
跳过2 id:242 NtUserGetGUIThreadInfo
跳过2 id:160 ZwConnectPort
跳过2 id:34 ZwRequestWaitReplyPort
跳过2 id:34 ZwRequestWaitReplyPort
跳过2 id:33 ZwQueryInformationToken
跳过2 id:18 ZwOpenKey
跳过2 id:289 ZwOpenKeyEx
跳过2 id:23 ZwQueryValueKey
跳过2 id:15 ZwClose
跳过2 id:15 ZwClose
跳过2 id:453 ZwTraceControl
跳过2 id:453 ZwTraceControl
跳过2 id:5 ZwCallbackReturn
跳过2 id:99 NtUserGetWindowDC
跳过5 id:80 ZwProtectVirtualMemory
跳过5 id:80 ZwProtectVirtualMemory
跳过2 id:176 NtGdiCreateSolidBrush
3、完整代码
#include <windows.h>
#include "detours/detours.h"
#include <stdio.h>
#include <locale.h>
//地址fs[0xc0]
typedef void (WINAPI *pWow64Transition)();
pWow64Transition tWow64Transition;
//跳过次数
DWORD dwCount = 0;
//跳过标记
BOOL bFlag = FALSE;
//保存的eax的值
DWORD dwEax = 0;
//ntdll中SSDT导出函数列表
char ntList[600][100];
//win32u中Shadow SSDT导出函数列表
char w32List[1600][100];
//初始化命令行输出映射
BOOL InitConsoleWindow()
{
FILE * pfile;
BOOL ret = AllocConsole();
if (ret)
{
//HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTitleA("Debug Output");
setlocale(LC_ALL, "chs");
freopen_s(&pfile, "CONOUT$", "w", stdout);
freopen_s(&pfile, "CONOUT$", "w", stderr);
freopen_s(&pfile, "CONIN$", "r", stdin);
}
return ret;
}
//处理函数
void WINAPI hookProc2()
{
DWORD dwCount;
DWORD dwId;
//跳过,防止输出信息时重入
if (bFlag)
{
dwCount++;
return;
}
bFlag = TRUE;
dwCount = dwCount;
dwCount = 0;
//取eax低16位,win32u中序号为1xxx,ntdll中为0xxx
dwId = dwEax & 0xFFFF;
if ((dwId&0x1000)==0x1000)
{
printf("跳过%d id:%d %s\r\n", dwCount, dwId&0xFFF, w32List[dwId & 0xFFF]);
}
else
{
printf("跳过%d id:%d %s\r\n", dwCount, dwId & 0xFFF, ntList[dwId & 0xFFF]);
}
bFlag = FALSE;
return;
}
__declspec(naked) void hookProc()
{
__asm
{
pushad
mov dwEax,eax
call hookProc2
popad
jmp tWow64Transition
}
}
void Hook()
{
tWow64Transition =(pWow64Transition) __readfsdword(0xC0);
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID&)tWow64Transition, hookProc);
DetourTransactionCommit();
}
void listNtdllExport()
{
char* funcName;
DWORD addr, nIndex;
// 获取导出表地址
IMAGE_DOS_HEADER* dosHeader = (IMAGE_DOS_HEADER*)GetModuleHandle(L"ntdll.dll");
IMAGE_NT_HEADERS* ntHeaders = (IMAGE_NT_HEADERS*)((BYTE*)dosHeader + dosHeader->e_lfanew);
IMAGE_EXPORT_DIRECTORY* exportDir = (IMAGE_EXPORT_DIRECTORY*)((BYTE*)dosHeader +
ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
// 获取导出表中函数名称和地址
DWORD* nameRVAs = (DWORD*)((BYTE*)dosHeader + exportDir->AddressOfNames);
DWORD* funcRVAs = (DWORD*)((BYTE*)dosHeader + exportDir->AddressOfFunctions);
WORD* ordinals = (WORD*)((BYTE*)dosHeader + exportDir->AddressOfNameOrdinals);
for (DWORD i = 0; i < exportDir->NumberOfNames; i++)
{
funcName = (char*)((BYTE*)dosHeader + nameRVAs[i]);
addr = (DWORD)((BYTE*)dosHeader + funcRVAs[ordinals[i]]);
//确保地址不是0,并且只检查Zw开头的函数
if (addr != 0 && *funcName == 'Z')
{
//77DE3260 > B8 29000000 mov eax,0x29
if (*(BYTE*)addr == 0xB8)
{
nIndex = *(DWORD*)(addr + 1);
if (nIndex < 0xFFFFFF)
{
nIndex &= 0xFFF;
strcpy_s(ntList[nIndex], funcName);
}
}
}
}
}
void listWin32uExport()
{
char* funcName;
DWORD addr, nIndex;
// 获取导出表地址
IMAGE_DOS_HEADER* dosHeader = (IMAGE_DOS_HEADER*)GetModuleHandle(L"win32u.dll");
//无窗口不存在这个dll
if (dosHeader == NULL) return;
IMAGE_NT_HEADERS* ntHeaders = (IMAGE_NT_HEADERS*)((BYTE*)dosHeader + dosHeader->e_lfanew);
IMAGE_EXPORT_DIRECTORY* exportDir = (IMAGE_EXPORT_DIRECTORY*)((BYTE*)dosHeader +
ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
// 获取导出表中函数名称和地址
DWORD* nameRVAs = (DWORD*)((BYTE*)dosHeader + exportDir->AddressOfNames);
DWORD* funcRVAs = (DWORD*)((BYTE*)dosHeader + exportDir->AddressOfFunctions);
WORD* ordinals = (WORD*)((BYTE*)dosHeader + exportDir->AddressOfNameOrdinals);
for (DWORD i = 0; i < exportDir->NumberOfNames; i++)
{
funcName = (char*)((BYTE*)dosHeader + nameRVAs[i]);
addr = (DWORD)((BYTE*)dosHeader + funcRVAs[ordinals[i]]);
if (addr != 0 && *funcName == 'N')
{
//77092220 w> B8 22110000 mov eax,0x1122
if (*(BYTE*)addr == 0xB8)
{
nIndex = *(DWORD*)(addr + 1);
if (nIndex < 0xFFFFFF)
{
nIndex &= 0xFFF;
strcpy_s(w32List[nIndex], funcName);
}
}
}
}
}
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
if (ul_reason_for_call == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(hModule);
InitConsoleWindow();
printf("DLL加载\r\n");
listNtdllExport();
listWin32uExport();
Hook();
}
if (ul_reason_for_call == DLL_PROCESS_DETACH)
{
printf("DLL卸载\r\n");
}
return TRUE;
}
四、结语
本文代码仅在win10 22H2测试通过,win7由于没有win32u.dll,相关函数是通过user32.dll来中转,且部分函数未导出,故不支持win7获取此部分。当然Ntdll还是可以的。
叠盾时间,本文全部依靠自己理解所写,如有用词不规范或者谬误之处,还请各位大佬指教。
最后小提示,由于hook的32转64进内核的地方,调用会非常非常频繁,添加任何代码都将大幅影响运行速度,加大系统运行开销。