写一个PE的壳_Part 2:ASLR+修复输入表(IAT)+重定位表支持(.reloc)
系列汇总
- 写一个PE的壳_Part 1:加载PE文件到内存
- 写一个PE的壳_Part 2:ASLR+修复输入表(IAT)+重定位表支持(.reloc)
- 写一个PE的壳_Part 3:Section里实现PE装载器
- 写一个PE的壳_Part 4:修复对ASLR支持+lief构建新PE
- 写一个PE的壳_Part 5:PE格式修复+lief源码修改
- 写一个PE的壳_Part 6:简单的混淆
文章目录
- Part 2:ASLR+修复输入表(IAT)+重定位表支持(.reloc)
- 1.ASLR预备知识
- 2.输入表支持
- detail 1:什么是输入表?
- detail 2:PE Header描述
- detail 3:真实的输入表
- detail 4:编程处理
- 数据结构
- 编程实现
- 扩展:LIEF中重建Import Table介绍
- 3.管理重定位表
- detail 1:什么是重定位表?
- detail 2:重定位表结构
- detail 3:编程处理
- 4.结果展示
- 5.参考
Part 2:ASLR+修复输入表(IAT)+重定位表支持(.reloc)
本文主要处理Part 1中遗留的2张表:输入表和重定位表(执行一个
ASLR
使能的文件不会出错)
当前现状:Part 1中执行loader.exe C:\Windows\SysWOW64\calc.exe
没有出现计算器,loader.exe
没有处理C:\Windows\SysWOW64\calc.exe
的输入表是一个主要原因
处理2个表前,最好先复习一下ASLR
的相关知识
1.ASLR预备知识
ASLR
(Address Space Layout Randomization,地址空间布局随机化)是一种针对缓冲区溢出的安全保护技术,从Vista开始支持;主要是使exe文件运行时加载到内存中的地址是随机的
- 产生原因
没有ASLR前,一般情况下,exe文件都会加载在OS给分配的虚拟内存0x400000上,微软自己的DLL总会加载在固定的地址上,这给程序安全带来的很大的隐患;因此需要将PE文件每次加载到内存的地址进行随机化
- 编译器支持
支持ASLR的前提是OS的内核版本必须是6
以上,且编译工具必须支持/DYNMAICBASE
;下面的界面中还可以设置PE文件的ImageBase(exe文件默认是0x400000,dll文件默认是0x10000000,默认值都是可以修改的)
.reloc
节区
支持ASLR的PE文件多了一个名为.reloc
的节区,一般情况下普通的exe文件不存在这个节区,开了ASLR技术的二进制才出现这个节区,是由编译器在编译时指定并保存在可执行文件中的
PE文件加载进入内存时,.reloc
节区主要是做重定位参考的
- PE header变化
File Header
中Characteristics
属性里,IMGE_FILE_RELOCS_STRIPPED
在不在支持ASLR时默认是勾选的,支持ASLR时不会勾选;增加ASLR的支持后,同一套源码产生的PE文件区段个数也会多一个
Optional Header
中DllCharacteristics
属性里,IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE
在不在支持ASLR时默认是不勾选的,支持ASLR时会勾选(是否勾选不要与上面弄混了)
题外话:逆向时,如果一个文件支持ASLR会给逆向增加难度,可以直接将
Optional Header
中DllCharacteristics
属性里的IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE
删掉,这样就不支持ASLR了;这样程序每次运行都会加载到同一地址,方便分析
2.输入表支持
结论:输入表支持就是我们要自己遍历INT,给IAT填写真实内存地址的过程
detail 1:什么是输入表?
简单归纳:就是将程序使用的第三方库的函数放在一个表格里进行统一管理的表
一般情况下,当一个PE二进制用额外的库时,它通常将输入表存储在.idata
区段;下面以一个ShellExecuteW
(被calc.exe导入)函数为例,说明一下输入表
calc.exe要调用ShellExecuteW
,是需要知道ShellExecuteW
函数在内存中的位置的;dll文件只有在运行时才会加载进内存,因此在编译阶段,编译器是不知道ShellExecuteW
在内存中的确切位置
输入表中的IAT就是为了解决上面的编译时和运行时的矛盾的
- 编译时:在调用
ShellExecuteW
的地方,ShellExecuteW
使用的是中转地址,或者可以理解成指向输入表中的IAT
- 运行时:当运行程序时,相应的dll文件被加载时,PE装载器会先将
ShellExecuteW
的真实地址在IAT
保留;都处理完后,才会执行用户写的代码,此时执行ShellExecuteW
就会通过IAT
找到真正的加载地址
结论:PE被装载进入内存后,输入表中真正有用的只有IAT了
下面是x32dbg加载calc.exe的截图,可以看到输入表中IAT
的身影
- 位置1:外部call指令,调用的是外部模块(
shell32.dll
),call指令(用FF15
操作码)的参数(38306700)来自于IAT
,被ShellExecuteW标识;具体是用函数名还是用序号(ordinal)取决取IAT
加载的方式 - 位置2:内部call指令,编译器知道被调用函数的目的地址,因此用
E8
操作码(“realtive call”
)
输入表的基本知识介绍完了,下面使用CFF查看一下输入表在PE中的相关部分
detail 2:PE Header描述
输入表信息这么重要,PE头文件哪里描述呢?可以从Data Directories
(是Optional Header
的一部分)开始学习
目录的顺序是事先确定的,默认个数是16
;与导入相关的有2个directories(都位于名为.idata
的section里):
Import Directory
:编号是01
,指向“Import Directory Table” (IDT
),告诉我们什么函数要被程序导入,即what
Import Address Table Directory
:编号是12
,指向“Import Address Table” (IAT
),将要导入的函数的地址放在IAT里,即where
题外话:Part 4中会看到
LIEF
建立一个空白PE时,Data Directories数组的个数指定的是15,导致输入表不能识别
detail 3:真实的输入表
CFF
查看IDT
的内容如下:
每一个dll都有一个条目记录需要导入的函数,下面通过编程处理输入表
detail 4:编程处理
现在直接说编程处理输入表,估计还是会不知所云,因为还有最后一项没有介绍,即输入表需要的数据结构
数据结构
Data Directories
中的Import Directory
指向一个数组,数组里每个元素都是IMAGE_IMPORT_DESCRIPTOR
的结构体,数组终止条件是一个全为0的IMAGE_IMPORT_DESCRIPTOR
的结构
每一个DLL都用一个IMAGE_IMPORT_DESCRIPTOR
的结构体进行描述,定义如下:
//每一个DLL都用一个 IMAGE_IMPORT_DESCRIPTOR 的结构体进行描述
typedef struct _IMAGE_IMPORT_DESCRIPTOR
{ _ANONYMOUS_UNION union{ DWORD Characteristics;DWORD OriginalFirstThunk; //真正有用的地方,指向IDT} DUMMYUNIONNAME;DWORD TimeDateStamp;DWORD ForwarderChain;DWORD Name; //DLL的名称DWORD FirstThunk; //指向IAT
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;
其中:OriginalFirstThunk
和FirstThunk
都指向一个元素大小是 DWORD的相同结构(IMAGE_THUNK_DATA
)的数组,NULL
是数组结尾
//DLL中的一个函数对应一个IMAGE_THUNK_DATA
typedef IMAGE_THUNK_DATA32 IMAGE_THUNK_DATA;
typedef struct _IMAGE_THUNK_DATA32 {union {DWORD ForwarderString; //PBYTE DWORD Function; //PDWORD,被输入的函数的内存地址DWORD Ordinal; //被输入API的序号DWORD AddressOfData; //PIMAGE_IMPORT_BY_NAME构} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
//IMAGE_THUNK_DATA最高位:
// 是1,函数以序号方式输入,后面位数代表函数序号
// 是0,函数以名称方式输入,DWORD整体是一个RVA,指向_IMAGE_IMPORT_BY_NAME结构//OriginalFirstThunk和FirstThunk指向相同的数组IMAGE_THUNK_DATA
typedef struct _IMAGE_IMPORT_BY_NAME {WORD Hint; //本函数在其驻留的DLL的输出表中的序号,非必须CHAR Name[1]; //指向输入函数名的ASCII的首地址,NULL结尾(定义的是可变长度)
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
编程实现
编程实现的思路:
- 1.遍历
OriginalFirstThunk
(变量lookup_table
,即IDT
)指向的数组,获得要导入的函数名(或者序号) - 2.然后将获得的函数地址放置在
FirstThunk
(变量address_table
,即IAT
)指向的镜像的位置
IMAGE_DATA_DIRECTORY* data_directory = p_NT_HDR->OptionalHeader.DataDirectory; //数据目录起始地址// load the address of the import descriptors array
IMAGE_IMPORT_DESCRIPTOR* import_descriptors = (IMAGE_IMPORT_DESCRIPTOR*) (ImageBase \+ data_directory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);// this array is null terminated
for (int i=0; import_descriptors[i].OriginalFirstThunk != 0; ++i) { //一次处理一个DLL// Get the name of the dll, and import itchar* module_name = ImageBase + import_descriptors[i].Name;HMODULE import_module = LoadLibraryA(module_name);if(import_module == NULL) {return NULL;}//先获取IDT和IAT// the lookup table points to function names or ordinals => it is the IDTIMAGE_THUNK_DATA* lookup_table = (IMAGE_THUNK_DATA*) (ImageBase + import_descriptors[i].OriginalFirstThunk);// the address table is a copy of the lookup table at first// but we put the addresses of the loaded function inside => that's the IATIMAGE_THUNK_DATA* address_table = (IMAGE_THUNK_DATA*) (ImageBase + import_descriptors[i].FirstThunk);// null terminated array, againfor(int i=0; lookup_table[i].u1.AddressOfData != 0; ++i) { //一次处理一个函数void* function_handle = NULL;// Check the lookup table for the adresse of the function name to importDWORD lookup_addr = lookup_table[i].u1.AddressOfData; //获取函数名if((lookup_addr & IMAGE_ORDINAL_FLAG) == 0) { //if first bit is not 1// [---import by name---] : get the IMAGE_IMPORT_BY_NAME structIMAGE_IMPORT_BY_NAME* image_import = (IMAGE_IMPORT_BY_NAME*) (ImageBase + lookup_addr);// this struct points to the ASCII function namechar* funct_name = (char*) &(image_import->Name);// get that function address from it's module and namefunction_handle = (void*) GetProcAddress(import_module, funct_name);//获取内存中的地址} else {// [---import by ordinal---], directlyfunction_handle = (void*) GetProcAddress(import_module, (LPSTR) lookup_addr);//此时lookup_addr是Ordinal}if(function_handle == NULL) {return NULL;}// change the IAT, and put the function address inside.address_table[i].u1.Function = (DWORD) function_handle; //存储一个函数的真实地址}
}
扩展:LIEF中重建Import Table介绍
输入表(或者叫导入表)通常用下面结构,下图是PE的Import Table被LIEF
处理前后的示意图
- lookup table:含有导入函数名的偏移量;在这个例子中,导入函数是来自
kernel32.dll
中的Sleep
和GetTickCount
- address table:大多数情况下,是等于lookup table的;在运行时,address table相应的内容会被替换成导入函数的地址;即Entry1拥有
Sleep
函数的地址,Entry2拥有GetTickCount
函数的地址
如果程序要调用Sleep函数,那么汇编代码应该如下所示
call address_table[entry1] ; call to sleep
在LIEF
中重建二进制文件时,LIEF
不会在任何时候修补汇编代码,这是重建导入表的一个强大约束。因为LIEF
不知道原始二进制文件中导入表的确切结构(地址表可能在查找表之前……)等信息;为了保持二进制文件的一致性,LIEF
通过上面图片的方式修补原始二进制文件
如果用过detours
等第三方库,看到Trampoline这个单词,第一个反应就是inline hook;这里更简单,就是单纯的增加了一层跳板来修补(或者说重建)Import Table
修补后,如果调用一个导入函数,汇编代码通常会有下面结构
call address_table_original[entry1] ; call to trampoline[0]
jmp *address_table[entry1] ; jump to Sleep address
参考:PE Format — LIEF Documentation (lief-project.github.io)
现在大部分工作已经做完了,为了适应ASLR
,处理一下重定位表是很有必要的
3.管理重定位表
detail 1:什么是重定位表?
到目前为止,我们做的工作主要细节如下:
- 1.打开了calc.exe文件,读取它的头信息进入内存
- 2.
Image Base
是PE装载器将calc.exe加载的默认内存的起始位置(现在使用VirtualAlloc实现)- 3.
ASLR
激活 ( 由“Dll can move
” 决定,在IMAGE_NT_HEADER.OptionalHeader.DllCharacteristics
中),表示calc.exe可以放在任意可用内存位置- 4.用第一个参数是
NULL
的VirtualAlloc
分配了一块内存(操作系统会选择一个合适的地址)- 5.操作系统给的随机地址现在是calc.exe的真正
ImageBase
- 6.导入calc.exe需要的函数并放置他们的地址在IAT中(Part 2中实现的)
再次回顾一下上面输入表使用的截图,位置1中的操作码是FF15
,操作数是00673038
;00673038
就是IAT中ShellExecuteW
的真实内存地址
通过CFF
查看一下导入表,shell32.dll
中只导入了一个函数,即ShellExecuteW
;可以看到IAT
的起始地址是0x00003038
,是一个相对地址,基准是shell32.dll
加载入内存的基址(0x670000)
这会引出一个问题,如果shell32.dll
的加载基址或者PE文件的加载基址不是常规的0x400000时,程序是怎么通过0x00003038
找到合适的内容呢?因此引入重定位表的内容;即重定位表可以解决程序加载在任意内存位置的问题
detail 2:重定位表结构
重定位表的结构比输入表的结构简单,我们也不必知道它内部是怎么工作的;脱壳一个软件时,输入表重建通常是必须的,对于重定位表,只要去使能ASLR即可(uncheck “Dll can move
”)
Data Directory中有relocation table的入口(RVA),重定位表是由一个个块直接相邻构成,每个块是由一个头和一个元素大小为word的数组组成
- 头部的结构
typedef struct _IMAGE_BASE_RELOCATION
{ DWORD VirtualAddress;DWORD SizeOfBlock; //块的大小,包括头
} IMAGE_BASE_RELOCATION, *PIMAGE_BASE_RELOCATION;
其中:VirtualAddress
是一个相对地址,是这个块开始重定位的基址;这是一个真实的页地址,因为重定位的偏移被限定在12 bits
(0x1000,4kb,windows的32位中的页大小)
- 一个元素大小为word的数组
头部信息后面是一个系列word大小的结构,直到块的终止(通过头部的SizeOfBlock
可以知道终止的位置),每一个word描述如下:
高4位:重定位的类型(只有一个会被使用)
低12位:偏移,相对头部的VirtualAddress的偏移
通过每个块的头部信息中的VirtualAddress可以知道,只要处理好VirtualAddress和模块的加载基址Image Base之间的关系,就可以解决掉ASLR
的问题
detail 3:编程处理
重定位表的处理应该在区块映射进内存和改变区块的属性之间进行处理,特别是.text
区块更改属性前
下面是Part 1中的load_PE
函数的主要逻辑,要明白重定位表的处理位置
void* load_PE (char* PE_data) {/** Parse header **//** Allocate Memory **//** Map PE sections in memory **//** Handle imports **//** Handle relocations **//** Map PE sections privileges **/return (void*) (ImageBase + entry_point_RVA);
}
处理重定位表的实现:
//this is how much we shifted the ImageBase
DWORD delta_VA_reloc = ((DWORD) ImageBase) - p_NT_HDR->OptionalHeader.ImageBase;// if there is a relocation table, and we actually shitfted the ImageBase
if(data_directory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress != 0 && delta_VA_reloc != 0) {//calculate the relocation table addressIMAGE_BASE_RELOCATION* p_reloc = (IMAGE_BASE_RELOCATION*) (ImageBase + data_directory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);//once again, a null terminated arraywhile(p_reloc->VirtualAddress != 0) { //处理一个block块// how any relocation in this block// ie the total size, minus the size of the "header", divided by 2 (those are words, so 2 bytes for each)DWORD size = (p_reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2;// the first relocation element in the block, right after the header (using pointer arithmetic again)WORD* reloc = (WORD*) (p_reloc + 1); //跳过sizeof(IMAGE_BASE_RELOCATION)for(int i=0; i < size; ++i) {//type is the first 4 bits of the relocation wordint type = reloc[i] >> 12;// offset is the last 12 bitsint offset = reloc[i] & 0x0fff;//this is the address we are going to changeDWORD* change_addr = (DWORD*) (ImageBase + p_reloc->VirtualAddress + offset);//核心// there is only one type used that needs to make a changeswitch(type){case IMAGE_REL_BASED_HIGHLOW :*change_addr += delta_VA_reloc; //更改地址break;default:break;}}// switch to the next relocation block, based on the sizep_reloc = (IMAGE_BASE_RELOCATION*) (((DWORD) p_reloc) + p_reloc->SizeOfBlock);}
}
4.结果展示
输入表和重定位表都处理完了,现在重新编译(x86_64-w64-mingw32-gcc.exe main.c -o loader.exe
)
注意:这里一定要编译成32位的程序,如果在64位电脑上运行,可以使用
x86_64-w64-mingw32-gcc.exe -m32
试试,不生效就直接使用i686-w64-mingw32-gcc.exe
这个专用32位版本的编译器吧!
运行loader
并加载一个32版本的ASLR使能的exe文件,如下命令会弹出windows自带计算器
loader.exe C:\Windows\SysWOW64\calc.exe
到目前为止,我们写了一个简单的执行exe的程序,但是这与调用system
和CreateProcess
函数所写的代码就多太多了,这么做的意义是什么呢?
system
和CreateProcess
函数:会创建另一个进程,然后执行calc.exe- 我们写的加载PE程序
loader.exe
:加载calc.exe进入我们自己申请的内存,然后转到模块的入口执行
后续教程中,我们不使用从文件系统中获取PE数据,而是写一个壳,从它的一个Section中获取PE文件,详细可以看Part 3
5.参考
1.Writing a PE packer – Part 2 : imports and relocations
2.《逆向工程核心原理》第41章 ASLR
3.写一个PE的壳_Part 1:加载PE文件到内存
写一个PE的壳_Part 2:ASLR+修复输入表(IAT)+重定位表支持(.reloc)相关推荐
- 写一个PE的壳_Part 4:修复对ASLR支持+lief构建新PE
系列汇总 写一个PE的壳_Part 1:加载PE文件到内存 写一个PE的壳_Part 2:ASLR+修复输入表(IAT)+重定位表支持(.reloc) 写一个PE的壳_Part 3:Section里实 ...
- 写一个PE的壳_Part 3:Section里实现PE装载器
系列汇总 写一个PE的壳_Part 1:加载PE文件到内存 写一个PE的壳_Part 2:ASLR+修复输入表(IAT)+重定位表支持(.reloc) 写一个PE的壳_Part 3:Section里实 ...
- 写一个PE的壳_Part 5:PE格式修复+lief源码修改
系列汇总 写一个PE的壳_Part 1:加载PE文件到内存 写一个PE的壳_Part 2:ASLR+修复输入表(IAT)+重定位表支持(.reloc) 写一个PE的壳_Part 3:Section里实 ...
- PE学习(六)第六章 栈与重定位表 实例栈溢出、模拟加载器加载DLL、遍历重定位表
第六章 栈与重定位表 16bit OS 存在长调用 lcall push cs,ip 相应的iret pop ip, cs 而call/ret only focus ip register 3 ...
- Windows PE第6章 栈与重定位表
第六章 栈与重定位表 本章主要介绍栈和代码重定位.站和重定位表两者并没有必然的联系,但都和代码有关.栈描述的是代码运行过程中,操作系统为调度程序之间相互调用关系,或临时存放操作数而设置的一种数据结构. ...
- PE结构基址重定位表
PE体系 PE结构&整体叙述 PE结构&导入表 PE结构&导出表 PE结构&基址重定位表 PE结构&绑定导入实现 PE结构&延迟加载导入表 重定位表定位 ...
- PE格式第七讲,重定位表
PE格式第七讲,重定位表 作者:IBinary 出处:http://www.cnblogs.com/iBinary/ 版权所有,欢迎保留原文链接进行转载:) 一丶何为重定位(注意,不是重定位表格) 首 ...
- PE文件-手工修改重定位表-WinHex-CFF Explorer
文章目录 1.CFF Explorer 2.计算添加后的重定位大小 3.作者答疑 1.CFF Explorer 如果需要修改exe,dll等的二进制代码,遇到添加绝对地址时,需要将绝对地址的位置添 ...
- 写一个能自动生成四则运算题目的软件,要求除了整数,还要支持正分数的四则运算。和同学们比较各自的程序功能、实现方法的异同。...
package Rational; import java.util.Random; import java.util.Scanner; public class szys { ...
最新文章
- 使用VS 自带的打包工具,制作winform安装项目
- 三态门三个状态vhdl_人防门是什么?为什么会侵线导致重庆地铁事故
- 深入浅出WPF(2)——解剖最简单的GUI程序
- 虚拟机开启以后电脑非常卡_专主开VT电脑版手机安卓模拟器开启VT 模拟器开启VT 虚拟机打开VT...
- 快速理解ASP.NET Core的认证与授权
- 2020蓝桥杯省赛---java---B---2(指数计算)
- 如何优化网站页面提高网页的加载速度
- 移植制造时保持资源的「统一」。
- setuna截图软件怎么用_苹果手机笔记怎么做?用哪款笔记软件好
- 推荐10个最好用的数据采集工具
- 错误解决:java.util.concurrent.ExecutionException: org.apache.catalina.LifecycleException: Failed to star
- 怎样区别7290喷壳机与原壳黑莓手机,里面有详图
- matlab积分法求椭圆周长,用MATLAB计算椭圆周长和牛顿迭代MATLAB实现.doc
- java多个文件加密压缩_java中文件如何加密压缩?
- git commit message——git提交日志规范备忘
- 刘彬20000词汇01
- 《羊驼亡命跑》 NFT 系列:羊驼跑酷套装来袭!
- python -m参数的含义和用法
- auto uninstaller 9.3.28下载安装教程
- 非数学类全国大学生数学竞赛总结
热门文章
- 赶紧更新!PC版微信被曝高危0day漏洞;特斯拉Autopilot源码窃取案尘埃落定
- 表达式和语句的简单理解
- python竖着展示诗_十八年-python诗词动画
- ChatGPT套壳网站汇总-5月22日更新
- SSH——Hibernate初学者之旅(五)
- 《利用python进行数据分析》第二版 第13章-Python建模库介 学习笔记
- windows神器,让你的效率直线提升
- 计算机专业考计量经济学,计量经济学期末考试题库(完整版)及答案()(47页)-原创力文档...
- 【JZOJ】WZK打雪仗
- 不外昨夜下战书当店的裘姓值班司理则称