PE文件从磁盘中向内存中加载映射的过程。总体思路就是将PE文件按照各种方式映射到内存中即可,通过学习PE文件向内存中加载的过程可以推导出shellcode直接加载内存执行的思路。

基本的载入流程可以概况一下步骤:

  1. 根据文件的大小申请具有合适权限的内存空间
  2. 用0进行初始化内存空间
  3. 将PE文件利用ReadFile映射进去
  4. 修改重定位表
  5. 根据PE文件的导入表,加载需要的dll。
  6. 获取导入函数的地址,写进导入表
  7. 修改PE的加载baseaddr
  8. 跳转到入口点开始执行

定位 - 利用数据目录表

    typedef struct _IMAGE_DATA_DIRECTORY {
      DWORD VirtualAddress;
      DWORD Size;
    } IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16

数据目录表(IDD)是可选头的最后一个项目。长度是16,可以这个值只有od对他是关注的,可以修改该值来反od的载入调试。里面就含有两项,一个是虚拟地址和大小。

image-20230515100257722

顺序是

  • 导出表
  • 导入表
  • 资源表
  • 异常信息表
  • 安全证书表
  • 重定位表
  • 调试信息表
  • 版权所以表
  • 全局指针表
  • TLS表
  • 加载配置表
  • 绑定导入表
  • IAT表
  • 延迟导入表
  • COM信息表
  • 保留

上述表中和程序加载相关就只有导入导出、重定向、IAT表这些

导出表

定位

EXE文件一般没有这个,显示的就是0

image-20230515100953110

DLL程序的这个结构体很大

image-20230515101126197

IDD中的该目录指向了数据目录表,这是RVA值,相对虚拟地址,在本地查看需要转换成FOA地址,文件偏移地址。

转换需要减去一个对齐,这里就是0x1000.

image-20230515101453898

导出表的位置一般位于所有节区的后面,紧跟着就是导出表。

结构

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;        
    DWORD   TimeDateStamp;            //时间戳
    WORD    MajorVersion;            
    WORD    MinorVersion;             
    DWORD   Name;                    // 指向该导出表文件名字符串
    DWORD   Base;                    // 导出函数起始序号
    DWORD   NumberOfFunctions;        // 所有导出函数的个数
    DWORD   NumberOfNames;            // 以函数名字导出的函数个数
    DWORD   AddressOfFunctions;     // 导出函数地址表RVA
    DWORD   AddressOfNames;         // 导出函数名称表RVA
    DWORD   AddressOfNameOrdinals;  // 导出函数序号表RVA
} IMAGE_EXPORT_DIRECTORY, * PIMAGE_EXPORT_DIRECTORY;

image-20230515102024319

导入表

一般是IDD表中的第二个结构体内容,指向了导入表所在的位置。

typedef struct _IMAGE_IMPORT_DESCRIPTOR {                                    
    union {                                    
        DWORD   Characteristics;                                               
        DWORD   OriginalFirstThunk;     //RVA 指向IMAGE_THUNK_DATA结构数组            
    };                                    
    DWORD   TimeDateStamp;                 //时间戳            
    DWORD   ForwarderChain;                                                  
    DWORD   Name;                        //RVA,指向dll名字,该名字已0结尾            
    DWORD   FirstThunk;                    //RVA,指向IMAGE_THUNK_DATA结构数组            
} IMAGE_IMPORT_DESCRIPTOR;                                    
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

导入表在文件中的位置一般在导出表的后面,就是最后节区的后第二个。最后一个导出表结构体全是0

image-20230515212154965

从010可以看到清楚的结构

image-20230515212304642

在结构体中的联合体中,可以看到

    union {                                    
        DWORD   Characteristics;                                               
        DWORD   OriginalFirstThunk;     //RVA 指向IMAGE_THUNK_DATA结构数组            
    };

【联合体内容共享内存】,这两个值保存的是同一个值,这个值可以用来遍历INT(image_name_table)导入名称表,引用博客中的一个图片

