滴水逆向三期实践5:新增节

如图,是新增节的大致思路,标红的位置为我们新增的部分,如果在已存在的节表后存在可以继续插入节表的空间,那么就多加一个节表。有了节表就要有对应的节,于是需要在文件末尾添加新的节存放代码或数据。

上述思路具体为:

1、判断是否有足够的空间,可以添加一个节表.

判断条件:

SizeOfHeader - (DOS + dos stub + PE标记 + 标准PE头 + 可选PE头 + 已存在节表) 

>= 2个节表的大小

为什么要两个节表的大小,因为除了根据SizeOfHeader判断头大小外,还可能会根据遍历节表直到整个节表全为0为止。还是为了保险起见,尽量在最后一个节表之后的再一整块节表的大小填充全0(这里改成1个节表的大小,插入后不填全0问题应该也不大,下同)


2、需要修改的数据

1) 添加一个新的节表(可以copy一份)

2) 在新增节表后面 填充一个节表大小的全0数据

3) 修改PE头中节的数量

4) 修改sizeOfImage的大小

5) 再在原有数据的最后,新增一个节的数据(内存对齐的整数倍).

6)修正新增节表的属性
 

如果没有新增节表的空间怎么办?

依然有办法添加新节表,思路为把DOS stub的数据全部覆盖掉,DOS_HEADER 的 lfanew 字段直接指向它的下一个字节,把剩下的整个头(整个NT头)包括节表全部抬升上来,那么新节表最后一个节表的位置和第一个节之间极大概率会有足够空间添加新节表的。

(注意区分节表和节的表述,节的位置肯定够的,直接在文件最后加就行了,文件会变得更大,所以要修改sizeOfImage的大小)

新增节表的属性就需要根据新增节内填充的数据而定了,比如这里我们希望像上篇文章一样注入MessageBox代码,那么新增节的属性需要具备可运行的功能,即Characteristics,该字段这里没有详述,可以参考其他文章。这里不会构造Characteristics的可以使用PE工具查看并拷贝值或直接写代码实现以下部分:

查看AddressOfEntryPoint指向的节的节表。那个节是起始代码运行的所在节,所以必然具备可运行代码的节属性。把该节表的Characteristics值拷贝到新节表Characteristics即可。

新节区在内存中的偏移(起始位置) = 内存中整个PE文件映射的大小

新节区在文件中的偏移 = 文件大小     这样最方便

接下来就是添加可运行代码的环节,与上一章节空白区注入代码要做的一模一样。只需要把新构造的代码作为新增节存放就好了。

新增节表的内存中大小直接就是添加代码的实际大小

新增节表的文件大小根据实际添加代码的大小进行文件对齐得出

具体C实现(包括判断空间不足后的头抬升操作)并附详细注释如下:

里面用到的所有自定义函数均可在前面的章节找到

这里插个极端情况,比如 xp 的记事本程序 notepad.exe 虽然按照上述条件判断是足够空间的,但是节表之后无缝紧跟着有一段有用的数据(后面会说到,就是绑定导入表),而且这段数据不能轻易挪走,这种极端情况下应该判定为需要把头进行抬升。而下面的程序没有写出这个判断,即应该还要判断待插入节表的位置是否全0。如果全0了才插入,否则需要头抬升。(程序中没有写这个)

#include "windows.h"
#include "stdio.h"

#define MESSAGEBOXADDR 0x76AF39A0  //这个值需要将任一exe文件拖入OD打开,搜索 MessageBoxA 记录它的地址到这里(每次开机都不同)

unsigned char ShellCode319[] =
{
	0x6A,0x00,0x6A,0x00,0x6A,0x00,0x6A,0x00,  //这行是push 四个0 作为 MessageBox 的参数
	0xE8,0x00,0x00,0x00,0x00,
	0xE9,0x00,0x00,0x00,0x00
};

