目前这个问题下最小的能在Windows系统上运行的Hello World是
Windows 上最小的“HelloWorld.exe”能有多小? - 潘安仁的回答,161Bytes,我看了他的答案后深受启发,在他的基础上更进一步,写出了97Bytes的Hello World。
先贴代码,注意在32位Windows系统上运行。(我只在WinXP32上运行通过)
ps:评论区有人指出,只能在WinXP上运行,XP之后的系统库的地址会变。
pss:将本回答最后一幅图片保存,后缀改成 .rar ,解压后也能获得程序。
4D5A0000504500004C01000048656C6C6F2C776F726C6421CC0002000B016AF5E8640C807C6A0CEB07CCCCCC1E000000680C00010050EB0C000001000400000004000000E8A4C0807C90EBFD0400E89AC0807C90EBFDCC00CCCCCC00CCCCCCCC03
————————————————分割线————————————————
为什么我的程序更小?
@潘安仁的程序输出Hello World用了USER32的MessageBoxA,这个api看上去很好,毕竟'USER32'是6Bytes,而其他的库名如'kernel32'是8Bytes,所以调用USER32中的函数更短?
错。因为载入PE文件时kernel32.dll和ntdll.dll会被自动载入,并且这两个库正常是卸不掉的,你可以试试FreeLibrary(GetModuleHandle("kernel32")),然后再用汇编代码强制调用kernel32的一个函数,正常运行有没有。所以如果调用kernel32的函数可以删去导入表,大大节省空间。
那kernel32有什么输出函数可以输出Hello world?
有。WriteConsoleA(),写控制台。如果是同一系统,这个函数的位置应该是不变的,在Winxp系统中,这个函数地址是7C81C0ED。同时这个函数还要用到控制台输出句柄,用GetStdHandle(),地址7C810C89。这下程序就小了很多。
————————————————分割线————————————————
下面是程序的总体思路:
1.先写汇编代码
Data: db 'Hello world!' push -B call 7C810C89 push 0 push 0 push c push Data push eax call 7C81C0ED
代码好长啊能不能缩短一些呢?我们注意到WriteConsoleA的最后两个参数都对我们无用,一个保存被写入的字节数,一个是保留参数,这种无用的东西就丢掉好了。于是变成了:
Data: db 'Hello world!' push -B call 7C810C89 push c push Data push eax call 7C81C0ED
这样函数还能正常返回吗?当然能,我们没将数据压入栈不代表不能pop出东西来,至于pop出来的是什么、由此而产生栈平衡问题都不关我事。
2.压缩出最小的文件头
我的程序没有其他外部命令,自然不需要导入表和导出表,节表也不需要,直接将代码写在文件头里,只需要DOS头、文件头和可选头。
这里就是
@潘安仁的神操作了,把PE头和DOS头重叠。
如图,加载32位PE文件时,系统对dos头只读取开头的MZ标志和e_ifanew,中间的值可以随意填写,这样我们把NT头提到地址为4的地方去,将DOS头和NT头重叠。
大家也可以尝试其他的位置,其中4是最小的可行位置。
还有一处要注意的地方是我们的是控制台程序,所以Subsystem设为3。
图中是一个可运行的PE程序,运行后直接死循环。
可以看到这样的程序还有不少空位,我们可以把代码插进去。
3.插入代码
文件头中的空白部分TimeDateStamp;PointerToSymbolTable;NumberOfSymbols;都是无用且可以随意填写的,刚好12个字节,我们把"Hello world!"放进去。
剩下不少空位我们把代码也放进去,如果放不下就放一个jmp短跳,跳到下一个空位。
放好之后如图:
图中我们放入代码后,还有不少空位,我都用0xCCh标记了出来。
4.修剪
这样我们的程序已经很小了,它是124字节,那么怎么把它变成97字节呢?相信你也猜到了,把最后一堆零删掉就好了,PE文件载入时空白内存自动填零,这堆零我们是不需要的。
最后就是这样:
运行效果:
输出了一行朴实无华的hello world,我们的程序还有不少空位,或许还可以把效果做炫酷一点,大家自己尝试。
5.也学着做一个总结:
这个问题是13年提出来的,现在已经是16年了,在浮躁的社会里,这个问题或许早已被遗忘在尘埃里。我看到了这个问题,看到了前面大神的回答,我知道我可以做得比他更小,于是我开始着手尝试。我花了几天来学习PE结构,找出缩小程序的方法,思考余暇,我不禁疑惑,做这种事,做出这种程序有什么意义,我为什么要有一个这么小的Hello world?我硬盘差几字节吗?或许你也有一样的困惑,然而我们追求这一系列极限有什么意义呢?更高更快更强,是要回到狩猎时代吗?我想这一切无关乎这种浅薄的利益,它像是一种证明,证明自己的高度。它竖起了一座里程碑,并鼓励后来人一步一个脚印,继续向上,去到山顶上,去睥睨众生。如此之后,我们浮躁的社会里,或许有了一点没那么浮躁的东西。
这是我 19 年做的一个实验,制作的是当时最新版本 Windows 10 操作系统可以正常运行的最小 64 位 PE 文件。虽然在未来的 Windows 操作系统上不一定能运行,但是读者也可以利用本文中的方法制作出新的最小 PE 文件。
GitHub 链接 →
本次实验尝试制作了 Windows 10 操作系统下的最小 64 位 PE 文件,该文件可以弹出带有文字提示的消息框,且满足实验的三项限制条件:
经过九个步骤后,最终制作出了 268 字节的 PE 文件。
实验步骤如下:
步骤 | 文件大小(字节) | 熵 |
---|---|---|
1 | 94208 | 5.924863 |
2 | 896 | 3.567715 |
3 | 896 | 2.220567 |
4 | 896 | 1.608427 |
5 | 896 | 1.607880 |
6 | 448 | 2.653766 |
7 | 308 | 2.473451 |
8 | 280 | 2.620173 |
9 | 268 | 2.667864 |
其中,第 3-5 步虽然文件大小没有减小,但文件的熵减小了,这说明文件内部有更多的零,也就说明后续步骤有更多的压缩空间。
第一步:使用常规方法,利用 C 语言编写程序,使用 MSVC 编译器生成普通的 PE 文件。
在 Windows 编程中,要弹出消息框,可以使用 MessageBoxA
或 MessageBoxW
这两个函数。其中,函数名以 A 结尾表示字符编码使用用户的当前代码页,以 W 结尾表示使用 UTF-16。
由于用户使用的代码页并不统一,出于兼容性的考虑,本次实验使用以 W 结尾的 MessageBoxW
函数。代码页的问题其实非常常见,相信许多人在 Windows 上使用 Python 编程时,都遇到过因为代码页不对而导致程序乱码或崩溃的问题。这里使用 MessageBoxW
函数,与代码页无关,所以可以避免程序乱码或崩溃。
编写的 C 语言代码 tiny.c
如下:
#include <Windows.h> int main() { MessageBoxW( NULL /* hWnd */, L"ABCDEFG" /* lpText, 16 bytes */, L" TinyPE on Windows 10" /* lpCaption, 48 bytes */, MB_ICONASTERISK | MB_TOPMOST | MB_SERVICE_NOTIFICATION /* uType */ ); return 0; }
接下来将 C 语言代码编译为 PE 文件。
安装 Visual Studio Build Tools 2019 后,「开始」菜单中会出现 Visual Studio 2019 文件夹。点击其中的 x64 Native Tools Command Prompt for VS 2019 打开命令行,切换到当前目录后,输入以下命令:
> cl /O1 /source-charset:utf-8 tiny.c /link /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup user32.lib Microsoft (R) C/C++ Optimizing Compiler Version 19.22.27905 for x64 Copyright (C) Microsoft Corporation. All rights reserved. tiny.c Microsoft (R) Incremental Linker Version 14.22.27905.0 Copyright (C) Microsoft Corporation. All rights reserved. /out:tiny.exe /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup user32.lib tiny.obj
编译生成 tiny.exe
文件,可以正常运行:
第二步:通过改变 MSVC 编译器编译时的选项,减小生成的 PE 文件大小。
参考 Minimize the size of your program – high level 和 Linker Options 可知,通过修改 C 语言代码和编译选项,可以减小编译生成的 PE 文件大小。
修改后的 C 语言代码 tiny.c
如下:
#include <Windows.h> void _() { MessageBoxW( NULL /* hWnd */, L"ABCDEFG" /* lpText, 16 bytes */, L" TinyPE on Windows 10" /* lpCaption, 48 bytes */, MB_ICONASTERISK | MB_TOPMOST | MB_SERVICE_NOTIFICATION /* uType */ ); }
使用以下命令行选项编译:
> cl /O1 /MD /GS- /source-charset:utf-8 tiny.c /link /NOLOGO /NODEFAULTLIB /SUBSYSTEM:WINDOWS /ENTRY:_ /MERGE:.rdata=. /MERGE:.pdata=. /MERGE:.text=. /SECTION:.,ER /ALIGN:16 user32.lib Microsoft (R) C/C++ Optimizing Compiler Version 19.22.27905 for x64 Copyright (C) Microsoft Corporation. All rights reserved. tiny.c LINK : warning LNK4108: /ALIGN specified without /DRIVER; image may not run LINK : warning LNK4254: section '.text' (60000020) merged into '.' (40000040) with different attributes
编译生成 tiny.exe
文件,可以正常运行。
第三步:使用 PE Tools 查看上一步产生的 PE 文件的内部结构,将其中一些明显无用的部分置零。此时文件大小并没有减小,但文件内部有了更多的零,这让后续步骤有了更多的压缩空间。
打开 PE Tools (v1.9.762.2018),单击“PE Editor”,然后打开 tiny.exe
。
找到以下三个部分,并置零:
修改后的 tiny.exe
文件可以正常运行。
第四步:在 PE 文件的内部结构中,MS-DOS stub 也是无用的部分,所以使用二进制编辑器手动将这部分内容置零。这一步也给了后续步骤更多的压缩空间。
使用二进制编辑器打开 tiny.exe
。
定位到以下部分,并置零:
0x02-0x3b
置零0x40-0x7f
置零这里需要注意,0x00-0x01
是 e_magic
,0x3c-0x3f
是 e_lfanew
,它们都是有用的字段,所以没有置零。
修改后的 tiny.exe
文件可以正常运行,内容如下:
$ xxd -p tiny.exe 4d5a00000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 c00000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 000000000000000000000000504500006486010000000000000000000000 0000f00022000b020e1690010000000000000000000000030000f0010000 000000400100000010000000100000000600000000000000060000000000 000080030000f00100000000000002006081000010000000000000100000 000000000000100000000000001000000000000000000000100000000000 000000000000200300002800000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 00000000f001000010000000000000000000000000000000000000000000 0000000000002e0000000000000082010000f001000090010000f0010000 000000000000000000000000200000605803000000000000000000000000 00003dd8afdc2000540069006e0079005000450020006f006e0020005700 69006e0064006f0077007300200031003000000041004200430044004500 460047000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000041b9400024004c8d05f3feff ff488d151cffffff33c948ff25d3feffffcccccc48030000000000000000 000066030000f00100000000000000000000000000000000000000000000 5803000000000000000000000000000094024d657373616765426f785700 5553455233322e646c6c00000000000000000000000000000000
可以使用 xxd -p -r
将以上文本转换回二进制文件。
第五步:详细分析 PE 文件的内部结构,理解 PE 文件中各部分的含义,然后使用汇编语言手动编写一个相同的 PE 文件。虽然这一步生成的文件和上一步相比没有区别,但使用汇编语言后,后续步骤就无需修改二进制文件本身,而可以在汇编代码上作修改,从而更简便地减小文件大小。
在理解 PE 文件的结构时,主要参考了以下资料:
另外,还使用了 PE Tools 和 PE Disassembler viewer 这两个工具。
在理解 PE 文件的结构后,使用汇编语言手动编写一个相同的 PE 文件。
在编写汇编语言时,要特别注意不能使用硬编码的数值,而是要使用伪指令计算得出相应的数值。例如,文件大小的数值 0x0380
使用 file_size equ $-$$
替换,机器指令 0x41b940002400
使用 mov r9d, 0x00240040
替换。这是因为如果使用了硬编码的数值,后续步骤中 PE 文件的结构发生变化时,这些数值并不会随之改变,文件就会损坏。
此外,在使用 NASM 汇编器时,根据官方文档,分别使用伪指令 db
、dw
、dd
、dq
声明 1、2、4、8 字节的数据。
编写的汇编语言文件 stretch.asm
如下:
BITS 64 %define align(n,r) (((n+(r-1))/r)*r) ; DOS Header dw 'MZ' ; e_magic dw 0 ; [UNUSED] e_cblp dw 0 ; [UNUSED] c_cp dw 0 ; [UNUSED] e_crlc dw 0 ; [UNUSED] e_cparhdr dw 0 ; [UNUSED] e_minalloc dw 0 ; [UNUSED] e_maxalloc dw 0 ; [UNUSED] e_ss dw 0 ; [UNUSED] e_sp dw 0 ; [UNUSED] e_csum dw 0 ; [UNUSED] e_ip dw 0 ; [UNUSED] e_cs dw 0 ; [UNUSED] e_lfarlc dw 0 ; [UNUSED] e_ovno times 4 dw 0 ; [UNUSED] e_res dw 0 ; [UNUSED] e_oemid dw 0 ; [UNUSED] e_oeminfo times 10 dw 0 ; [UNUSED] e_res2 dd pe_hdr ; e_lfanew ; DOS Stub times 8 dq 0 ; [UNUSED] DOS Stub ; Rich Header times 8 dq 0 ; [UNUSED] Rich Header ; PE Header pe_hdr: dw 'PE', 0 ; Signature ; Image File Header dw 0x8664 ; Machine dw 0x01 ; NumberOfSections dd 0 ; [UNUSED] TimeDateStamp dd 0 ; PointerToSymbolTable dd 0 ; NumberOfSymbols dw opt_hdr_size ; SizeOfOptionalHeader dw 0x22 ; Characteristics ; Optional Header, COFF Standard Fields opt_hdr: dw 0x020b ; Magic (PE32+) db 0x0e ; MajorLinkerVersion db 0x16 ; MinorLinkerVersion dd code_size ; SizeOfCode dd 0 ; SizeOfInitializedData dd 0 ; SizeOfUninitializedData dd entry ; AddressOfEntryPoint dd iatbl ; BaseOfCode ; Optional Header, NT Additional Fields dq 0x000140000000 ; ImageBase dd 0x10 ; SectionAlignment dd 0x10 ; FileAlignment dw 0x06 ; MajorOperatingSystemVersion dw 0 ; MinorOperatingSystemVersion dw 0 ; MajorImageVersion dw 0 ; MinorImageVersion dw 0x06 ; MajorSubsystemVersion dw 0 ; MinorSubsystemVersion dd 0 ; Reserved1 dd file_size ; SizeOfImage dd hdr_size ; SizeOfHeaders dd 0 ; CheckSum dw 0x02 ; Subsystem (Windows GUI) dw 0x8160 ; DllCharacteristics dq 0x100000 ; SizeOfStackReserve dq 0x1000 ; SizeOfStackCommit dq 0x100000 ; SizeOfHeapReserve dq 0x1000 ; SizeOfHeapCommit dd 0 ; LoaderFlags dd 0x10 ; NumberOfRvaAndSizes ; Optional Header, Data Directories dd 0 ; Export, RVA dd 0 ; Export, Size dd itbl ; Import, RVA dd itbl_size ; Import, Size dd 0 ; Resource, RVA dd 0 ; Resource, Size dd 0 ; Exception, RVA dd 0 ; Exception, Size dd 0 ; Certificate, RVA dd 0 ; Certificate, Size dd 0 ; Base Relocation, RVA dd 0 ; Base Relocation, Size dd 0 ; Debug, RVA dd 0 ; Debug, Size dd 0 ; Architecture, RVA dd 0 ; Architecture, Size dd 0 ; Global Ptr, RVA dd 0 ; Global Ptr, Size dd 0 ; TLS, RVA dd 0 ; TLS, Size dd 0 ; Load Config, RVA dd 0 ; Load Config, Size dd 0 ; Bound Import, RVA dd 0 ; Bound Import, Size dd iatbl ; IAT, RVA dd iatbl_size ; IAT, Size dd 0 ; Delay Import Descriptor, RVA dd 0 ; Delay Import Descriptor, Size dd 0 ; CLR Runtime Header, RVA dd 0 ; CLR Runtime Header, Size dd 0 ; Reserved, RVA dd 0 ; Reserved, Size opt_hdr_size equ $-opt_hdr ; Section Table section_name db '.' ; Name times 8-($-section_name) db 0 dd sect_size ; VirtualSize dd iatbl ; VirtualAddress dd code_size ; SizeOfRawData dd iatbl ; PointerToRawData dd 0 ; PointerToRelocations dd 0 ; PointerToLinenumbers dw 0 ; NumberOfRelocations dw 0 ; NumberOfLinenumbers dd 0x60000020 ; Characteristics hdr_size equ $-$$ code: ; Import Address Directory iatbl: dq symbol dq 0 iatbl_size equ $-iatbl ; Strings title: db 0x3d,0xd8,0xaf,0xdc,0x20,0x00,0x54,0x00 db 0x69,0x00,0x6e,0x00,0x79,0x00,0x50,0x00 db 0x45,0x00,0x20,0x00,0x6f,0x00,0x6e,0x00 db 0x20,0x00,0x57,0x00,0x69,0x00,0x6e,0x00 db 0x64,0x00,0x6f,0x00,0x77,0x00,0x73,0x00 db 0x20,0x00,0x31,0x00,0x30,0x00,0,0 content: db 0x41,0x00,0x42,0x00,0x43,0x00,0x44,0x00 db 0x45,0x00,0x46,0x00,0x47,0x00,0,0 ; Debug Table times 24 dq 0 ; [UNUSED] Debug Table ; Entry entry: mov r9d, 0x00240040 ; uType lea r8, [rel title] ; lpCaption lea rdx, [rel content] ; lpText xor ecx, ecx ; hWnd jmp [rel iatbl] ; MessageBoxW times align($-$$,16)-($-$$) db 0xcc ; Import Directory itbl: dq intbl ; OriginalFirstThunk dd 0 ; TimeDateStamp dd dll_name ; ForwarderChain dd iatbl ; Name dq 0 ; FirstThunk times 3 dd 0 itbl_size equ $-itbl ; Import Name Table intbl: dq symbol dq 0 ; Symbol symbol: dw 0x0294 ; [UNUSED] Function Order db 'MessageBoxW', 0 ; Function Name dll_name: db 'USER32.dll', 0 db 0 sect_size equ $-code times align($-$$,16)-($-$$) db 0 code_size equ $-code file_size equ $-$$
编译:
$ nasm -f bin -o stretch.exe -l stretch.lst stretch.asm
编译生成 stretch.exe
,可以正常运行。
这时发现,虽然 stretch.exe
是参照 tiny.exe
手动编写的,理论上应该完全相同,但是实际上两者略有区别:
$ diff =(xxd step4/tiny.exe) =(xxd step5/stretch.exe) 50c50 < 00000310: 1cff ffff 33c9 48ff 25d3 feff ffcc cccc ....3.H.%....... --- > 00000310: 1cff ffff 31c9 ff25 d4fe ffff cccc cccc ....1..%........
xor ecx, ecx
指令在 tiny.exe
中以机器码 0x33c9
表示,而在 stretch.exe
中以 0x31c9
表示。由 XOR — Logical Exclusive OR 可知,这是由于指令编码方式不同,不影响指令的效果:
$ echo 33c9 | xxd -p -r - | ndisasm -b 64 - 00000000 33C9 xor ecx,ecx $ echo 31c9 | xxd -p -r - | ndisasm -b 64 - 00000000 31C9 xor ecx,ecx
jmp [rel iatbl]
指令在 tiny.exe
中以机器码 0x48ff25d3feffff
表示,而在 stretch.exe
中以 0xff25d4feffff
表示。这两条指令的跳转地址没有区别:
$ echo 48ff25d3feffff | xxd -p -r - | ndisasm -b 64 - 00000000 48FF25D3FEFFFF jmp qword [rel 0xfffffffffffffeda] $ echo ff25d4feffff | xxd -p -r - | ndisasm -b 64 - 00000000 FF25D4FEFFFF jmp [rel 0xfffffffffffffeda]
由 Stack Overflow 上的一个回答可知,机器码中的 48
前缀表示 REX.W,会被处理器忽略。这一前缀可能与 Windows x64 的 unwind data 有关。不论如何,从运行结果上看,这一修改不影响指令的效果。
由此可知,这一步使用汇编语言编写的 stretch.exe
和上一步的 tiny.exe
是等价的。
第六步:根据对 PE 文件内部结构的分析,从汇编代码上删除所有可以直接删除的无用部分,从而减小文件大小。
删除的部分如下:
NumberOfRvaAndSizes
设置为 2itbl
后用于对齐的字节修改后的 stretch.asm
如下:
$ diff step5/stretch.asm step6/stretch.asm 26,31d25 < ; DOS Stub < times 8 dq 0 ; [UNUSED] DOS Stub < < ; Rich Header < times 8 dq 0 ; [UNUSED] Rich Header < 77c71 < dd 0x10 ; NumberOfRvaAndSizes --- > dd 0x02 ; NumberOfRvaAndSizes 84,111d77 < dd 0 ; Resource, RVA < dd 0 ; Resource, Size < dd 0 ; Exception, RVA < dd 0 ; Exception, Size < dd 0 ; Certificate, RVA < dd 0 ; Certificate, Size < dd 0 ; Base Relocation, RVA < dd 0 ; Base Relocation, Size < dd 0 ; Debug, RVA < dd 0 ; Debug, Size < dd 0 ; Architecture, RVA < dd 0 ; Architecture, Size < dd 0 ; Global Ptr, RVA < dd 0 ; Global Ptr, Size < dd 0 ; TLS, RVA < dd 0 ; TLS, Size < dd 0 ; Load Config, RVA < dd 0 ; Load Config, Size < dd 0 ; Bound Import, RVA < dd 0 ; Bound Import, Size < dd iatbl ; IAT, RVA < dd iatbl_size ; IAT, Size < dd 0 ; Delay Import Descriptor, RVA < dd 0 ; Delay Import Descriptor, Size < dd 0 ; CLR Runtime Header, RVA < dd 0 ; CLR Runtime Header, Size < dd 0 ; Reserved, RVA < dd 0 ; Reserved, Size 150,152d115 < ; Debug Table < times 24 dq 0 ; [UNUSED] Debug Table < 170,171d132 < < times 3 dd 0
编译:
$ nasm -f bin -o stretch.exe -l stretch.lst stretch.asm
编译生成 stretch.exe
,可以正常运行。
第七步:虽然在上一步中删除了所有可以直接删除的无用部分,但还有一些无用字段不可以被直接删除。这是因为 PE 文件含有多个文件头,这些无用字段位于文件头中,而文件头的格式是固定的,也就是说即使文件头中的某个字段没有被使用,它也要在文件头中占据相应的位置,所以不能直接删除。为此,可以采取重叠的方法,通过精心选取合适的重叠方式,将多个文件头重叠在一起,并保证重叠之后,每个重叠的位置最多只能对应一个有用字段。这样就可以在不破坏文件头的前提下,有效地减小文件大小。
要判断一个字段是否是有用字段,可以采用修改的方法,比如将字段的值修改为 0。如果修改以后程序出现问题,则说明该字段是有用字段,否则是无用字段。
判断出有用字段和无用字段后,可以将有用字段与无用字段相互重叠,从而减小文件大小。例如,将 PE header 的起始位置设置为 DOS header 的 0x04
处,可以将两个文件头的字段重叠。重叠时可能存在多种不同的重叠方法,本次实验只选择其中一种方法。
另外,虽然 Import table 和 DLLFuncEntry 都是有用字段,但是两者可以重叠。这是因为 Import table 只在程序加载前使用,DLLFuncEntry 只在程序加载后使用,所以并不会产生冲突。
重叠后的 stretch.asm
如下:
BITS 64 ; DOS Header dw 'MZ' ; e_magic dw 0 ; [UNUSED] e_cblp pe_hdr: ; PE Header dw 'PE' ; [UNUSED] c_cp ; Signature dw 0 ; [UNUSED] e_crlc ; Signature (Cont) ; Image File Header dw 0x8664 ; [UNUSED] e_cparhdr ; Machine code: symbol: ; Symbol dw 0x01 ; [UNUSED] e_minalloc ; NumberOfSections ; [UNUSED] Function Order db 'MessageBoxW', 0 ; Function Name times 14-($-symbol) db 0; [UNUSED] e_maxalloc ; [UNUSED] TimeDateStamp ; [UNUSED] e_ss ; [UNUSED] TimeDateStamp (Cont) ; [UNUSED] e_sp ; [UNUSED] PointerToSymbolTable ; [UNUSED] e_csum ; [UNUSED] PointerToSymbolTable (Cont) ; [UNUSED] e_ip ; [UNUSED] NumberOfSymbols ; [UNUSED] e_cs ; [UNUSED] NumberOfSymbols (Cont) dw opt_hdr_size ; [UNUSED] e_lfarlc ; SizeOfOptionalHeader dw 0x22 ; [UNUSED] e_ovno ; Characteristics opt_hdr: ; Optional Header, COFF Standard Fields dw 0x020b ; [UNUSED] e_res ; Magic (PE32+) db 0 ; [UNUSED] e_res (Cont) ; [UNUSED] MajorLinkerVersion db 0 ; [UNUSED] e_res (Cont) ; [UNUSED] MinorLinkerVersion dd code_size ; [UNUSED] e_res (Cont) ; SizeOfCode dw 0 ; [UNUSED] e_oemid ; [UNUSED] SizeOfInitializedData dw 0 ; [UNUSED] e_oeminfo ; [UNUSED] SizeOfInitializedData (Cont) dd 0 ; [UNUSED] e_res2 ; [UNUSED] SizeOfUninitializedData dd entry ; [UNUSED] e_res2 (Cont) ; AddressOfEntryPoint dd code ; [UNUSED] e_res2 (Cont) ; BaseOfCode ; Optional Header, NT Additional Fields dq 0x000140000000 ; [UNUSED] e_res2 (Cont) ; ImageBase dd pe_hdr ; e_lfanew ; [MODIFIED] SectionAlignment (0x10 -> 0x04) dd 0x04 ; [MODIFIED] FileAlignment (0x10) dw 0x06 ; [UNUSED] MajorOperatingSystemVersion dw 0 ; [UNUSED] MinorOperatingSystemVersion dw 0 ; [UNUSED] MajorImageVersion dw 0 ; [UNUSED] MinorImageVersion dw 0x06 ; MajorSubsystemVersion dw 0 ; MinorSubsystemVersion dd 0 ; [UNUSED] Reserved1 dd file_size ; SizeOfImage dd hdr_size ; SizeOfHeaders dd 0 ; [UNUSED] CheckSum dw 0x02 ; Subsystem (Windows GUI) dw 0x8160 ; DllCharacteristics dq 0x100000 ; SizeOfStackReserve dq 0x1000 ; SizeOfStackCommit dq 0x100000 ; SizeOfHeapReserve dll_name: ; DLLName db 'USER32.dll', 0 ; DLLName times 12-($-dll_name) db 0 ; [UNUSED] SizeOfHeapCommit ; [UNUSED] LoaderFlags dd 0x02 ; [MODIFIED] NumberOfRvaAndSizes (0x10) ; Optional Header, Data Directories dd 0 ; [UNUSED] Export, RVA dd 0 ; [UNUSED] Export, Size iatbl: ; Import Address Directory dd itbl ; Import, RVA ; [USEDAFTERLOAD] DLLFuncEntry dd itbl_size ; Import, Size ; [USEDAFTERLOAD] DLLFuncEntry (Cont) iatbl_size equ $-iatbl opt_hdr_size equ $-opt_hdr ; Section Table section_name db '.', 0 ; Name times 8-($-section_name) db 0 dd sect_size ; VirtualSize dd iatbl ; VirtualAddress dd code_size ; SizeOfRawData dd iatbl ; PointerToRawData content: ; Strings db 0x41,0x00,0x42,0x00,0x43,0x00,0x44,0x00 db 0x45,0x00,0x46,0x00,0x47,0x00,0,0 ; [UNUSED] PointerToRelocations ; [UNUSED] PointerToLinenumbers ; [UNUSED] NumberOfRelocations ; [UNUSED] NumberOfLinenumbers ; [UNUSED] Characteristics hdr_size equ $-$$ title: db 0x3d,0xd8,0xaf,0xdc,0x20,0x00,0x54,0x00 db 0x69,0x00,0x6e,0x00,0x79,0x00,0x50,0x00 db 0x45,0x00,0x20,0x00,0x6f,0x00,0x6e,0x00 db 0x20,0x00,0x57,0x00,0x69,0x00,0x6e,0x00 db 0x64,0x00,0x6f,0x00,0x77,0x00,0x73,0x00 db 0x20,0x00,0x31,0x00,0x30,0x00,0,0 ; Entry entry: mov r9d, 0x00240040 ; uType lea r8, [rel title] ; lpCaption lea rdx, [rel content] ; lpText xor ecx, ecx ; hWnd jmp [rel iatbl] ; MessageBoxW itbl: ; Import Directory dq intbl ; OriginalFirstThunk dd 0 ; [UNUSED] TimeDateStamp dd dll_name ; ForwarderChain dd iatbl ; Name intbl: ; Import Name Table dq symbol ; [UNUSED] FirstThunk ; Symbol dq 0 ; nullptr itbl_size equ $-itbl sect_size equ $-code code_size equ $-code file_size equ $-$$
编译:
$ nasm -f bin -o stretch.exe -l stretch.lst stretch.asm
编译生成 stretch.exe
,可以正常运行。
第八步:PE 文件中还含有五条指令的机器码,如果将这五条指令的机器码也按上一步的方法与文件头重叠,就可以进一步减小文件的大小。但是,指令的机器码太长,没有办法做到「见缝插针」,放入文件头无用字段的空隙中。为此,想到可以将五条指令拆开,并在前四条指令的后面分别加一条短跳转指令跳到下一条指令的位置,这样每条指令就可以作为一个独立的部分插入到不同的空隙中了。然而,即使这样做仍然有两条指令太长,没有办法插入空隙。这时,通过对 x64 指令集的深入学习和充分掌握,想到在这两条指令以另一个寄存器作为基址时,可以使对应的机器码更短,而指令结果不变。这样机器码也插入到了文件头无用字段的空隙中,从而进一步减小了文件大小。
在上一步骤中,能重叠的字段都已经被重叠了,而指令的机器码还没有作过变化:
$ grep -A 5 entry: stretch.lst 94 entry: 95 000000F4 41B940002400 mov r9d, 0x00240040 ; uType 96 000000FA 4C8D05C3FFFFFF lea r8, [rel title] ; lpCaption 97 00000101 488D15ACFFFFFF lea rdx, [rel content] ; lpText 98 00000108 31C9 xor ecx, ecx ; hWnd 99 0000010A FF2584FFFFFF jmp [rel iatbl] ; MessageBoxW
其中,前四条指令用于函数调用的参数传递。根据 x64 calling convention,在 Windows x64 中,函数的前四个参数分别使用 RCX、RDX、R8 和 R9 传递,只有当参数大于四个时才使用堆栈传递。MessageBoxW
的 hWnd
和 uType
两个参数的长度为 32 位而不是 64 位,所以要将 RCX 替换为 ECX、R9 替换为 R9D。第五条指令是跳转指令,为了减小 PE 文件的大小,不必处理 MessageBoxW
函数的返回值,直接使用 jmp
指令跳转到目标函数。
这五条指令的机器码作为一个整体共占 28 字节,但上一步经过一番重叠,最大的无用字段也只有 8 字节。因此,没有办法做到「见缝插针」,将机器码放入文件头无用字段的空隙中。这时,想到可以将指令拆开,然后在前四条指令的后面分别加一条短跳转指令跳到下一条指令的位置。由 JMP — Jump 可知,短跳转指令的跳转范围可以达到 –128 至 +127,而机器码只占 2 个字节,所以这种做法是可行的。
汇编指令 | 机器码长度 |
---|---|
mov r9d, 0x00240040 + jmp | 8 |
lea r8, [rel title] + jmp | 9 |
lea rdx, [rel content] + jmp | 9 |
xor ecx, ecx + jmp | 4 |
jmp [rel iatbl] | 6 |
但是,通过上表可以看出,两条 lea
指令的机器码仍占 9 个字节。而上面提到,最大的无用字段也只有 8 字节,所以对于这两条指令,仍然没办法做到「见缝插针」。
在 x64dbg 中调试时发现,当程序执行到用户代码的入口点时,RDX 寄存器的值会被设置为入口地址:
这时,通过对 x64 指令集的深入学习和充分掌握,意识到在 lea
指令中,如果以 RDX 寄存器作为基址,可以使对应的机器码更短。RDX 寄存器的值被设置为入口地址,也就是说 RDX 寄存器的值不是随机的,也就具备了作基址的条件。
在原来的程序中,以 RIP 寄存器作为基址,对应的机器码长度为 7:
汇编指令 | 机器码 | 长度 |
---|---|---|
lea r8, [rip-0x4d] | 0x4c8d05b3ffffff | 7 |
lea rdx, [rip-0x44] | 0x488d15bcffffff | 7 |
而以 RDX 寄存器作为基址时,对应的机器码长度仅为 4:
汇编指令 | 机器码 | 长度 |
---|---|---|
lea r8, [rdx-0x4d] | 0x4c8d42b3 | 4 |
lea rdx, [rdx-0x44] | 0x488d52bc | 4 |
因此,将两条lea
指令改为以 RDX 寄存器作为基址。修改后的汇编指令如下:
汇编指令 | 机器码长度 |
---|---|
mov r9d, 0x00240040 + jmp | 8 |
lea r8, [rdx+title-entry] + jmp | 6 |
lea rdx, [rdx+content-entry] + jmp | 6 |
xor ecx, ecx + jmp | 4 |
jmp [rel iatbl] | 6 |
这样就可以与无用字段重叠了。
修改后的 stretch.asm
如下:
BITS 64 ; DOS Header dw 'MZ' ; e_magic dw 0 ; [UNUSED] e_cblp pe_hdr: ; PE Header dw 'PE' ; [UNUSED] c_cp ; Signature dw 0 ; [UNUSED] e_crlc ; Signature (Cont) ; Image File Header dw 0x8664 ; [UNUSED] e_cparhdr ; Machine code: symbol: ; Symbol dw 0x01 ; [UNUSED] e_minalloc ; NumberOfSections ; [UNUSED] Function Order db 'MessageBoxW', 0 ; Function Name times 14-($-symbol) db 0; [UNUSED] e_maxalloc ; [UNUSED] TimeDateStamp ; [UNUSED] e_ss ; [UNUSED] TimeDateStamp (Cont) ; [UNUSED] e_sp ; [UNUSED] PointerToSymbolTable ; [UNUSED] e_csum ; [UNUSED] PointerToSymbolTable (Cont) ; [UNUSED] e_ip ; [UNUSED] NumberOfSymbols ; [UNUSED] e_cs ; [UNUSED] NumberOfSymbols (Cont) dw opt_hdr_size ; [UNUSED] e_lfarlc ; SizeOfOptionalHeader dw 0x22 ; [UNUSED] e_ovno ; Characteristics opt_hdr: ; Optional Header, COFF Standard Fields dw 0x020b ; [UNUSED] e_res ; Magic (PE32+) db 0 ; [UNUSED] e_res (Cont) ; [UNUSED] MajorLinkerVersion db 0 ; [UNUSED] e_res (Cont) ; [UNUSED] MinorLinkerVersion dd code_size ; [UNUSED] e_res (Cont) ; SizeOfCode code_4: ; Code Fragment 4 jmp [rel iatbl] ; MessageBoxW times 8-($-code_4) db 0 ; [UNUSED] e_oemid ; [UNUSED] SizeOfInitializedData ; [UNUSED] e_oeminfo ; [UNUSED] SizeOfInitializedData (Cont) ; [UNUSED] e_res2 ; [UNUSED] SizeOfUninitializedData dd entry ; [UNUSED] e_res2 (Cont) ; AddressOfEntryPoint dd code ; [UNUSED] e_res2 (Cont) ; BaseOfCode ; Optional Header, NT Additional Fields dq 0x000140000000 ; [UNUSED] e_res2 (Cont) ; ImageBase dd pe_hdr ; e_lfanew ; [MODIFIED] SectionAlignment (0x10 -> 0x04) dd 0x04 ; [MODIFIED] FileAlignment (0x10) code_3: ; Code Fragment 3 lea rdx, [rdx+content-entry] ; lpText jmp code_4 times 8-($-code_3) db 0 ; [UNUSED] MajorOperatingSystemVersion ; [UNUSED] MinorOperatingSystemVersion ; [UNUSED] MajorImageVersion ; [UNUSED] MinorImageVersion dw 0x06 ; MajorSubsystemVersion dw 0 ; MinorSubsystemVersion dd 0 ; [UNUSED] Reserved1 dd file_size ; SizeOfImage dd hdr_size ; SizeOfHeaders dd 0 ; [UNUSED] CheckSum dw 0x02 ; Subsystem (Windows GUI) dw 0x8160 ; DllCharacteristics dq 0x100000 ; SizeOfStackReserve dq 0x1000 ; SizeOfStackCommit dq 0x100000 ; SizeOfHeapReserve dll_name: ; DLLName db 'USER32.dll', 0 ; DLLName times 12-($-dll_name) db 0 ; [UNUSED] SizeOfHeapCommit ; [UNUSED] LoaderFlags dd 0x02 ; [MODIFIED] NumberOfRvaAndSizes (0x10) ; Optional Header, Data Directories code_2: ; Code Fragment 2 mov r9d, 0x00240040 ; uType jmp code_3 times 8-($-code_2) db 0 ; [UNUSED] Export, RVA ; [UNUSED] Export, Size iatbl: ; Import Address Directory dd itbl ; Import, RVA ; [USEDAFTERLOAD] DLLFuncEntry dd itbl_size ; Import, Size ; [USEDAFTERLOAD] DLLFuncEntry (Cont) iatbl_size equ $-iatbl opt_hdr_size equ $-opt_hdr ; Section Table section_name db '.', 0 ; Name code_1: ; Code Fragment 1 lea r8, [rdx+title-entry] ; lpCaption jmp code_2 times 8-($-section_name) db 0 dd sect_size ; VirtualSize dd iatbl ; VirtualAddress dd code_size ; SizeOfRawData dd iatbl ; PointerToRawData content: ; Strings db 0x41,0x00,0x42,0x00,0x43,0x00,0x44,0x00 db 0x45,0x00,0x46,0x00,0x47,0x00,0,0 ; [UNUSED] PointerToRelocations ; [UNUSED] PointerToLinenumbers ; [UNUSED] NumberOfRelocations ; [UNUSED] NumberOfLinenumbers ; [UNUSED] Characteristics hdr_size equ $-$$ title: db 0x3d,0xd8,0xaf,0xdc,0x20,0x00,0x54,0x00 db 0x69,0x00,0x6e,0x00,0x79,0x00,0x50,0x00 db 0x45,0x00,0x20,0x00,0x6f,0x00,0x6e,0x00 db 0x20,0x00,0x57,0x00,0x69,0x00,0x6e,0x00 db 0x64,0x00,0x6f,0x00,0x77,0x00,0x73,0x00 db 0x20,0x00,0x31,0x00,0x30,0x00,0,0 itbl: ; Import Directory dq intbl ; OriginalFirstThunk entry: ; Code Fragment 0 xor ecx, ecx ; hWnd jmp code_1 times 4-($-entry) db 0 ; [UNUSED] TimeDateStamp dd dll_name ; ForwarderChain dd iatbl ; Name intbl: ; Import Name Table dq symbol ; [UNUSED] FirstThunk ; Symbol itbl_size equ $-itbl dq 0 ; nullptr sect_size equ $-code code_size equ $-code file_size equ $-$$
编译:
$ nasm -f bin -o stretch.exe -l stretch.lst stretch.asm
编译生成 stretch.exe
,可以正常运行。
第九步:删除文件末尾的 0,因为程序加载时会在末尾自动填充零。
程序加载时会在末尾自动填充 0,因此文件末尾的 0 可以删去。
将修改后的结果保存为 stretch.asm
:
$ diff step8/stretch.asm step9/stretch.asm 113c113 < dq symbol ; [UNUSED] FirstThunk ; Symbol --- > dd symbol 115d114 < dq 0 ; nullptr
编译:
$ nasm -f bin -o stretch.exe -l stretch.lst stretch.asm
编译生成 stretch.exe
,可以正常运行:
$ xxd stretch.exe 00000000: 4d5a 0000 5045 0000 6486 0100 4d65 7373 MZ..PE..d...Mess 00000010: 6167 6542 6f78 5700 8000 2200 0b02 0000 ageBoxW..."..... 00000020: 0201 0000 ff25 6a00 0000 0000 fc00 0000 .....%j......... 00000030: 0a00 0000 0000 0040 0100 0000 0400 0000 .......@........ 00000040: 0400 0000 488d 52b8 ebda 0000 0600 0000 ....H.R......... 00000050: 0000 0000 0c01 0000 c400 0000 0000 0000 ................ 00000060: 0200 6081 0000 1000 0000 0000 0010 0000 ..`............. 00000070: 0000 0000 0000 1000 0000 0000 5553 4552 ............USER 00000080: 3332 2e64 6c6c 0000 0200 0000 41b9 4000 32.dll......A.@. 00000090: 2400 ebb0 f400 0000 1800 0000 2e00 4c8d $.............L. 000000a0: 42c8 ebe8 0201 0000 9400 0000 0201 0000 B............... 000000b0: 9400 0000 4100 4200 4300 4400 4500 4600 ....A.B.C.D.E.F. 000000c0: 4700 0000 3dd8 afdc 2000 5400 6900 6e00 G...=... .T.i.n. 000000d0: 7900 5000 4500 2000 6f00 6e00 2000 5700 y.P.E. .o.n. .W. 000000e0: 6900 6e00 6400 6f00 7700 7300 2000 3100 i.n.d.o.w.s. .1. 000000f0: 3000 0000 0801 0000 0000 0000 31c9 eb9e 0...........1... 00000100: 7c00 0000 9400 0000 0a00 0000 |...........
除文中已说明的参考资料外,本实验还参考了以下资料:
还有debug这个神兵利器。
COM格式的大小为37B,基本上就是存储Hello world!消耗的空间了。
——————————
Hello world!一共是12个字符,如果是16位程序的话,存储12个字符需要24个字节,这个估计就是最小25B的由来了。
。
——————————
然后再利用com2exe,最后得到hello.exe文件 69B
——————————————————————
这确实不是PE,这是十几年前的做法了,现在已经过时了,唉。
@王滨三体人比较擅长操纵人类社会心态吧。
“逆向主义”与“圣母思维”是人类社会必然产生的两大思想毒瘤,前者诞生于极端集权社会,后者诞生于极端民主社会。
“逆向主义”是仇恨自己所属的集体,引狼入室不惜玉石俱焚,“汉奸”是也
“圣母思维”是同情自己的敌人,幻想“与异族友好共存”“56个文明,56支花,大宇宙兄弟姐妹是一家”,忽视生存竞争的残酷,目前西方社会上这种思维正在蔓延
前者的表现是叶文杰、ETO(以及极端环境保护主义者等ETO的后备群体)
后者表现为程心和娘化的那部分人类社会群体
三体对他们的心态把握很准,话术堪称精妙
对付“逆向主义”的ETO,在ETO成员 问三体征服地球后会怎么做时,三体人如下回复
"我们会消灭人类,当然最后我会消灭你们(ETO))”(可能记忆有误)
这句话精妙之处在于后半句太切中ETO降临派心理了,看似很是啰嗦,消灭人类不就是要消灭ETO吗?但这满足了ETO降临派的复仇心理,暗示他们可以看到全人类的毁灭后笑着死在最后,夙愿得逞的感觉简直太爽了。
为什么不说“帮助皇军,好处大大滴”之类的话呢,人家ETO降临派是高尚纯粹的好嘛,这样说等于质疑他们的理想与忠诚,太羞辱了人了。反而说最后消灭你们很自然,符合三体风格,甚至让ETO以为主子也有不擅话术欺骗的弱点,害怕主子被虚伪狡诈的人类欺负,从而产生主子需要我们,我们必须为主子做点什么的动力。
ETO降临派思想类似抖m,当三体小萝莉拿起皮鞭狠狠的抽打他们身体的时候,那种欲仙欲死的滋味可以意会。这一句话,极大的培养了ETO的忠诚,为后来破壁人做了铺垫。
通过ETO养成的例子,就知道智子(三体人)为什么照顾帮助程心了
圣母养成计划呗