吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1641|回复: 11
收起左侧

[原创] 【翻译】如何创建最小的PE文件

  [复制链接]
zhengzongyezhi 发表于 2025-2-8 01:01
本帖最后由 zhengzongyezhi 于 2025-2-8 18:22 编辑

【原文地址】http://www.phreedom.org/research/tinype/【个人翻译,请勿转载】

最小的PE文件

我们第一个任务是构建能被windows加载并执行的最小PE文件。我们将从一个简单的C程序开始:
编译一个简单的C程序
图片1.png

我们使用VS2005对这个程序进行编译和链接:
图片2.png

所生成的文件大小为45056字节。这显然是不可接受的。
移除C的运行时库

二进制文件的非常大的一部分就是C的运行时库。如果我们使用/NODEFAULTLIB选项对该程序进行链接,我们就能得到一个小得多的输出文件。我们也将子系统设置为Win32 GUI来从程序中移除控制台窗口。
图片3.png

/O1编译选项对生成代码的大小进行优化。对.text节区的反汇编显示主函数已经被优化为了4个字节:
图片4.png

PE文件的大小现已减少到1024字节。
减少file alignment

我们看这个1024字节大小的文件的dumpbin输出,我们可以看到file alignment被设置为了512字节大小。.text节区从文件偏移0x200开始。文件头和.text节区之间以0填充。PE官方规范声明file alignment最小值为512,但Microsoft链接器可以处理具有更小file alignment值的PE文件。Windows加载器也会忽略非法alignment并正常执行文件。
图片5.png

PE文件的大小现在为468字节。
转到汇编程序并移除DOSstub
为进一步缩小文件,我们需要编辑PE文件头中的所有字段。我们将反汇编468字节大小的C程序并将其转成可以被NASM汇编的汇编源码。我们将使用下面的命令去构建PE文件:
图片6.png

在这里我们仅移除在DOS模式中输出“this program cannot be run in DOS mode”的DOS stub。PE文件仍需要MZ头,但仅限于e_magic和e_lfanew字段。我们可以将MZ头中的其他字段用0填充。同样,在PE头中也存在大量修改后不会影响程序正常运行的字段。在下面的源码中以红色标识。
图片7.png 图片8.png 图片9.png 图片10.png

为找出哪些字段可被任意修改,我们使用了一个ruby编写的简单的asm fuzzer。它遍历汇编源码中的所有头部字段并使用随机值替换它们。如果生成的程序崩溃或是不能返回42,我们就认为这个字段是不能任意修改的。现在汇编后的PE文件大小为356字节。
对MZ头进行合并
MZ头中的e_lfanew字段包含从文件起始位置到PE头的偏移。通常PE头是在MZ头和DOS stub之后,但如果我们将e_lfanew设置为比0x40更小的值,PE头将从MZ头中开始。这就可以合并MZ头和PE头的一些字段并生成更小的文件。PE头不能从偏移0处开始,因为我们需要文件的头两个字节为MZ。根据PE规范PE头须在8字节处对齐,但Windows加载器仅需要在4字节处对齐。这就使得e_lfanew的最小值为4。如果PE头在偏移4处开始,其中大多字段将覆盖MZ头中的未使用字段。唯一需要谨慎处理的是e_lfanew字段,它和PE头中的SectionAlignment重叠。因为e_lfanew必须为4,因此SectionAlignment也就为4。PE规范声明如果Section Alignment小于页大小,file alignment也必须设为同样的值,因此FileAlignment和SectionAlignment都必须为4。幸运的是节区数据已在4字节处对齐,因此将file alignment从1改为4不会增大文件。
图片11.png 图片12.png

现在文件大小减为296字节。

移除data directories
PE optional头最后部分的data directories通常包含到导入导出表、debugging info、重定位和其他OS相关数据的指针。我们的PE文件不需要这些因此data directories为空。移除data directories将节省大量空间。PE规范指出data directories的数目在NumberOfRvaAndSizes字段和PE optional头的SizeOfOptionalHeader字段中声明。如果我们将NumberOfRvaAndSizes置为0并减少SizeOfOptionalHeader,我们就可以将data directories从文件中移除。
图片13.png

绝大多数访问data directories的函数都会检查NumberOfRvaAndSizes字段防止访问非法内存。唯一的例外就是WindowsXP中的debug directory。若debug directory大小不为0,则加载器会因为ntdll!LdrpCheckForSecuROMImage的非法内存访问而崩溃(虽然NumberOfRvaAndSizes为0)。因此我们需确保从optional头开始0x94偏移出的四字为0。在我们的PE文件中,该地址已经超出了由文件映射的内存范围因此被OS置为0。现在PE文件大小为168字节。
对PE节区头进行合并
Windows加载器期望在optional头后找到PE节区头。它通过将SizeOfOptionalHeader与optional头地址相加计算第一个节区头的地址。然而访问optional头字段的代码不会检查其大小(即SizeOfOptionalHeader字段在访问optional头时并未使用)。因此我们可以将SizeOfOptionalHeader设置为一个比其实际值更小的值,将PE节区头置于optional头中未使用的空间。如下所示:
图片14.png 图片15.png

