一份內核重載代碼的學習筆記

 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 基址重定位

 如果變量都是由鏡像地址+偏移算的話,那直接用就行,但是,全局變量的地址確實直接儲存的,它不管鏡像在哪,反正就是那個數,所以需要通過重定位表依次手動修改。

  1. 首先通過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。那麼問題來了

  1. 如何找到KiFasrCallEntry,來HOOK?
  2. 到底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裏是地址表的指針,該搭的車都搭了。

--總結一下思路:

  1. 先通過SSDT_HOOK HOOK OpenProcess函數找到進入KiFastCallEntry函數的地址。
  2. 向上搜索找到硬編碼爲2be1 c1e902的指令作爲進行InLine_HOOK的位置來HOOK KiFastCallEntry。
  3. 寫自己的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這個文章的大佬學下來的,加上了一些我學這份代碼時遇到的問題(和解決方案),和作爲小白不理解的地方(和解釋),有不正確的地方,還請各位大佬斧正。(手動滑稽)
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章