众所周知,windows下可执行文件必须符合一定的格式要求,微软官方称之为PE文件(关于PE文件的详细介绍这里就不赘述了,google一下可以找到大把);用户在界面双击exe时,有个叫做explorer的进程会监测并接受到这个事件,然后根据注册表中的信息取得文件名,再以Explorer.exe这个文件名调用CreateProcess函数去运行用户双击的exe;PC中用户一般都是这样运行exe的,所以很多用户态的exe都是exlporer的子进程。

用process hacker截图如下:

那么这个explorer究竟是怎么成功“运行”这个exe的了?这里面涉及到大量细枝末节就不深究了,本文先把主干思路捋一遍!

  • 分配内存  
    既然是运行,肯定是需要放在内存的,所以首先要开辟内存空间,才能把exe从磁盘加载进来;以32位为例,由于每个进程都有自己的4GB虚拟空间,所以还涉及到新生成页表、填充CR3等琐碎的细节工作;

  • 加载到内存
    内存分配好后,接着就该把exe从磁盘读取到内存了;

  • 重定位(文章末尾有扩展,详细介绍imagebase、VA、RVA、PointerToRawData、foa等概念)
    这一步我个人觉得是最关键、最容易出错的了!PE文件在编译器编译的时候,编译器是不知道文件会被加载到那个VA的(一般exe默认从40000开始,这个还好;但是dll默认从100000开始,这个就不同了。一个exe一般会调用多个dll,后面加载的dll肯定会和前面加载dll的imagebase冲突),这个时候只能把dll或exe加载到其他虚拟地址;一旦改变了imagebase,涉及到地址硬编码的地方都要改了,包括:全局/静态变量、子函数调用;所以PE文件里面单独有个relc段,标明了需要重新定位和生成VA的地址;由于硬编码存放的都是相对地址,所以重定位后新VA的计算公式也很简单,

  • 填写导入表
    一个exe的运行,很多时候要依赖操作系统提供的函数,举个最简单的例子:比如我要打印一段string,console下要用到printf或cout,MFC要用到messagebox,这些都是操作系统提供的API,编译器编译时也是不知道这些系统函数究竟被操作系统放在了内存的哪个地方,call的时候该往哪跳转了?所以只能把需要用到的这些系统函数统一放在一张叫做导入表的表格,explorer加载的时候还要挨个遍历导入表,一旦发现该PE文件用到了某些系统API,需要用这些API在内存的真实地址替换PE文件中call的地址(这也是用OD、x96dbg这些常见的调试器能找到这些系统函数的根本原因:都是系统提供的嘛,函数名必须保存起来,否则加载的时候没法替换成内存中真正的地址)!

好了,到此为止exe被加载的核心步骤都缕过了;具体实现上,explorer调用了createPorcess来加载和运行exe,这就直接导致了一个后果:被任务管理器或process hacker检测到(这里和通过loadLibrary类似:只要是通过windows提供的API使用内存,都会在某些地方被记录,这也是windows常见的内存管理方式之一,用了必须记录!所以规避检测的方式之一就是自己实现exe或dll的加载和运行,不依赖window的API)!为了躲避任务管理器或process hacker的监察,只能不调用createProcess,而是自己模拟PE加载的思路重新实现一遍了(类似于自己重新openProcess函数一样吧)!

自己实现PE loader核心思路代码如下(参考第5个链接):

int main()
{char szFileName[] = "D:\\software\\PELoader-master1\\test.exe";//打开文件,设置属性可读可写HANDLE hFile = CreateFileA(szFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL);if (INVALID_HANDLE_VALUE == hFile){printf("文件打开失败\n");return 1;}//获取文件大小DWORD dwFileSize = GetFileSize(hFile, NULL);//申请空间char* pData = new char[dwFileSize];if (NULL == pData){printf("空间申请失败\n");return 2;}//将文件读取到内存中DWORD dwRet = 0;ReadFile(hFile, pData, dwFileSize, &dwRet, NULL);CloseHandle(hFile);//将内存中exe加载到程序中char* chBaseAddress = RunExe(pData, dwFileSize);delete[] pData;system("pause");return 0;
}