13

该结构体中还存在另一个指针,FirstThunk,他可以用来遍历IAT

14

重定位

exe一般不需要进行重定位,因为他优先加载到4G的虚拟内存中,但是dll需要。

    typedef struct _IMAGE_BASE_RELOCATION {
      DWORD VirtualAddress;
      DWORD SizeOfBlock;
    } IMAGE_BASE_RELOCATION;
    typedef IMAGE_BASE_RELOCATION UNALIGNED *PIMAGE_BASE_RELOCATION;

#define IMAGE_SIZEOF_BASE_RELOCATION 8

这个重定位表,最后一个结构体的标记是VirtualAddress与SizeOfBlock都为0,其他的都有意义值。该结构位于程序的最后面,

image-20230515103305944

注:如果一个程序不会被别的程序加载,那么这个程序的重定位表是用不上的,因为除了内核重载的情况,主程序的2G虚拟内存空间不会被占用。

参考:

PE文件加载流程_玖哥爱吃肉的博客-CSDN博客

模拟载入

  1. 根据文件的大小申请具有合适权限的内存空间
  2. 用0进行初始化内存空间
  3. 将PE文件利用ReadFile映射进去
  4. 修改重定位表(EXE不用重定位,直接忽略掉)
  5. 根据PE文件的导入表,加载需要的dll
  6. 获取导入函数的地址,写进导入表
  7. 修改PE的加载baseaddr
  8. 跳转到入口点开始执行

初始化空间

首先根据文件的映像大小申请空间并且用0初始化,因为PE文件都是用0进行对齐的。VirtualAlloc开辟地址,转换类型char,方便后续往里面写字节。

    char path[] = "C:\\Users\\rootkit\\Desktop\\loadPE\\64test.exe";

    // 根据PE文件结构得到映像大小
    DWORD SizeOfImage = GetSizeOfPE(path, 1);
    printf("[++] size of image is [0x%x]\n", SizeOfImage);

    // 开辟可利用空间在内存中
    char* chBaseAddress = (char*)VirtualAlloc(NULL, SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (NULL == chBaseAddress)
    {
        printf("申请进程空间失败 %d\n", GetLastError());
        return NULL;
    }

    // 用 0 进行初始化处理
    memset(chBaseAddress, 0, SizeOfImage);
    printf("[////] addr is %p\n", chBaseAddress);

里面的主要调用函数还是使用winnt.h里的PE结构的一些结构体,然后使用fread的方式来将内容读到结构体里

DWORD GetSizeOfPE(char* peFile, int options)
{
    DWORD size = 0;
    FILE* fp = fopen(peFile, "rb");

    // read PE e_lfnew
    fseek(fp, 0x3c, SEEK_SET);

    // get nt offset
    unsigned char nt_offset = 0;
    fread(&nt_offset, 1, 1, fp);
    //printf("->%x<-", nt_offset);

    // get option head offset, 24byte from PE\x00\x00
    IMAGE_NT_HEADERS nt_header;
    IMAGE_OPTIONAL_HEADER opt_header;

    //IMAGE_FILE_HEADER file_header;
    fseek(fp, nt_offset, SEEK_SET);
    fread(&nt_header, 1, 0xf8, fp);
    opt_header = nt_header.OptionalHeader;
    switch (options)
    {
        // 1 是计算映像大小
    case 1:
        size = opt_header.SizeOfImage;
        fclose(fp);
        return size;
        break;
        // 2 是计算所有头的大小
    case 2:
        size = nt_header.OptionalHeader.SizeOfHeaders;
        fclose(fp);
        return size;
        break;
        // 3 是获取区段数量
    case 3:
        size = nt_header.FileHeader.NumberOfSections;
        fclose(fp);
        return size;
        break;
        // 4 定位节头表的位置
    case 4:
        // PE签名大小+nt头的开始位置+文件头大小+可选头大小
        size = sizeof(IMAGE_FILE_HEADER) + nt_header.FileHeader.SizeOfOptionalHeader + 4 + nt_offset;
        fclose(fp);
        return size;
        break;
    default:
        printf("case error\n");
        return 0;
    }
}

将头迁移过去

头部大小不需要进行一些对齐之类的操作,所以直接复制过去就可以。大小可以直接从 nt_header.OptionalHeader.SizeOfHeaders这个值来获得。

        // 计算所有头的大小
    DWORD sizeofheader_all = GetSizeOfPE(path, 2);
    // 获取区段数量
    DWORD numofsection_all = GetSizeOfPE(path, 3);

    // 获得程序内容
    FILE* fp = fopen(path, "rb");
    // get size of file in disk
    fseek(fp, 0, SEEK_END);
    unsigned long sizeofstaticfile = ftell(fp);
    // get static file context
    char* pe_buffer = (char*)malloc(sizeof(char) * sizeofstaticfile);
    fseek(fp, 0, SEEK_SET);
    fread(pe_buffer, 1, sizeofstaticfile, fp);
    // 先把头部拷过去
    memcpy(chBaseAddress, pe_buffer, sizeofheader_all);

将节区迁移过去

映射节区的方式需要使用节区头来进行。复制到内存中的地址就是开辟空间的baseaddress+节区头中的对应节区的VirtualAddress的值来进行。

// 根据节区头来将节区复制到内容中去
    PIMAGE_SECTION_HEADER psection_header = (PIMAGE_SECTION_HEADER)malloc(0x28);
    // 定位到节头表的位置
    int num_section_header = GetSizeOfPE(path, 4);

    // 遍历所有节区,根据节区头从磁盘映射到内存中
    for (i = 0; i < numofsection_all; i++)
    {
        fseek(fp, num_section_header+(0x28*i), SEEK_SET);
        fread(psection_header, 1, sizeofheader_all - num_section_header, fp);
        // 判读是否是最后一个节区
        if (psection_header->VirtualAddress == 0 || psection_header->SizeOfRawData == 0)
        {
            psection_header++;
            continue;
        }
        DWORD srcaddr = psection_header->PointerToRawData;
        DWORD sizeofdata = psection_header->SizeOfRawData;
        char* section_context = (char*)malloc(sizeofdata);            

        fseek(fp, srcaddr, SEEK_SET);
        fread(section_context, 1, sizeofdata, fp);
        // 计算对应的位置
        char* destaddr = (char*)(psection_header->VirtualAddress + chBaseAddress);
        memcpy(destaddr, section_context, sizeofdata);
        printf("[**] %s section copy success\n", psection_header->Name);
        free(section_context);
        section_context = NULL;
        psection_header = NULL;

    }
    printf("section copy finish\n");
    // 重写重定位
    // 重定位后的地址 = 需要重定位的地址 - 默认加载基址 + 当前加载基址
    // 重定位个屁,exe不用重定位
    // 直接修复导入表

需要注意的是,上述代码在malloc的时候会时不时的出现一些问题,这些bug尚未找到解决方式,gpt给出的代码也有这个错误。

修复导入表

修复导入表就是根据PE结构中的IID结构体数组来执行。思路就是先通过IID结构体中拿到导入模块的Name的RVA,然后检查是否已经载入内存中,没有的话就loadlibrary去载入。

然后再通过OriginalFirstThunkFirstThunk获取导入函数的名称和地址。

然后根据获得的导出名称或者是导出号,通过GetProcAddress去载入。

    char* Pdllname = NULL;
    HMODULE hDll = NULL;
    PIMAGE_THUNK_DATA lpImportNameArray = NULL;
    PIMAGE_IMPORT_BY_NAME lpImportByName = NULL;
    PIMAGE_THUNK_DATA lpImportFuncAddrArray = NULL;
    FARPROC lpFuncAddress = NULL;

    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)chBaseAddress;
    PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(chBaseAddress + pDos->e_lfanew);
    unsigned long v_size = pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
    PIMAGE_IMPORT_DESCRIPTOR piid = (PIMAGE_IMPORT_DESCRIPTOR)((char*)pDos + v_size);

    while (1)
    {
        // 判断是不是最后一个
        if (piid->OriginalFirstThunk==0)
        {
            break;
        }

        // 动态载入需要的dll
        Pdllname = (char*)((char*)pDos + piid->Name);
        // 拿到模块的句柄,没有就loadlibrary载入
        hDll = GetModuleHandleA(Pdllname);
        if (hDll == NULL)
        {
            hDll = LoadLibraryA(Pdllname);
            if (hDll == NULL)
            {
                piid++;
                continue;
            }
        }
        printf("[>>] get IID success\n");
        i = 0;
        // 获取OriginalFirstThunk以及对应的  导入函数名称表  首地址
        lpImportNameArray = (PIMAGE_THUNK_DATA)((char*)pDos + piid->OriginalFirstThunk);
        // 获取FirstThunk以及对应的  导入函数地址表  首地址
        lpImportFuncAddrArray = (PIMAGE_THUNK_DATA)((char*)pDos + piid->FirstThunk);
        while (TRUE)
        {
            if (lpImportNameArray[i].u1.AddressOfData == 0)
            {
                break;
            }

            // 获取IMAGE_IMPORT_BY_NAME结构
            lpImportByName = (PIMAGE_IMPORT_BY_NAME)((char*)pDos + lpImportNameArray[i].u1.AddressOfData);

            // 判断导出函数是 序号导出 or 名称导出
            if (0x80000000 & lpImportNameArray[i].u1.Ordinal)
            {
                // 序号导出
                lpFuncAddress = GetProcAddress(hDll, (LPCSTR)(lpImportNameArray[i].u1.Ordinal & 0x0000FFFF));
            }
            else
            {
                // 名称导出
                lpFuncAddress = GetProcAddress(hDll, (LPCSTR)lpImportByName->Name);
            }
            lpImportFuncAddrArray[i].u1.Function = lpFuncAddress;
            i++;
        }

        piid++;
    }
    printf("[%%] reparie IAT success\n");
    // 修改PE文件的加载基地址,设置入口点

