PE文件(五)代码节空白区添加代码

学习目的

本节的目的就是教会我们在一个可执行文件的代码节的空白区添加一段代码。

大致思路:正常的文件中OEP记录着程序入口的地址,现在我们将此可执行文件的程序入口OEP地址指向call0 x123456指令的地址,使其先执行我们添加的代码,执行完指令后再jmp 0x456789跳转回原来正常的文件OEP指向的程序入口地址,之后程序正常运行

注意在代码节空白区添加代码相当于给在硬盘上的文件中添加数据,添加完后再运行文件。该过程可以理解为文件注入。但是我们平时说的注入,则是文件在运行时把代码添加进去,这个过程可以理解为内存注入

在本节课中,我们将以添加一个MessageBox函数到一个可执行文件的代码节(.text)空白区为例进行讲解。

预备知识

MessageBox函数原型:

int MessageBox(

HWND hWnd,

LPCTSTR lpText,

LPCTSTR lpCaption = NULL,

UINT nType = MB_OK

);

四个参数说明:

hWnd:表示窗口句柄,用来该对话框属于哪个主窗口。如果该参数为空(0/NULL),则该对话框不属于任何窗口

lpText:字符串,指显示在对话框中的内容

lpCaption:字符串,指对话框的标题;如果此参数为空,则默认使用“错误”作为标题

nType:指定显示按钮的数目及形式,表名使用的图标样式、缺省按钮是什么、以及消息框的强制回应等

MessageBox()函数功能是弹出一个标准的Windows对话框,相当于程序的一个断点。该函数不是以C语言函数库的标准函数的方式去执行,而是通过使用MessageBox函数去调用Windows的API,即MessageBoxA。使用该API时需要包含头文件windows.h。如果一个程序关联了user32.dll动态链接库,则此程序就有Windows API函数MessageBoxA

我们知道一个文件是由一堆二进制数据组成的,所以我们要想将MessageBox函数加到一个可执行文件中,不是直接把函数加进去,而是需要把这个函数对应的二进制数据添加到文件中

由于一个函数的二进制数据过于复杂,所以我们将MessageBox函数的二进制数据添加到文件中是一件很困难的事情。所以我们只需要使用push指令将该函数需要的4个参数传入,找到此文件中的MessageBox函数的地址,使用call指令调用它,此时该函数会调用MessageBoxA API,于是我们便完成了添加MessageBox函数的操作

指令硬编码

由于计算机只认识二进制数据,所以我们需要知道call指令 jmp指令 push指令的硬编码以及使用它们需要传入的各种参数的硬编码

硬编码查看:当一个程序编译以后,我们可以进入反汇编中,右键打开code bytes选项即可显示硬编码。此时我们可以看到每条指令的左边都是它对应的硬编码。硬编码都是二进制数,在本节学习中用十六进制显示

call指令有不同类型的硬编码,一般都是E8类型的。其中当call指令的硬编码为E8时,使用该指令时后面加上4个字节call要跳转的的相对地址。而call指令的硬编码为FF15,使用时后面加上4个字节call要跳转的地方的绝对地址,即ImageBase + RVA后的地址值

jmp指令的硬编码是E9,使用时后面加上4个字节大小的jmp要跳转的相对地址

push指令的硬编码是6A,使用时后面加上需要传入函数参数的值

相对地址X的计算公式:X = 文件在虚拟内存状态下真正要跳转的地址 – 文件在虚拟内存状态下E8这条指令的下一行地址。由于call指令的硬编码固定一共占5字节,即一字节call硬编码和四字节地址,所以E8指令的下一行地址为E8当前地址+5。所以公式最终为:X = 要跳转的地址 - (E8的地址 + 该指令长度),

实例说明

构造一个需要调用函数的C代码:

#include <stdio.h>

void Function(int a,int b,int c,int d){}

int main(int argc,char* argv[])

{

    Function(1,2,3,4);

    return 0;

}

查看反汇编:

其中E8为call指令硬编码,E8 7F FE FF FF 为调用函数相对地址,00101014为调用函数的这真实地址。

根据公式:X = 要跳转的真实地址 - (E8的地址 + 该指令长度)可知:0x00401014 - (0x00401190 + 5)= 0xFFFFFE7F,由于内存小端序存储,所以为内存中存储的地址为7F FE FF FF,因此最后call指令的硬编码为E8 7F FE FF FF

预备工作

在我们使用call指令和jmp指令前,需要获取MessageBoxAPI函数的地址和程序本来的入口地址