其他代码如下(老规矩:精华都在注释了):

#include <windows.h>
#include <stdio.h>//跳转到入口点执行
bool CallEntry(char* chBaseAddress)
{PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)chBaseAddress;PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(chBaseAddress + pDos->e_lfanew);char* ExeEntry = (char*)(chBaseAddress + pNt->OptionalHeader.AddressOfEntryPoint);// 跳转到入口点处执行__asm{mov eax, ExeEntryjmp eax}return TRUE;
}//设置默认加载基址
bool SetImageBase(char* chBaseAddress)
{PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)chBaseAddress;PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(chBaseAddress + pDos->e_lfanew);pNt->OptionalHeader.ImageBase = (ULONG32)chBaseAddress;return TRUE;
}//填写导入表
bool ImportTable(char* chBaseAddress)
{PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)chBaseAddress;PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(chBaseAddress + pDos->e_lfanew);PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pDos +pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);// 循环遍历DLL导入表中的DLL及获取导入表中的函数地址char* lpDllName = NULL;HMODULE hDll = NULL;PIMAGE_THUNK_DATA lpImportNameArray = NULL;PIMAGE_IMPORT_BY_NAME lpImportByName = NULL;PIMAGE_THUNK_DATA lpImportFuncAddrArray = NULL;FARPROC lpFuncAddress = NULL;DWORD i = 0;while (TRUE){if (0 == pImportTable->OriginalFirstThunk){break;}// 获取导入表中DLL的名称并加载DLLlpDllName = (char*)((DWORD)pDos + pImportTable->Name);//看看这个dll是否已经加载hDll = GetModuleHandleA(lpDllName);//如果没有加载,那么先加载到内存if (NULL == hDll){hDll = LoadLibraryA(lpDllName);if (NULL == hDll){pImportTable++;continue;}}i = 0;// 获取OriginalFirstThunk以及对应的导入函数名称表首地址lpImportNameArray = (PIMAGE_THUNK_DATA)((DWORD)pDos + pImportTable->OriginalFirstThunk);// 获取FirstThunk以及对应的导入函数地址表首地址lpImportFuncAddrArray = (PIMAGE_THUNK_DATA)((DWORD)pDos + pImportTable->FirstThunk);while (TRUE){if (0 == lpImportNameArray[i].u1.AddressOfData){break;}// 获取IMAGE_IMPORT_BY_NAME结构lpImportByName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pDos + lpImportNameArray[i].u1.AddressOfData);// 判断导出函数是序号导出还是函数名称导出if (0x80000000 & lpImportNameArray[i].u1.Ordinal){// 序号导出// 当IMAGE_THUNK_DATA值的最高位为1时,表示函数以序号方式输入,这时,低位被看做是一个函数序号lpFuncAddress = GetProcAddress(hDll, (LPCSTR)(lpImportNameArray[i].u1.Ordinal & 0x0000FFFF));}else{// 名称导出lpFuncAddress = GetProcAddress(hDll, (LPCSTR)lpImportByName->Name);}// 注意此处的函数地址表的赋值,要对照PE格式进行装载,不要理解错了!!!// 把需要调用其他dll函数的VA写回导入表,就能通过call跳转到这里执行了lpImportFuncAddrArray[i].u1.Function = (DWORD)lpFuncAddress;i++;}pImportTable++;}return TRUE;}//修复重定位表
bool RelocationTable(char* chBaseAddress)
{PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)chBaseAddress;PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(chBaseAddress + pDos->e_lfanew);PIMAGE_BASE_RELOCATION pLoc = (PIMAGE_BASE_RELOCATION)(chBaseAddress + pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);//判断是否有重定位表if ((char*)pLoc == (char*)pDos){return TRUE;}while ((pLoc->VirtualAddress + pLoc->SizeOfBlock) != 0) //开始扫描重定位表{WORD* pLocData = (WORD*)((PBYTE)pLoc + sizeof(IMAGE_BASE_RELOCATION));//计算需要修正的重定位项(地址)的数目int nNumberOfReloc = (pLoc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);for (int i = 0; i < nNumberOfReloc; i++){if ((DWORD)(pLocData[i] & 0x0000F000) == 0x00003000) //这是一个需要修正的地址{DWORD* pAddress = (DWORD*)((PBYTE)pDos + pLoc->VirtualAddress + (pLocData[i] & 0x0FFF));DWORD dwDelta = (DWORD)pDos - pNt->OptionalHeader.ImageBase;//实际的imageBase减去pe文件里面标识的imagebase得到“移动的距离”*pAddress += dwDelta;//把移动的距离在原地址加上去}}//转移到下一个节进行处理pLoc = (PIMAGE_BASE_RELOCATION)((PBYTE)pLoc + pLoc->SizeOfBlock);}return TRUE;
}//将内存中的文件映射到进程内存空间中
bool MapFile(char* pFileBuff, char* chBaseAddress)
{PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuff;PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pFileBuff + pDos->e_lfanew);PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);//所有头 + 结表头的大小DWORD dwSizeOfHeaders = pNt->OptionalHeader.SizeOfHeaders;//获取区段数量int nNumerOfSections = pNt->FileHeader.NumberOfSections;// 将前一部分都拷贝过去RtlCopyMemory(chBaseAddress, pFileBuff, dwSizeOfHeaders);char* chSrcMem = NULL;char* chDestMem = NULL;DWORD dwSizeOfRawData = 0;for (int i = 0; i < nNumerOfSections; i++){if ((0 == pSection->VirtualAddress) ||(0 == pSection->SizeOfRawData)){pSection++;continue;}// 拷贝节区chSrcMem = (char*)((DWORD)pFileBuff + pSection->PointerToRawData);chDestMem = (char*)((DWORD)chBaseAddress + pSection->VirtualAddress);dwSizeOfRawData = pSection->SizeOfRawData;RtlCopyMemory(chDestMem, chSrcMem, dwSizeOfRawData);pSection++;}return TRUE;
}//获取镜像大小,传入的是文件的开始地址
DWORD GetSizeOfImage(char* pFileBuff)
{DWORD dwSizeOfImage = 0;PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuff;PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pFileBuff + pDos->e_lfanew);dwSizeOfImage = pNt->OptionalHeader.SizeOfImage;return dwSizeOfImage;
}//运行文件
char* RunExe(char* pFileBuff, DWORD dwSize)
{char* chBaseAddress = NULL;//获取镜像大小DWORD dwSizeOfImage = GetSizeOfImage(pFileBuff);//根据镜像大小在进程中开辟一块内存空间chBaseAddress = (char*)VirtualAlloc(NULL, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);if (NULL == chBaseAddress){printf("申请进程空间失败\n");return NULL;}//将申请的进程空间全部填0RtlZeroMemory(chBaseAddress, dwSizeOfImage);//将内存中的exe数据映射到peloader进程内存中,避免重新生成一个进程,这是隐藏exe的方式之一if (FALSE == MapFile(pFileBuff, chBaseAddress)){printf("内存映射失败\n");return NULL;}//修复重定位if (FALSE == RelocationTable(chBaseAddress)){printf("重定位修复失败\n");return NULL;}//填写导入表if (FALSE == ImportTable(chBaseAddress)){printf("填写导入表失败\n");return NULL;}//将页属性都设置为可读可写可执行DWORD dwOldProtect = 0;if (FALSE == VirtualProtect(chBaseAddress, dwSizeOfImage, PAGE_EXECUTE_READWRITE, &dwOldProtect)){printf("设置页属性失败\n");return NULL;}//设置默认加载基址if (FALSE == SetImageBase(chBaseAddress)){printf("设置默认加载基址失败\n");return NULL;}//跳转到入口点执行if (FALSE == CallEntry(chBaseAddress)){printf("跳转到入口点失败\n");return NULL;}return chBaseAddress;}

从代码看:这个pe loader本质上是在loader的进程开辟空间,然后运行exe的,所以exe的代码和数据其实都在loader的空间,并未单独生成一个进程,所以任务管理器、process hacker是都查不到的!这里也是把exe想办法当成了shellcode在用!整体感觉就像“寄生”一样!

效果如下:单独双击运行test.exe:这就是最终呈现的效果;

最后,编译exe的时候出于安全考虑,建议随机基址选是,编译生成的exe每次被加载的时候imagebase都是变化的,能在一定程度上增加逆向的难度,让逆向变得很繁琐,有效消耗逆向人员的时间和精力!

扩展:很多小伙伴刚接触PE的时候,分不清楚imagebase、VA、RVA、PointerToRawData、foa等概念,这里来缕一缕;

(1)imageBase:整个文件(比如pe、sys、dll等)在虚拟内存中的起始地址;以32位为例,exe默认都是从400000开始的;OD中查询PE文件头就是imageBase;上面说的重定位也是从imageBase这里开始重新计算新地址;

(2)virtualAddress:OD中左边的地址列就是VA,也就是在虚拟内存中的地址;

(3)RVA: related virtual address,翻译成中文就是相对虚拟地址;这个“相对”怎么理解了?“相对”就是VA和当前所在区段的距离;比如一个VA=0x401010,很明显是属于text段的,由于text段的基址是401000,那么这个地址的RVA=0x401010-0x401000=0x10;

(4)PointerToRawData:我也不知道怎么翻译成中文合适,所以干脆不翻译了;为什么会有这么一个概念了? 或则说这个概念想表达啥了?由于历史原因,很久以前磁盘的价格是很贵的,为了节约磁盘空间,pe文件尽量“压缩”式地存放在磁盘中。为了标注各个段在磁盘中的位置,就衍生出了PointerToRawData:即磁盘中,每个段头部相对于文件开始位置的距离;当运行程序时,需要把文件加载到内存。由于采用了虚拟地址、页交换等技术,虚拟内存空间大很多,没必要“节约”着用了,为了提高cpu寻址的效率,就需要内存对齐了,直观感觉就是下图中绿色的部分;这就导致了另一个问题:同样一个段,在磁盘中相对文件起始的距离,和内存中相对imageBase的距离是不一样的(因为地址对齐,拉伸了)! 用010editor这种软件是可以查到PointerToRawData的,如下:

(5)FOA: file offset address,又叫file address,简称FA,也就是磁盘文件内部的地址,计算出这个地址有利于静态查找和破解打补丁(比如改if跳转逻辑)。比如我们用OD找到了一个内存虚拟地址,怎么根据这个地址在磁盘的文件中找到同样的地址了?原理很简单,如下:

先计算出RAV,也就是当前虚拟地址相对于所在段的距离,比如上面的0x401010-0x401000=0x10,也就是这个地址距离text段的偏移是0x10;现在问题就转换成了怎么找text段在文件中的起始地址了?也很简单,直接查PointerToRawData呗!比如这个值是0x200,那么FA=PointerToRawData+RVA=0x200+0x10=0x210!在磁盘文件内部0x210的位置就能找到了!

[Rootkit] 进程隐藏 - 内存加载(寄生僵尸进程)相关推荐

  1. 进程:execve加载流程

    续上一篇<<ELF:加载过程>>中分析elf解析器.解析器填充等内容后,本章分析elf可执行程序加载过程. 目录 1. 源码流程 1.1 execve 2. 源码结构 3. 部 ...

  2. exe和dll的内存加载

    这两天学习了PE结构后,做了一个简单的内存加载demo.在完成这个demo期间也是遇到了很多问题,大部分的问题最后都得到了解决,但还是有一些问题依然困扰着我,我在最后将会提到.不过总的来说也算是顺利完 ...

  3. Android中apk加固完善篇之内存加载dex方案实现原理(不落地方式加载)

    一.前言 时隔半年,困扰的问题始终是需要解决的,之前也算是没时间弄,今天因为有人在此提起这个问题,那么就不能不解决了,这里写一篇文章记录一下吧.那么是什么问题呢? 就是关于之前的一个话题:Androi ...

  4. 【Flutter】Image 组件 ( 内存加载 Placeholder | transparent_image 透明图像插件 )

    文章目录 一.transparent_image 透明图像插件 二.内存加载 Placeholder 三.完整代码示例 四.相关资源 一.transparent_image 透明图像插件 安装 tra ...

  5. APK加壳【3】通用内存加载dex方案分析

    来源 Andorid APK反逆向解决方案:梆梆加固原理探寻 CSDN 作者Jack_Jia 该篇博文中的:"3. 如何使DexClassLoader加载加密的dex文件? "这部 ...

  6. APK加壳【2】内存加载dex实现详解

    来源 本文要实验的方案同样来源于CSDN大牛Jack_Jia的一篇翻译博文: Android4.0内存Dex数据动态加载技术 原文的地址是 http://2013.hackitoergosum.org ...

  7. 支持64位系统的XOR加密后内存加载PE绕过杀毒软件

    http://bbs.pediy.com/showthread.php?t=203910 绝对自动支持32.64位的内存加载源码 无聊逛看雪时,看到了这个. 然后到github上找到了源.就是这里:h ...

  8. C语言实现shellcode通用框架二:文件下载执行或内存加载

    简介: 承接接上篇.上篇(C语言实现shellcode通用框架一:解密执行)我们的第二层shellcode核心代码都是事先加密好嵌套在第一层shellcode中,核心代码更新起来不方便.所以联网更新显 ...

  9. 【ARMv8 编程】A64 内存访问指令——内存加载指令

    与所有先前的 ARM 处理器一样,ARMv8 架构是一种加载/存储架构.这意味着没有数据处理指令直接对内存中的数据进行操作.数据必须首先被加载到寄存器中,修改,然后存储到内存中.该程序必须指定地址.要 ...

最新文章

  1. Pretty Login便携版:Windows 7登录界面修改器
  2. HDU 6114 Chess 【组合数】(2017百度之星程序设计大赛 - 初赛(B))
  3. ubuntu 中的qt怎么调用graphics.h_Qt 标准对话框之 QFileDialog
  4. Bootstrap4+MySQL前后端综合实训-Day08-PM【ajax获取表单标签内容、根据“栏目信息”添加“新闻信息”、新闻管理系统-项目展示】
  5. 每天一道LeetCode-----以单词为单位逆序字符串,每个单词之间以一个空格分隔(原字符串中可能有多个空格)
  6. jvm 助记符_您的JVM是否泄漏文件描述符-像我的一样?
  7. 20191215周学习总结
  8. 佩奇,是你吗?曝新款AirPods外观酷似吹风机
  9. .Net操作Excel后彻底释放资源
  10. Haproxy均衡负载部署和配置文件详解
  11. Java重写《C经典100题》 --08
  12. 如果计算机正执行屏幕保护程序 当用户,计算机一级考试参考试题(含答案)篇篇一.doc...
  13. Linux 常用 shell 命令
  14. jdk11 及jdk8阿里云快速下载链接
  15. MeasureSpec的简单说明
  16. 有道词典翻译功能数字有时无法翻译出来解决方案
  17. 【券后价16.80元】【海蓝蓝】夹心海苔脆芝麻海苔即食罐装海苔宝宝辅食儿童零食40克...
  18. 区分苹果开发者的网址(开发者网址和管理您的appid网址)及证书信息
  19. 2021年起重机司机(限桥式起重机)考试及起重机司机(限桥式起重机)考试报名
  20. iperf 服务端发送数据_Iperf详细使用说明

热门文章

  1. 【GStreamer 】5-2 USB相机转RTSP网络视频流优化
  2. 微信小程序常用CSS总结
  3. 多文件Makefile编写
  4. 你使用的授权码与本机不匹配,请重新授权
  5. DEAP(Database for Emotion Analysis using Physiological Signals)介绍
  6. js 四舍五入方法,重写js四舍五入方法
  7. 蓝桥杯 Java 基础练习 vip试题
  8. IDEA 出现问题:tomcat热部署没反应解决方案(JAVA 小虚竹,建议收藏)
  9. 无有线网络下安装并配置debian
  10. canvas使用Ajax上传图片PHP,使用ajax上传图片,并且使用canvas实现出上传进度效果...