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

EXE文件内存加载

cac55 2024-11-20 12:53 11 浏览 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
安全客 - 有思想的安全新媒体

相关推荐

正点原子开拓者FPGA开发板资料连载第四十章 SD卡图片显示实验

1)实验平台:正点原子开拓者FPGA开发板2)摘自《开拓者FPGA开发指南》关注官方微信号公众号,获取更多资料:正点原子3)全套实验源码+手册+视频下载地址:http://www.openedv.c...

东芝存储改名为铠侠了,铠侠microSD卡128GB全网首测

作为一个数码爱好者,平时总爱把玩各种科技数码产品,最近又迷上了口袋云台相机,大疆OsmoPocket、飞宇口袋相机、SnoppaVmate口袋相机什么的,不过这类产品由于设计的机身体积很小(毕竟为...

SD存储卡卡面上奇奇怪怪的图标,你知道几个?

现在对高像素照片、连拍、4K甚至8K的需求越来越多,对存储卡的传输速度、容量等,要求也越来越多了。但是,看到SD存储卡卡面上奇奇怪怪的图标,让人非常迷惑。这篇文章让你简单认识这些图标和奇奇怪怪的数字。...

拍摄4K视频上选!铠侠 EXCERIA PLUS microSD卡

大家好,我是波导终结者。今天跟大家分享的是铠侠的EXCERIAPLUS极至光速microSDXCUHS-1存储卡,名字有点长,但是不用担心,我会帮大家梳理好存储卡的选购建议。有不少刚入门的朋友...

高速稳定,一卡多用:铠侠极至光速microSD存储卡评测

Hello,大家好,我是小胖子。半个月前收到了KIOXIA铠侠寄来的一张256GB的TF卡,用了大半个月,让我们看看这款产品表现如何吧。其实很多人并不太了解铠侠,问我铠侠是什么品牌,好不好。其实,东芝...

读速205MB/s、V30规格,雷克沙SILVER系列存储卡再添新成员

IT之家6月19日消息,雷克沙今日推出3款SILVER系列SD/microSD存储卡新品,支持4K60fps录像。据介绍,该系列存储卡均符合V30标准,其中micr...

相机、无人机拍视频,选择SD存储卡有什么需要知道的?

本文章不涉及产品推荐导购行为,致力于给到小白带来基础知识。相机一般使用SD卡,无人机一般使用microSD卡(也叫TF卡),使用的标准和图标标识是一样的。相机、无人机拍视频,选择SD存储卡有什么需要知...

PNY推出适用Switch 2的microSD Express卡,读取速度高达890MB/s

任天堂Switch2开始预订,其比前代产品变得更加昂贵,各种配件的价格都高于预期,这也包括转向microSDExpress存储。此时,PNY推出了新款microSDExpress闪存卡。新款mi...

SD卡迎来25周年:全球售出120亿张,容量翻50万倍

IT之家5月21日消息,科技媒体betanews今天(5月21日)发布博文,报道称SD卡迎来了25周年的生日。自2000年首款SD存储卡问世以来,已走过25个年头...

微单相机买一款什么样的SD卡才够用?写入速度更为关键

最近,评价君朋友发现自己的卡拍摄视频时候总断流,于是感觉写入速度应该是不够的,打算换卡,评价君正好跟他说道说道。目前的SD存储卡,很多只标注读取速度,比如95MB/s,80MB/s等等,而没有写写入速...

金士顿Canvas Go!Plus 系列存储卡评测

前言2020年,金士顿推出了CanvasGo!Plus系列存储卡,凭借其优秀的读写速度和稳定性获得了广大用户的认可。时隔5年,金士顿推出了其全新升级产品:SDG4/SDCG4,可选容量覆盖64GB...

TF卡速度等级|MK米客方德(tf卡速度等级图)

TF卡(TransFlash卡,又称MicroSD卡)是一种常见的便携式存储媒体,广泛用于智能手机、相机、平板电脑等设备中。TF卡的性能通常由速度等级来衡量,这些等级反映了TF卡的数据传输速度。拓优星...

关于SD卡,看这张表就够了(sd卡的作用)

这里是溢图科技(原“相机笔记”)。这两天有不少存储产品促销,随之而来的就是关于SD卡的一些提问。文章以前已经写过很多了,这里主要给大家看一张表格:上面就是SD卡协会官方制作的“族谱”,明确给出了不同版...

轻量化储存的首选——凯侠极致光速256G microSD存储卡实测

对于摄影师而言,我们经常会接触到相关存储设备,像照片拍摄中给相机安装的SD卡,视频录制中外录高规格画面的SSD等,都属于专业的存储介质,被应用于商业拍摄、电影级别拍摄之中。而针对生活中我们日常用于拍摄...

首发1569元,读取速度可达250MB/s,闪迪推出最新2TB至尊超极速存储卡

近日,闪迪(SanDisk)正式发布了其最新的2TB至尊超极速microSDXCUHS-I存储卡。据悉,这款存储卡的读取速度可达250MB/s,写入速度则达到150MB/s。这意味着用户在处理高分辨...

取消回复欢迎 发表评论: