Write-up by loulous24, under CC-BY-NC-SA 4.0
“Sésame, ouvre-toi”, “Open sesame!” in English, is a series of two hardware challenges of the FCSC 2024. Let’s take a look at the second challenge.
Looking at the door
I worked last year with a submarine bootloader, see it here if you want to have an introduction to bootloaders with another challenge.
After launching the first challenge, it appears that U-Boot is used as the bootloader.
U-Boot 2023.07.02 (Jul 11 2023 - 15:20:44 +0000)
DRAM: 24 MiB
Core: 41 devices, 10 uclasses, devicetree: board
Loading Environment from nowhere... OK
In: pl011@9000000
Out: pl011@9000000
Err: pl011@9000000
Autoboot in 10 seconds
After 10 seconds, something is booted.
## Booting kernel from Legacy Image at 40200000 ...
Image Name: EFI Shell
Created: 1980-01-01 0:00:00 UTC
Image Type: AArch64 EFI Firmware Kernel Image (no loading done) (uncompressed)
Data Size: 1028096 Bytes = 1004 KiB
Load Address: 00000000
Entry Point: 00000000
Verifying Checksum ... OK
XIP Kernel Image (no loading done)
No EFI system partition
No EFI system partition
Failed to persist EFI variables
## Transferring control to EFI (at address 40200040) ...
Booting /MemoryMapped(0x0,0x40200040,0xfb000)
It appears that it is an UEFI Shell from EDK II.
UEFI Interactive Shell v2.2
EDK II
UEFI v2.100 (Das U-Boot, 0x20230700)
map: No mapping found.
Shell>
During the 10 seconds even if I try to write some characters, nothing happens and the UEFI boots every time. The goal is probably to send the password to have a bootloader shell.
To make it simple, the process of starting a computer is quite complex and requires a lot of steps. It is very low-level.
A quick recall, the bootloader is the first process to be loaded in memory and executed. It is often very tiny and in a different hardware piece than the hard drive (such as a non-volatile memory as a ROM, EEPROM or NOR flash). Its task is to initialise other pieces of software and to make them run. So here we have a 2-stages bootloading process.
The first stage is the bootloader by itself and the second stage is the UEFI (which do not launch another image).
One cool thing is that U-Boot stores its configuration parameters directly inside the bootloader so by doing a strings bootloader.bin
, it is possible to find some interesting information.
bootargs=-delay 0
bootcmd=bootm 0x40200000; poweroff
bootdelay=10
baudrate=115200
loadaddr=0x40200000
arch=arm
cpu=armv8
board=qemu-arm
board_name=qemu-arm
vendor=emulation
boot_targets=qfw usb scsi virtio nvme dhcp
fdt_addr=0x40000000
fdt_high=0xffffffff
initrd_high=0xffffffff
kernel_addr_r=0x40400000
pxefile_addr_r=0x40300000
ramdisk_addr_r=0x44000000
scriptaddr=0x40200000
So it is an ARM architecture and the code at address 0x40200000
is booted. After an exit, it is powered off (so it is not possible to go back to the bootloader).
Inspecting the first door
For the first challenge, I have done a decompilation of the bootloader using Ghidra. One of my main error at that time was to try to decompile with an ARM family whereas it is an ARM 64 bits (the name of the family is AARCH64, a little bit different). It is possible to check that it is 64 bits by doing a strings bootloader.bin | grep -i 'aarch'
and seeing the line aarch64-unknown-linux-gnu-gcc (GCC) 13.2.0
.
So, after opening it in Ghidra with family AARCH64 and little-endian, it is possible to see the password in clear text among the strings. I supposed the cross-reference was a reference from the main function.
The code around the string FAKEPASSWORD
looks like that.
printf((byte *)s_Autoboot_in_%d_seconds_00064fa3);
uStack_54 = 0;
local_stopkey = (char *)0x0;
len_stopkey = 0;
local_44 = 0;
delaykey = (char *)FUN_0001df98((undefined **)s_bootdelaykey_00064fbb,uVar6,ppuVar13,in_x3,
(char *)in_x4,(ulong)in_x5,(ulong)in_x6,in_x7);
stopkey = (char *)FUN_0001df98((undefined **)s_bootstopkey_00064fc8,uVar6,ppuVar13,in_x3,
(char *)in_x4,(ulong)in_x5,(ulong)in_x6,in_x7);
if (delaykey == (char *)0x0) {
delaykey = &DAT_0005f05f;
}
if (stopkey == (char *)0x0) {
stopkey = s_FAKEPASSWORD_00064eef;
}
local_delaykey = delaykey;
local_stopkey = stopkey;
lVar10 = len(delaykey);
len_delaykey = (uint)lVar10;
if (0x40 < (uint)lVar10) {
len_delaykey = 0x40;
}
cnt_input = 0;
len_delaykey2 = len_delaykey;
lVar10 = len(stopkey);
len_stopkey = (uint)lVar10;
if (0x40 < len_stopkey) {
len_stopkey = 0x40;
}
if (len_delaykey < len_stopkey) {
len_delaykey = len_stopkey;
}
do {
uVar12 = is_something?();
if ((int)uVar12 != 0) {
if ((int)cnt_input < (int)len_delaykey) {
uVar6 = read?();
local_40[(int)cnt_input] = (char)uVar6;
cnt_input = cnt_input + 1;
}
else {
delaykey = local_40;
for (iVar11 = 0; iVar11 < (int)(len_delaykey - 1); iVar11 = iVar11 + 1) {
*delaykey = delaykey[1];
delaykey = delaykey + 1;
}
uVar6 = read?();
len_key = len_delaykey;
if ((int)len_delaykey < 1) {
len_key = 1;
}
local_40[(int)(len_key - 1)] = (char)uVar6;
}
}
key_ptr = &local_delaykey;
bVar1 = false;
iVar11 = 2;
while( true ) {
len_key = *(uint *)(key_ptr + 1);
ppuVar13 = (undefined **)(ulong)len_key;
if ((len_key != 0) && (len_key <= cnt_input)) {
ppuVar13 = (undefined **)(ulong)len_key;
iVar4 = strncmp(local_40 + ((long)(int)cnt_input - (ulong)len_key),*key_ptr,
(long)ppuVar13);
if (iVar4 == 0) {
bVar1 = true;
}
}
key_ptr = key_ptr + 2;
if (iVar11 == 1) break;
iVar11 = 1;
}
wait_long(10000);
if (bVar1) goto LAB_000167d8;
uVar6 = FUN_0000566c();
} while (uVar6 <= (ulong)(lVar9 + (int)uVar3 * lVar7));
So there are two keys, a stop key and a delay key and FAKEPASSWORD
is hard-coded as the default stop key. After that, it is a piece of code that takes the n last characters entered and compares them with the stop key or the delay key.
So the key is perhaps stored inside the bootloader on the server. But is it possible to have access to it ?
This is where the UEFI makes an appearance!!
Looking back when we are inside
So even if the UEFI is the second stage process, it is still possible to access to the first stage of the process, if it is not erased. But the UEFI is a shell, it is not a direct backdoor to the entire system.
There are several commands available. I have tested all of them (but not all at the beginning, it was one of my main error, see later). The useful ones here are help
for having a list and some infos about the other commands, cls
to clear the string (it is quickly a landfill) and dmem
to inspect the content of the memory.
This is the help manual about dmem for the format of the command parameters.
Displays the contents of system or device memory.
DMEM [-b] [address] [size] [-MMIO]
-b - Displays one screen at a time.
-MMIO - Forces address cycles to the PCI bus.
address - Specifies a starting address in hexadecimal format.
size - Specifies the number of bytes to display in hexadecimal format.
NOTES:
1. This command displays the contents of system memory or device memory.
2. Enter address and size in hexadecimal format.
3. If address is not specified, the contents of the UEFI System Table
are displayed. Otherwise, memory starting at the specified address is displayed.
4. Size specifies the number of bytes to display. If size is not specified,
512 bytes are displayed.
5. If MMIO is not specified, main system memory is displayed. Otherwise,
device memory is displayed through the use of the
EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL.
It is possible to inspect the memory that way. When using the UEFI, there are tons of ANSI escape codes. So I decided to write a piece of code in Python with the help of pwntools to parse directly the output of the UEFI. At that point, I had in mind to code something for extracting big pieces of memory.
It sends the command with the write format and return the hexdump directly or parse it and remove the ANSI codes.
def readaddress(io, addr, size=0x1000, hexdump=True):
io.sendline(f"dmem {addr:x} {size:x}".encode())
io.readuntil(b"Memory Address ")
io.readline()
data = io.readuntil(b"\x1b[0;37;40m\x1b[25;1H\x1b[1;33;40mShell> \x1b[0;37;40m\x1b[0;37;40m", drop=True).replace(b"\x1b[0;37;40m ", b"").replace(b"\r", b"")
if hexdump:
return data
final_data = bytearray()
for l in data.split(b"\n"):
try:
line_data = binascii.unhexlify(l[10:l.find(b' *')].replace(b'-', b'').replace(b' ', b''))
except binascii.Error:
print(l)
print(l[10:l.find(b' *')])
print(l[10:l.find(b' *')].replace(b'-', b' '))
exit()
final_data.extend(line_data)
return bytes(final_data)
With that, it is possible to look at the memory around the same position as where FAKEPASSWORD
is in bootloader.img
.
extract = readaddress(io, 0x64e00)
print(extract)
The password is just before the string HUSH_VERSION
, which the same way as in the file given. It is b3stfr1end98
. Is it a reference to the‘98 World Cup or to Windows 98 or something else? I don’t know!
So now, it is possible to enter it during the autoboot process and profit!
I knew already how to read parameters of U-Boot bootloader from last year with the command env print -a
.
arch=arm
baudrate=115200
board=qemu-arm
board_name=qemu-arm
boot_targets=qfw usb scsi virtio nvme dhcp
bootargs=-delay 0
bootcmd=bootm 0x40200000; poweroff
bootdelay=10
cpu=armv8
fdt_addr=0x40000000
fdt_high=0xffffffff
fdtcontroladdr=415cf9b0
initrd_high=0xffffffff
kernel_addr_r=0x40400000
loadaddr=0x40200000
printflag=hash sha256 40904000 1000 flaghash; echo FCSC{$flaghash};
pxefile_addr_r=0x40300000
ramdisk_addr_r=0x44000000
scriptaddr=0x40200000
stderr=pl011@9000000
stdin=pl011@9000000
stdout=pl011@9000000
vendor=emulation
The flag can be obtained with another command that gives.
sha256 for 40904000 ... 40904fff ==> XXX
FCSC{XXX}