kernel panik icon

How I debug bootaa64.efi with gdb and IDA


Result

What are these things?

Feel free to skip this section if you have already know. Thank you :)

Windows, like all other OSes, need to be loaded by a bootloader. On x86 system, if your computer use BIOS, it will look for the MBR on your disk, load it and then run it, then the MBR performs it tasks to load the OS. If you have UEFI, it will look for EFI file in the folder EFI\BOOT\ in your EFI partition. On Windows x86(_64), this file is called bootx64.efi.

On ARM64 (aka aarch64) platform, this file is called bootaa64.efi. It does exactly the same work: load and run Windows, but on ARM64 platform.

TL;DR: bootaa64.efi is Windows EFI Boot Loader for ARM64 platform.

Background

I'm trying to run Windows ARM64 on my phone using KVM. The phone has Linux kernel version 3.18.91, and the KVM module on that kernel lacks (most of) the things Windows ARM64 requires to works. It just hangs when I run it on KVM (hang with blackscreen and absolutely no error message. That's why I have to find where it stucks by debugging.

Why not GDB only? Or WinDBG?

Each executable file on Windows is links with the corresponding .pdb file (Program Debug Database) on the Windows Debug Symbol Server. It provides debug symbols (eg function names, variable names, class names, structures,...) to make it easier to debug and read the program's disassemblied code.

GDB cannot load .pdb files, which makes it hard to read and debug assembly instructions with naked eyes (really awful if you can imagine that)

WinDBG is used to debug the NT kernel and things after the kernel has been started up, and we need to enable debug in the bootloader. But we are debugging the bootloader, it doesn't work, how can we boot to it to enable it ? :D WinDBG can also be used for debugging EFI, but it needs to be used with Intel UDK (UEFI Development Kit). And guess what, that thing only works on x86(_64) platform :)

Using IDA with GDB backend might be the best option I can think of. IDA loads .pdb files, make graphs,... and connect to GDB to debug. In fact, you can also use things like Ghidra or radare, but I haven't tried it yet.

Environment

QEMU with IDA is just enough for the tools. I run QEMU on my phone, so I can try both TCG (Tiny Code Generator, pure software emulation) and KVM (hardware virtualization)

You should have the EDK2 debug image, which provides you verbose logs. You can compile it by your own, or you can get it from precompiled site here. REMEMBER TO GET THE AARCH64 DEBUG VERSION!!!

If you download the precompiled firmware from the above site, or you compile it by your own, you might need to resize the firmware to exact 64MB to fit QEMU's flash size, or it will complain. To make a file exactly 64MB, you can try:

    dd if=/dev/zero bs=1M count=64 of=output.img
dd if=input.img bs=1M of=output.img conv=notrunc
    

with input.img is the input file and output.img is the output file with the size of 64MB.

Or, you can download my resized image built on my own here :)

And of course, you need your bootaa64.efi (or any other EFI files you want to debug). Put it in a FAT32 partition to be able to load it from EDK2.

Something to note about TCG debugging vs KVM debugging

TCG is fully software emulation. Breakpoints should work on it normally

But KVM is a different story. Since it uses hardware virtualization, so to use breakpoint, we need to set hardware breakpoint. Support for hardware breakpoint on KVM on ARM64 is initialized here (maybe?) If your kernel doesn't support guest debugging on KVM, breakpoints might not work and QEMU will complain about guest debug not supported on this kernel (like my kernel :D). In that case, single stepping and breakpoints might not work until you patch the kernel. The alternative (but quick and dirty way) is to catch where the code stuck using debugger, then patch the binary, then copy it again to the drive, then run it again. That's not efficient but at least it works :)

Let's debugging

1. Load the file bootaa64.efi to IDA64

Remember to allow IDA to download pdb file from the debug server. After that, you will get this:

step 1

This file will contain some MRS and MSR instructions. IDA will not decode it for you to a human-readable form:

awful register encoding

You can use the script highlight_arm_system_insn.py from this repo. After that, it will be much better:

ah, much better version

Now it's good to go!

2. Setup QEMU with gdb port open

You need to install QEMU in your system. How you do this is your own business, there are plenty of instructions from the Internet. If you just want to use TCG (pure emulation), then you can run QEMU anywhere you want, not just arm64 hardware with KVM support.

Here is the command line I used for QEMU:

The TCG (pure emulation) version:

    # TCG version (pure emulation)
qemu-system-aarch64 -M virt,virtualization=on -cpu cortex-a53 -smp 1 -m 1024 -pflash flash0.img -pflash flash1.img -device qemu-xhci -device usb-mouse -device usb-tablet -device usb-kbd -device ramfb -drive if=none,format=vpc,id=iso,media=disk,file=/storage/7AB4-F25D/22000.1_PROFESSIONAL_ARM64_EN-US.vhd -device usb-storage,drive=iso -s -S -vnc :1 -serial stdio

# KVM version (hardware virtualization)
qemu-system-aarch64 -M virt -cpu cortex-a53 --enable-kvm -smp 2 -m 1024 -pflash flash0.img -pflash flash1.img -device qemu-xhci -device usb-mouse -device usb-tablet -device usb-kbd -device ramfb -drive if=none,format=vpc,id=iso,media=disk,file=/storage/7AB4-F25D/22000.1_PROFESSIONAL_ARM64_EN-US.vhd -device usb-storage,drive=iso -s -S -vnc :1 -serial stdio
    

Some note about the command line:

- flash0.img and flash1.img are QEMU_EFI.fd and QEMU_VARS.fd but resized to 64MB. They are from my Google Drive folder above.

- For the easy of modifcation, I copied the Windows ARM64 ISO to a VHD using Rufus, then put it on a SD card and plug it into my phone. You can boot from other disk image, for example ISO, or even raw disk if you want to. Just remember to change the field format, media (disk for emulating hard disk and cdrom for emulating CD-ROM deivce) and file path.

QEMU will pause on run to wait for start signal from IDA (or GDB client). QEMU will output the serial log (which will contain the output of EDK2 log) to stdio (aka the terminal you are running the command from), and open port 5901 for guest VNC framebuffer (feel free to change if you have that port already used), and port 1234 for GDB (also feel free to change, by remove -s option and add -gdb yourport, replace yourport with the port you would like to use)

3. Connect IDA to the GDB server

Go to IDA, click the Debugger menu and click Select Debugger. Alternatively you can press F9

Debugger option in the menu

A menu will appear

Select debugger menu

Select Remote GDB debugger. Then go to Debugger -> Process options...

Process options menu

Here, set the port to 1234 and the IP to the IP of the machine that is used to run QEMU

Network options

4. Pre-debugging

This may sound unusual, but you need to do it.

In the first run, DON'T set any breakpoint. Just start QEMU (with the above command line), then connect the VNC client to port 5901, then press F9 in IDA to start the debugger. When you see a question box like this:

Confirm attach to remote debug

Click Yes to attach to QEMU's gdbstub. Then IDA will stop at this screen:

EDK2 startup

(If you wonder what this code is, then it's QEMU's initialize code when it start. Remember we tell QEMU to pause on launch with -S?)

Now press F9 again to start QEMU then quickly return to your VNC screen on port 5901.

When you see this EDK2 screen with the text Start boot option, please quickly and repeatedly press ESC to tell EDK2 to go to boot menu (or you might have to wait for iPXE to timeout):

EDK2 boot menu

Go to Boot Maintenance Manager -> Boot From File, then select the EFI file you want to debug from File Explorer, then press Enter to load it.

Note: In some cases, especially when you debugging bootaa64.efi, you need to press some key to force the EFI to load, or it will return to the File Explorer screen

Now quickly return to IDA and press the Pause button to pause the program. In the console (or whatever the serial is output to), you will see some log like this:

    [Bds]Booting bootaa64.efi
