-
-
《逆向工程核心原理》9-14章总结
-
发表于: 2022-10-25 17:56 5955
-
Process Explorer是一款优秀的进程管理工具,可以查看进程和DLL等,但火绒剑似乎功能与其相差无几,先代替其用着吧。
cdecl和stdcall两种函数调用约定的区别在于由谁来负责处理栈。
- cdecl方式的好处在于可以向被调用的函数传递长度可变的参数,由调用者main()函数直接清理栈。
- stdcall方式常用于Win32 API,由被调用者函数内部清理栈,可以获得更好的兼容性。
- 另外fastcall与stdcall基本类似,但通常会使用寄存器传递部分参数(前2个)。
x86系统中使用EAX寄存器传递函数的返回值。
PE头内部信息大多以RVA(相对虚拟地址)形式存在,原因在于DLL的重定位。
PE头由DOS头、DOS存根(可选)、NT头、节区头组成。
DOS头(IMAGE_DOS_HEADER)
- 为了兼容DOS系统
- e_magic表示DOS签名(4D5A->ascci"MZ")
- e_lfanew(000000E0)指示NT头(IMAGE_NT_HEADER)的偏移
DOS存根(stub)
- 是个可选项,大小不固定,代码与数据混合而成
- Windows 32位环境下不执行
NT头(IMAGE_NT_HEADERS)
- 第一个成员为Signature(50450000h->"PE"00)
- 还有文件头和可选头
NT头:文件头(IMAGE_FILE_HEADER)
- Machine指示CPU的machine码(1对1)
- NumberOfSections用来指出文件中存在的节区数量(一定要大于0)
- SizeOfOptionalHeader用来指出IMAGE_OPTIONAL_HEADER32结构体的长度
- Characteristics用于标识文件属性(0002h代表EXE,2000h代表是DLL)
NT头:可选头(IMAGE_OPTIONAL_HEADER32)
- Magic值为10B时为IMAGE_OPTIONAL_HEADER32结构体,Magic值为20B时为IMAGE_OPTIONAL_HEADER64结构体
- AddressOfEntryPoint持有EP的RVA值,指出程序最先执行的代码起始地址
- ImageBase指出文件的优先装入地址,开始执行时EIP = ImageBase + AddressOfEntryPoint
- SectionAlignment指定了节区在内存中的最小单位,FileAlignment指定了节区在磁盘文件中的最小单位
- SizeOfImage指定了PE Image在虚拟内存中所占空间的大小
- SizeOfHeaders用来指出整个PE头的大小
- Subsystem的值用来区分系统驱动文件(.sys)与普通的可执行文件(.exe,*.dll)
- NumberOfRvaAndSizes用来指定DataDirectory数组的个数
- DataDirectory是由IMAGE_DATA_DIRECTORY结构体组成的数组,数组的每项都有被定义的值
节区头(IMAGE_SECTION_HEADER)
- VirtualSize指定了内存中节区所占大小
- VirtualAddress指定了内存中节区起始地址(RVA)
- SizeOfRawData指定了磁盘文件中节区所占大小
- PointerToRawData指定了磁盘文件中节区起始位置
- Characteristic指定了节区属性
- Name成员不像C语言中的字符串以NULL结束,也没有“必须使用ASCII值”的限制,节区的Name仅供参考,数据节区的Name也可以是.code
“映像”(Image):磁盘文件中的PE与内存中的PE具有不同的形态,将装载到内存中的形态称为“映像”以示区别。
RVA to RAW:
1 2 | RAW - PointerToRawData = RVA - VirtualAddress RAW = RVA - VirtualAddress + PointerToRawData |
DLL的优势:
- 不需要把库包含到程序中,只要单独组成DLL文件,需要时调用即可
- 内存映射技术使加载后的DLL代码、资源在多个进程中实现共享(内存映射?知识盲区了)
- 更新库时只要替换相关的DLL文件即可,简单易行
IMAGE_IMPORT_DESCRIPTOR
- 导入多少个库就存在多少个IMAGE_IMPORT_DESCRIPTOR结构体,这些结构体形成了数组,且结构体数组最后以NULL结构体结束。
- OriginalFirstThunk指定了INT的地址(RVA)
- Name指出了库名称字符串的地址(RVA),是一个字符串指针,指向导入函数所属的库文件名称
- FirstThunk指定了IAT的地址(RVA)
- IAT与INT是长整型(long)数组,以NULL结束,两者大小应相同
- INT中各元素的值为IMAGE_IMPORT_BY_NAME结构体指针
- IMAGE_OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress的值即是IMAGE_IMPORT_DESCRIPTOR结构体数组的起始地址
IAT输入顺序:
- 读取IID(IMAGE_IMPORT_DESCRIPTOR)的Name成员,获取库名称字符串(“kernel32.dll”)
- 装载相应库
->LoadLibrary("kernel32.dll") - 读取IID的OriginalFirstThunk成员,获取INT地址
- 逐一读取INT中数组的值,获取相应IMAGE_IMPORT_BY_NAME地址
- 使用IMAGE_IMPORT_BY_NAME的Hint(ordinal)或Name项,获取相应函数的起始地址
->GetProcAddress("GetCurrentThreadld") - 读取IID的FirstThunk(IAT)成员,获取IAT地址
- 将上面获得的函数地址输入相应的IAT数组值
- 重复以上步骤4-7,直到INT结束(遇到NULL时)
IMAGE_EXPORT_DIRECTORY
- IMAGE_OPTINAL_HEADER32.DataDirectory[0].VirtualAddress的值即是IMAGE_EXPORT_DIRECTORY结构体数组的起始地址。
- NumberOfFunctions指定了实际Export函数的个数
- NumberOfNames指定了Export函数中具名的函数个数
- AddressOfFunctions指示了Export函数地址数组
- AddressOfNames指示了函数名称地址数组
- AddressOfNameOrdinals指示了Ordinal地址数组
- 从库中获得函数地址的API为GetProcAddress()函数
GetProcAddress()操作原理
- 利用AddressOfNames成员转到“函数名称数组”
- “函数名称数组”中存储着字符串地址。通过比较(strcmp)字符串,查找指定的函数名称(此时数组的索引称为name_index)
- 利用AddressOfNameOrdinals成员,转到ordinal数组
- 在ordinal数组中通过name_index查找相应ordinal值
- 利用AddressOfFunctions成员转到“函数地址数组”(EAT)
- 在“函数地址数组”中将刚刚求得的ordinal用作数组索引,获得指定函数的起始地址
对于没有函数名称的导出函数,可以通过Ordinal查找到它们的地址。从Ordinal值中减去IMAGE_EXPORT_DIRECTORY.Base成员后得到一个值,使用该值作为“函数地址数组”的索引,即可查找到相应函数地址。
运行时压缩器是针对可执行文件而言的,可执行文件内部含有解压缩代码,文件在运行瞬间于内存中解压缩后执行。
notepad.exe与notepad_upx.exe的比较项目:
- PE头的大小一样
- 节区名称改变(“.text”->“UPX0”,“.data”->“UPX1”)
- 第一个节区的RawDataSize=0(文件中大小为0)
- EP位于第二个节区
- 资源节区(.rsrc)大小几乎无变化
notepad_upx.exe其第一个节区的的VirtualSize值被设置为10000(而SizeOfRawData值为0)。这就是说,经过UPX压缩后的PE文件在运行瞬间将文件中的压缩的代码解压到内存中的第一个节区。更详细一些,解压缩代码与压缩的源代码都在第二个节区,文件运行时首先执行解压缩代码,把处于压缩状态的源代码解压到第一个节区,解压结束后即运行源文件的EP代码。
至此把9-14章重新回顾了一遍,特别是PE文件结构这一块在过了一遍之后见解加深不少,之前还有点迷糊,现在感觉像是豁然开朗一般,有点爽。
赞赏
他的文章
- [分享]《逆向工程核心原理》15-20章总结 6726
- 《逆向工程核心原理》9-14章总结 5956
- [分享]《逆向工程核心原理》1-8章总结 8954
看原图
赞赏
雪币:
留言: