本版
查看: 1652|回复: 1

[经验分享] 如何创建最小的PE文件

[复制链接]
楼主(1#)
发表于 2025-3-2 19:42:37 | 只看该作者 |只看大图 |倒序浏览 |阅读模式
本帖最后由 e0yue 于 2025-3-2 21:28 编辑

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


我们使用VS2005对这个程序进行编译和链接:

所生成的文件大小为45056字节。这显然是不可接受的。
移除C的运行时库
二进制文件的非常大的一部分就是C的运行时库。如果我们使用/NODEFAULTLIB选项对该程序进行链接,
我们就能得到一个小得多的输出文件。我们也将子系统设置为Win32 GUI来从程序中移除控制台窗口。

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



PE文件的大小现已减少到1024字节。
减少file alignment
我们看这个1024字节大小的文件的dumpbin输出,我们可以看到file alignment被设置为了512字节大小。

.text节区从文件偏移0x200开始。文件头和.text节区之间以0填充。PE官方规范声明file alignment最小值为512,
但Microsoft链接器可以处理具有更小file alignment值的PE文件。Windows加载器也会忽略非法alignment并正常执行文件。

PE文件的大小现在为468字节。
转到汇编程序并移除DOSstub
为进一步缩小文件,我们需要编辑PE文件头中的所有字段。

我们将反汇编468字节大小的C程序并将其转成可以被NASM汇编的汇编源码。
我们将使用下面的命令去构建PE文件:

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
































现在文件大小减为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从文件中移除。

绝大多数访问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头中未使用的空间。如下所示:











这种对头的修改方式会导致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文件的完整汇编代码如下:







带有导入信息的最小PE文件
不幸的是该97字节大小的PE文件在Windows2000中不能运行。因为加载器会尝试调用KERNEL32中的函数,

但KERNEL32.DLL未被加载。其他版本的Windows会自动加载,但在Windows2000中我们需要确保KERNEL32.DLL在文件的导入表中被显式列出。
添加导入表
导入表的结构相对复杂,但从KERNEL32添加一个序数(ordinal)导入相对简单。

我们需要将我们想导入DLL的名称置于name字段并创建两个相同的IMAGE_THUNK_DATA数组,分别用于INT和IAT。
当加载器解析导入信息时,它将读取INT中的序数信息并将函数地址设置在IAT中。
最终文件大小为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函数。




回复

举报

沙发(2#)
发表于 2026-9-11 09:10:03 | 只看该作者
过于复杂,看不懂。
回复

举报

联系我们(Contact)|手机版|萝卜头IT论坛 ( 苏ICP备15050961号-1 )

GMT+8, 2026-10-18 10:36 , Processed in 0.07 second(s), 20 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

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