FSOpen: Open '\efi\boot\bootaa64.efi' Success
[Bds] Expand PciRoot(0x0)/Pci(0x2,0x0)/USB(0x7,0x0)/USB(0x0,0x0)/HD(1,GPT,25669F20-B036-4F50-BFDE-9F52D29783C5,0x800,0x9FF7DF)/\efi\boot\bootaa64.efi -> PciRoot(0x0)/Pci(0x2,0x0)/USB(0x7,0x0)/USB(0x0,0x0)/HD(1,GPT,25669F20-B036-4F50-BFDE-9F52D29783C5,0x800,0x9FF7DF)/\efi\boot\bootaa64.efi
[Security] 3rd party image[0] can be loaded after EndOfDxe: PciRoot(0x0)/Pci(0x2,0x0)/USB(0x7,0x0)/USB(0x0,0x0)/HD(1,GPT,25669F20-B036-4F50-BFDE-9F52D29783C5,0x800,0x9FF7DF)/\efi\boot\bootaa64.efi.
InstallProtocolInterface: 5B1B31A1-9562-11D2-8E3F-00A0C969723B 7CEB6D40
ConvertPages: failed to find range 10000000 - 101EDFFF
add-symbol-file bootmgfw.pdb 0x7BADC400
Loading driver at 0x0007BADC000 EntryPoint=0x0007BAFEC90 bootmgfw.efi
InstallProtocolInterface: BC62157E-3E33-4FEC-9920-2D3B36D750DF 7EAD2498
ProtectUefiImageCommon - 0x7CEB6D40
  - 0x000000007BADC000 - 0x00000000001EE000
SetUefiImageMemoryAttributes - 0x000000007BADC000 - 0x0000000000001000 (0x0000000000004008)
SetUefiImageMemoryAttributes - 0x000000007BADD000 - 0x000000000018E000 (0x0000000000020008)
SetUefiImageMemoryAttributes - 0x000000007BC6B000 - 0x0000000000001000 (0x0000000000020008)
SetUefiImageMemoryAttributes - 0x000000007BC6C000 - 0x000000000005E000 (0x0000000000004008)
ConvertPages: failed to find range 102000 - 102FFF
    

You can notice that our entry point is at the address 0x0007BAFEC90 in the memory. The entry point can be reached in IDA by pressing g and then enter the name EfiEntry. You will then notice that our EfiEntry is at a different address than the real EntryPoint address in QEMU:

EntryPoint address before rebase

We now need to rebase the EFI to match that real address. In the IDAPython's console, you can calculate the delta between two address:

    Python>0x7BAFEC90 - 0x10022C90
0x6badc000
    

Now open the rebase menu by clicking on Edit -> Segments -> Rebase program...:

rebase menu

Now select Shift delta, then enter the delta value, in this case 0x6badc000, then press OK:

rebase

Wait for IDA to rebase a little bit until it has done.

5. Debugging

Set a breakpoint at the location you want. I will set a breakpoint at EfiEntry. Then repeat the exact same process in the section 4. Pre-debugging, from the begining until you press Enter to load bootaa64.efi from File Explorer.

When your EFI is loaded, IDA should popup and pause at the breakpoint:

Result

6. Some more notes

Your code might be stuck as an exception handler. Even worse, some exception handler handle nothing, it's just a forever loop. For example the BlKernelSp0SystemErrorHandler and BlKernelExceptionHandler:

Exception handler

So if your code stuck at this kind of exception handler, you will have to find out what cause the exception. To do this, open the System Register watcher by clicking on Debugger -> Debugger windows -> org.qemu.gdb.arm.sys.regs

Open system register watcher

Then find the register ELR_EL1, which holds the address to return from exception. The value on this register is actually the address of the code that cause the exception:

System register watcher

Copy that address, then go to it by return to IDA View and press g (remember to add the 0x prefix). You should see the code that cause that exception. In my case, the exception was caused by a BRK #0xF004 instruction, which is an instruction to trigger debug breakpoint:

BRK instruction

You should further inspect the control flow to see why it goes there.

As usual, you can post any comment here

Thank you for viewing my post!