这种对头的修改方式会导致dumpbin崩溃,但windbg !dh命令仍可以正确地解析头。现在PE文件大小为128字节。
最小的PE文件
下一步显而易见:我们可以将4个字节的代码移入头中未使用的字段内,例如TimeDateStamp字段。这就让optional头处于PE文件的结尾处。现在好像我们无法进一步缩小文件大小了,因为PE头起始于最小的偏移处并且它有确定的大小。它后面跟着同样起始于最小偏移处的PE optional头。文件中的其余数据也都包含在了这两个头中。但仍有可以修改的地方。PE文件被映射到一个4KB的内存页上。因为文件比4KB小,页的其余空间被0填充。如果我们移除PE optional头的最后几个字段,那么被映射到内存页上后,它们被0填充。0对于optional头的最后七个字段是合法值,因此我们可以移除它们并节省26个字节。文件中最后的单字是Subsystem字段,它必须被置为2。因为intel是小端序架构,该字的第一个字节为2且第二个字节为0。我们可以在文件中使用一个字节对该字段的值进行存储并以此节省一个字节。最终PE文件的完整汇编代码如下:
图片16.png 图片17.png

现在我们真的达到了极限。从文件开始处偏移0x94的字段为Subsystem,它必须被设为2。我们不能移除或绕过它。因此这就是最小的PE文件。该PE文件的大小为97字节。

带有导入信息的最小PE文件
不幸的是该97字节大小的PE文件在Windows2000中不能运行。因为加载器会尝试调用KERNEL32中的函数,但KERNEL32.DLL未被加载。其他版本的Windows会自动加载,但在Windows2000中我们需要确保KERNEL32.DLL在文件的导入表中被显式列出。

添加导入表
导入表的结构相对复杂,但从KERNEL32添加一个序数(ordinal)导入相对简单。我们需要将我们想导入DLL的名称置于name字段并创建两个相同的IMAGE_THUNK_DATA数组,分别用于INT和IAT。当加载器解析导入信息时,它将读取INT中的序数信息并将函数地址设置在IAT中。
图片18.png 图片19.png

带有一个序数导入信息的PE文件大小为209字节。

合并导入表

209字节相对于一个导入函数来说开销不可接受。首先我们需要移除INT。它是IAT的一份拷贝且链接器不使用该表。移除该表将节省8字节。导入表大小为40字节,但其中只有三个字段是真正要用的。这就可以将其合并到optional头中。
图片20.png

现在其大小减为161字节。

合并IAT和DLL名称

最后两个在PE头之外的结构是IAT和导入DLL的名称。我们可以将IAT合并到PE节区头未使用的8字节的name字段中。DLL名称可以存储在optional头最后未使用的字段和export data directory的8字节中。这就给了名称字符串16个字节(包括null终止符)的空间。Data directory最后的字段是导入表的大小,但该字段加载器并未实际使用因此可以置为0。导入表指针的最后三个字节同样为0,因为指针存于小端序的4字节中。我们可以移除文件末尾所有的0值字节,正如我们在上面97字节大小的PE文件中所做的那样。最终的PE文件的完整汇编代码如下:
图片21.png 图片22.png 图片23.png

最终文件大小为133字节。
可以从Internet下载文件的最小PE文件
Tiny PE挑战的目标是编写一个可以从互联网下载文件并执行它的最小PE文件。标准方式是调用URLDownloadToFileA 然后通过WinExec去执行该文件,但这种方法需要加载URLMON.DLL并调用多个方法,会导致PE文件大小的显著增加。Windows XP中很少有人知道的特性之一WebDAV Mini-Redirector。它可以将所有Windows应用中使用的UNC路径解析为URLs并尝试通过WebDAV协议去访问。这就意味着我们可以向WinExec传递一个UNC路径然后redirector会自动通过80端口的WebDAV协议去下载它。更有趣的是你可以在文件的导入表中声明一个UNC路径。如果我们将[url=]\\66.93.68.6\z[/url]作为导入DLL的名称,Windows加载器会尝试在我们的服务器上下载该DLL文件。这就使得我们可以创建一个PE文件,它可以什么也不做(without executing a single line of code)就能实现我们的目标。我们仅需要将payload置于DLL文件的DLLMain函数中,然后将该DLL文件置于WebDAV服务器上并将其UNC路径置于文件的导入表中。当加载器处理PE文件的导入信息时,它会自动从服务器下载DLL文件并执行DLLMain函数。
图片24.png

使用UNC导入的PE文件大小为133字节。

免费评分

参与人数 3威望 +1 吾爱币 +22 热心值 +3 收起 理由
Hmily + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
笙若 + 1 + 1 谢谢@Thanks!
Stargaze + 1 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

怜渠客 发表于 2025-2-8 20:13
提个小建议,代码可以不用截图,可以放到代码框里更清晰
Hmily 发表于 2025-2-8 17:39
 楼主| zhengzongyezhi 发表于 2025-2-8 18:23
Hmily 发表于 2025-2-8 17:39
图片需要上传贴到正文中,目前这种方式不行,可以参考:https://www.52pojie.cn/misc.php?mod=faq&action=f ...

thanks 已修改
YB19841223 发表于 2025-2-8 19:02
谢谢,下载备用!!!????
fast123 发表于 2025-2-8 20:14
学习了,学习了
 楼主| zhengzongyezhi 发表于 2025-2-8 20:24
怜渠客 发表于 2025-2-8 20:13
提个小建议,代码可以不用截图,可以放到代码框里更清晰

因为翻译的时候是直接写在word里的,当时直接粘的截图,懒得再写markdown了
younger4862 发表于 2025-2-9 09:23
利害,学习学习
shallies 发表于 2025-2-9 10:11
厉害,受教了!
苏紫方璇 发表于 2025-2-9 11:43
想起来原来有个视频,用画图点出来了个pe文件
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-4-21 15:25

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表