百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

EXE文件内存加载

cac55 2024-11-20 12:53 13 浏览 0 评论

0x01 前言

作为一名安全菜鸟,单纯的了解某一个方面是并不合格的,安全并不仅限于某一门语言、某一个OS,现如今安全研究的技术栈要求的更深、更广。虽说 PE 文件内存加载已经是多年前的技术,但是招不在新、有用就行,内存加载技术仍然有非常广泛的应用(隐藏自身,至于为什么要隐藏自身,dddd),由于笔者之前认知的偏差导致对PE相关的知识仅停留在知道的地步,并没有静下心来去认真分析学习,借此机会补足一下技术点,同时顺便为自己的恶意代码分析的学习之旅开个头。

0x02 关键步骤

0x1 Section 对齐

因为exe以文件形式存储的时候区段间的对齐方式与在内存中的对齐方式不尽相同,因此在手动加载exe时不能单纯的将文件格式的 exe 直接拷贝到内存中,而是应当根据内存区段(page size)的对齐方式做对齐处理。

为了验证一下,随便找一个 exe 文件作为学习资料。

FileAlignment 为 0x200,实际的Section也均是以 0x200 为单位进行对齐的,实际调试时就会发现section的对齐变为了SectionAlignment的大小:0x1000

0x2 导入表修复

导入表是PE文件从其它第三方程序中导入API,以供本程序调用的机制(与导出表对应),在exe运行起来的时, 加载器会遍历导入表, 将导入表中所有dll 都加载到进程中,被加载的DLL的DllMain就会被调用,通过导入表可以知道程序使用了哪些函数,导入表是一个数组,以全为零结尾。
要理解导入表,首先要理解PE文件分为两种编译方式:动态链接、静态链接。
静态链接方式:在程序执行之前完成所有的组装工作,生成一个可执行的目标文件(EXE文件)。
动态链接方式:在程序已经为了执行被装入内存之后完成链接工作,并且在内存中一般只保留该编译单元的一份拷贝。
静态链接优势在于其可移植性较强,基本上不依赖于系统的dll(自己全打包好了),动态链接的优势在于程序主体较小,占用系统资源不多。
动态链接库的两种链接方法:
(1) 装载时动态链接(Load-time Dynamic Linking):这种用法的前提是在编译之前已经明确知道要调用DLL中的哪几个函数,编译时在目标文件中只保留必要的链接信息,而不含DLL函数的代码;当程序执行时,调用函数的时候利用链接信息加载DLL函数代码并在内存中将其链接入调用程序的执行空间中(全部函数加载进内存),其主要目的是便于代码共享。(动态加载程序,处在加载阶段,主要为了共享代码,共享代码内存)
(2) 运行时动态链接(Run-time Dynamic Linking):这种方式是指在编译之前并不知道将会调用哪些DLL函数,完全是在运行过程中根据需要决定应调用哪个函数,将其加载到内存中(只加载调用的函数进内存),并标识内存地址,其他程序也可以使用该程序,并用LoadLibrary和GetProcAddress动态获得DLL函数的入口地址。(dll在内存中只存在一份,处在运行阶段)
相较于静态链接所有函数均在一个exe文件里,要调用某个函数时只需要按照写死的偏移进行调用,动态链接就存在一个找函数的问题:
当程序运行起来需要某个系统函数时,哪个dll包含该函数?dll加载到内存里之后地址是不确定的,如何按照从内存中定位到所需的函数地址。
PE文件中的导入表就可以解决上述问题。

要理解导入表首先要了解以下这几个结构:

IMAGE_DATA_DIRECTORY

IMAGE_DATA_DIRECTORY 位于 IMAGE_Optional_header 中的最后一个字段,是一个由16个_IMAGE_DATA_DIRECTORY 结构体构成的结构体数组,每个结构体由两个字段构成,分别为VirtualAddress和Size字段:

  • VirtualAddress字段记录了对应数据结构的RVA。
  • Size字段记录了该数据结构的大小。


Offset (PE/PE32+)    Description
96/112                        Export table address and size
104/120                        Import table address and size
112/128                        Resource table address and size
120/136                        Exception table address and size
128/144                        Certificate table address and size
136/152                        Base relocation table address and size
144/160                        Debugging information starting address and size
152/168                        Architecture-specific data address and size
160/176                        Global pointer register relative virtual address
168/184                        Thread local storage (TLS) table address and size
176/192                        Load configuration table address and size
184/200                        Bound import table address and size
192/208                        Import address table address and size
200/216                        Delay import descriptor address and size
208/224                        The CLR header address and size
216/232                        Reserved

