Step to UEFI (206)EFI 文件研究(3):显示自定义字符串

目标:对于一个已经存在,但是没有 Source Code 的 EFI Application ,通过一种方法来插入代码实现在运行时显示自定义字符串。

前面关于 EFI 文件的研究提到对于一个 EFI Application 来说,运行后会把全部内容加载到内存中。如果我们能在EFI中找到足够大的 “缝隙” ,可以将代码插其中然后通过修改EntryPoint 处加入跳转到缝隙处我们的代码执行之后再跳转会继续执行。

经过观察,我发现“MZ”标志后有一段可以使用的“缝隙”,这次试验就在这里加入代码完成。

首先遇到的问题是:我们需要插入什么样的代码来完成显示。众所周知,UEFI 下需要使用ConOut的OutputString来实现显示。当然无法手工直接编写汇编语句,于是在 \MdePkg\Library\UefiApplicationEntryPoint\ApplicationEntryPoint.c文件中,直接插入要显示的代码:

EFI_STATUS
EFIAPI
_ModuleEntryPoint (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
   SystemTable->ConOut->OutputString(SystemTable->ConOut,L"www.lab-z.com\n\r");
   
  EFI_STATUS                 Status;

  
  if (_gUefiDriverRevision != 0) {
    //
    // Make sure that the EFI/UEFI spec revision of the platform is >= EFI/UEFI spec revision of the application.
    //
    if (SystemTable->Hdr.Revision < _gUefiDriverRevision) {
      return EFI_INCOMPATIBLE_VERSION;
    }
  }
…….省略……

我们通过在编译过程中插入 /FAsc /Od 指令的方法强制生成汇编代码。查看上面代码生成的内容如下:

$LN25:
  00000	48 89 5c 24 08	 mov	 QWORD PTR [rsp+8], rbx
  00005	57		 push	 rdi
  00006	48 83 ec 20	 sub	 rsp, 32			; 00000020H
; 46   :   
; 47   :     SystemTable->ConOut->OutputString(SystemTable->ConOut,L"www.lab-z.com\n\r");

  0000a	48 8b 42 40	 mov	 rax, QWORD PTR [rdx+64]
  0000e	48 8b da	 mov	 rbx, rdx
  00011	48 8b f9	 mov	 rdi, rcx
  00014	48 8d 15 00 00
	00 00		 lea	 rdx, OFFSET FLAT:??_C@_1CA@KGEHCEOJ@?$AAw?$AAw?$AAw?$AA?4?$AAl?$AAa?$AAb?$AA?9?$AAz?$AA?4?$AAc?$AAo?$AAm?$AA?6?$AA?$AN?$AA?$AA@
  0001b	48 8b c8	 mov	 rcx, rax
  0001e	ff 50 08	 call	 QWORD PTR [rax+8]
; File c:\buildbs\201903\mdepkg\library\uefibootservicestablelib\uefibootservicestablelib.c

最开始处标记的代码编译器生成的,不对应任何C语句。其中的 lea rdx,Offset 部分没有在编译阶段生成,所以为 00。从上面可以看出来,UEFI 调用Application 之后,ImageHandle 作为第一个参数放在RCX中,RDX中存放的第二个参数是指向SystemTable的指针。

接下来,我们修改Application 的 EntryPoint 为跳转,即从 0x2C0跳转到0x002。为了简单起见,使用NASM设计代码如下:

[BITS 64]
org 2C0h
jmp 0x2

Nasm 编译后再反编译,获得机器码如下:

C:\NASM>ndisasm -b 64 inst
00000000  E93DFDFFFF        jmp qword 0xfffffffffffffd42

接下来我们需要恢复现场,去掉前面ApplicationEntryPoint.c 插入的代码后再重新编译检查EFI中需要替换的代码。同样查看生成的ApplicationEntryPoint.cod文件:

_ModuleEntryPoint PROC					; COMDAT
; File c:\buildbs\201903\mdepkg\library\uefibootservicestablelib\uefibootservicestablelib.c

; 62   :   gBS = SystemTable->BootServices;

  00000	48 8b 42 60	 mov	 rax, QWORD PTR [rdx+96]
  00004	48 89 05 00 00
	00 00		 mov	 QWORD PTR gBS, rax
; File c:\buildbs\201903\mdepkg\library\uefiruntimeservicestablelib\uefiruntimeservicestablelib.c

; 46   :   gRT = SystemTable->RuntimeServices;

  0000b	48 8b 42 58	 mov	 rax, QWORD PTR [rdx+88]
  0000f	48 89 05 00 00
	00 00		 mov	 QWORD PTR gRT, rax
; File c:\buildbs\201903\mdepkg\library\uefibootservicestablelib\uefibootservicestablelib.c

; 50   :   gImageHandle = ImageHandle;

  00016	48 89 0d 00 00
	00 00		 mov	 QWORD PTR gImageHandle, rcx

就是说,我们需要将原来在 0x2C0处的值替换成跳转的代码 E9 7D FD FF FF 。原来处于0x2C0处的机器可以从COD文件中看到(特别注意,我们打开 EFI看到0x2C0处的机器是 48 8B 42 60 48 89 05 45 1B 00 00,这是OBJ文件在Link 之后的结果,和CL.EXE 直接生成的有所差别)。

之后,我们需要跳转到它的下一条语句(mov QWORD PTR gBS, rax这个的下一条)在 0x2C0+ 0xB。

有了上面的经验接下来设计位于 0x02 处的代码:

BITS 64
DEFAULT REL
nop
nop     ;还可以使用ORG 2 设定编译的起始偏移,这里使用NOP 是为了便于反编译观察
;EFI文件之前EnteryPoint处的动作我们需要重新做一次
DB 0x48,0x8b,0x42,0x60        ; mov rax, QWORD PTR [rdx+96]
DB 0x48,0x89,0x05,0x03,0x1E,0x00,0x00   ;mov QWORD PTR gBS, rax

push rdi ;为了保证OutputString()正常工作,需要多Push 8Bytes
push rcx
push rdx
sub rsp,80 ;从实践上来看,下面的函数会损坏堆栈
mov rax,[dword rdx+64] ;SystemTable->ConOut
lea rdx,[MyString]
mov rcx,rax
call [rax+8]   ;Call SystemTable->ConOut->OutputString()
add rsp,80
pop  rdx
pop  rcx
pop  rdi

jmp 0x2CB

MyString:
    db "www.lab-z.com",0

直接使用 Nasm inst.asm 即可编译,编译后再使用 ndisasm -b 64 inst反编译查看:

可以看到反编译后,前面的指令都是能够和汇编代码对应上的。接下来就是直接将上面的代码写入从 0x02开始的EFI文件中。

之后可以在 NT32模拟器上运行,同样也可以在实体机上运行:

接下来介绍解决这个问题的时候遇到的各种坑:

1.0x3C 处的0xB8,这是属于Dos Header 上的 E_lfanew ,给出了 PE Header 的起始位置,这里是不可以覆盖的,否则会产生 Load Error。如果你的代码很大,有可能会不小心覆盖掉,这样会导致EFI Image 加载错误。这也是为什么这次试验字符串放置在0x40的原因;

2.代码起始处的 push rdi , 不一定是RDI,任何8Bytes的寄存器都可以,但是如果没有这语句在调用ConOut的时候这个函数内部会发生错误;

3.实践发现在调用 SystemTable->ConOut 的时候会损坏堆栈,比如下面的调用方式(按道理2个参数的情况下是通过寄存器来进行参数传递的):

push rdx 
 Call ConOut
pop rdx 

执行后会导致 rdx 内容的损坏。因此,这里采用先做PUSH,之后  sub rsp,80 把堆栈指针移开这样的操作来避免堆栈内容的损坏。

4.修改之前的 EFI文件EntryPoint 处的 48 89 05 45 1B 00 00 (mov QWORD PTR gBS, rax),这是一个相对的操作,搬移到其他地址之后需要重新修正。比如,之前这个指令位于 0x2C4:

实际访问到的位置在 0x2C4 + 7 (这个指令的长度) + 0x1B45=0x1E10;当这个指令被移动到0x006 处执行时,需要重新计算其中的偏移: 0x1E10-7-0x006=0x1E03,所以对应机器码如下:

上述使用【参考1】提供的反编译工具。此外,还有 ConOut 中字符串偏移的计算方法与此类似,有兴趣的朋友可以手工试验。

上面的问题都可以使用之前提到的动态调试方法进行查看验证,特别是一些偏移计算上,如果不是很确定可以通过 UltraEdit 进行编辑不断试验和调整。

本文提到的Hello.EFI 原始文件和修改后的文件下载:

参考:

1. https://defuse.ca/online-x86-assembler.htm#disassembly2

《Step to UEFI (206)EFI 文件研究(3):显示自定义字符串》有2个想法

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注