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

EXE文件内存加载 explore.exe占用内存过大

liebian365 2024-10-30 04:50 22 浏览 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
安全客 - 有思想的安全新媒体

相关推荐

“版本末期”了?下周平衡补丁!国服最强5套牌!上分首选

明天,酒馆战棋就将迎来大更新,也聊了很多天战棋相关的内容了,趁此机会,给兄弟们穿插一篇构筑模式的卡组推荐!老规矩,我们先来看10职业胜率。目前10职业胜率排名与一周前基本类似,没有太多的变化。平衡补丁...

VS2017 C++ 程序报错“error C2065:“M_PI”: 未声明的标识符&quot;

首先,程序中头文件的选择,要选择头文件,在文件中是没有对M_PI的定义的。选择:项目——>”XXX属性"——>配置属性——>C/C++——>预处理器——>预处理器定义,...

东营交警实名曝光一批酒驾人员名单 88人受处罚

齐鲁网·闪电新闻5月24日讯酒后驾驶是对自己和他人生命安全极不负责的行为,为守护大家的平安出行路,东营交警一直将酒驾作为重点打击对象。5月23日,东营交警公布最新一批饮酒、醉酒名单。对以下驾驶人醉酒...

Qt界面——搭配QCustomPlot(qt platform)

这是我第一个使用QCustomPlot控件的上位机,通过串口精确的5ms发送一次数据,再将读取的数据绘制到图表中。界面方面,尝试卡片式设计,外加QSS简单的配了个色。QCustomPlot官网:Qt...

大话西游2分享赢取种族坐骑手办!PK趣闻录由你书写

老友相聚,仗剑江湖!《大话西游2》2021全民PK季4月激燃打响,各PK玩法鏖战齐开,零门槛参与热情高涨。PK季期间,不仅各种玩法奖励丰厚,参与PK趣闻录活动,投稿自己在PK季遇到的趣事,还有机会带走...

测试谷歌VS Code AI 编程插件 Gemini Code Assist

用ClaudeSonnet3.7的天气测试编码,让谷歌VSCodeAI编程插件GeminiCodeAssist自动编程。生成的文件在浏览器中的效果如下:(附源代码)VSCode...

顾爷想知道第4.5期 国服便利性到底需优化啥?

前段时间DNF国服推出了名为“阿拉德B计划”的系列改版计划,截至目前我们已经看到了两项实装。不过关于便利性上,国服似乎还有很多路要走。自从顾爷回归DNF以来,几乎每天都在跟我抱怨关于DNF里面各种各样...

掌握Visual Studio项目配置【基础篇】

1.前言VisualStudio是Windows上最常用的C++集成开发环境之一,简称VS。VS功能十分强大,对应的,其配置系统较为复杂。不管是对于初学者还是有一定开发经验的开发者来说,捋清楚VS...

还嫌LED驱动设计套路深?那就来看看这篇文章吧

随着LED在各个领域的不同应用需求,LED驱动电路也在不断进步和发展。本文从LED的特性入手,推导出适合LED的电源驱动类型,再进一步介绍各类LED驱动设计。设计必读:LED四个关键特性特性一:非线...

Visual Studio Community 2022(VS2022)安装图文方法

直接上步骤:1,首先可以下载安装一个VisualStudio安装器,叫做VisualStudioinstaller。这个安装文件很小,很快就安装完成了。2,打开VisualStudioins...

Qt添加MSVC构建套件的方法(qt添加c++11)

前言有些时候,在Windows下因为某些需求需要使用MSVC编译器对程序进行编译,假设我们安装Qt的时候又只是安装了MingW构建套件,那么此时我们该如何给现有的Qt添加一个MSVC构建套件呢?本文以...

Qt为什么站稳c++GUI的top1(qt c)

为什么现在QT越来越成为c++界面编程的第一选择,从事QT编程多年,在这之前做C++界面都是基于MFC。当时为什么会从MFC转到QT?主要原因是MFC开发界面想做得好看一些十分困难,引用第三方基于MF...

qt开发IDE应该选择VS还是qt creator

如果一个公司选择了qt来开发自己的产品,在面临IDE的选择时会出现vs或者qtcreator,选择qt的IDE需要结合产品需求、部署平台、项目定位、程序猿本身和公司战略,因为大的软件产品需要明确IDE...

Qt 5.14.2超详细安装教程,不会来打我

Qt简介Qt(官方发音[kju:t],音同cute)是一个跨平台的C++开库,主要用来开发图形用户界面(GraphicalUserInterface,GUI)程序。Qt是纯C++开...

Cygwin配置与使用(四)——VI字体和颜色的配置

简介:VI的操作模式,基本上VI可以分为三种状态,分别是命令模式(commandmode)、插入模式(Insertmode)和底行模式(lastlinemode),各模式的功能区分如下:1)...

取消回复欢迎 发表评论: