一份内核重载代码的学习笔记

 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这个文章的大佬学下来的,加上了一些我学这份代码时遇到的问题(和解决方案),和作为小白不理解的地方(和解释),有不正确的地方,还请各位大佬斧正。(手动滑稽)
 

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