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
Now, the bootloader takes only 2 seconds to perform the check and the UEFI Shell is loaded after. I used the same python code for the memory dump as before (see the write-up for the easier version) which parses directly the output of the UEFI.
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)
Trying to break a metal door
So back again with a second bootloader. Same protocol, putting it into Ghidra. Now it is looking for a parameter named bootstopkeysha256
. It is possible to find a sha256 string b2986a18ce759031c3215a13d01f8290193b2dac8556a7b4a784197955806310
which is the SHA256 of the string FAKEPASSWORD
. The rest of the code is also looking for characters, there is several hash functions defined inside the bootloader and an array of struct pointing to these functions. The code here is using the function sha256
.
I extracted part of the remote bootloader to see its secrets. The remote sha256 is 12eb7b15dc2f4aed25371a49f3048c56fda5eacb600196e3f5aa842757412c7e
and the printflag
command is printflag=hash sha256 40900100 1000 flaghash; echo FCSC{$flaghash};
.
This is where I spent a lot of time for wrong reasons.
I tried to bruteforce the flag. As the first final password was weak, I tried many combinations around the rockyou.txt
and some derivations with the number 98
. I also ran a bruteforce attack up to 8 characters.
During that time, I examined the code closely to see if there was a vulnerability hidden inside the code that allows to bypass the check. Nothing comes out.
I tried to look at the memory directly from the UEFI Shell, but it was not working. I knew that the hash cracking was probably not the right solutions, but I was not able at all to understand why it was not possible to look at the memory around 0x40900000
. Last year, it was understandable, something was connected only remotely, so I thought here it was something only accessible from the bootloader and disconnected after that.
I was a little bit hopeless and it was my last hardware challenge, so I decided to try to do other challenges.
The light at the end of the dark tunnel
The day after, I started again, also because my opponent was showing off. First, I was not really conscious that it was possible to auto-modify the code of the UEFI. No memory regions are protected at that point.
This is where 2 other useful commands arrives. memmap
is useful for seeing the memory mapping and mm
to modify the memory. This is the output of memmap
.
Type Start End # Pages Attributes
Available 0000000040000000-00000000402CDFFF 00000000000002CE 0000000000000008
BS_Data 00000000402CE000-00000000403BEFFF 00000000000000F1 0000000000000008
LoaderCode 00000000403BF000-00000000404B9FFF 00000000000000FB 0000000000000008
BS_Data 00000000404BA000-00000000404BDFFF 0000000000000004 0000000000000008
ACPI_Recl 00000000404BE000-00000000405C0FFF 0000000000000103 0000000000000008
BS_Data 00000000405C1000-00000000405C1FFF 0000000000000001 0000000000000008
RT_Data 00000000405C2000-00000000405C3FFF 0000000000000002 8000000000000008
BS_Data 00000000405C4000-00000000405C4FFF 0000000000000001 0000000000000008
RT_Data 00000000405C5000-00000000405C6FFF 0000000000000002 8000000000000008
BS_Data 00000000405C7000-00000000405C7FFF 0000000000000001 0000000000000008
RT_Data 00000000405C8000-00000000405CBFFF 0000000000000004 8000000000000008
BS_Data 00000000405CC000-00000000405CEFFF 0000000000000003 0000000000000008
BS_Code 00000000405CF000-000000004174FFFF 0000000000001181 0000000000000008
RT_Code 0000000041750000-000000004175FFFF 0000000000000010 8000000000000008
BS_Code 0000000041760000-00000000417FFFFF 00000000000000A0 0000000000000008
Reserved : 0 Pages (0 Bytes)
LoaderCode: 251 Pages (1,028,096 Bytes)
LoaderData: 0 Pages (0 Bytes)
BS_Code : 4,641 Pages (19,009,536 Bytes)
BS_Data : 251 Pages (1,028,096 Bytes)
RT_Code : 16 Pages (65,536 Bytes)
RT_Data : 8 Pages (32,768 Bytes)
ACPI_Recl : 259 Pages (1,060,864 Bytes)
ACPI_NVS : 0 Pages (0 Bytes)
MMIO : 0 Pages (0 Bytes)
MMIO_Port : 0 Pages (0 Bytes)
PalCode : 0 Pages (0 Bytes)
Unaccepted: 0 Pages (0 Bytes)
Available : 718 Pages (2,940,928 Bytes)
Persistent: 0 Pages (0 Bytes)
--------------
Total Memory: 24 MB (25,165,824 Bytes)
There are several interesting data here. One is coming from the output of the first stage bootloader: the UEFI is at 0x40200000. It is a part available inside memory. From the help page of dmem
(see above), it is possible to have the address of the UEFI System Table which is 0x41751B88. It is inside an RT_Code
region.
It is by trying to understand how mm
was working that I figured out that the message after doing dmem 40900100
(the memory region used to compute the flag) was really weird.
dmem ACCESS DENIED: Invalid argument - '<null string>'
Why is it access denied ? Is it that it is not possible to access it or is it, in fact, not defined inside the memory mapping even if it is written as so.
Then I understood that it is still possible to access the memory just before the flag up to 0x409000fe and just after including 0x40901100. It looked really suspicious.
The hardened double door
So I decided to extract the UEFI with my python code. It is too slow for downloading it at once
extracted_efi = bytearray()
#begin = 0x40200000
#begin = 0x402d0000
end = 0x402fb040
size, step = end-begin, 0x2000
with open("efi.cpio", "ab") as efi_extracted:
for i in range(begin, end, step):
part_i = readaddress(io, i, size=step, hexdump=False)
extracted_efi.extend(part_i)
efi_extracted.write(part_i)
print(f"Received address 0x{i:x}-0x{i+step-1:x}, {(i-begin+step)/size*100}%")
It took two downloads to have the complete file of 1008 Ko. It appears to be a CPIO archive (classical format for a uImage).
I opened it in Ghidra, saw a string dmem ACCESS DENIED
and saw the cross-reference. This piece of code is interesting (and suspicious).
if ((local_30 + local_28 < (byte *)0x40900100) || (0x409010ff < local_28)) {
if (bVar4) goto LAB_00039914;
[...]
}
}
else {
in_x6 = unaff_x22;
FUN_00016d60(-1,-1,0,0x22c,DAT_000f4588,L"dmem ACCESS DENIED",unaff_x22,in_x7);
uVar8 = 2;
}
So there is a hard-coded check to avoid looking at the memory at that specific place!!!! Not only the bootloader was patched but also the UEFI (the double door).
Stealing the treasure
Now, it is time to think as thieves and to find a way to bypass it. I have chosen to patch the UEFI directly into memory to step over this condition and be able to perform the memory extraction.
So I used mm to modify the data stored at address 0x40239928 (to bypass the jump). However, it was not working because it is not the place where the executed code is. This is where the uImage is, but it is placed elsewhere for the execution. By looking at the memory map, I figured out that the LoaderCode part size was exactly the size of the UEFI. I looked at the beginning, and it was the same file. So I patched at that point 0x403BF00+000398e8 to change the jump condition of the check.
mm 403f88e8 0x14000044 -w 4
And now dmem
is working again. This command prints well the memory.
dmem 40900100 1000
To compute the sha256, I did a snippet to do the patch, extract the memory and hash it.
shell_header = io.readuntil(b"Shell> \x1b[0;37;40m\x1b[0;37;40m")
io.sendline(f"mm 403f88e8 0x14000044 -w 4".encode())
shell_header = io.readuntil(b"Shell> \x1b[0;37;40m\x1b[0;37;40m")
data = readaddress(io, "40900100", size=0x1000, hexdump=False)
flag_hex = hashlib.sha256(data).hexdigest()
print(f"FCSC{{{flag_hex}}}")
And this is finally the second flag!!
Profit
As a summary, this is a memory map of what is interesting here.
0x00000000-0x0008b198 : ROM (with u-boot)
0x40200000-0x402fb040 : EFI (uImage with EFI uncompressed)
0x403bf000-0x404ba000 : EFI (UEFI running)
0x41751000-0x417dc198 : RAM u-boot
A good rest after finally locking this category… So this was a really nice challenge ! A big thanks to erdnaxe
for the challenge (and to the organisers).