【UEFI】编写x64极小EFI程序
# 前言
前段时间有个群友想玩EFI,这个人我不好用准确的方式去描述,容易被当做人身攻击,懂的自然懂。
总之呢,他要一个最简EFI程序,那就开整吧。
# 构造文件头
EFI文件一般走PE文件标准,我们在构造文件的时候要搞出来以下内容:
1. DOS头
2. NT头
3. 节表
为了抠字节数,我们用汇编来构造。汇编器选用NASM,非常便携。
## 构造DOS头
DOS头只需要管`e_magic`和`e_lfanew`两个成员即可,前者填`MZ`这两个字符,后者填充NT头相对于映像基址的偏移即可,代码如下:
```Assembly
; DOS Header
dw 'MZ' ; e_magic
dw 0 ; e_cblp
dw 0 ; c_cp
dw 0 ; e_crlc
dw 0 ; e_cparhdr
dw 0 ; e_minalloc
dw 0 ; e_maxalloc
dw 0 ; e_ss
dw 0 ; e_sp
dw 0 ; e_csum
dw 0 ; e_ip
dw 0 ; e_cs
dw 0 ; e_lfarlc
dw 0 ; e_ovno
times 4 dw 0 ; e_res
dw 0 ; e_oemid
dw 0 ; e_oeminfo
times 10 dw 0 ; e_res2
dd pe_hdr ; e_lfanew
```
## 构造NT头
一般来说DOS头和NT头之间隔着一个DOS Stub用于存放在MSDOS下执行的代码。这里直接阉割了,让NT头紧贴DOS头。
NT头分三个部分:签名、文件头、可选头。
签名占4个字节,前两个字节是`PE`,后两个字节为零。
文件头里值得填充的内容有:处理器类型、有几个节、可选头有多大、PE文件属性。
处理器类型填`0x8664`,也就是`IMAGE_FILE_MACHINE_AMD64`。这里顺便给大家看个图乐呵乐呵:
为了压缩文件,我们在PE文件里只留一个节即可。
可选头大小这一条我们通过汇编器的宏来推断即可。
至于PE文件属性,我们要标明它是可执行映像、并且能处理大于2GB内存的程序,所以填0x22,即`IMAGE_FILE_EXECUTABLE_IMAGE|IMAGE_FILE_LARGE_ADDRESS_AWARE`。
可选头本身其实是不可选的,必须要填好。值得关注的填就行了,别的就全部清零。
首先是魔数,PE32+文件直接填`0x20B`就好。
然后是代码大小,这玩意让汇编器去推算即可。
然后是入口点。这里要注意加载器会为了内存对齐而转移节的位置。因此不可以让汇编器推算入口点的地址。我们直接把代码放在节的开头上,因此指定页对齐粒度`0x1000`即可。
至于代码基地址,填代码段的基地址就好。这里也填`0x1000`。
代码的内存基地址,这里可以填零,但我选择照着链接器的默认习惯填`0x140000000`。u1s1,PE加载器,尤其是EFI的PE加载器基本上不可能真的把映像加载到那里去。
然后是节对齐边界。这个东西嘛,虽然文档上说可以小于页边界,但是给不给加载就是另外一回事了。VMware的EFI固件不支持。
接着是文件对齐边界。虽然文档上说不可以小于512,实际上意义不大。一般不会闲着蛋疼真的去检查节表里的文件偏移是不是没对齐。我们用最小可能的1字节对齐,填`0x1`。
然后是映像大小。这里占了两个页,因此填`0x2000`。
文件头大小这块,让汇编器自己去推算。
重头是PE文件子系统这块,我们是写EFI应用程序,因此这里要填`IMAGE_SUBSYSTEM_EFI_APPLICATION`,即`0xA`。
而DLL属性这块,我们填以下内容进去:
`IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA`,映像支持高熵地址,其实就是随机地址的意思,但是同时这个地址很“混乱”。高熵地址通常是作为溢出攻击的最后一道保险用的。
`IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE`,映像基址可以动态选择,意味着代码要支持重定向。如果不支持,那基本上EFI固件不可能加载我们的代码了,因为那个地址未必可用。但由于我们不想为了这个再去专门写个重定向表,因此就要避免在代码里引用绝对地址。一切地址引用全部以相对地址来实现。
`IMAGE_DLLCHARACTERISTICS_NX_COMPAT`,映像与NX机制兼容,意味着数据区不可执行。笑死,我们最小PE没有数据区。
汇总起来,我们要填`0x160`进去。
最后填数据目录数量,这个是定死的16个,即使我们一个都用不到。
综上所述,上代码:
```Assembly
; PE Header
pe_hdr:
dw 'PE', 0 ; Signature
; Image File Header
dw 0x8664 ; Machine
dw 0x01 ; NumberOfSections
dd 0 ; 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 0x00 ; MajorLinkerVersion
db 0x00 ; MinorLinkerVersion
dd code_size ; SizeOfCode
dd 0 ; SizeOfInitializedData
dd 0 ; SizeOfUninitializedData
dd 0x1000 ; AddressOfEntryPoint
dd 0x1000 ; BaseOfCode
; Optional Header, NT Additional Fields
dq 0x140000000 ; ImageBase
dd 0x1000 ; SectionAlignment
dd 0x1 ; FileAlignment
dw 0x00 ; MajorOperatingSystemVersion
dw 0 ; MinorOperatingSystemVersion
dw 0 ; MajorImageVersion
dw 0 ; MinorImageVersion
dw 0x00 ; MajorSubsystemVersion
dw 0 ; MinorSubsystemVersion
dd 0 ; Reserved1
dd 0x2000 ; SizeOfImage
dd hdr_size ; SizeOfHeaders
dd 0 ; CheckSum
dw 0x0A ; Subsystem (EFI APPLICATION)
dw 0x160 ; 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 0 ; Import, RVA
dd 0 ; 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 0 ; IAT, RVA
dd 0 ; 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
```
## 构造节表
由于我们只留一个节,因此填充一个节头即可。
首先是节名,虽然你爱填啥填啥,但我们出于习惯,填个`.text`进去。
节的大小让汇编器去推断。
节的内存基址偏移直接按最小对齐地址,填0x1000。
节的文件基址偏移让汇编器去推断。
至于节的属性,有以下几点:
`IMAGE_SCN_CNT_CODE`,这个节包含代码。
`IMAGE_SCN_MEM_EXECUTE`,这个节所在内存应该要可执行。
`IMAGE_SCN_MEM_READ`,这个节所在的内存应该要可读。
建议不要包含`IMAGE_SCN_CNT_INITIALIZED_DATA`,虽然你这么做会表示你很诚实,但是加载器可能会出于安全考虑而不予加载。
代码如下:
```Assembly
; Section Table
section_name db '.text' ; Name
times 8-($-section_name) db 0
dd sect_size ; VirtualSize
dd 4096 ; VirtualAddress
dd code_size ; SizeOfRawData
dd code ; PointerToRawData
dd 0 ; PointerToRelocations
dd 0 ; PointerToLinenumbers
dw 0 ; NumberOfRelocations
dw 0 ; NumberOfLinenumbers
dd 0x68000020 ; Characteristics
hdr_size equ $-$$
```
# 构造代码
文件头终于构造好了,现在到了构造代码的时候了。
我们构造的EFI程序用C语言表达就是:
```C
#include <Uefi.h>
EFI_STATUS EfiMain(IN EFI_HANDLE ImageHandle,IN EFI_SYSTEM_TABLE *SystemTable)
{
return SystemTable->ConOut->OutputString(SystemTable->ConOut,L"Hello UEFI!\r\n");
}
```
根据微软x64调用约定,第一个参数是`rcx`寄存器,第二个参数是`rdx`寄存器,那么我们要引用`SystemTable`参数中的`ConOut->OutputString`函数。
那么根据EFI的文档推算,在x64下:
`ConOut`成员在`EFI_SYSTEM_TABLE`结构体中的偏移为64。
`OutputString`成员相对`EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL`的偏移是8。
我们就应该写这样的汇编代码:
```Assembly
mov rcx,qword ; ConOut=SystemTable->ConOut
mov rdx,hello_text
call ; return ConOut->OutputString(ConOut,hello_text)
ret
```
然而这里存在问题。我们构造的代码目标是raw,因此要汇编器推算地址的时候不会计算映像偏移,因此`mov rdx,hello_text`指令会移动一个立即数过去,这个立即数就是`hello_text`这个label在文件中的偏移了。
因此我们要改变策略,必须使用相对地址。因此`mov rdx,hello_text`要改成`lea rdx,`。
最后要注意的是我们必须输出unicode字符串,因此要用NASM的unicode相关宏来描述我们要输出的字符串。
代码如下:
```Assembly
code:
mov rcx,qword ; ConOut=SystemTable->ConOut
lea rdx,
call ; return ConOut->OutputString(ConOut,hello_text)
ret
hello_text:
dw __?utf16?__("Hello UEFI!"),13,10,0
```
# 收尾
最后,由于前面为了让汇编器自动推算文件大小来填充文件头,需要在文件结尾声明常量:
```Assembly
sect_size equ $-code
code_size equ $-code
```
至此,最小EFI文件构造完毕。它的大小是:
嗯,411字节,还行。
最后再看看VMware里的运行效果:
他还说想要用C语言写的。我觉得为了缩短篇幅还是算了吧,反正再说了他又不会真的去玩。
另外我怀疑这货没有论坛账号,让他注册个账号再下载代码吧。
**** Hidden Message *****
修正一下,汇编代码还有一处地方可以做优化。
那就是`call`+`ret`指令可以优化为`jmp`指令。这样可以再减少一个字节的输出。
有点意思的.....;P 好6——
(好奇为什么 efi 程序使用 pe 文件头呢.. 是不是和微软有蜜汁妥协 楼主大能,感谢感谢 看弹幕流什么时候来这个帖子回帖 用NV的泄露证书给EFI文件签名,能否让你的BIN在开启了SB的真机上运行?
我只知道这证书泄露不久之后,就被微软的更新包给拉黑了。 第一次知道 NASM 竟然还有 __?utf16?__ 这种东西
Golden Blonde 发表于 2022-10-6 21:53
用NV的泄露证书给EFI文件签名,能否让你的BIN在开启了SB的真机上运行?
我只知道这证书泄露不久之后,就被 ...
自己造个签名扔进自己机器上的数据库就行了。
如果想要在任意开启SB的机器上跑,需要签EV并且还要找微软给你签,因为一般固件的数据库里都有微软的签名。 tangptr@126.com 发表于 2022-10-7 22:53
自己造个签名扔进自己机器上的数据库就行了。
如果想要在任意开启SB的机器上跑,需要签EV并且还要找微软 ...
懂了,就等哪天微软的签名漏了。
PS:注意一下8和9。8. Microsoft will not sign EFI submissions that use EFI_IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER. Instead, we recommend transitioning to EFI_IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER. This prevents unnecessary use of runtime EFI drivers.
9. Use of EFI Byte Code (EBC): Microsoft will not sign EFI submissions that are EBC-based submissions. Golden Blonde 发表于 2022-10-10 14:17
懂了,就等哪天微软的签名漏了。
PS:注意一下8和9。
8的话确实没啥好办法,最多塞白名单。
9的话你见过几个EBC的玩意,编译器都难找。。。
页:
[1]