原理
分析PE结构,找到第一个节,假设它是代码节(后面的实验都是基于这个前提),获取其内存偏移 VirtualAddress,计算它最后一条指令的偏移的下一个字节,作为代码插入点。插入调用MessageBoxA的硬编码,和jmp到原入口点的代码。跳转地址计算公式是:X = 要跳转的地址 - 下一条指令的地址。要注意,公式中涉及的“地址”指的都是运行时的地址,即通过 [ImageBase + 偏移] 得到。
修改程序入口点,使运行时先执行我插入的代码,然后跳转到原来的入口点。
代码使用vc6开发,如果要迁移到其他环境如vs,则需要注意字符串参数的类型,要做相应的调整。另外,只适用于32位程序。
向代码节添加代码的函数
// 向代码节添加MessageBox代码
// 向代码节添加代码不需要担心内存对齐后大小发生变化
// 默认第一个节是代码节,但是这样判断不一定准确,应该遍历节表,根据属性找代码节
BOOL AddCodeToCodeSec(LPCSTR lpszFile, LPCSTR lpszOutFile)
{
BYTE shellcode[] =
{
0x6A, 0x00, 0x6A, 0x00, 0x6A, 0x00, 0x6A, 0x00, // push 0 push 0 push 0 push 0
0xE8, 0x00, 0x00, 0x00, 0x00, // call MessageBoxA
0xE9, 0x00, 0x00, 0x00, 0x00 // jmp OEP
};
DWORD dwShellCodeSize = 18;
DWORD dwCodeRva = 0; // 插入的位置RVA
LPVOID pFileBuffer = NULL;
LPVOID pImageBuffer = NULL;
LPVOID pNewBuffer = NULL;
DWORD dwFileBufferSize = 0;
DWORD dwImageBufferSize = 0;
DWORD dwNewBufferSize = 0;
// 读取PE到内存中
if ((dwFileBufferSize = ReadPEFile(lpszFile, &pFileBuffer)) == 0)
{
printf("读取失败\n");
return FALSE;
}
// 拉伸成内存映像
dwImageBufferSize = CopyFileBufferToImageBuffer(pFileBuffer, &pImageBuffer);
if (0 == dwImageBufferSize)
{
free(pFileBuffer);
return FALSE;
}
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pImageBuffer;
PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew);
PIMAGE_FILE_HEADER pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)pDosHeader + pDosHeader->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionHeader = \
(PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);
DWORD dwCodeSecIndex = -1;
// 遍历节表,找到代码节
for (int i = 0; i < pPEHeader->NumberOfSections; i++)
{
if ((pSectionHeader[i].Characteristics & 0x60000020) == 0x60000020)
{
dwCodeSecIndex = i;
break;
}
}
if (dwCodeSecIndex == -1)
{
printf("找不到代码节\n");
free(pFileBuffer);
free(pImageBuffer);
return FALSE;
}
// 计算插入点RVA
dwCodeRva = pSectionHeader[dwCodeSecIndex].VirtualAddress + pSectionHeader[dwCodeSecIndex].Misc.VirtualSize;
// 是否有足够的空间插入代码,要考虑到代码节是最后一个节的情况
if (dwCodeSecIndex + 1 == pPEHeader->NumberOfSections)
{
if (dwCodeRva + dwShellCodeSize > pOptionHeader->SizeOfImage)
{
printf("代码节没有足够的空间插入代码\n");
free(pFileBuffer);
free(pImageBuffer);
return FALSE;
}
}
else
{
DWORD dwUnuseSize = pSectionHeader[dwCodeSecIndex + 1].VirtualAddress - \
pSectionHeader[dwCodeSecIndex].VirtualAddress - pSectionHeader[dwCodeSecIndex].Misc.VirtualSize;
if (dwUnuseSize < dwShellCodeSize)
{
printf("代码节没有足够的空间插入代码\n");
free(pFileBuffer);
free(pImageBuffer);
return FALSE;
}
}
// 代码插入点偏移 = VA + VSIZE
dwCodeRva = pSectionHeader->VirtualAddress + pSectionHeader->Misc.VirtualSize;
// 代码插入
memcpy((LPVOID)((DWORD)pImageBuffer + dwCodeRva), shellcode, dwShellCodeSize);
// 修正地址
DWORD MsgBoxAddr = (DWORD)&MessageBoxA; // 获取MessageBox的地址
DWORD hardCodeAddr = MsgBoxAddr - (pOptionHeader->ImageBase + dwCodeRva + 13);
memcpy((LPVOID)((DWORD)pImageBuffer + dwCodeRva + 9), &hardCodeAddr, 4);
hardCodeAddr = pOptionHeader->ImageBase + pOptionHeader->AddressOfEntryPoint \
- (pOptionHeader->ImageBase + dwCodeRva + 18);
memcpy((LPVOID)((DWORD)pImageBuffer + dwCodeRva + 14), &hardCodeAddr, 4);
// 修改入口点
pOptionHeader->AddressOfEntryPoint = dwCodeRva;
// 转成文件对齐
dwNewBufferSize = CopyImageBufferToFileBuffer(pImageBuffer, &pNewBuffer);
if (dwNewBufferSize != dwFileBufferSize)
{
printf("可能丢失数据\n");
}
MemoryToFile(pNewBuffer, dwNewBufferSize, lpszOutFile);
free(pFileBuffer);
free(pImageBuffer);
free(pNewBuffer);
printf("插入代码成功\n");
return TRUE;
}
完整代码
#include "headers.h"
// 读取PE文件到内存中,返回读取的字节数;读取失败返回0
DWORD ReadPEFile(LPCSTR lpszFile, LPVOID *pFileBuffer)
{
FILE *pFile = NULL;
DWORD dwFileSize = 0;
pFile = fopen(lpszFile, "rb");
if (pFile == NULL)
{
printf("打开文件失败\n");
return 0;
}
fseek(pFile, 0, SEEK_END);
dwFileSize = ftell(pFile);
fseek(pFile, 0, SEEK_SET);
*pFileBuffer = malloc(dwFileSize);
if (*pFileBuffer == NULL)
{
printf("分配内存失败\n");
fclose(pFile);
return 0;
}
DWORD dwRead = fread(*pFileBuffer, 1, dwFileSize, pFile);
fclose(pFile);
if (dwRead != dwFileSize)
{
printf("文件大小 = %d\t实际写入内存字节 = %d\t写入失败\n", dwFileSize, dwRead);
return 0;
}
if (!IsPEFile(*pFileBuffer, dwRead))
{
printf("不是有效的PE文件\n");
return 0;
}
return dwRead;
}
// 验证是否PE文件
BOOL IsPEFile(LPVOID pFileBuffer, DWORD dwSize)
{
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew);
PIMAGE_FILE_HEADER pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)pDosHeader + pDosHeader->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionHeader = \
(PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);
if (*((PWORD)pDosHeader) != IMAGE_DOS_SIGNATURE)
{
printf("不是有效的MZ标志\n");
return FALSE;
}
if (pNTHeader->Signature != IMAGE_NT_SIGNATURE)
{
printf("不是有效的PE标记\n");
return FALSE;
}
return TRUE;
}
// 打印PE头信息
VOID PrintNTHeaders(LPCSTR lpszFile)
{
LPVOID pFileBuffer = NULL;
DWORD dwFileSize = ReadPEFile(lpszFile, &pFileBuffer);
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pDosHeader + pDosHeader->e_lfanew);
PIMAGE_FILE_HEADER pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)pDosHeader + pDosHeader->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + sizeof(IMAGE_FILE_HEADER));
PIMAGE_SECTION_HEADER pSectionHeader = \
(PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);
if (dwFileSize == 0)
{
printf("读取文件失败\n");
return;
}
//打印DOS头
puts("----DOS HEADER----");
printf("e_magic = %x\n", pDosHeader->e_magic);
printf("e_lfanew = %x\n", pDosHeader->e_lfanew);
//打印NT头
printf("----标准PE头----\n");
printf("Machine = %x\n", pPEHeader->Machine);
printf("NumberOfSections = %x\n", pPEHeader->NumberOfSections);
printf("TimeDateStamp = %x\n", pPEHead