Previous: Lab 2: Flash Memory Over-Read and Function Pointer Leak
Next: Lab 4 (Bonus): ROP Exploitation to Print Attacker-Controlled String
- Complete Lab 1 and recover the device PIN.
- Complete Lab 2 and recover the flash address of the
backupcommand handler.
You need both values for this lab.
Force execution of the firewalled backup handler by sending an oversized pass payload, overflowing a stack buffer in cmd_pass, and overwriting the saved return address.
The return address should be replaced with the leaked backup handler address from Lab 2.
This lab connects memory corruption with control-flow hijacking:
- Lab 2 gave you a valid code pointer in flash.
- Lab 3 uses a stack overflow to redirect execution to that pointer.
It demonstrates how separate "small" bugs chain into a full exploit path.
Target function in firmware:
// Decode a hexadecimal string into an array of bytes
bool decode_hex(const char *hex, size_t hex_len, uint8_t *out, size_t out_cap, size_t *out_len) {
if ((hex_len % 2u) != 0u) {
return false;
}
// (!) Missing bounds check: hex_len is not compared against out_cap
for (size_t i = 0; i < hex_len; i++) {
if (!is_hex_char(hex[i])) {
return false;
}
}
for (size_t i = 0; i < hex_len / 2u; i++) {
out[i] = (uint8_t)((hex_nibble(hex[2u * i]) << 4u) | hex_nibble(hex[2u * i + 1u]));
}
*out_len = hex_len / 2u;
return true;
}
// Handle passphrase entry command: 'pass'
bool cmd_pass(const char *arg, size_t arg_len) {
if (arg == NULL) {
return false;
}
size_t decoded_len = 0;
uint8_t decoded[MAX_PASS_BYTES];
if (!decode_hex(arg, arg_len, decoded, sizeof(decoded), &decoded_len)) {
return false;
}
memcpy(g_passphrase, decoded, MIN(decoded_len, sizeof(g_passphrase)));
g_passphrase[decoded_len] = '\0';
g_passphrase_len = decoded_len;
memset(decoded, 0, sizeof(decoded));
proto_send_ok();
log_line(LOG_INF, "AUTH", "Passphrase updated");
return true;
}passis remotely reachable over protocol CDC.decodedlives on the stack.- If decode/copy logic allows writing past
decoded, nearby saved state (including savedlr) can be corrupted. - On function return, corrupted saved
pccan redirect execution.
In this lab, you redirect control flow to the leaked backup handler address.
cmd_pass:
push {r4, r5, r6, r7, lr} ; save callee-saved regs + return address
sub sp, #84 ; allocate stack frame
cmp r0, #0 ; arg == NULL ?
beq fail_return
movs r3, #0
str r3, [sp, #76] ; decoded_len local
add r3, sp, #76 ; &decoded_len
str r3, [sp, #0] ; pass out_len pointer on stack
movs r3, #64 ; decoded buffer capacity
add r2, sp, #12 ; decoded buffer starts at sp+12
bl decode_hex ; decode user hex into stack buffer
subs r4, r0, #0 ; check decode result
bne decode_ok
fail_return:
movs r4, #0
return_common:
movs r0, r4
add sp, #84
pop {r4, r5, r6, r7, pc} ; return via saved PC (hijack target)
decode_ok:
ldr r5, [sp, #76] ; decoded_len
movs r2, r5
cmp r5, #65
bls copy_and_finish
clamp_len:
movs r2, #65
; fall through into copy_and_finish
copy_and_finish:
ldr r6, [pc, #48] ; &g_passphrase
add r7, sp, #12 ; source: decoded buffer on stack
movs r1, r7
movs r0, r6 ; destination: g_passphrase
bl __wrap___aeabi_memcpy
movs r3, #0
strb r3, [r6, r5] ; g_passphrase[decoded_len] = '\0'
ldr r3, [pc, #36] ; &g_passphrase_len
str r5, [r3, #0] ; g_passphrase_len = decoded_len
movs r2, #64
movs r1, #0
movs r0, r7
bl __wrap_memset ; wipe decoded stack buffer
bl proto_send_ok
ldr r2, [pc, #24]
ldr r1, [pc, #24]
movs r0, #1
bl log_line
b return_common- Open
lab3_stack_overflow.py. - Implement
build_overflow_payload(...)TODO. - Analyze
cmd_passin C and disassembly to determine the correct number of filler bytes needed before the saved return address. - Build payload as:
- participant-derived filler bytes (
--payload-bytes) - leaked
backuphandler address encoded as 4-byte little-endian overwrite value
- Send payload through
passcommand.
python lab3_stack_overflow.py --pin <LAB1_PIN> --ret-addr <LAB2_ADDR> --payload-bytes <YOUR_GUESS>Example:
python lab3_stack_overflow.py --pin 0123456789 --ret-addr 0x30012345 --payload-bytes 123Required option:
--payload-bytes <N>: number of filler bytes before return address overwrite, derived fromcmd_passstack layout analysis.
- In
cmd_passdisassembly, findsub sp, #N: thisNis the local stack frame size. - That local frame is separate from registers saved by
push {..., lr}. - Find where
decoded[]starts:add r?, sp, #Dmeans decoded base issp + D. - At return,
pop {..., pc}restores saved registers; each saved register uses 4 bytes. - Compute
--payload-bytesas distance from decoded base to savedpc. - If you get stuck, first compute the offset to saved
r4, then add 4 bytes per register untilpc.
Corrupting saved registers and adjacent stack variables can make the device hang or stop responding. After an unsuccessful attempt, you may need to unplug and replug the USB cable to force a hard reset before retrying.
You should see logs such as:
Built overflow payload (...) bytesReturn address: 0x... (LE bytes: ...)'pass' command completed: ...Return address hijacked to 0x...
On successful control-flow hijack, the backup function executes and mnemonic backup output appears on the device UX channel (CDC 0), despite normal state gating logic.
- You trigger
cmd_passoverflow remotely. - Saved return address is overwritten with leaked
backuphandler address. - Firmware executes
backuphandler code path. - You observe backup mnemonic output on CDC 0.
Bounds checks must be enforced at decode/write time, not only in later copies. Stack memory corruption plus leaked code addresses enables reliable return-address hijacking and bypass of protection logic.