EXE文件内存加载
cac55 2024-11-20 12:53 10 浏览 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
安全客 - 有思想的安全新媒体
相关推荐
- 终于,你可以在 iPhone 上玩《饥荒》了
-
继七月登陆iPad平台后,冒险生存游戏《饥荒》(Don'tStarve)经过两个月时间终于更新并适配了iPhone,此前我已就游戏在iPad上的表现写过详尽评测和上手攻略,故本文不再对游...
- 2025年最适合Macbook新手掌握的5个免费工具,效率立马飙升!
-
刚入手Macbook是否觉得操作不熟?担心新手期过长难以熟练提高效率?别担心!本文精选五款国区AppStore免费可下载的官方认证工具,所有选择均基于新手核心痛点与迁移成本考量,解决「系统维护」「操作...
- 苹果iOS 13.4和iPadOS 13.4正式更新,支持鼠标、键盘操作
-
智东西(公众号:zhidxcom)编|王颖智东西3月25日消息,苹果今天向用户推送了iOS13.4和iPadOS13.4系统更新通知。iPadOS13.4增加了对iPad鼠标和触控板的支持,...
- 苹果即将发布macOS 15 用户界面将迎来重大革新
-
苹果公司计划于6月举行的全球开发者大会(WWDC)上,震撼发布全新的macOS15操作系统。据CNMO最新报道,此次更新将彻底革新“菜单和应用程序用户界面”的排列方式,为用户带来全新的使用体验。ma...
- **Bartender 5:菜单栏管理神器**(菜单栏工具)
-
提供免费下载网站Mavom.cn**Bartender**让你可以隐藏、重新排列或移动菜单栏应用,保持桌面整洁。**主要功能:*****整理菜单栏应用**:随心所欲地隐藏或显示应用。***更新提醒...
- Mac用户必备!12款最实用的高效App,绝对值得收藏
-
作为一名数码博主,日常的工作不仅包括写文章,还涉及到大量的内容创作、视频编辑和资料管理。随着使用Mac的时间越来越长,我发现一台强大的Mac电脑,若没有合适的App加持,效果往往大打折扣。因此,我深入...
- 苹果电脑死机了按什么键(mac卡死按哪三个键)
-
苹果电脑以其卓越的性能和稳定的系统而闻名,但在使用过程中,偶尔也会遇到死机或应用程序无响应的情况。这时,掌握一些有效的强制重启或关闭方法就显得尤为重要。本文将详细介绍苹果电脑在死机时可以采取的几种处理...
- 怎么查看macbook硬盘是不是原装的
-
要查看MacBook的硬盘是否是原装的,可以采取以下几种方法:###通过系统信息检查1.**查看设备信息**:打开苹果菜单栏中的“关于本机”选项,然后选择“存储”或“磁盘工具”。这将显示你电脑上已...
- 苹果MacBook一定要进行的6个设置|新手必备省电技巧
-
一、MacBook省电设置技巧1、电池偏好设置打开“系统偏好设置”,选择“电池”,选择第二项“电池”,不同的系统版本和机型在这个界面会有所差别。勾选“使用电池电源时使显示屏略暗一些”,勾选“优化电池充...
- 在 Mac 菜单栏也能控制 HomeKit 家居设备
-
想要控制家里的HomeKit设备,我们可以利用Apple官方的家庭App。但在Mac上,家庭App不能算得上好用,不像iOS可以从控制中心直接操作,在Mac上必须打开家庭A...
- 苹果手机里这个图标是什么意思?原来这是个监听器!一直都不知道
-
不知道大家最近都有没有关注iPhone的新消息呢?iPhone11出来之后,不少小伙伴都被圈粉啦!小编不得不说绿色的那款是真好看啊!当然不仅是好看,用过苹果手机的小伙伴都知道,苹果手机里有很多超好用的...
- 如何解决苹果电脑弹出本地项目钥匙串提示?
-
Mac电脑使用的时候,因为通过iCloud同步钥匙串,或者是修改本地账户密码,会反复弹出某项目想要登录使用“钥匙串”的提示,且无法关闭的现象。那我们该如何解决呢?快和小编一起来看看吧!具体方法如下1....
- MAC小技巧:如何快速调整Dock栏的大小
-
苹果mac系统dock栏怎么缩小?想要自己调节一下dock栏的大小,该怎么调节呢?下面我们就来看看详细的苹果Mac电脑如何快速调整Dock栏的大小样式教程,需要的朋友可以参考下。1、在Dock栏右侧,...
- 新买了苹果电脑不会用?给小白的使用手册,MacOS入门必备
-
咱们很多小伙伴都是十几年甚至二十几年的Windows老用户了,如果换成苹果电脑,可能会一脸懵逼,一时间不知道怎么使用。毕竟苹果电脑搭载的是MacOS操作系统,除了系统界面和操作上有区别外,电脑键盘上有...
- 苹果macOS 15设置界面将迎来重大更新 更智能更美观
-
【CNMO科技消息】苹果计划在6月WWDC全球开发者大会上震撼发布macOS15。据CNMO了解,此次更新将彻底革新“菜单和应用程序用户界面”的排列方式。macOSVentura系统中的“系统设置...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- 如何绘制折线图 (52)
- javaabstract (48)
- 新浪微博头像 (53)
- grub4dos (66)
- s扫描器 (51)
- httpfile dll (48)
- ps实例教程 (55)
- taskmgr (51)
- s spline (61)
- vnc远程控制 (47)
- 数据丢失 (47)
- wbem (57)
- flac文件 (72)
- 网页制作基础教程 (53)
- 镜像文件刻录 (61)
- ug5 0软件免费下载 (78)
- debian下载 (53)
- ubuntu10 04 (60)
- web qq登录 (59)
- 笔记本变成无线路由 (52)
- flash player 11 4 (50)
- 右键菜单清理 (78)
- cuteftp 注册码 (57)
- ospf协议 (53)
- ms17 010 下载 (60)