Flag:
Hero{SUCC3SSFULLY_3XPL0173D_B00TL04D3R}
u-boot.bin (custom U-Boot image)nc dyn08.heroctf.fr 12324nc dyn05.heroctf.fr 11076Hero{...}The challenge claims the bootloader uses AES-128-ECB, and your goal is to recover the flag that it prints.
u-boot.binFirst we look at the binary locally:
file u-boot.bin
We confirm it’s a U-Boot binary for ARM.
Then we search for anything related to boot prompts or secrets:
strings -n 6 u-boot.bin | grep -B1 "Press passphrase"
Output:
bFoL^GMSThEpSPVX2LU@Zx^58M$h!nMaoRBzu7Wa
Press passphrase to stop autoboot, or wait %d seconds to start boot
So the hidden passphrase that stops autoboot is:
bFoL^GMSThEpSPVX2LU@Zx^58M$h!nMaoRBzu7Wa
We’ll need this on the remote instance.
Connect to the remote bootloader:
nc dyn05.heroctf.fr 11076
You see:
Press ENTER to start boot
Press Enter.
Then U-Boot starts and shows:
Press passphrase to stop autoboot, or wait 5 seconds to start boot
Paste the passphrase:
bFoL^GMSThEpSPVX2LU@Zx^58M$h!nMaoRBzu7Wa
Now you drop into a U-Boot shell:
=>
Check the environment:
=> printenv
Relevant entries:
fwaddr=0x40200000
fwsize=0x00000B30
bootcmd=go 0x40200000
So the interesting payload (the custom bootloader code) is at:
0x402000000xB30 bytesAnd go 0x40200000 is what U-Boot would normally execute.
We use U-Boot’s memory-dump command to read out the firmware:
=> md.b 0x40200000 0xB30
This prints a full hex dump of the 0xB30 bytes starting at 0x40200000.
Among the output, near the end, we see:
40200af0: 1e ff 2f e1 42 6f 6f 74 20 63 6f 6d 70 6c 65 74 ../.Boot complet
40200b00: 65 64 2c 20 68 65 72 65 20 69 73 20 79 6f 75 72 ed, here is your
40200b10: 20 66 6c 61 67 20 3a 20 00 00 00 00 30 31 32 33 flag : ....0123
40200b20: 34 35 36 37 38 39 41 42 43 44 45 46 00 00 00 00 456789ABCDEF....
So the firmware prints:
“Boot completed, here is your flag : ”
followed by a hex string (initially the placeholder "0123456789ABCDEF").
Copy the entire md.b output into a local file dump.txt:
cd ~/bootloader
cat > dump.txt
# paste all md.b lines here
# Ctrl+D to finish
Convert to a binary fw.bin:
cat > parse_dump.py << 'EOF'
import re
data = bytearray()
with open("dump.txt") as f:
for line in f:
m = re.search(r':\s+((?:[0-9a-fA-F]{2}\s+)+)', line)
if not m:
continue
for b in m.group(1).strip().split():
data.append(int(b, 16))
with open("fw.bin", "wb") as out:
out.write(data)
print("Wrote", len(data), "bytes to fw.bin")
EOF
python3 parse_dump.py
# -> Wrote 2864 bytes to fw.bin
2864 decimal is 0xB30, so the dump is complete.
fw.bin (ARM blob)Load the firmware at its runtime base address (0x40200000) using radare2:
r2 -a arm -b 32 -m 0x40200000 fw.bin
Inside r2:
aaa
afl
Among the functions we see:
fcn.4020000c – main function of the tiny bootloaderfcn.40200830 – context/key setupfcn.40200874 – block cipher (AES-128 ECB)fcn.402000cc, fcn.402000e4 – printing helpersfcn.402009b0 – hex encoder using "0123456789ABCDEF"Disassemble fcn.4020000c:
pdf @ fcn.4020000c
Important excerpt:
fcn.4020000c ():
push {r4, r5, r6, r7, lr}
sub sp, sp, 0xf4
; 1) initialize context using data at 0x40200b34
ldr r1, [0x402000b8] ; r1 = 0x40200b34
add r0, sp, 0x30 ; r0 = &var_30 (context)
bl fcn.40200830 ; init(ctx, 0x40200b34)
; 2) prepare buffer at [sp..sp+0x30]
sub r4, sp, 1
mov r2, 0x30
mov r1, 0
mov r0, sp
bl fcn.40200a68 ; fill 0x30 bytes
; 3) XOR-decode bytes from 0x40200b43 into [sp..]
mov r1, r4
ldr r2, [0x402000bc] ; r2 = 0x40200b43
add r0, r2, 0x27 ; r0 = r2 + 0x27 (end)
loop_xor:
ldrb r3, [r2, 1]!
eor r3, r3, 0x23
cmp r0, r2
strb r3, [r1, 1]!
bne loop_xor
; 4) call AES on 3×16-byte blocks
mov r1, sp
add r0, sp, 0x30
bl fcn.40200874 ; AES(ctx, sp)
add r0, sp, 0x30
add r1, sp, 0x10
bl fcn.40200874 ; AES(ctx, sp+0x10)
add r0, sp, 0x30
add r1, sp, 0x20
bl fcn.40200874 ; AES(ctx, sp+0x20)
; 5) print message
ldr r0, =0x40200af4 ; "Boot completed, here is your flag : "
bl fcn.402000e4
; 6) hex-encode the ciphertext using "0123456789ABCDEF"
ldr r5, =0x40200b1c ; "0123456789ABCDEF"
add r7, sp, 0x2f
hex_loop:
ldrb r3, [sp], #1
and r2, r3, 0xf
ldrb r6, [r5, r2]
ldrb r0, [r5, r3, lsr 4]
bl fcn.402000cc ; putchar(high)
mov r0, r6
bl fcn.402000cc ; putchar(low)
cmp r7, r4
bne hex_loop
; 7) newline & loop forever
ldr r0, [0x402000c8]
bl fcn.402000e4
b .
Conceptually:
0x40200b43.0x23 into a 0x27-byte buffer.On the remote, running:
=> go 0x40200000
prints:
Boot completed, here is your flag : CE04188B3AA1F39921E5ABBCB0BD7531BB72...
That long hex string is AES ciphertext, not the final CTF flag.
The true flag is the XOR-decoded 0x27-byte plaintext before AES.
0x40200b30 onward)Notice that:
fwsize = 0xB30, so 0x40200000 .. 0x40200b2f was dumped into fw.bin.0x40200b34 and 0x40200b43, which are just beyond the dumped region.So we perform another dump on the remote:
=> md.b 0x40200b30 0x60
This gives us the extra 0x60 bytes containing:
0x40200b340x40200b43We copy that into dump2.txt and parse it:
cd ~/bootloader
cat > dump2.txt
# paste md.b 0x40200b30 0x60 output
# Ctrl+D to finish
Parsing:
cat > parse_dump2.py << 'EOF'
import re
data = bytearray()
with open("dump2.txt") as f:
for line in f:
m = re.search(r':\s+((?:[0-9a-fA-F]{2}\s+)+)', line)
if not m:
continue
for b in m.group(1).strip().split():
data.append(int(b, 16))
with open("extra.bin", "wb") as out:
out.write(data)
print("Wrote", len(data), "bytes to extra.bin")
EOF
python3 parse_dump2.py
# -> Wrote 96 bytes to extra.bin (0x60)
So extra.bin contains exactly the bytes from 0x40200b30 to 0x40200b8f.
From the disassembly:
0x40200040.r2 is initialized from [0x402000bc], which is 0x40200b43.ldrb r3, [r2, 1]! and stops when r2 == r0, where r0 = r2 + 0x27.So the obfuscated bytes live at:
0x40200b44 (because of the pre-increment)0x27 bytesextra.bin is the region starting at 0x40200b30, so:
Offset of the start inside extra.bin:
0x40200b44 - 0x40200b30 = 0x14
Now we can just undo the XOR of 0x23 in Python:
cat > recover_flag.py << 'EOF'
data = open("extra.bin", "rb").read()
start = 0x14 # offset of 0x40200b44 into extra.bin
length = 0x27
enc = data[start:start+length]
plain = bytes(b ^ 0x23 for b in enc)
print("Decoded bytes:", plain)
print("As ASCII:", plain.decode("ascii"))
EOF
python3 recover_flag.py
Output:
Decoded bytes: b'Hero{SUCC3SSFULLY_3XPL0173D_B00TL04D3R}'
As ASCII: Hero{SUCC3SSFULLY_3XPL0173D_B00TL04D3R}
This is the real flag.
We never needed to actually compute the AES key or decrypt the ciphertext. The “crypto” is pure theater: the flag is present in (lightly) obfuscated form in memory before the AES step.
Flag:
Hero{SUCC3SSFULLY_3XPL0173D_B00TL04D3R}