iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0

上一篇我們確認了一件很重要的事:透過 Stack Buffer Overflow 加上 cyclic 找出 Offset,成功把 Saved RIP 蓋成 0x4242424242424242,也就是程式 ret 之後要跳去哪裡,已經可以由我們決定。

但回頭看一下上一篇的流程,其實還蠻煩的——產生 cyclic pattern、貼進程式、Crash、開 GDB 看 Stack、算 Offset、自己用 Python 拼 Payload、再丟回去跑。如果每一題 Pwn 都要這樣手動操作,大概寫沒幾題就會開始懷疑人生。

所以今天要介紹一個接下來幾乎每篇都會出現的工具:Pwntools。

0x00 Pwntools 是什麼?

Pwntools 是一套 Python Library,專門用來寫 Exploit。它把 Pwn 常用的操作都包好了:啟動 Process、連線 Remote Server、收送資料、產生 cyclic pattern、計算 Offset、Pack / Unpack Integer、讀取 ELF、查 Symbol、組 Payload⋯⋯這些事情如果自己做,大概會需要 subprocess、socket、struct.pack 各種東西混在一起。用 Pwntools 之後通常可以省掉非常多工。

一個典型的 Exploit Script 大概長這樣:

from pwn import *

elf = ELF("./chall")
io = process("./chall")

payload = b"A" * 40
payload += p64(0xdeadbeef)

io.sendline(payload)
io.interactive()

現在可能還看不懂每一行在幹嘛,沒關係,今天就是要把這些最常用的東西一個一個拆開來看。

0x01 安裝 Pwntools

如果環境中還沒有 Pwntools,直接用 pip 裝:

python3 -m pip install --upgrade pwntools

裝完之後進 Python 確認能不能正常 import:

python3
>>> from pwn import *

沒噴 Error 基本上就沒問題了。也可以跑 pwn version 確認版本。

0x02 第一個 Pwntools Script

沿用昨天的 Binary chall,建一個 solve.py:

from pwn import *

io = process("./chall")

io.interactive()

跑起來之後應該會看到程式輸出 Input:,等你輸入東西。

這裡 process("./chall") 就是幫我們啟動 ./chall,而 io 就是之後拿來跟這個 Process 溝通的物件——送資料、收資料都透過它。

0x03 send / recv

Pwn 最常做的事情其實很單純:收資料、送資料。Pwntools 幫我們包好了一堆方便的 function。

送資料:

io.send(b"AAAA")        # 送出 AAAA
io.sendline(b"AAAA")    # 送出 AAAA\n(自動加換行)

收資料:

io.recv()                # 收一些資料回來
io.recvline()            # 收一整行
io.recvuntil(b"Input:")  # 一直讀到看見 "Input:" 為止

0x04 sendlineafter

CTF 題目常常會有那種一問一答的互動,像是先印 Name: 等你輸入,再印 Password: 等你輸入。如果每次都自己寫 recvuntil 再 sendline,程式碼會變得很囉唆:

io.recvuntil(b"Name:")
io.sendline(b"shark")

io.recvuntil(b"Password:")
io.sendline(b"password")

Pwntools 有一個更簡潔的寫法,把「等到看見某個字串」和「送出資料」合在一起:

io.sendlineafter(b"Name:", b"shark")
io.sendlineafter(b"Password:", b"password")

像我們昨天的程式會輸出 Input:,就可以直接寫成:

io.sendlineafter(b"Input:", b"AAAA")

0x05 為什麼前面都有 b?

你可能有注意到,Pwntools 裡的字串前面常常會加一個 b,寫成 b"AAAA" 而不是 "AAAA"。

這個 b 代表 bytes。看起來差不多,但其實是不同的型別:

a = "AAAA"
b = b"AAAA"

print(type(a))  # <class 'str'>
print(type(b))  # <class 'bytes'>

在 Binary Exploitation 裡,我們處理的是 raw bytes,不是一般的文字字串。Payload 裡面可能會出現 \x00、\xff、\x90 這種東西,所以寫 Exploit 時通常都直接用 b"..."。

這也是剛開始寫 Pwntools 最容易踩到的坑——如果不小心把 str 跟 bytes 混在一起,就會噴出:

can't concat str to bytes

看到這個 Error 的話,回去檢查哪邊少了 b 就好。

0x06 p64 是什麼?

上一篇的 Payload 結構是 40 Bytes Padding 加上 8 Bytes Return Address。假設今天想把 RIP 蓋成 0xdeadbeef,不能直接這樣寫:

payload = b"A" * 40 + 0xdeadbeef  # 這會噴 Error

因為 0xdeadbeef 是一個 Integer,我們需要把它轉換成 CPU 實際看到的 bytes。Pwntools 提供了 p64() 來做這件事——把一個 Integer pack 成 64-bit Little Endian 的 bytes:

payload = b"A" * 40
payload += p64(0xdeadbeef)

如果是 32-bit 的 Binary,就用 p32()。簡單記:

  • 32-bit → p32()
  • 64-bit → p64()

之後處理 Leak 的時候還會看到反過來的 u32() 和 u64(),到時候再說。

0x07 把昨天的 Payload 寫成 Script