//**************************************************************************								
//Align:计算对齐后的值					
//参数说明:								
//x  需要进行对齐的值								
//Alignment 对齐大小						
//返回值说明:								
//返回x进行Alignment值对齐后的值								
//**************************************************************************	
int Align(int x, int Alignment)
{
	if (x%Alignment==0)
	{
		return x;
	}
	else
	{
		return (1 + (x / Alignment)) * Alignment;
	}
}


void h319()
{
	char FilePath[] = "CRACKME.EXE";	//CRACKME.EXE        CrackHead.exe
	char CopyFilePath[] = "CRACKMEcopy.EXE";	//CRACKMEcopy.EXE       CrackHeadcopy.exe
	LPVOID pFileBuffer = NULL;				//会被函数改变的 函数输出之一
	LPVOID* ppFileBuffer = &pFileBuffer;	//传进函数的形参
	LPVOID pNewFileBuffer = NULL;
	int SizeOfFileBuffer;
	PIMAGE_DOS_HEADER pDosHeader = NULL;
	PIMAGE_NT_HEADERS pNTHeader = NULL;
	PIMAGE_FILE_HEADER pFileHeader = NULL;
	PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL;
	PIMAGE_SECTION_HEADER pSectionHeader = NULL;
	PIMAGE_SECTION_HEADER pLastSectionHeader = NULL;	//指向最后一个节表
	PIMAGE_SECTION_HEADER pNewSectionHeader = NULL;		//指向最后一个节表的下一个节表,即不存在的节表作为新开辟的节表
	bool isUplift = false;
	DWORD CallX = NULL;	//即E8后跟的4字节
	DWORD JmpX = NULL;	//即E9后跟的4字节

	SizeOfFileBuffer = ReadPEFile(FilePath, ppFileBuffer);	//pFileBuffer即指向已装载到内存中的exe首部
															/*pFileBuffer = *ppFileBuffer;*/
	if (!SizeOfFileBuffer)
	{
		printf("文件读取失败\n");
		return;
	}
	//Dos头
	pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;	// 强转 DOS_HEADER 结构体指针
	//NT头
	pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);
	//PE头
	pFileHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);	//NT头地址 + 4 为 FileHeader 首址
	//可选PE头	
	pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);//SIZEOF_FILE_HEADER为固定值且不存在于PE文件字段中
	//首个节表
	pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);

	//这里由于是之前写的代码,后来发现可以用winnt.h 中定义的 IMAGE_FIRST_SECTION( NTheader ) 宏定义函数来实现从nt头中找第一个节表

	for (int i = 1; i < pFileHeader->NumberOfSections; i++, pSectionHeader++)	//注意这里i从1开始 i < NumberOfSections
	{}	//出循环后pSectionHeader指向最后一个节表				
	pLastSectionHeader = pSectionHeader;
	pNewSectionHeader = pLastSectionHeader + 1;

	//节表结束的位置+两个节表的大小 仍然≤ 头大小,才可继续。插一个留两个为了插入一个新节表后仍有一个节表的位置填充0
	if ((DWORD)pNewSectionHeader + IMAGE_SIZEOF_SECTION_HEADER * 2 <= (DWORD)pFileBuffer + pOptionalHeader->SizeOfHeaders)
	{	//要节表后留出两个节表的空位且全为0,保证其中没有可能使用的数据,才允许插入新节表。
		PBYTE pTemp = (PBYTE)pNewSectionHeader;
		for (int i = 0; i < IMAGE_SIZEOF_SECTION_HEADER * 2; i++, pTemp++)
		{
			if (*pTemp)
			{
				printf("节表插入空位存在数据,需进行头抬升\n");
				isUplift = true;
				break;
			}
		}
	}
	else
	{
		printf("无节表插入空位,需进行头抬升\n");
		isUplift = true;
	}

	//isUplift = true; //头抬升测试
	if (isUplift)
	{
		if ((DWORD)pFileBuffer + sizeof(IMAGE_DOS_HEADER) - (DWORD)pNTHeader >= IMAGE_SIZEOF_SECTION_HEADER * 2)
		{
			printf("可抬升NT头\n");
			//开始拷贝,将NT头拷贝到DOS头结束之后,长度为NT头开始到最后一个节表结束时的长度,即pNewSectionHeader
			memcpy((void*)((DWORD)pFileBuffer + sizeof(IMAGE_DOS_HEADER)), pNTHeader, (DWORD)pNewSectionHeader - (DWORD)pNTHeader);
			//拷贝后重置e_lfanew位置
			//printf(" e_lfanew: %x\n", pDosHeader->e_lfanew);
			pDosHeader->e_lfanew = sizeof(IMAGE_DOS_HEADER);
			//printf("e_lfanew: %x\n", pDosHeader->e_lfanew);
			//抬升后更新所有被抬升的头指针
			//NT头
			pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);
			//PE头
			pFileHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);	//NT头地址 + 4 为 FileHeader 首址
			//可选PE头	
			pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);//SIZEOF_FILE_HEADER为固定值且不存在于PE文件字段中
			//首个节表
			pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
			for (int i = 1; i < pFileHeader->NumberOfSections; i++, pSectionHeader++)	//注意这里i从1开始 i < NumberOfSections
			{}	//出循环后pSectionHeader指向最后一个节表				
			pLastSectionHeader = pSectionHeader;
			pNewSectionHeader = pLastSectionHeader + 1;
			//验证代码,判断是否是有效的PE标志
			if (*((PDWORD)((DWORD)pFileBuffer + pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE)	//基址pFileBuffer + lfanew 为 NTHeader首址
			{
				printf("抬升后验证失败,不是有效的PE标志\n");
				return ;
			}
			printf("抬升成功!\n");
			//抬升成功后将最后一个节表后两个节表位置的空间置零
			memset(pNewSectionHeader, 0, IMAGE_SIZEOF_SECTION_HEADER * 2);

		}
		else
		{
			printf("不可抬升NT头,不可插入节表\n");
			free(pFileBuffer);
			return;
		}
	}

	//开始构造新节表
	printf("可插入节表\n");
	strcpy((char*)pNewSectionHeader->Name, (char*)".NewSec");
	pNewSectionHeader->Misc.VirtualSize = sizeof(ShellCode319);

	//节区在内存中的偏移 = 内存中整个PE文件映射的大小
	pNewSectionHeader->VirtualAddress = pOptionalHeader->SizeOfImage;

	//节区在文件对齐中的大小  以VirtualSize内存对齐向上取整
	printf("FileAlignment:%x\n", pOptionalHeader->FileAlignment);
	pNewSectionHeader->SizeOfRawData = Align(sizeof(ShellCode319), pOptionalHeader->FileAlignment);
	printf("SizeOfRawData:%x\n", pNewSectionHeader->SizeOfRawData);

	//节区在文件中的偏移 = 文件大小  (也可以是最后一个节区所在文件中位置 + 最后一个节区在文件对齐中的大小)二者一致
	pNewSectionHeader->PointerToRawData = SizeOfFileBuffer;//pLastSectionHeader->PointerToRawData + pLastSectionHeader->SizeOfRawData;
	pNewSectionHeader->PointerToRelocations = 0;
	pNewSectionHeader->PointerToLinenumbers = 0;
	pNewSectionHeader->NumberOfRelocations = 0;
	pNewSectionHeader->NumberOfLinenumbers = 0;
	pNewSectionHeader->Characteristics = 0x60000020;

	//新节表构造完毕, 修改节的数量
	pFileHeader->NumberOfSections++;

	//修改sizeOfImage的大小
	//节区在内存对齐中的大小  以VirtualSize内存对齐向上取整,对齐后加到SizeOfImage中
	printf("SectionAlignment: %x\nSizeOfImage:%x\n", pOptionalHeader->SectionAlignment, pOptionalHeader->SizeOfImage);
	pOptionalHeader->SizeOfImage += Align(sizeof(ShellCode319), pOptionalHeader->SectionAlignment);
	printf("SizeOfImage:%x\n", pOptionalHeader->SizeOfImage);

	//开辟新空间
	int SizeOfNewFileBuffer = SizeOfFileBuffer + pNewSectionHeader->SizeOfRawData;
	pNewFileBuffer = malloc(SizeOfNewFileBuffer);

	//在放入新空间前修改OEP的值并记录修改前的旧值
	DWORD OddAddressOfEntryPoint = pOptionalHeader->AddressOfEntryPoint;
	pOptionalHeader->AddressOfEntryPoint = pNewSectionHeader->VirtualAddress;

	//新节区之前的值全部复制,新节表部分已改变
	memcpy(pNewFileBuffer, pFileBuffer, SizeOfFileBuffer);

	//新节区全部置零
	memset((void *)((DWORD)pNewFileBuffer + pNewSectionHeader->PointerToRawData), 0, pNewSectionHeader->SizeOfRawData);

	//在新节区中放入ShallCode
	//X即E8后的数 = 要跳转的地址 - (E8所在地址 + 5)            (E8 所在地址+5 即 call指令的下一条指令的地址)
	//那么要跳转的地址即messageboxA地址。E8所在地址即 ImageBase内存运行基址 + VirtualAddress节所在偏移 +8 才到E8 (∵ShallCode就在节开头)
	CallX = MESSAGEBOXADDR - (pOptionalHeader->ImageBase + pNewSectionHeader->VirtualAddress + 8 + 5);

	//jump 要跳转的地址即OEP程序入口点, X = 程序入口点 - E9所在地址 + 5
	//这里程序入口点即ImageBase基址 + 修改前的OddAddressOfEntryPoint         E9所在地址计算同上   下式是化简后约去了ImageBase
	JmpX = OddAddressOfEntryPoint - (pNewSectionHeader->VirtualAddress + 13 + 5);

	//将上述计算后的值放入ShellCode319
	*(PDWORD)(ShellCode319 + 9) = CallX;
	*(PDWORD)(ShellCode319 + 14) = JmpX;
	for (int i = 0; i < sizeof(ShellCode319); i++)
	{
		printf("%x ", ShellCode319[i]);
	}
	printf("\n");

	//拷贝ShellCode319到内存中
	memcpy((void*)((DWORD)pNewFileBuffer + pNewSectionHeader->PointerToRawData), ShellCode319, sizeof(ShellCode319));
	MemeryToFile(pNewFileBuffer, SizeOfNewFileBuffer, CopyFilePath);
	free(pNewFileBuffer);
	free(pFileBuffer);
}

