0x00 前言
參考文章:
https://blog.csdn.net/whatday/article/details/14160875
https://blog.csdn.net/pjz969/article/details/8615085
首先重載內核不是什麼新技術,對於繞過HOOK是一種一刀切的做法,不過對於我這樣初探內核的小白還是很有總結意義的。
本篇涉及的知識點:0環和3環通信,PE結構(除了基礎的頭外還有重定位表),用到了兩種HOOK(SSDT HOOK,Inline HOOK),API函數調用(逆了一個函數KiFastCallEntry)。
流程:
1)按照字節對齊把文件中的PE結構在內存裏展開
2)利用重定位表修改全局變量
3)COPY自己的SSDT表
4)函數調用時換成新的內核(HOOK KiFastCallEntry)
0x01 展開PE結構
因爲我的環境是win7 32位,所以要把ntkrnlpa.exe文件在內存裏展開.
1. 首先,在內核裏使用RtlInitUnicodeString創建對象,打開文件使用ZwCreateFile,並且要初始化InitializeObjectAttributes。
2. 通過PE結構的知識,DOS頭,PE頭,節表,是緊挨在一起的無需擴展,只需確定在文件中的偏移和大小利用ZwReadFile即可獲取:
1)DOS頭的偏移就是0,大小爲sizeof(IMAGE_DOS_HEADER);
2)PE頭的偏移由DOS最後一個成員確定ImageDosHeader.e_lfanew,大小爲sizeof(IMAGE_NT_HEADER);
3)節表的偏移爲+= sizeof(IMAGE_NT_HEADERS),因爲節表裏HEADER的數量不確定,所以需要到
IMAGE_NT_HEADER->IMAGE_FILE_HEADER.NumberOfSection確定節的數目,所以大小爲 sizeof(IMAGE_SECTION_HEADER) * 前者。
4)每個節的偏移由
IMAGE_SCTION_HEADER->SectionHeader[i].VirTualAddress(相對於文件開頭),
節的大小需要判斷:比較SectionHeader[i].Misc.VirTualSize(實際的大小)和SectionHeader[i].SizeOfRawData(按字節對齊 時文件中的大小),因爲全局變量的關係,所以兩者大小不一定,取大的那個。
3. 第三,就是用RtlCopyMemory一段一段COPY DOS頭,PE頭,節表,和每個節。
void LoadKernel()
{
NTSTATUS status;
UNICODE_STRING uFileName;
HANDLE hFile;
OBJECT_ATTRIBUTES ObjAttr;
IO_STATUS_BLOCK IoStatusBlock;
LARGE_INTEGER FileOffset;
ULONG retsize;
PVOID lpVirtualPointer;
ULONG uLoop;
ULONG SectionVirtualAddress, SectionSize;
IMAGE_DOS_HEADER ImageDosHeader;
IMAGE_NT_HEADERS ImageNtHeader;
IMAGE_SECTION_HEADER* lpImageSectionHeader;
InitializeObjectAttributes(&ObjAttr, \
& uFileName, \
OBJ_CASE_INSENSITIVE, \
NULL, \
NULL);
RtlInitUnicodeString(&uFileName, L"\\??\\C:\\WINDOWS\\system32\\ntkrnlpa.exe");
//打開文件
status = ZwCreateFile(\
& hFile, \
FILE_ALL_ACCESS, \
& ObjAttr, \
& IoStatusBlock, \
0, \
FILE_ATTRIBUTE_NORMAL, \
FILE_SHARE_READ, \
FILE_OPEN, \
FILE_NON_DIRECTORY_FILE, \
NULL, \
0);
if (!NT_SUCCESS(status))
{
KdPrint(("CreateFile Failed!"));
return;
}
//讀取DOS頭
FileOffset.QuadPart = 0;
status = ZwReadFile(hFile, \
NULL, \
NULL, \
NULL, \
& IoStatusBlock, \
& ImageDosHeader, \
sizeof(IMAGE_DOS_HEADER), \
& FileOffset, \
0);
if (!NT_SUCCESS(status))
{
KdPrint(("Read ImageDosHeader Failed!"));
ZwClose(hFile);
return;
}
//讀取NT頭
FileOffset.QuadPart = ImageDosHeader.e_lfanew;
status = ZwReadFile(hFile, \
NULL, \
NULL, \
NULL, \
& IoStatusBlock, \
& ImageNtHeader, \
sizeof(IMAGE_NT_HEADERS), \
& FileOffset, \
0);
if (!NT_SUCCESS(status))
{
KdPrint(("Read ImageNtHeaders Failed!"));
ZwClose(hFile);
return;
}
//讀取節表
lpImageSectionHeader = (IMAGE_SECTION_HEADER*)ExAllocatePool(NonPagedPool, \
sizeof(IMAGE_SECTION_HEADER) * ImageNtHeader.FileHeader.NumberOfSections);
FileOffset.QuadPart += sizeof(IMAGE_NT_HEADERS);
status = ZwReadFile(hFile, \
NULL, \
NULL, \
NULL, \
& IoStatusBlock, \
lpImageSectionHeader, \
sizeof(IMAGE_SECTION_HEADER) * ImageNtHeader.FileHeader.NumberOfSections, \
& FileOffset, \
0);
if (!NT_SUCCESS(status))
{
KdPrint(("Read ImageSectionHeader Failed!"));
ExFreePool(lpImageSectionHeader);
ZwClose(hFile);
return;
}
//COPY數據到內存
lpVirtualPointer = ExAllocatePool(NonPagedPool, \
ImageNtHeader.OptionalHeader.SizeOfImage);
if (lpVirtualPointer == 0)
{
KdPrint(("lpVirtualPointer Alloc space Failed!"));
ZwClose(hFile);
return;
}
memset(lpVirtualPointer, 0, ImageNtHeader.OptionalHeader.SizeOfImage);
//COPY DOS頭
RtlCopyMemory(lpVirtualPointer, \
& ImageDosHeader, \
sizeof(IMAGE_DOS_HEADER));
//COPY NT頭
RtlCopyMemory((PVOID)((ULONG)lpVirtualPointer + ImageDosHeader.e_lfanew), \
& ImageNtHeader, \
sizeof(IMAGE_NT_HEADERS));
//COPY 區表
RtlCopyMemory((PVOID)((ULONG)lpVirtualPointer + ImageDosHeader.e_lfanew + sizeof(IMAGE_NT_HEADERS)), \
lpImageSectionHeader, \
sizeof(IMAGE_SECTION_HEADER) * ImageNtHeader.FileHeader.NumberOfSections);
//依次COPY 各區段數據
for (uLoop = 0;uLoop < ImageNtHeader.FileHeader.NumberOfSections;uLoop++)
{
SectionVirtualAddress = lpImageSectionHeader[uLoop].VirtualAddress;//對應區段相對偏移
if (lpImageSectionHeader[uLoop].Misc.VirtualSize > lpImageSectionHeader[uLoop].SizeOfRawData)
SectionSize = lpImageSectionHeader[uLoop].Misc.VirtualSize;//取最大的佔用空間
else
SectionSize = lpImageSectionHeader[uLoop].SizeOfRawData;
FileOffset.QuadPart = lpImageSectionHeader[uLoop].PointerToRawData;//對應區段的超始地址
status = ZwReadFile(hFile, \
NULL, \
NULL, \
NULL, \
& IoStatusBlock, \
(PVOID)((ULONG)lpVirtualPointer + SectionVirtualAddress), \
SectionSize, \
& FileOffset, \
0);
if (!NT_SUCCESS(status))
{
KdPrint(("SectionData Read Failed!"));
ExFreePool(lpImageSectionHeader);
ExFreePool(lpVirtualPointer);
ZwClose(hFile);
return;
}
}
ExFreePool(lpImageSectionHeader);//釋放區段內存空間
KdPrint(("lpVirtualPointer: %X", lpVirtualPointer));
FixBaseRelocTable(lpVirtualPointer);
SetNewSSDT(lpVirtualPointer);
ZwClose(hFile);//關閉句柄
}
0x02 基址重定位
如果變量都是由鏡像地址+偏移算的話,那直接用就行,但是,全局變量的地址確實直接儲存的,它不管鏡像在哪,反正就是那個數,所以需要通過重定位表依次手動修改。
- 首先通過PE結構定位到重定位表
IMAGE_NT_HEADER->OptionalHeader.DataDirArry[IMAGE_DIRECTORY_ENTRY_BASERELOC]
2)
它的結構:
一行是4字節,一個TyOffset成員是2字節,全局變量的地址的地址 對 鏡像基地址 的偏移依次是VirtualAddress + TyOffset[i],這裏儲存的都是全局變量的地址(這裏一種是3型的地址,這種是全局變量有用的,其他類型的只是爲了使字節對齊而填充的,沒用),之後依次修改這裏的地址(即:加上內核首地址到imagebase的偏移)。
注意: SDK10裏TypeOffset[1] 被註釋掉了
這種寫法是不定長數組,用0標識數組末尾,最後除了用指針也沒有更好的辦法。解決代碼如下:
typedef struct
{
USHORT offset : 12;
USHORT type : 4;
}TypeOffset;
TypeOffset* pTypeOffset = (TypeOffset*)(pImageBaseRelocation + 1);
最後附上代碼:
void FixBaseRelocTable(PVOID pNewImage)
{
ULONG OriginalImageBase;
ULONG uRelocTableSize;
ULONG* uRelocAddress;
ULONG uIndex;
PIMAGE_DOS_HEADER pImageDosHeader;
PIMAGE_NT_HEADERS pImageNtHeader;
IMAGE_DATA_DIRECTORY ImageDataDirectory;
IMAGE_BASE_RELOCATION* pImageBaseRelocation;
//將新內核地址作爲一個PE頭,尋找重定位頭
pImageDosHeader = (PIMAGE_DOS_HEADER)pNewImage;
//定位到PE頭
pImageNtHeader = (PIMAGE_NT_HEADERS)((ULONG)pNewImage + pImageDosHeader->e_lfanew);
//定位到可選PE頭裏拿到ImageBase
OriginalImageBase = pImageNtHeader->OptionalHeader.ImageBase;
//定位到可選PE頭的DataDirArray裏的重定位表
ImageDataDirectory = pImageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
//重定位表的實際地址
pImageBaseRelocation = (PIMAGE_BASE_RELOCATION)((ULONG)pNewImage + ImageDataDirectory.VirtualAddress);
if (pImageBaseRelocation == NULL)
{
return;
}
while (pImageBaseRelocation->SizeOfBlock)
{
typedef struct
{
USHORT offset : 12;
USHORT type : 4;
}TypeOffset;
TypeOffset* pTypeOffset = (TypeOffset*)(pImageBaseRelocation + 1);
uRelocTableSize = (pImageBaseRelocation->SizeOfBlock - 8) / 2;
for (uIndex = 0; uIndex < uRelocTableSize; uIndex++)
{
if (pTypeOffset[uIndex].type == 3)
{
uRelocAddress = (ULONG*)(pTypeOffset[uIndex].offset + pImageBaseRelocation->VirtualAddress + (ULONG)pNewImage);
*uRelocAddress = *uRelocAddress + (OrigImage - OriginalImageBase);
}
}
pImageBaseRelocation = (IMAGE_BASE_RELOCATION*)((ULONG)pImageBaseRelocation + pImageBaseRelocation->SizeOfBlock);
}
}
0x03 SSDT重定位
SSDT重定位就相對比較簡單了。首先通過新內核地址-老內核地址,得到相對偏移,之後老內核的ssdt指針加上相對偏移,得到新內核的ssdt指針,把系統服務表的成員依次填充:
NumberOfServices 是函數的數量,不用偏移,ServiceTableBase是函數地址表的指針,需要加上偏移,表裏的每個成員依次加上偏移。
見代碼:
VOID SetNewSSDT(PVOID pNewImage)
{
ULONG uNewKernelInc;
ULONG uOffset;
ULONG i;
//實際地址相對位移
uNewKernelInc = (ULONG)pNewImage - OrigImage;
//新的SSDT地址
pNewSSDT = (ServiceDescriptorTableEntry_t*)((ULONG)&KeServiceDescriptorTable + uNewKernelInc);
if (!MmIsAddressValid(pNewSSDT))//判斷新的SSDT地址是否合法
{
KdPrint(("pNewSSDT is unaviable!"));
return;
}
//數量
pNewSSDT->NumberOfSerivce = KeServiceDescriptorTable.NumberOfSerivce;
//表相對的位置
uOffset = (ULONG)KeServiceDescriptorTable.ServiceTableBase - OrigImage;
//新函數地址
pNewSSDT->ServiceTableBase = (unsigned int*)((ULONG)pNewImage + uOffset);
if (!MmIsAddressValid(pNewSSDT->ServiceTableBase))
{
KdPrint(("pNewSSDT->ServiceTableBase: %X", pNewSSDT->ServiceTableBase));
return;
}
for (i = 0; i < pNewSSDT->NumberOfSerivce; i++)
{
pNewSSDT->ServiceTableBase[i] += uNewKernelInc;
}
}
0X04 函數調用時換成新的內核(HOOK KiFastCallEntry)
現在已經重載好了文件,搭好了“來時的路”,但是,當請求來的時候,依然不會上咱們的道,還是走原來的“路”,所以我們就需要“半路打劫”來HOOK KiFastCallEntry走上咱們的SSDT。那麼問題來了
- 如何找到KiFasrCallEntry,來HOOK?
- 到底HOOK KiFasrCallEntry的那個位置?
爲了解決這兩個問題,有必要逆一下KiFasrCallEntry這個函數
(詳細的分析可以參考之前寫的《API函數調用的過程》那篇)
以下是逆向分析完的結果:
///////////////--KiFastCallEntry--/////////////////
nt!KiFastCallEntry+0x8d:
804df781 8bf8 mov edi,eax//取出系統服務號
804df783 c1ef08 shr edi,8 //服務號右移8爲
804df786 83e730 and edi,30h//檢測第12位(第13個數)
//如果爲1:0011 0000 & 0001 0000爲0x10
//如果爲0:0011 0000 & 0000 0000爲0x00
804df789 8bcf mov ecx,edi//ecx存0h或10h
804df78b 03bee0000000 add edi,dword ptr [esi+0E0h]
//_KTHREAD+0xE0 -->ServiceTable(系統服務表)
//如果ecx裏是0h則指向第一張表 --->Ntoskrl.exe裏的函數
//如果ecx裏是10h則指向第二張表 --->Win32k.exe裏的函數
804df791 8bd8 mov ebx,eax
804df793 25ff0f0000 and eax,0FFFh//只看後12位
804df798 3b4708 cmp eax,dword ptr [edi+8]
// _SYSTEM_SERVICE_TABLE
{
ServiceTableBase;//指向系統服務函數地址表
ServiceCounterTableBase;
NumberOfService;//服務函數的個數
ParamTableBase;//參數表基址
}
//+8是數量,換句話說 判斷編號是否超範圍
804df79b 0f8341fdffff jae nt!KiBBTUnexpectedRange (804df4e2)
//超過就越界
804df7a1 83f910 cmp ecx,10h
//判斷是否是第二長系統服務表
804df7a4 751a jne nt!KiFastCallEntry+0xcc (804df7c0)
//就進win32k.exe了
//////////////////////////////////////////////////////////////////////////////
804df7a6 8b0d18f0dfff mov ecx,dword ptr ds:[0FFDFF018h]
804df7ac 33db xor ebx,ebx
804df7ae 0b99700f0000 or ebx,dword ptr [ecx+0F70h]
804df7b4 740a je nt!KiFastCallEntry+0xcc (804df7c0)
804df7b6 52 push edx
804df7b7 50 push eax
804df7b8 ff1564b25580 call dword ptr [nt!KeGdiFlushUserBatch (8055b264)]
804df7be 58 pop eax
804df7bf 5a pop edx
/////////////////////////////////////////////////////////////////////////////////////////
//如果是第一張系統服務表的話,跳到這來
804df7c0 ff0538f6dfff inc dword ptr ds:[0FFDFF638h]
804df7c6 8bf2 mov esi,edx//edx裏存放着3環傳入參數的指針
804df7c8 8b5f0c mov ebx,dword ptr [edi+0Ch]
//edi-->ServiceTable(系統服務表),ServiceTable(系統服務表)+0x0C-->參數表的地址
804df7cb 33c9 xor ecx,ecx
804df7cd 8a0c18 mov cl,byte ptr [eax+ebx]
//參數地址 + 編號
804df7d0 8b3f mov edi,dword ptr [edi]
//edi--->ServiceTableBase系統服務函數地址表
804df7d2 8b1c87 mov ebx,dword ptr [edi+eax*4]
//地址 + 4 * 編號 就是 要調用的0環的函數
804df7d5 2be1 sub esp,ecx
//參數有幾個字節棧就擡多高
804df7d7 c1e902 shr ecx,2
//與下面的rep一起看,因爲下面是dword,所以這裏要/4
804df7da 8bfc mov edi,esp
//設置要copy的目的地 edi本身就有這個含義
804df7dc 3b35d40b5680 cmp esi,dword ptr [nt!MmUserProbeAddress (80560bd4)]
//判斷你存放參數的地址是否是3環地址範圍內
804df7e2 0f83a8010000 jae nt!KiSystemCallExit2+0x9f (804df990)
/////////
804df7e8 f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
//開始把傳入的參數copy到棧裏
804df7ea ffd3 call ebx//終於調用函數啦!!!
804df7ec 8be5 mov esp,ebp
804df7ee 8b0d24f1dfff mov ecx,dword ptr ds:[0FFDFF124h]
804df7f4 8b553c mov edx,dword ptr [ebp+3Ch]
804df7f7 899134010000 mov dword ptr [ecx+134h],edx
第一個問題:如何定位?
因爲無論調用哪個API(就用OpenProcess),都是通過call ebx調用的,此時的棧裏(ebp+0x04)已經存有下一條指令的地址,因此只需要隨便HOOK一個API函數,通過這個地址我們就可以進入KiFastCallEntry這個函數
用匯編寫就可以:
ULONG retaddr;
UCHAR* p = NULL;
int i = 0;
__asm
{
pushad
mov eax, [ebx + 0x04]
mov retaddr, eax
popad
}
p = (UCHAR*)retaddr;
第二個問題:HOOK的位置?
KiFastCallEntry中的指令,真是寸士寸金
首先不能在call ebx之後,那就重頭看 1.首先判斷是用了哪個系統服務表,這塊不能動;2.之後是一層一層用系統服務號算要調用的地址,3.之後是傳入3環的參數,並且寄存器也不能瞎改,看看360大廠選的位置:
804df7d5 2be1 sub esp,ecx
804df7d7 c1e902 shr ecx,2
着實厲害,eax裏存的是編號,ebx是函數的地址,edi裏是地址表的指針,該搭的車都搭了。
--總結一下思路:
- 先通過SSDT_HOOK HOOK OpenProcess函數找到進入KiFastCallEntry函數的地址。
- 向上搜索找到硬編碼爲2be1 c1e902的指令作爲進行InLine_HOOK的位置來HOOK KiFastCallEntry。
- 寫自己的MyKiFastCallEntry()
注意:
函數調用完畢之後棧控件佈局,指令pushad將
32位的通用寄存器保存在棧中,棧空間佈局爲:
[esp + 00] <=> eflag
[esp + 04] <=> edi
[esp + 08] <=> esi
[esp + 0C] <=> ebp
[esp + 10] <=> esp
[esp + 14] <=> ebx <<-- 使用函數返回值來修改,把原來的函數換成自己的函數
[esp + 18] <=> edx
[esp + 1C] <=> ecx
[esp + 20] <=> eax
附上代碼:
ULONG display(ULONG ServiceTableBase, ULONG FuncIndex, ULONG OrigFuncAddress)
{
if (ServiceTableBase == (ULONG)KeServiceDescriptorTable.ServiceTableBase)
{
if (!strcmp((char*)PsGetCurrentProcess() + 0x174, "cheatengine-i38"))
{
return pNewSSDT->ServiceTableBase[FuncIndex];//用咱自己的ssdt
}
}
return OrigFuncAddress;
}
void __declspec(naked) MyFastCallEntry()
{
__asm
{
pushad
pushfd
push ebx//ebx裏的ssdt
push eax//ssdt [Index]編號
push edi//ServiceTable(系統服務表)
call display
mov[esp + 0x14], eax
/*
函數調用完畢之後棧控件佈局,指令pushad將
32位的通用寄存器保存在棧中,棧空間佈局爲:
[esp + 00] <=> eflag
[esp + 04] <=> edi
[esp + 08] <=> esi
[esp + 0C] <=> ebp
[esp + 10] <=> esp
[esp + 14] <=> ebx <<-- 使用函數返回值來修改,把原來的ssdt換成自己的ssdt
[esp + 18] <=> edx
[esp + 1C] <=> ecx
[esp + 20] <=> eax
*/
popfd
popad
sub esp, ecx
shr ecx, 2
jmp jmp_ret
}
}
0x05 總結
我是跟着http://bbs.pediy.com/showthread.php?t=177555這個文章的大佬學下來的,加上了一些我學這份代碼時遇到的問題(和解決方案),和作爲小白不理解的地方(和解釋),有不正確的地方,還請各位大佬斧正。(手動滑稽)