这里的代码没有什么问题,可以正常执行。

修改EP执行

修改PE文件的入口点,然后跳到入口点去执行

    // 修改PE文件的加载基地址,设置入口点

    pNt->OptionalHeader.ImageBase = (DWORD)(uintptr_t)chBaseAddress;
    char* EntryPoint = (DWORD)(uintptr_t)((char*)chBaseAddress + pNt->OptionalHeader.AddressOfEntryPoint);
    printf("[##] reset OEP success\n");
    free(pe_buffer);

    CreateRemoteThread(GetCurrentProcess(), 0, SizeOfImage, (LPTHREAD_START_ROUTINE)EntryPoint, 0, 0, NULL);

    // https://blog.csdn.net/v_wus/article/details/122112081
    //__asm
    //{
    //    mov eax, EntryPoint
    //    jmp eax
    //}

所有代码

#include<Windows.h>
#include<stdio.h>
#include<winnt.h>

#pragma warning(disable : 4996)

// use to calc the size in PE
DWORD GetSizeOfPE(char* peFile, int options)
{
    DWORD size = 0;
    FILE* fp = fopen(peFile, "rb");

    // read PE e_lfnew
    fseek(fp, 0x3c, SEEK_SET);

    // get nt offset
    unsigned char nt_offset = 0;
    fread(&nt_offset, 1, 1, fp);
    //printf("->%x<-", nt_offset);

    // get option head offset, 24byte from PE\x00\x00
    IMAGE_NT_HEADERS nt_header;
    IMAGE_OPTIONAL_HEADER opt_header;

    //IMAGE_FILE_HEADER file_header;
    fseek(fp, nt_offset, SEEK_SET);
    fread(&nt_header, 1, 0xf8, fp);
    opt_header = nt_header.OptionalHeader;
    switch (options)
    {
        // 1 是计算映像大小
    case 1:
        size = opt_header.SizeOfImage;
        fclose(fp);
        return size;
        break;
        // 2 是计算所有头的大小
    case 2:
        size = nt_header.OptionalHeader.SizeOfHeaders;
        fclose(fp);
        return size;
        break;
        // 3 是获取区段数量
    case 3:
        size = nt_header.FileHeader.NumberOfSections;
        fclose(fp);
        return size;
        break;
        // 4 定位节头表的位置
    case 4:
        // PE签名大小+nt头的开始位置+文件头大小+可选头大小
        size = sizeof(IMAGE_FILE_HEADER) + nt_header.FileHeader.SizeOfOptionalHeader + 4 + nt_offset;
        fclose(fp);
        return size;
        break;
    default:
        printf("case error\n");
        return 0;
    }
}