1.获取MessageBox函数的地址:使用OD打开可执行文件,OD会模拟文件运行时装入内存中的状态。在左下角的命令栏输入bp MessageBoxA后回车。由于弹窗代表着断点,所以该指令表示在此函数起始位置设置了一个断点。接着我们点击上方栏中的B按钮查看断点,接着双击我们刚设置的断点就会跳转到断点所在地址,这个地址就是MessageBoxA函数的起始地址

2.获取程序本来的入口地址:通过可选PE头中的AddressOfEntryPoint字段值可以知道程序入口地址的相对于PE头偏移量,再通过可选PE头中的ImageBase字段知道文件装载到虚拟内存中的起始地址。根据ImageBase + AddressOfEntryPoint可以得出来程序在4GB虚拟内存中的真正的入口地址

添加流程

1.判断要添加代码的空白区内存大小是否能存放得下添加的硬编码(使用节对应的节表中的Misc.VirtualSize – SizeOfRawData即可计算空白区的大小)。因为我们使用一个push指令长度为2字节(一个一字节push硬编码大小,一个一字节地址大小),MessageBox函数一共需要push四个参数,所以需要四个push指令一共八字节大小。call和jmp指令长度为5字节,加起来一共是18字节。所以要留空的空间至少需要18字节

2.将要添加的代码转换成对应的硬编码:由于需要计算call指令E8 和 push指令E9后面的地址,所以要用公式X = 要跳转的地址 - (E8的地址 + 5)进行计算。

当我们使用UE编辑器或者winhex打开可执行文件进行编辑修改时,由于UE编辑器打开的文件的状态是在硬盘上的状态,所以我们修改的数据是FileBuffer中的数据,而不是ImageBuffer中的。此时计算公式的地址需要从文件地址转换成内存地址后再计算的

如果可执行文件是ipmsg.exe时,其文件对齐和内存对齐是一样的,所以该文件在硬盘上的数据格式和加载到内存中的格式是一样的。此时那使用UE打开文件时,上面显示的地址可以直接使用

但是如果可执行文件是notepad.exe等时,由于文件对齐和内存对齐不一样,从硬盘加载到虚拟内存是需要拉伸的,所以如果此时用UE添加硬编码计算E8 E9后面的地址值时需要使用内存地址,所以多了一个FOA转化成RVA的过程

3.定位到任意一个节后面的空白区,作为添加的硬编码的起始地址,数据最好添加在代码节中(一般默认为.text),这是因为代码区的属性一般是可执行的。所以数据加到代码节中不用修改节的characteristic属性。

由于我们添加的硬编码中有call和jmp指令后面的地址都是在虚拟内存中的地址,所以我们如果写程序来模拟这个过程,需要在拉伸后的文件中添加硬编码,这样更方便计算E8 和 E9后面跟的地址值。如果我们在文件在硬盘上的状态中添加也可以,但是计算E8 和 E9 后面跟的地址值在使用公式前,需要先把文件地址转换成内存地址

4.最后找到程序的AddressOfEntryPoint字段所在地址,将此字段的值改为添加的硬编码的地址

作业

 一.手动在代码空白区添加代码

我们以notepad为例

注意!!注意!!注意!!一定要用xp及xp一下的系统进行实践,否则会因为系统原因导致我们无法正常运行修改以后的程序!!本人痛的教训,花了几天都不成功一直以为是自己的问题

MessageBoxA内存地址为0x77D507EA

程序相对内存入口地址0x0000739D

文件对齐粒度 0x00000200

内存对齐粒度 0x00001000

.text节起始位置0x00000400     

.text数据大小是0x00007748

.text节结束位置0x00000400   + 0x00007748= 0x00007B48

.data节起始位置0x00007C00

两节中间空白区大小 0x00007C00- 0x00007B48=  0XB8 > 0x12(要添加的硬编码的大小)

我们通过010Editor可以清晰看到,确实有大片空白区

 我们将在0x00007B48处添加我们的代码

6A 00 6A 00 6A 00 6A 00 E8 00 00 00 00 E9 00 00 00 00

这是预备代码,现在计算MessageBoxA的相对地址

在此之前我们要计算下一指令在内存中的地址:

ImageBase = 0x01000000

.text节VirtualAddress =  0x00001000          

.text节的VirtualSize =  00007748 

因此我们添加代码在内存的位置是0x01000000 + 0x00001000  + 00007748 =  0x1008748

所以E8下一指令地址为0x1008748 + 0xD =  0x1008755

X = 0x77D507EA - 0x1008755 = 0x76D48095

在内存中 95 80 D4 76排列

现在我们E9后添加原虚拟内存中相对程序入口地址

E9指令下一条指令内存地址为:0x1008755 + 0x5 = 0X100875A

