找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 3202|回复: 10

【UEFI】编写x64极小EFI程序

[复制链接]
发表于 2022-10-2 11:36:03 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×

前言

前段时间有个群友想玩EFI,这个人我不好用准确的方式去描述,容易被当做人身攻击,懂的自然懂。
总之呢,他要一个最简EFI程序,那就开整吧。

构造文件头

EFI文件一般走PE文件标准,我们在构造文件的时候要搞出来以下内容:

  1. DOS头
  2. NT头
  3. 节表

为了抠字节数,我们用汇编来构造。汇编器选用NASM,非常便携。

构造DOS头

DOS头只需要管e_magice_lfanew两个成员即可,前者填MZ这两个字符,后者填充NT头相对于映像基址的偏移即可,代码如下:

; 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

构造NT头

一般来说DOS头和NT头之间隔着一个DOS Stub用于存放在MSDOS下执行的代码。这里直接阉割了,让NT头紧贴DOS头。
NT头分三个部分:签名、文件头、可选头。
签名占4个字节,前两个字节是PE,后两个字节为零。
文件头里值得填充的内容有:处理器类型、有几个节、可选头有多大、PE文件属性。
处理器类型填0x8664,也就是IMAGE_FILE_MACHINE_AMD64。这里顺便给大家看个图乐呵乐呵:


Capture.JPG

为了压缩文件,我们在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个,即使我们一个都用不到。

综上所述,上代码:

; 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 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,虽然你这么做会表示你很诚实,但是加载器可能会出于安全考虑而不予加载。

代码如下:

; 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语言表达就是:

#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。
我们就应该写这样的汇编代码:

mov rcx,qword [rdx+64]  ; ConOut=SystemTable->ConOut
mov rdx,hello_text
call [rcx+8]            ; return ConOut->OutputString(ConOut,hello_text)
ret

然而这里存在问题。我们构造的代码目标是raw,因此要汇编器推算地址的时候不会计算映像偏移,因此mov rdx,hello_text指令会移动一个立即数过去,这个立即数就是hello_text这个label在文件中的偏移了。
因此我们要改变策略,必须使用相对地址。因此mov rdx,hello_text要改成lea rdx,[rel hello_text]
最后要注意的是我们必须输出unicode字符串,因此要用NASM的unicode相关宏来描述我们要输出的字符串。
代码如下:

code:
    mov rcx,qword [rdx+64]  ; ConOut=SystemTable->ConOut
    lea rdx,[rel hello_text]
    call [rcx+8]            ; return ConOut->OutputString(ConOut,hello_text)
    ret

hello_text:
    dw __?utf16?__("Hello UEFI!"),13,10,0

收尾

最后,由于前面为了让汇编器自动推算文件大小来填充文件头,需要在文件结尾声明常量:

sect_size equ $-code
code_size equ $-code

至此,最小EFI文件构造完毕。它的大小是:


Capture2.JPG

嗯,411字节,还行。
最后再看看VMware里的运行效果:


General UEFI Development-2022-10-01-23-29-53.png

他还说想要用C语言写的。我觉得为了缩短篇幅还是算了吧,反正再说了他又不会真的去玩。
另外我怀疑这货没有论坛账号,让他注册个账号再下载代码吧。


游客,如果您要查看本帖隐藏内容请回复
回复

使用道具 举报

 楼主| 发表于 2022-10-10 03:05:27 | 显示全部楼层

修正一下,汇编代码还有一处地方可以做优化。
那就是call+ret指令可以优化为jmp指令。这样可以再减少一个字节的输出。

回复 赞! 靠!

使用道具 举报

发表于 2022-10-2 18:31:19 | 显示全部楼层
有点意思的.....
回复 赞! 靠!

使用道具 举报

发表于 2022-10-3 08:57:20 | 显示全部楼层
好6——
(好奇为什么 efi 程序使用 pe 文件头呢.. 是不是和微软有蜜汁妥协
回复 赞! 靠!

使用道具 举报

发表于 2022-10-4 08:38:12 | 显示全部楼层
楼主大能,感谢感谢
回复 赞! 靠!

使用道具 举报

发表于 2022-10-5 10:22:25 | 显示全部楼层
看弹幕流什么时候来这个帖子回帖
回复 赞! 靠!

使用道具 举报

发表于 2022-10-6 21:53:52 | 显示全部楼层
用NV的泄露证书给EFI文件签名,能否让你的BIN在开启了SB的真机上运行?

我只知道这证书泄露不久之后,就被微软的更新包给拉黑了。
回复 赞! 靠!

使用道具 举报

发表于 2022-10-7 18:41:46 | 显示全部楼层
第一次知道 NASM 竟然还有 __?utf16?__ 这种东西
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 2022-10-7 22:53:45 | 显示全部楼层
Golden Blonde 发表于 2022-10-6 21:53
用NV的泄露证书给EFI文件签名,能否让你的BIN在开启了SB的真机上运行?

我只知道这证书泄露不久之后,就被 ...

自己造个签名扔进自己机器上的数据库就行了。
如果想要在任意开启SB的机器上跑,需要签EV并且还要找微软给你签,因为一般固件的数据库里都有微软的签名。
回复 赞! 靠!

使用道具 举报

发表于 2022-10-10 14:17:49 | 显示全部楼层
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.
回复 赞! 靠!

使用道具 举报

 楼主| 发表于 2022-10-10 23:48:13 | 显示全部楼层
Golden Blonde 发表于 2022-10-10 14:17
懂了,就等哪天微软的签名漏了。

PS:注意一下8和9。

8的话确实没啥好办法,最多塞白名单。
9的话你见过几个EBC的玩意,编译器都难找。。。
回复 赞! 靠!

使用道具 举报

本版积分规则

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-12-26 23:12 , Processed in 0.042991 second(s), 29 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

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