int main()
{
    unsigned int i;
    char path[] = "C:\\Users\\rootkit\\Desktop\\loadPE\\64test.exe";

    // 根据PE文件结构得到映像大小
    DWORD SizeOfImage = GetSizeOfPE(path, 1);
    printf("[++] size of image is [0x%x]\n", SizeOfImage);

    // 开辟可利用空间在内存中
    char* chBaseAddress = (char*)VirtualAlloc(NULL, SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (NULL == chBaseAddress)
    {
        printf("申请进程空间失败 %d\n", GetLastError());
        return NULL;
    }

    // 用 0 进行初始化处理
    memset(chBaseAddress, 0, SizeOfImage);
    printf("[////] addr is %p\n", chBaseAddress);
    // 从磁盘到上述空间中进行映射
        // 计算所有头的大小
    DWORD sizeofheader_all = GetSizeOfPE(path, 2);
    // 获取区段数量
    DWORD numofsection_all = GetSizeOfPE(path, 3);

    // 获得程序内容
    FILE* fp = fopen(path, "rb");
    // get size of file in disk
    fseek(fp, 0, SEEK_END);
    unsigned long sizeofstaticfile = ftell(fp);
    // get static file context
    char* pe_buffer = (char*)malloc(sizeof(char) * sizeofstaticfile);
    fseek(fp, 0, SEEK_SET);
    fread(pe_buffer, 1, sizeofstaticfile, fp);
    // 先把头部拷过去
    memcpy(chBaseAddress, pe_buffer, sizeofheader_all);
    // 根据节区头来将节区复制到内容中去
    PIMAGE_SECTION_HEADER psection_header = (PIMAGE_SECTION_HEADER)malloc(0x28);
    // 定位到节头表的位置
    int num_section_header = GetSizeOfPE(path, 4);

    // 遍历所有节区,根据节区头从磁盘映射到内存中
    for (i = 0; i < numofsection_all; i++)
    {
        fseek(fp, num_section_header+(0x28*i), SEEK_SET);
        fread(psection_header, 1, sizeofheader_all - num_section_header, fp);
        // 判读是否是最后一个节区
        if (psection_header->VirtualAddress == 0 || psection_header->SizeOfRawData == 0)
        {
            psection_header++;
            continue;
        }
        DWORD srcaddr = psection_header->PointerToRawData;
        DWORD sizeofdata = psection_header->SizeOfRawData;
        char* section_context = (char*)malloc(sizeofdata);            

        fseek(fp, srcaddr, SEEK_SET);
        fread(section_context, 1, sizeofdata, fp);
        char* destaddr = (char*)(psection_header->VirtualAddress + chBaseAddress);
        memcpy(destaddr, section_context, sizeofdata);
        printf("[**] %s section copy success\n", psection_header->Name);
        free(section_context);
        section_context = NULL;
        psection_header = NULL;

    }
    printf("section copy finish\n");
    // 重写重定位
    // 重定位后的地址 = 需要重定位的地址 - 默认加载基址 + 当前加载基址
    // 重定位个屁,exe不用重定位
    // 直接修复导入表

    char* Pdllname = NULL;
    HMODULE hDll = NULL;
    PIMAGE_THUNK_DATA lpImportNameArray = NULL;
    PIMAGE_IMPORT_BY_NAME lpImportByName = NULL;
    PIMAGE_THUNK_DATA lpImportFuncAddrArray = NULL;
    FARPROC lpFuncAddress = NULL;

    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)chBaseAddress;
    PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(chBaseAddress + pDos->e_lfanew);
    unsigned long v_size = pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
    PIMAGE_IMPORT_DESCRIPTOR piid = (PIMAGE_IMPORT_DESCRIPTOR)((char*)pDos + v_size);

    while (1)
    {
        // 判断是不是最后一个
        if (piid->OriginalFirstThunk==0)
        {
            break;
        }

        // 动态载入需要的dll
        Pdllname = (char*)((char*)pDos + piid->Name);
        // 拿到模块的句柄,没有就loadlibrary载入
        hDll = GetModuleHandleA(Pdllname);
        if (hDll == NULL)
        {
            hDll = LoadLibraryA(Pdllname);
            if (hDll == NULL)
            {
                piid++;
                continue;
            }
        }
        printf("[>>] get IID success\n");
        i = 0;
        // 获取OriginalFirstThunk以及对应的  导入函数名称表  首地址
        lpImportNameArray = (PIMAGE_THUNK_DATA)((char*)pDos + piid->OriginalFirstThunk);
        // 获取FirstThunk以及对应的  导入函数地址表  首地址
        lpImportFuncAddrArray = (PIMAGE_THUNK_DATA)((char*)pDos + piid->FirstThunk);
        while (TRUE)
        {
            if (lpImportNameArray[i].u1.AddressOfData == 0)
            {
                break;
            }

            // 获取IMAGE_IMPORT_BY_NAME结构
            lpImportByName = (PIMAGE_IMPORT_BY_NAME)((char*)pDos + lpImportNameArray[i].u1.AddressOfData);

            // 判断导出函数是 序号导出 or 名称导出
            if (0x80000000 & lpImportNameArray[i].u1.Ordinal)
            {
                // 序号导出
                lpFuncAddress = GetProcAddress(hDll, (LPCSTR)(lpImportNameArray[i].u1.Ordinal & 0x0000FFFF));
            }
            else
            {
                // 名称导出
                lpFuncAddress = GetProcAddress(hDll, (LPCSTR)lpImportByName->Name);
            }
            lpImportFuncAddrArray[i].u1.Function = lpFuncAddress;
            i++;
        }

        piid++;
    }
    printf("[%%] reparie IAT success\n");
    // 修改PE文件的加载基地址,设置入口点

    pNt->OptionalHeader.ImageBase = (DWORD)(uintptr_t)chBaseAddress;
    char* EntryPoint = (DWORD)(uintptr_t)((char*)chBaseAddress + pNt->OptionalHeader.AddressOfEntryPoint);
    printf("[##] reset OEP success\n");
    free(pe_buffer);

    CreateRemoteThread(GetCurrentProcess(), 0, SizeOfImage, (LPTHREAD_START_ROUTINE)EntryPoint, 0, 0, NULL);

    // https://blog.csdn.net/v_wus/article/details/122112081
    //__asm
    //{
    //    mov eax, EntryPoint
    //    jmp eax
    //}
    return 0;
}

一些问题

vs在64bit的时候,无法使用上述语法内敛汇编,所以使用CreateRemoteThread函数来执行。

上述代码执行并不稳定,在malloc的时候会出现不能稳定复现的问题,如果malloc不报错,在GetModuleHandleA的时候也会出现问题,目前还没有搞清楚为啥。

参考:

原创]PE加载器的简单实现-编程技术-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)

没有版权,随便复制,免费的知识应该共享 all right reserved,powered by Gitbook该文章修订时间: 2023-05-19 22:36:44

results matching ""

    No results matching ""