根据微软提供的信息,IMAGE_DATA_DIRECTORY 的第二项指向的就是导入表了。

IMAGE_IMPORT_DESCRIPTOR

既然已经找到了导入表,就需要根据导入表内的元素来加载对应的dll,获取不同的函数地址了,此时就需要用到 IMAGE_IMPORT_DESCRIPTOR 结构了,该结构的详细内容如下:

Offset    Size    Field
0         4       Import Lookup Table RVA
4         4       Time/Date Stamp
8         4       Forwarder Chain
12        4       Name RVA
16        4       Import Address Table RVA


typedef struct _IMAGE_IMPORT_DESCRIPTOR {  
    union {  
        DWORD Characteristics;  
        DWORD OriginalFirstThunk;//(1) 指向导入名称表(INT)的RAV*   
    };  
    DWORD   TimeDateStamp;  // (2) 时间标识  
    DWORD   ForwarderChain; // (3) 转发链,如果不转发则此值为0 
    DWORD   Name;       // (4) 指向导入映像文件的名字*  
    DWORD   FirstThunk; // (5) 指向导入地址表(IAT)的RAV*
} IMAGE_IMPORT_DESCRIPTOR;

在修复IAT的过程中最重要的两个字段就是 OriginalFirstThunk 和 FirstThunk了:
根据OriginalFirstThunk获取要用到的函数名,将获取到的函数地址填到FirstThunk中。

0x03 具体分析

为了方便理解和记忆,默认读取的 PE 文件格式不存在问题,不做错误处理。
相较于dll的内存加载,exe的内存加载简化了很多,其中省略掉的一个大步骤就是导出表的修复。

文件读取

文件读取步骤基本上可以说条条大路通罗马,只要将 PE 文件完整的读取到内存中可供后续处理即可,除了把一个文件放在目录中进行读取以外还有很多种方式,比如将要加载的 exe 转换成shellcode进行加载、将shellcode进行简单xor后在内存xor回来再加载。。。

ifstream inFile("nc.exe", ios::in | ios::binary);
stringstream tmp;
tmp << inFile.rdbuf();
unsigned char* content = (unsigned char*)tmp.str().c_str();

初始内存分配

因为 exe 文件默认有加载基址,一般情况下在运行 exe 的时候会首先尝试加载到默认地址上去,因此就要根据 exe 的默认加载基址和映像大小(exe加载到内存后的大小:SizeOfImage)来分配内存

SIZE_T SizeOfImage = pFileNtHeader->OptionalHeader.SizeOfImage;// 获取加载基址
DWORD base = pFileNtHeader->OptionalHeader.ImageBase;
// 分配内存
unsigned char* memExeBase = (unsigned char*)VirtualAlloc((LPVOID)base, SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(memExeBase, content, pFileNtHeader->OptionalHeader.SizeOfHeaders);

分配内存时的 VirtualAlloc指定的页类型为 MEM_COMMIT|MEM_RESERVE这里是一个小小的延迟分配的知识点,如果是 MEM_RESERVE的话只有当对该段内存进行内存操作时才会被真正Load进入物理内存中。页权限使用的是PAGE_EXECUTE_READWRITE,这在实际编码过程中是一个很不好的习惯,为了更清晰的理解 exe 内存加载的核心流程,就省略了根据section来确定内存权限的步骤。

拷贝Header

分配内存完毕后首先要将 PE header 拷贝到相应的地址空间去,因为后续的操作均需要用到。

memcpy(memExeBase, content, pFileNtHeader->OptionalHeader.SizeOfHeaders);
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)memExeBase;
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(memExeBase + pDosHeader->e_lfanew);

之后要根据新分配的内存地址计算新的 DOS头 和 NT头。

修复ImageBase

因为已经根据ImageBase分配了内存,所以需要将拷贝后的OptionalHeader中的ImageBase字段根据实际内存地址进行更新,如果开启了aslr的话需要根据实际的内存地址更新ImageBase,分配到默认基址上的话没有必要。

pNtHeader->OptionalHeader.ImageBase = (DWORD)memExeBase;

拷贝区段

拷贝区段这部分是内存加载的第一个关键点,要根据内存页的大小来将原本的文件区段进行处理。在文件中Section通常以 0x200 进行对齐,内存中页大小单位为 0x1000, 因此内存对齐单位为 0x1000,所以当PE文件加载到内存中后需要对Section进行变换。