相对地址为:0x01000000 + 0x0000739D -  0X100875A = FFFFEC43

所以内存排布为 43 EC FF FF

如下图便是我们添加代码的最终版

现在我们修改原程序入口为我们添加代码的地址

添加代码虚拟内存相对地址0x00007B48 - 0x00000400 + 0x00001000 = 0x8748        

内存排布为 48 87

如下图便是我们修改入口的最终结果

修改完毕以后,我们打开notepad验证一下

成功了!

二.代码实现在代码空白区添加代码

#include<stdio.h>
#include<Windows.h>
#include<string.h>
DWORD ReadPEFileSize(const char* lpszFile) //将一个文件的硬盘内存数据读取到自定义的文件内存缓冲区
{
	FILE* pFile = NULL;
	pFile = fopen(lpszFile, "rb");
	DWORD FileSize = 0;
	if (!pFile)
	{
		printf("无法打开EXE文件");
		return 0;
	}
	fseek(pFile, 0, SEEK_END);
	FileSize = ftell(pFile);
	return FileSize;
}
char* ReadPEFile(const char* lpszFile)
{
	FILE* pFile = NULL;
	pFile = fopen(lpszFile, "rb");
	DWORD FileSize = ReadPEFileSize(lpszFile);
	fseek(pFile, 0, SEEK_SET);
	char* pFileBuffer = NULL;
	pFileBuffer = (char*)malloc(sizeof(char) * FileSize);
	if (!pFileBuffer)
	{
		printf("分配空间失败");
		fclose(pFile);
		return 0;
	}
	size_t i = fread(pFileBuffer, FileSize, 1, pFile);
	if (!i)
	{
		printf("读取数据失败!");
		free(pFileBuffer);
		fclose(pFile);
		return 0;
	}
	IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)pFileBuffer;
	IMAGE_NT_HEADERS* pNTHeader = (IMAGE_NT_HEADERS*)((char*)pFileBuffer + pDosHeader->e_lfanew); //(char*)pFileBuffer只有此处转换为了char*类型,之后加减是为char的大小为一个单位进行加减
	if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
	{
		printf("不是有效MZ标志,结束\n");
		free(pFileBuffer);
		fclose(pFile);
		return 0;
	}
	if (pNTHeader->Signature != IMAGE_NT_SIGNATURE)
	{
		printf("不是有效的PE标志,打印结束\n");
		free(pFileBuffer);
		pFileBuffer = NULL;
		return 0;
	}
	fclose(pFile);
	return pFileBuffer;
}
BOOL shellcode(const char* newBuffer, const char* lpszFile, DWORD size)
{
	char shellcode[18] = { 0x6A, 0x00, 0x6A, 0x00, 0x6A,0x00, 0x6A, 0x00, 0xE8, 0x00,  0x00,  0x00, 0x00, 0xE9, 0x00, 0x00, 0x00, 0x00};
	char* pFileBuffer = ReadPEFile(lpszFile); //获取自定义文件内存缓冲区指针
	if (pFileBuffer == NULL)
	{
		printf("缓冲区指针无效\n");
		return FALSE;
	}
	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer; //获取自定义文件内存缓冲区DOS头指针
	PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((char*)pFileBuffer + pDosHeader->e_lfanew); //获取NT头指针
	PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)((char*)pNTHeader + 4);//获取标准PE头指针
	PIMAGE_OPTIONAL_HEADER pOptionHeader = (PIMAGE_OPTIONAL_HEADER)((char*)pFileHeader + 20);//获取可选PE头指针
	PIMAGE_SECTION_HEADER pSecHeader = (PIMAGE_SECTION_HEADER)((char*)pOptionHeader + pFileHeader->SizeOfOptionalHeader); // 获取文件节表指针
	char* pSecBuffer1 = (char*)pDosHeader + pSecHeader->PointerToRawData; //获取第一个节的指针
	DWORD SectionSize = pSecHeader->Misc.VirtualSize; //获取第一个节数据大小
	char* pSecBuffer1End = pSecBuffer1 + SectionSize; //获取第一个节末尾位置
	pSecHeader++;//指向第二个节
	char* pSecBuffer2 = (char*)pSecHeader->PointerToRawData;//获取第二个节的起始位置
	if ((DWORD)pSecBuffer2 - (DWORD)pSecBuffer1End < sizeof(shellcode))
	{
		printf("该代码节空白区空间不足以添加shellcode");
		return FALSE;
	}
	pSecHeader--; //指向第一个节
	DWORD NewShellcodePoint = (DWORD)pOptionHeader->ImageBase + (DWORD)pSecHeader->VirtualAddress + (DWORD)pSecHeader->Misc.VirtualSize + 0xD;//指向虚拟内存中E8下一条指令地址
	DWORD MessageAddress = (DWORD)0x77D507EA - NewShellcodePoint; //获取E8指令所要的相对地址
	*(DWORD*)(shellcode + 9) = MessageAddress; 
	NewShellcodePoint += 0x5; //获取虚拟内存中E9下一条指令地址
	DWORD EnterAdderss = (DWORD)pOptionHeader->ImageBase + (DWORD)pOptionHeader->AddressOfEntryPoint - (DWORD)NewShellcodePoint; //获取E9所需要虚拟内存相对地址
	*(DWORD*)(shellcode + 14) = EnterAdderss;
	memcpy(pSecBuffer1End, shellcode, sizeof(shellcode)); //将预备shellcode添加至空白区
	pOptionHeader->AddressOfEntryPoint = (DWORD)pSecHeader->Misc.VirtualSize + (DWORD)pSecHeader->VirtualAddress ; //修改程序入口为添加代码处
	FILE* NewFile = fopen(newBuffer, "wb");
	DWORD Success = fwrite(pFileBuffer, size, 1, NewFile);
	if (!Success)
	{
		printf("存盘失败");
	}
	free(pFileBuffer);
	return TRUE;
}


int main(int argc, char* argv[])
{
	const char* lpszFile = "C:\\Documents and Settings\\Administrator\\桌面\\复件 NOTEPAD.EXE";
	const char* NewlpszFile = "C:\\Documents and Settings\\Administrator\\桌面\\NewNOTEPAD.EXE";
	DWORD size = ReadPEFileSize(lpszFile);
	if (!shellcode(NewlpszFile, lpszFile, size))
	{
		printf("注入shellcode失败");
	}
	return 0;
}

如下是运行结果

非常的nice

三.代码实现在任意节空白区添加代码

#include<stdio.h>
#include<Windows.h>
#include<string.h>
DWORD ReadPEFileSize(const char* lpszFile) //将一个文件的硬盘内存数据读取到自定义的文件内存缓冲区
{
	FILE* pFile = NULL;
	pFile = fopen(lpszFile, "rb");
	DWORD FileSize = 0;
	if (!pFile)
	{
		printf("无法打开EXE文件");
		return 0;
	}
	fseek(pFile, 0, SEEK_END);
	FileSize = ftell(pFile);
	return FileSize;
}
char* ReadPEFile(const char* lpszFile)
{
	FILE* pFile = NULL;
	pFile = fopen(lpszFile, "rb");
	DWORD FileSize = ReadPEFileSize(lpszFile);
	fseek(pFile, 0, SEEK_SET);
	char* pFileBuffer = NULL;
	pFileBuffer = (char*)malloc(sizeof(char) * FileSize);
	if (!pFileBuffer)
	{
		printf("分配空间失败");
		fclose(pFile);
		return 0;
	}
	size_t i = fread(pFileBuffer, FileSize, 1, pFile);
	if (!i)
	{
		printf("读取数据失败!");
		free(pFileBuffer);
		fclose(pFile);
		return 0;
	}
	IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)pFileBuffer;
	IMAGE_NT_HEADERS* pNTHeader = (IMAGE_NT_HEADERS*)((char*)pFileBuffer + pDosHeader->e_lfanew); //(char*)pFileBuffer只有此处转换为了char*类型,之后加减是为char的大小为一个单位进行加减
	if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
	{
		printf("不是有效MZ标志,结束\n");
		free(pFileBuffer);
		fclose(pFile);
		return 0;
	}
	if (pNTHeader->Signature != IMAGE_NT_SIGNATURE)
	{
		printf("不是有效的PE标志,打印结束\n");
		free(pFileBuffer);
		pFileBuffer = NULL;
		return 0;
	}
	fclose(pFile);
	return pFileBuffer;
}
BOOL shellcode(const char* newBuffer, const char* lpszFile, DWORD size)
{
	char shellcode[18] = { 0x6A, 0x00, 0x6A, 0x00, 0x6A,0x00, 0x6A, 0x00, 0xE8, 0x00,  0x00,  0x00, 0x00, 0xE9, 0x00, 0x00, 0x00, 0x00};
	char* pFileBuffer = ReadPEFile(lpszFile); //获取自定义文件内存缓冲区指针
	if (pFileBuffer == NULL)
	{
		printf("缓冲区指针无效\n");
		return FALSE;
	}
	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer; //获取自定义文件内存缓冲区DOS头指针
	PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((char*)pFileBuffer + pDosHeader->e_lfanew); //获取NT头指针
	PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)((char*)pNTHeader + 4);//获取标准PE头指针
	PIMAGE_OPTIONAL_HEADER pOptionHeader = (PIMAGE_OPTIONAL_HEADER)((char*)pFileHeader + 20);//获取可选PE头指针
	PIMAGE_SECTION_HEADER pSecHeader = (PIMAGE_SECTION_HEADER)((char*)pOptionHeader + pFileHeader->SizeOfOptionalHeader); // 获取文件节表指针
	PIMAGE_SECTION_HEADER pSecHeader1 = pSecHeader;
	DWORD n = 0;
	printf("请输入你要插入1-%d的哪个节\n",pFileHeader->NumberOfSections);
	scanf("%d", &n);
	while (n > pFileHeader->NumberOfSections)
	{
		printf("选择节错误,请重新选择\n");
		scanf("%d", &n);
	}
	pSecHeader += n - 1;
	char* pSecBufferN1 = (char*)pDosHeader + pSecHeader->PointerToRawData; //获取指定节的指针
	DWORD SectionSize = pSecHeader->Misc.VirtualSize; //获取指定节数据大小
	char* pSecBuffer1End = pSecBufferN1 + SectionSize; //获取指定节末尾位置
	if (n < pFileHeader->NumberOfSections)
	{
		pSecHeader++;//指向下一个节
		char* pSecBufferN2 = (char*)pDosHeader + pSecHeader->PointerToRawData;//获取下一个节的起始位置
		if ((DWORD)pSecBufferN2 - (DWORD)pSecBuffer1End < sizeof(shellcode))
		{
			printf("该节空白区空间不足以添加shellcode\n");
			return FALSE;
		}
        printf("该节空白区空间足以添加shellcode\n");
        pSecHeader--; //指向指定节
	}
	else if(n = pFileHeader->NumberOfSections)
	{
		if (pSecHeader->SizeOfRawData - pSecHeader->Misc.VirtualSize < sizeof(shellcode))
		{
			printf("该节空白区空间不足以添加shellcode\n");
			return FALSE;
		}
	}
	pSecHeader->Characteristics = pSecHeader->Characteristics | pSecHeader1->Characteristics;
	DWORD NewShellcodePoint = (DWORD)pOptionHeader->ImageBase + (DWORD)pSecHeader->VirtualAddress + (DWORD)pSecHeader->Misc.VirtualSize + 0xD;//指向虚拟内存中E8下一条指令地址
	DWORD MessageAddress = (DWORD)0x77D507EA - NewShellcodePoint; //获取E8指令所要的相对地址
	*(DWORD*)(shellcode + 9) = MessageAddress; 
	NewShellcodePoint += 0x5; //获取虚拟内存中E9下一条指令地址
	DWORD EnterAdderss = (DWORD)pOptionHeader->ImageBase + (DWORD)pOptionHeader->AddressOfEntryPoint - (DWORD)NewShellcodePoint; //获取E9所需要虚拟内存相对地址
	*(DWORD*)(shellcode + 14) = EnterAdderss;
	memcpy(pSecBuffer1End, shellcode, sizeof(shellcode)); //将预备shellcode添加至空白区
	pOptionHeader->AddressOfEntryPoint = (DWORD)pSecHeader->Misc.VirtualSize + (DWORD)pSecHeader->VirtualAddress ; //修改程序入口为添加代码处
	FILE* NewFile = fopen(newBuffer, "wb");
	DWORD Success = fwrite(pFileBuffer, size, 1, NewFile);
	if (!Success)
	{
		printf("存盘失败\n");
	}
	else
	{
        printf("shellcode添加成功!\n");
	}
	free(pFileBuffer);
	return TRUE;
}


int main(int argc, char* argv[])
{
	const char* lpszFile = "C:\\Users\\扶摇\\Desktop\\复件 NOTEPAD.exe";
	const char* NewlpszFile = "C:\\Users\\扶摇\\Desktop\\NewNOTEPAD.exe";
	DWORD size = ReadPEFileSize(lpszFile);
	if (!shellcode(NewlpszFile, lpszFile, size))
	{
		printf("注入shellcode失败\n");
	}
	return 0;
}

如下我们以插入第三个节作为一个简单的演示

很nice

注意:notepad第二个节的VirtualSize远大于SizeOfRawData,这是因为该节含有未初始化的数据。由于我们上述代码是直接在硬盘中添加数据,所以此特殊情况无法代码实现。具体实现操作可将其在内存中添加数据,这是万能的方法,但此处不再赘述,感兴趣者自行尝试

  • 25
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值