上一篇我們是這樣手動塞 Payload 的:

python3 -c 'import sys; sys.stdout.buffer.write(b"A"*40 + b"B"*8)'

現在用 Pwntools 改寫:

from pwn import *

io = process("./chall")

offset = 40

payload = b"A" * offset
payload += p64(0x4242424242424242)

io.sendlineafter(b"Input:", payload)

io.wait()

跑起來之後程式一樣會 Segfault,因為 0x4242424242424242 當然不是一個合法的地址。但重點不是讓它不 Crash,而是我們已經可以用 Script 穩定地產生 Payload 了。

0x08 Pwntools 也有 cyclic

上一篇我們用 pwndbg 的 cyclic 100 來產生 pattern,其實 Pwntools 裡面也有同樣的功能:

from pwn import *

print(cyclic(100))
# aaaabaaacaaadaaaeaaafaaag...

如果 Crash 之後在 Stack 上看到某段 pattern,比如 kaaa,可以直接用 cyclic_find 算出它的 Offset:

offset = cyclic_find(b"kaaa")
print(offset)

所以之後找 Offset 不需要另外寫 pattern generator,Pwntools 都幫你做好了。

0x09 ELF()

另一個之後會一直看到的東西是 ELF():

from pwn import *

elf = ELF("./chall")

Pwntools 會幫你解析這個 ELF Binary,之後就可以從 elf 裡面拿到各種資訊,像是 elf.arch 可以看 Architecture。

更實用的是,假設 Binary 裡面有一個 function 叫 win,我們不需要自己跑 nm chall 然後手動把 address 複製到 Exploit 裡,可以直接:

win = elf.symbols["win"]

從 ELF Symbol Table 把 win 的 address 拉出來用。之後做 ret2win 的時候這招會非常方便。

0x0A Local 跟 Remote

Pwntools 另一個很讚的地方是,本機測試跟打遠端的 Exploit 幾乎不用改。本機就用 process(),遠端就換成 remote():

io = process("./chall")             # 本機
io = remote("example.com", 1337)    # 遠端

後面的 io.sendline()、io.recvuntil()、io.interactive() 全部都一樣,不用動。

所以 Exploit 常常會寫成這樣,用一個 flag 切換 local / remote:

from pwn import *

elf = ELF("./chall")

if args.REMOTE:
    io = remote("example.com", 1337)
else:
    io = process("./chall")

平常本機測就直接 python3 solve.py,要打遠端就 python3 solve.py REMOTE。打 CTF 的時候幾乎每個人都是這樣寫的。

0x0B interactive()

最後一個很重要的 function 是 io.interactive()。它的意思就是:Exploit 做完了,接下來把控制權交回 Terminal,讓我自己跟程式互動。

為什麼需要這個?因為如果 Exploit 成功拿到 Shell,你會想要自己打指令,像是 whoami、cat flag 之類的。如果沒有 io.interactive(),Script 送完 Payload 就直接結束了,Shell 拿到也沒用。

所以之後的 Exploit 結尾幾乎都是這個 pattern:

io.sendline(payload)
io.interactive()

Exploit 負責把程式打到我們要的狀態,最後再交給人去操作。

0x0C 一個基本的 Exploit Template

整理一下今天學到的東西,可以先存一個基本的 Template,之後寫題直接拿來改:

from pwn import *

context.binary = elf = ELF("./chall")

if args.REMOTE:
    io = remote("example.com", 1337)
else:
    io = process(elf.path)

offset = 40

payload = b"A" * offset
payload += p64(0x4242424242424242)

io.sendlineafter(b"Input:", payload)

io.interactive()

仔細看的話,真正跟漏洞有關的其實只有中間那三行:

offset = 40
payload = b"A" * offset
payload += p64(0x4242424242424242)

其他都在處理啟動程式、建立連線、收送資料這些重複性的工作。Pwntools 就是幫我們把這些 boilerplate 處理掉,讓我們可以專心在 Exploit 本身。

0x0D 小結

今天沒有介紹新的漏洞,而是把前面手動做的事情開始用 Script 自動化。整理一下今天碰到的 Pwntools function:

用途 Function
啟動 / 連線 process(), remote()
送資料 send(), sendline(), sendlineafter()
收資料 recv(), recvline(), recvuntil()
找 Offset cyclic(), cyclic_find()
Pack / Unpack p32(), p64(), u32(), u64()
讀 ELF ELF()
互動 interactive()

真正開始寫題目之後,每一題都在重複 ELF → process → recv → 組 Payload → send → interactive 這個流程,寫個幾題自然就記住了。

上一篇我們已經知道 Offset 是 40,也確認 Saved RIP 可以被控制。今天把這件事寫成了 Script。現在真正剩下的問題就只有一個:RIP 到底要跳去哪裡?

如果 Binary 裡面剛好藏著一個從來不會被正常呼叫的 function:

void win() {
    system("/bin/sh");
}

那有沒有可能可以跳到這個 function 呢?
Day 20 就來看 Pwn 最經典的入門題型——ret2win。


上一篇
Day18|RIP Control
下一篇
Day20|ret2win
系列文
從零開始の Binary CTF:30 天系統化學習 Rev & Pwn 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言