// 拷贝区段
    PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(pNtHeader);
    int sectionSize;
    for (int i = 0; i < pNtHeader->FileHeader.NumberOfSections; i++, section++) {
        if (section->SizeOfRawData == 0) {
            sectionSize = pNtHeader->OptionalHeader.SectionAlignment; // 最小内存Seciton单位为 SectionAlignment的大小
        }
        else {
            sectionSize = section->SizeOfRawData;
        }
        if (sectionSize > 0) {
            void* dest = memExeBase + section->VirtualAddress;
            memcpy(dest, content + section->PointerToRawData, sectionSize);
        }
    }

修复导入表

OriginalFirstChunk指向的INT表表项以4字节为单位,全0结尾,如果最高位为1则代表是函数序号,反之则是一个RVA,指向IMAGE_IMPORT_BY_NAME结构,INT表实际上的功能是获取要导入的函数名。目前没有遇到最高位为1的情况。

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;                 
    CHAR   Name[1];               //函数名称,0结尾.
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

IAT表项则直接是一个指向真实函数地址的指针。

PIMAGE_IMPORT_DESCRIPTOR pImportDesc;
bool result = true;
PIMAGE_DATA_DIRECTORY pDataDir = (PIMAGE_DATA_DIRECTORY)(&pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]);    // 获取IMAGE_DATA_DIRECTORY 位置
pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)(memExeBase + pDataDir->VirtualAddress);// 获取第一个IMAGE_IMPORT_DESCRIPTOR
for (;!IsBadReadPtr(pImportDesc,sizeof(IMAGE_IMPORT_DESCRIPTOR)) && pImportDesc->Name;pImportDesc++) {
    uintptr_t* thunkRef;
    FARPROC* funcRef;
    HMODULE handle = LoadLibraryA((LPCSTR)(memExeBase + pImportDesc->Name)); // 加载dll(此处也是可以手工加载的)
    if (pImportDesc->OriginalFirstThunk) {
        thunkRef = (uintptr_t*)(memExeBase + pImportDesc->OriginalFirstThunk);
        funcRef = (FARPROC*)(memExeBase + pImportDesc->FirstThunk);
    }
    else {
        thunkRef = (uintptr_t*)(memExeBase + pImportDesc->FirstThunk);
        funcRef = (FARPROC*)(memExeBase + pImportDesc->FirstThunk);
    }
    for (; *thunkRef; thunkRef++, funcRef++) {
        if (IMAGE_SNAP_BY_ORDINAL(*thunkRef)) { // 判断OriginalFirstThunk表项最高位为1的情况
            *funcRef = GetProcAddress(handle, (LPCSTR)(IMAGE_ORDINAL(*thunkRef)));//修复导入表
        }
        else {
            PIMAGE_IMPORT_BY_NAME thunkData = (PIMAGE_IMPORT_BY_NAME)(memExeBase + (*thunkRef));
            *funcRef = GetProcAddress(handle, (LPCSTR)&thunkData->Name); //修复导入表
        }
        if (*funcRef == 0) {
            cout << " error import " << endl;
            exit(0);
        }
    }
}

上述修复导入表的代码实际完成的工作就是遍历导入表中的 IMAGE_IMPORT_DESCRIPTOR结构,根据dll名称将对应的dll加载到内存中,并根据OriginalFirstThunk字段来获取所需的函数名进而使用GetProcAddress来获取该函数在内存中的实际地址填充到FirstThunk字段指向的空间中。

获取入口点启动程序

if (pNtHeader->OptionalHeader.AddressOfEntryPoint != 0) {
    ExeEntryProc exeEntry = (ExeEntryProc)(LPVOID)(memExeBase + pNtHeader->OptionalHeader.AddressOfEntryPoint);
    exeEntry();
}

结语

至此exe的内存加载就已经结束了,诱发我写下这篇文章的一个主要原因是回忆起之前看过的几篇APT相关的分析文章,涉及到主机的远控目前内存加载已经是标配,杀软动态检测的对抗方式种类繁多,静态对抗的方法以内存加载为王。

参考链接

https://www.cnblogs.com/iBinary/p/9740757.html
https://github.com/fancycode/MemoryModule
https://blog.csdn.net/Apollon_krj/article/details/77417063
https://www.cnblogs.com/tracylee/archive/2012/10/15/2723816.html

本文由D4ck原创发布
转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/260054
安全客 - 有思想的安全新媒体

相关推荐

Mac电脑强制删除任何软件方法-含自启动应用