运行程序打印内容如下:

可插入节表
FileAlignment:200
SizeOfRawData:200
SectionAlignment: 1000
SizeOfImage:8000
SizeOfImage:9000
6a 0 6a 0 6a 0 6a 0 e8 93 b9 6e 76 e9 ee 8f ff ff
success

修改头抬升标志,测试抬升的头,运行程序打印内容如下:

可抬升NT头
抬升成功!
可插入节表
FileAlignment:200
SizeOfRawData:200
SectionAlignment: 1000
SizeOfImage:8000
SizeOfImage:9000
6a 0 6a 0 6a 0 6a 0 e8 93 b9 6e 76 e9 ee 8f ff ff
success

与上一章一样 运行新生成的另一份exe 会先弹窗,点击确定后才是原程序。

使用 PETool 查看原PE文件(exe)的节表:

 

新增节情况:

可见多了一节,.NewSec是我们在代码中起的名字,内存中偏移为上一节的结束位置的内存对齐后的值,文件中偏移也为上一节的结束位置的文件对齐后的值,标志Characteristics与CODE节一致

原 AddressOfEntryPoint:

新 AddressOfEntryPoint:

 正好落在新节区内,就是新增节的起始位置

头抬升的情况:

原exe的 lfanew:

头抬升后的 lfanew:(没有头抬升则此值不变)

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值