对于打工者来说,进入企业上班使用的电脑大概率是会被监控起来,比如各种流行的数据防泄漏DLP,奇安信天擎,甚至360安全卫士,这些安全软件你想卸载是非常困难的,甚至卸载后它自己又安装回来了,并且还在你不...

Linux基础知识 | 文件与目录大全讲解

1.linux文件权限与目录配置1.文件属性Linux一般将文件可存取的身份分为三个类别,分别是owner/group/others,且三种身份各read/write/execute等权限文...

文件保护不妥协:2025 年 10 款顶级加密工具推荐

数据安全无小事,2025年这10款加密工具凭借独特功能脱颖而出,从个人到企业场景全覆盖,第一款为Ping32,其余为国外英文软件。1.Ping32企业级加密核心工具,支持200+文件格...

省心省力 一个软件搞定系统维护_省心安装在哪里能找到

◆系统类似于我们居住的房间,需要经常打理才能保持清洁、高效。虽然它本身也自带一些清理和优化的工具,但借助于好用的第三方工具来执行这方面的任务,会更让人省心省力。下面笔者就为大家介绍一款集多项功能于一身...

JAVA程序员常用的几个工具类_java程序员一般用什么软件写程序

好的工具做起事来常常事半功倍,下面介绍几个开发中常用到的工具类,收藏一下,也许后面真的会用到。字符串处理:org.apache.commons.lang.StringUtilsisBlank(Char...

手工解决Windows10的若干难题_windows10系统卡顿怎么解决

【电脑报在线】很多朋友已经开始使用Win10,估计还只是测试版本的原因,使用过程中难免会出现一些问题,这里介绍解决一些解决难题的技巧。技巧1:让ProjectSpartan“重归正途”从10074...

System32文件夹千万不能删除,看完这篇你就知道为什么了

C:\Windows\System32目录是Windows操作系统的关键部分,重要的系统文件存储在该目录中。网上的一些恶作剧者可能会告诉你删除它,但你不应该尝试去操作,如果你尝试的话,我们会告诉你会发...

Windows.old 文件夹:系统备份的解析与安全删除指南

Windows.old是Windows系统升级(如Win10升Win11)或重装时,系统自动在C盘创建的备份文件夹,其核心作用是保留旧系统的文件、程序与配置,为“回退旧系统”提供保...

遇到疑难杂症?Windows 10回收站问题巧解决

回收站是Windows10的一个重要组件。然而,我们在使用过程中,可能会遇到一些问题。例如,不论回收站里有没有文件,都显示同一个图标,让人无法判别回收站的空和满的真实情况;没有了像Windows7...

卸载软件怎么彻底删掉?简单几个步骤彻底卸载,电脑小白看过来

日常工作学习生活中,我们需要在安装一些软件程序,但随着软件的更新迭代速度,很多时候我们需要重新下载安装新的程序,这时就需要将旧的一些软件程序进行卸载。但是卸载软件虽然很简单,但是很多小伙伴们表示卸载不...

用不上就删!如何完全卸载OneDrive?

作为Windows10自带的云盘,OneDrive为资料的自动备份和同步提供了方便。然而,从隐私或其他方面考虑,有些人不愿意使用OneDrive。但Windows10本身不提供直接卸载OneDri...

【Linux知识】Linux下快速删除大量文件/文件夹方法

在Linux下,如果需要快速删除大量文件或文件夹,可以使用如下方法:使用rm命令删除文件:可以使用rm命令删除文件,例如:rm-rf/path/to/directory/*这个命令会递...

清理系统不用第三方工具_清理系统垃圾用什么软件

清理优化系统一定要借助于优化工具吗?其实,手动优化系统也没有那么神秘,掌握了方法和技巧,系统清理也是一件简单和随心的事。一方面要为每一个可能产生累赘的文件找到清理的方法,另一方面要寻找能够提高工作效率...

系统小技巧:软件卸载不了?这里办法多

在正常情况下,我们都是通过软件程序组中的卸载图标,或利用控制面板中的“程序和功能”模块来卸载软件的。但有时,我们也会发现利用卸载图标无法卸载软件或者卸载图标干脆丢失找不到了,甚至控制面板中卸载软件的功...

麒麟系统无法删除文件夹_麒麟系统删除文件权限不够

删除文件夹方法例:sudorm-rf文件夹名称。删除文件方法例:sudorm-r文件名包括扩展名。如果没有权限,给文件夹加一下权限再删。加最高权限chmod775文件名加可执行权限...

取消回复欢迎 发表评论: