環境
- Ubuntu 18.04 / Kali 2022.3
- gdb-gef, ghidra
Patchelf
- 由於部分題目需要使用到 Ret2lib 等等的攻擊,如果靶機與本機的 libc 版本不同,可能會造成攻擊的腳本失效
- 因此這邊使用 Patchelf 來使兩邊版本一致,以下使用
applestore
這一題作為範例
- 先安裝 patchelf 和 glibc-all-in-one
- patchelf 我應該是直接
sudo apt install patchelf
,有點忘了
- glibc-all-in-one 就直接照 GitHub 指示安裝
- 接著查看題目提供的 lib 檔案版本
- 假設檔案為
libc_32.so.6
strings ./libc_32.so.6 | grep ubuntu

- 那我們就需要
2.23-0ubuntu5
的 ld 連結檔
- 接著使用 glibc-all-in-one 下載連結檔
- 記得要根據題目是 32 還是 64 位元下載不同檔案
- 如果沒有對應的版本那就自己上網找載點,然後將
download
裡面的 SOURCE
修改後執行
- 下載完的檔案會在
libs/[version]
裡面,而我們需要其中 ld
開頭的檔案

- 直接複製一份到題目資料夾
- 現在題目資料夾裡應該有
- 題目 ELF
- 題目 lib
- 下載到對應的 ld

- 先綁定 ld
patchelf --set-interpreter [ld] [elf]
patchelf --set-interpreter ./ld-2.23.so applestore
- 然後綁定 lib
patchelf --replace-needed [original-lib] [lib] [elf]
patchelf --replace-needed libc.so.6 ./libc_32.so.6 applestore
- 可以使用
ldd [elf]
查看原本的 libc 是啥
- 就可以直接執行了,一樣使用
ldd
查看也確實是綁定在題目提供的 lib 上面
相關資源
- 通常我們會需要使用 python 的 pwntools 模組來幫助我們向程式傳遞訊息
- 例如給定一個目標 Address 做後續攻擊利用等…
- 然而我們也會需要 gdb 幫助我們得到系統資訊
- 以下為範例腳本,可在使用 pwntools 的同時將 gdb attach 到目標程式上面
Start
Recon
- 先試著執行一次
- 要求輸入,似乎有 BOF

- gdb 執行,先確認防護措施
- 全部未啟用

- 用 ghidra 看一下流程
- 大致上可分為幾塊
- 將 ESP 和 _exit 的 address 推入 stack
- 清空 EAX ~ EDX
- 在 stack 中推進字串
Let's start the CTF:
INT 0x80
為呼叫 system call
- 前面在存入呼叫號和參數
- AL = EAX 前 8 位;同理 BL、DL
- x86-32 calling convention: EAX EBX ECX EDX ESI EDI EBP
- 0x4: write
- arg: fd(0x1), buf(ESP), count(0x14)
- 簡單來說就是呼叫 write(1, ESP("Let's start the CTF:"), 0x14)
- 清空 EBX,AL DL 更換,一樣呼叫 system call
- 0x3: read
- arg: fd(0x0), buf(ESP), count(0x3c)
- 簡單來說就是 read 0x3c 個字到 ESP 上
- ESP 加上 0x14 後 RET
- 概念上次排除掉原本在 buf 的
Let's start the CTF:
然後 RET
- RET 直接從 stack 拿 32 個 bits 當作下一個執行的指令
Frame stack 結構
- 一些暫存器的功能
- EIP: 下次要執行的指令地址
- EBP: stack base address
- ESP: 目前 stack address
- 一個 Stack Frame 大概長這樣
Stack |
… |
buffer |
… |
saved rbp |
return address |
- 在 buf 區塊存取時,如果沒有規定上限或是上限超出範圍,就可能使 buf 區塊的資料蓋到 rbp 甚至 RET address
Stack |
… |
"AAAAAAAA" |
"AAAAAAAA" |
saved rbp "AAAAAAAA" |
return address "AAAA" |
… |
漏洞利用
- 經過上述分析,其實蠻明顯具有 BOF 漏洞
- 可以先觀察一下執行 read 前的 stack 狀態

- 基本上就是存放了 write 的字串,共有 0x14 個字
- 接著最下面放有開頭推入的 exit address 與 ESP
- 然後隨便輸入個
AAAA
讓他 read 看看

- 可以發現 stack 最上面變成了 AAAA(0x41414141)
- 堆疊中只有 0x14 個空間,但讀取的上限卻是 0x3c,因此輸入夠長的話就可以蓋掉 RET Address
- 設計一下 payload
- 前面先給予 0x14 個 A 來蓋掉原先的字串空間,後面就可以直接給定任意地址了
- 問題來了,我們要跳到哪裡?
- 我們希望能夠拿到 RCE,因此得到 shell 是最好的,然而這隻程式似乎沒有 system 等等的地方能夠利用
- 那就使用 system call 來達到…
- 對照一下文件,execve 的 system call 應該就是我們要的
- number(EAX): 0x0b
- arg0(EBX): filename = /bin/sh
- arg1(ECX): argv = 空白即可
- arg2(EDX): envp = 空白即可
- 重新整理一下,我們希望執行到的 shell code asm 為
- PUSH 的時候實際上需要分兩次,因為 32 bits 一次只能推 4 個字元
- 因為沒有做保護,因此我們可以把 shell code 推入到 stack 中,然後把 address 指定到 stack 上面
- stack address 在哪裡?就是一開始推入的 ESP
- 所以我們先做一次 BOF,將 stack 字串的部分用乾淨,然後把目標 address 設回 write 的地方
Stack |
… |
"AAAAAAAA" |
"AAAAAAAA" |
"AAAAAAAA" |
_exit "0x08048087" |
saved_ESP |
- 然後因為跳轉回 write,因此會印出 ESP,也就是 stack 的 Address
- 再來二次 BOF,前面一樣用 0x14 個 A 來填充,後面的目標 address 放入第一次得到的 ESP,後面再放入準備好的 shellcode
- 然後這段 code 其實是錯的,因為第一次 BOF 時執行了
ADD ESP,0x14
,導致 stack 產生了 0x14
的偏移
- 最後 exploit script
- 如果在 debug 模式(本機)下使用,會發現開啟了 shell 在相同目錄(可以用
ls
cat
等指令測試)
- 但因為還在 debug mode,因此會有些其他訊息而有點亂

- 線上模式的話則會直接得到 remote shell
- 用
find
指令可直接搜尋 flag 相關檔案

- 成功得到 Flag
orw
Recon
- 題目描述寫道,flag 在
/home/orw/flag
,並且我們只能使用open
read
write
的 syscall
- 然後直接執行的話會出錯,有可能是因為其他 syscall 被擋住導致的,但我們可以直接 nc 連過去看看

- 要求我們輸入 shellcode
- 沒什麼頭緒,用 ghidra 分析看看
- 發現有 main function
- 一開始執行了一個 function,等等再分析
- printf 出字串
- 讀取大小為 200 的 input 放到 shllcode 變數中
- 將 shllcode 當作 code 執行
- 看起來並沒有很複雜,接著我們看看第一個 function 做了什麼
- 其實 google 一下可以知道 function name 代表的應該是 Secure Computing,可以推測該 function 的功能是禁止 syscall
- 但我們還是實際分析一下

- 前面看起來只是做一些變數處理
- 後面看起來比較重要的應該是 prctl 的部分
- 查一下 man,可以知道第一個 arg 會決定他的行為
- 對應原始碼,得知 0x26 為
PR_SET_NO_NEW_PRIVS
但看不太懂他的解釋,先跳過
- 同理對照 0x16 為
PR_SET_SECCOMP
,這個就比較單純了,它會使現在的 thread 只能呼叫 read write 和 exit
- 其實搞不太懂這邊的詳細實作,但其實結合題目說明大概就知道這個 function 就是限制住 syscall 的部分
Shellcode 撰寫
- 題目其實很明確,就是要你幹一段 shellcode 去讀取 flag,且只能利用 write read 和 open
- 那流程大概就是
- open flag file
- read the open fd
- write to the screen
- 一樣對照 syscall docs 去實作
- open
- EAX: 0x05
- arg0(EBX): filename(/home/orw/flag)
- arg1(ECX): flags
- arg2(EDX): mode
- return(EAX): fd
- read
- EAX: 0x03
- arg0(EBX): fd(from open)
- arg1(ECX): buf = esp
- arg2(EDX): count
- write
- EAX: 0x04
- arg0(EBX): fd = 0(顯示到畫面上)
- arg1(ECX): buf = esp
- arg2(EDX): count
- 嘗試撰寫 shellcode
- 最後 exploit script
- 值得注意的是 push 到 stack 時因為大小限制,要拆成 4 個一組
calc
Recon
- 稍微執行一下看看程式能幹嘛
- 印出
=== Welcome to SECPROG calculator ===
字串
- 讀輸入,然後會做計算
- 輸入空白會印出
Merry Christmas!
字串並結束程式
- 從題目名稱和上述測試可以知道是一個計算機程式,接著用 ghidra 分析看看
- 以下分析的變數名稱已經經過修改
- 發現有
main
function 存在,然後馬上發現測試時的兩個字串,並且中間有一個自訂 function calc()
calc

- 可以看到使用一個無窮迴圈,合理猜測是可以不停接收使用者訊息的原因
- 裡面使用
bzero
來清空一個 0x400
的空間 user_input
,用於存放使用者輸入(宣告的地方可以知道他是個陣列)
- 接著這邊執行了另一個 function
get_expr
,那我們先分析一個這個 function
get_expr

- 裡面的變數名稱已經被我整理過,整個功能變得很明顯了
- 首先,輸入的兩個參數分別為 bzero 清空過的一個陣列和
0x400
的固定整數
- 然後一個迴圈會跑
0x400
次,每次讀一個使用者的輸入
- 如果沒讀到或是讀到換行就跳出迴圈
- 如果是運算符號或是數字就存到陣列中的位置,接著讀下一個字
- 最後回傳迴圈跑了幾次,也就是輸入的 size
- 回到
calc
- 接續剛剛的
get_expr
,可以得知回傳值是 input 的大小
- 如果大小為
0
就跳出迴圈,因此會印出字串然後結束
- 如果不為
0
則會對一個變數的地址做 init_pool
,一樣來看看這個 function
init_pool

- 一樣很單純,把輸入的指標後面
100
格清空
- 這邊可以注意的是,ghidra 前面分析
pool
為一個 int,但這邊很明顯是一個 array
- 因此其實
calc
中另一個接續在後面的陣列,其實可以和 pool
視為同一個部分
IDA 的話可以手動修正合併,Ghidra 沒辦法XD
- 回到
calc
- 清空後會把最開始的
user_input
和剛剛清空的指標丟進 parse_expr
中,然後把回傳值印出
parse_expr
- 首先,最外面的判斷會是否為
0
~ 9
,如果不是才會進去
- 進去之後,會先將前面掃過但還未處理的數字切出來設為
part_input
- 判斷
part_input
是否為零,是的話就跳到錯誤
- 再來判斷
part_input
是否大於零,如果是就丟到 pool
中
- 這裡的儲存方式很重要,
pool
的第一格代表目前存到哪裡,也就是 size
- 然後讓 index 存取第一格後,將數字放到 index 那一格去
- 接下來是去判斷下一個字元如果不是結尾,卻也不是數字,那就代表有連續的運算子出現,跳到錯誤
- 如果目前
token
是空的,把運算子的部分存到 token
裡面
- 如果
token
不是空的,會比較 token
和目前符號的優先度做不同處理
- 使用到了
eval
,來看看他如何實作
eval
- 根據給定的運算子 (
token
) 不同,會對 pool
做不同運算
- 回到
parse_expr
6.
- 如果已經結束,就把所有
token
和 pool
中的數字拿去做運算
- 其實最開始測試程式的時候,某一個測資讓我有點在意
- 最開始只是想測試有沒有支援負數,但意外發現開頭如果是運算符號就會怪怪的,例如:
- 回到 ghidra 去比對分析一下這樣的輸入會如何運作
- 假設輸入
+56+9
- 第一個
+
- 不是數字,因此會直接進去 if 裡面
i
為 0
,因此 part_input
會只有 \0
- 將
\0
用 atoi
會回傳 0
,因此不會改動到 pool
- 下一個字元是數字,不會跳到錯誤
- token 被存起來,因此
tokens[0]
= +
- token 被存了,跳過
- 還沒到最後,跳過
- 第二個
+
- 不是數字,因此會直接進去 if 裡面
0
< 56
,因此會去更新 pool
pool_index
= 0
pool[0]
= 1
pool[1]
= 56
- 下一個字元是數字,不會跳到錯誤
- 剛剛 token 存了第一個
+
,因此不會進去
- 目前進到 switch 的是當前符號
+
,也就是 0x2b
- 執行
eval(pool, tokens[token_index])
- 執行完之後 pool 會變為
pool[0]
= 56
pool[1]
= 56
- 也就是說,原先用於當作 size 的第零格會被拿去運算
- 這也導致了下一次的寫入位置會亂掉
- 如果還有數字要寫入,下次會寫到
pool[57]
的位置
- 也就是說,我們可以在任意位置寫入資料了
- 但位址的起始會被加上
pool
的位址
- 寫入在
pool[x]
的概念
- 同時我們也可以思考一下,這樣的怪異輸入最後的回傳值是什麼,是否可以利用
calc
中,輸出的部分長這樣
- 前面說明過,
pool_array
其實是 Ghidra 沒解好,實際上他應該是 pool[1]
的概念
- 因此可以全部換成
pool
去表示
pool[1 + pool[0] - 1]
- 在簡化成
pool[pool[0]]
- 推導過後,可以知道最後的輸出其實就是把第零格當作 index,然後輸出該格
- 結合上面破壞
pool[0]
的手法,可以做到 leak 任意記憶體
漏洞利用
- 先看一下程式防護

- 可以看到有 NX 防護,表示 stack 中的資料無法當作指令去執行
- 所以不能用 Start 那題的方法達到 RCE
- 改用 ROP gadget 的方式做
- 預想 stack 架構

- 一樣是利用
execve("/bin/sh")
- 如果成功建構出這樣的 stack 就可以在 return 回 main 的地方觸發 ROP Chain,進而得到 shell
- 經過前面的分析,我們可以在任意位址寫入資料
- 想法是先 leak 出 EBP,接著寫入 ROP gadget,最後寫入 RET address 使其執行
- 先用 gdb 直接找出 main 的 return 和 main frame 的 main_EBP 為多少

- 將斷點設在 calc 中,因為在進入 function 時會把 return address 也推入 stack 中,再把 EBP 也推進去
- 因此 return address 會在 EBP + 4 的位址上
- return address 在 main_EBP +
0x1c
- 然後也一樣先找出
pool[0]
的位址

- 位址在
0xffffcaa8
= main_EBP + 0x5c0
- 一格的空間大小為
0x4
,算起來 return address 在 pool[361]
- (
0x5c0
- 0x1c
) / 0x4
= 0x169
= 361
- 我們可以先測試一下上面的推論有沒有錯誤
- 輸入
+360+[TARGET_ADDR]
應該可以跳到目標地址
- 我們跳到 puts 字串的地方看看
- 地址為
0804947b
= 134517883

- 可以看到我成功讓應該結束的地方又跳回開頭了
- 先利用 ROPgadget 找出能利用的 gadget address
ROPgadget --binary ./calc --only "ret|pop"
- 我們需要的都剛好有
0x0805c34b : pop eax ; ret
0x0806497b : pop edi ; pop esi ; pop ebx ; ret
ROPgadget --binary ./calc --only "int"
- 再次整理 stack 資訊

- 因為 "/bin/sh" 的部分需要知道 stack 的 Address,因此順便計算一下與 EBP_main 的相對位置
- 再整理上面運算得到的結果,如果我們輸入
+X+Y
pool[X + 1] = Y
pool[X] = pool[X] + Y
- 簡單寫一個 ROP Chain
- 執行後發現…失敗QQ
- 原因有二
- 輸入
0
會視為錯誤,導致沒有成功寫入
- 寫入 main_EBP 的時候會溢位
- 這時候就得利用
pool[X] = pool[X] + Y
,利用運算的部分寫入 0
- 測試一下,可以在 gdb 中將中斷點設在 calc return to main 的地方

- 可以看到 stack 中完全如我們的預期
- 實際執行也可以正確使用到 shell
- 解除 debug mode,即可成功 RCE,取得 flag
3x17
Recon
- 先執行程式看看
- 執行後要求輸入
addr
- 接著要求輸入
data

- 沒什麼明顯的輸出、錯誤,用 Ghidra 逆向看看
- 該 binary 被 stripped,看不到 function name,從 entry 點去找
main
function
- 到
main
,可以看到裡面有 addr:
和 data:
合理推斷是 write
- 後面有等待使用者輸入,推斷是
read

- 剩下一個
FUN_0040ee70
,裡面超級複雜,直接改用 gdb 去確認一下進去的參數和回傳值

- 輸入
123
,回傳 0x7b
- 也就是說,會把傳入的 string 當作 10 進制的數字,然後轉成 address 的表示法(hex)
- 統整一下流程
- 讀入
0x18
大小的 string 作為 user_input
- 將 user_input 當作 10 進制,透過
strtol
轉為 Hex(addr)
- 接著在 addr 寫入
0x18
- 另外,有個放在 DATA 區段的變數每次都會讓自己++,只有當它為
1
的時候才會工作。
- 當然,這讓我們可以在任意地址寫入
0x18
大小的值,如何進一步利用?
多次寫入
- 我們接下來的目標是想辦法將一次的寫入改為多次寫入,增加利用度。
- 在整個 Linux 程式的啟動過程中,每個 function 大概是這樣
- .init
- .init_array[0]
- .init_array[1]
- …
- main
- .fini
- …
- .fini_array[1]
- .fini_array[0]
- 我們回到
entry
,可以看到兩個 main
前後的 function,剛好一個會遞增執行一個是遞減執行,分別對應到 init
和 fini
- 接著,如果我們將
.fini_array[1]
執行時的地址改寫為 main
,並將 .fini_array[0]
改寫為 .fini
,那麼應該就能無限執行 main
,不停進行寫入了。
- 由於可寫入空間是
0x16
,並且目標位置是位於連續的陣列之中,我們可以直接一次寫入兩個 function(注意順序)
- 完成後執行可以看到多次跳出
addr:
和 data:
字串,表示成功
- 我們現在可以無限次任意寫入,也可以控制下一個要執行的 function(RIP)
- 然而,卻無法得知 RSP,因此無法控制 stack 上的資料
- 換個想法,我們是否有辦法在已知區段
X
做好 ROP chain,再將 RSP 改寫過去呢
- 在
main
的尾端我們可以看到

- 其中
LEAVE
相當於
- 用途是將 stack 中丟掉一個 frame
- 這表示我們可以將跳轉的 fini[0] 改到
LEAVE
上面,他會將 RBP 寫到 RSP 中,接著執行 RET
執行 RSP
- 那麼,在 Call
fini[0]
(預計被改寫成 LEAVE
) 之前,RBP 是多少呢
- 直接用 GDB 暫停在
CALL
的指令,得知 RBP 為 0x4b40f0

- 因此若是跳轉到
LEAVE
上面,接下來執行的地方就是會 0x4b40f0
的下一行,也就是 0x4b4100
- 再度測試一下有沒有問題
- 執行後成功停留在
0xdeadbeef
上面,表示成功
ROP 構建
- 現在已經可以在任意地方寫入,並且跳轉到
0x4b4100
,因此我們只需在 0x4b4100
事先建構好 ROP Chain 即可
- 目標為
execve("/bin/sh", 0, 0)
- 在 64 位元的系統中,我們需要在 rax =
0x3b
時呼叫 syscall
來執行 execve
- 且 x64 的 Linux Calling Convention 為
- Function(rdi, rsi, rdx, rcx, r8, r9)
- 因此 ROP 的排序為
- pop_rax
- 0x3b
- pop_rdi
- addr_"/bin/sh\x00"
- pop_rsi
- 0
- pop_rdx
- 0
- syscall
- 首先在任意一個可讀可寫,又不會影響到程式執行的地方寫入
/bin/sh\x00
- 接著利用
ROPgadget
找出需要的 Address 即可
- 最後腳本
dubblesort
Recon
- 不知道為什麼 Ubuntu 18.04 沒辦法執行該題目,因此該題開始環境改用 Kali 2022.3
- 該題目有附 libc,有可能需要 Ret2Lib
- 先執行程式看看流程
- 詢問名稱
- 詢問要輸入幾個數字
- 依序輸入數字們
- 吐出排序好的數字
- 使用 Ghidra 分析一下
- 名稱讀入
0x40
的大小資料
- 接著用 int
n
紀錄接下來有幾筆數字
- 用一個空間為
8
的 array 去存每個數字,方法是用一個 pointer 指向開頭,每次掃完就將 pointer 往後一格
- 接著我們進入排序的部分
- 參數有兩個
number_array
與 input_n
- 先印出
Processing......
字串後,sleep 一秒假裝計算
- 如果
input_n
為 1
,直接結束
- 接著掃過 array 一輪,每次比較兩個數字,將數字大的往後移
- 然後針對前
n - 1
個數字重複做,直到只剩一個數字便排序完畢
- 最後就將 Array 依序印出,結束程式
漏洞利用
- 可以利用的點有
- 名稱沒有用
\0
截斷也沒有判斷長度
- 數字數量可隨意輸入
- 因此 stack 是可以控制的,來確定一下程式防護

- 可以看到具有 canary 保護,因此沒辦法隨意覆蓋
- 那麼我們有兩個需要解決的點
- 如何洩漏目標 lib address
- 如何 bypass canary
- 首先利用名稱的部分洩漏出 libc address
- 先洩漏出 GOT Address,再透過計算位移量來得到 libc base address 和其他所有函數(如 system)的 address
- libc Address = GOT Address - Offset
readelf -S [lib]
- 在本機可以知道 .got.plt 的 address 為
0021dff4
- 而遠端的話則是
001b0000
,使用給定的 libc 即可得到
- 接著透過名稱的地方洩漏 offset
- 先輸入
aaaa
並用 GDB 觀察一下 stack

- 可以看到距離目標相差
20
個 byte,因此總共需要 24
個字元當作 padding
- 其實我不太確定前面那些看起來也像是 address 的值是什麼
- 另外,可以注意到
0xffffcfb0
的地方有個 0a
,那是輸入的 \n
,代表 leak 出來的 address 還要減掉 0x0a
才是正確的 address
- 整理一下目前的 payload
- 執行起來可以看到確實吐給我們一個像是地址的東西,雖然不知道正不正確,但先繼續
- 接下來我們希望利用輸入數字的部分覆蓋掉
RET
的目標 Address,讓程式跳到 libc 中的目標 function
- 我們先隨便輸入
1
2
3
做為要排序的數字,然後利用 GDB 檢查與 ebp 相差多少
- 要多少數字才能蓋到 Return address
- 透過
gef config context.nb_lines_stack [NUM]
改變 stack 顯示的行數
- 透過
context stack
顯示 stack
- 可以看到第
33
個數字才是 Return Address
- 我們先將程式跳回
main
試試看

- 可以看到程式顯示
stack smashing detected
- 這代表該程式包含 canary,只要我們覆蓋掉特定 stack 程式就會錯誤,且我們沒有辦法洩漏出 canary 的值
- 那麼,有沒有辦法只蓋掉想蓋的資料,而保持原先 canary 的資料不變呢
- 試著輸入非法字元看看

- 使用英文字或是
.
都會直接結束導致失敗
- 最後發現
+
可以繼續輸入,並不會改變 stack 上的值

- 應該是被當作正數了
- 原本想要先 Return 回
main
測試看看,但因為數字會被排序,如果 RET 的目標 Address 數字比 Canary 小的話,會導致 Canary 被排序到不同的位置,造成 stack smash
- 即便使用了
+
維持 stack 值,也要記得不能排序動到
- 同樣的,我原本想要偷懶將前面的數字全部輸入
+
,這樣就不用找 Canary 在哪裡,但因為 stack 原值不是照大小排序,一樣會動到 canary 導致錯誤,只好找 canary
- 但我不會找,手動一個一個嘗試
- Canary 位於
25
格的數字
- 所以我們的目標應該是
- 1~24: 1(或是其他小數字,不可大過 canary)
- 25:
+
- 26~33:
libc_addr
+ system_addr
- 34:
libc_addr
+ bin/sh_addr
- 由於
bin/sh
剛好比 system
數字還要大,不會受到排序影響
- system 與 bin/sh 可以直接用 pwntools 的功能取得
libc = ELF("./libc_32.so.6")
system = libc.sym["system"]
binsh = next(libc.search(b'/bin/sh'))
- 實際測試之後,本機一直無法成功,應該是因為 link 到不同的 libc 的原因
- 然而連遠端測試都無法成功,只好一個一個部分手動除錯
- 發現最開始輸入名稱的部分需要
28
格才能 leak 出 libc address
- 然後最後的 Return Address 位於第
34
格(總共需要 35
個數字)
- 最後 exploit:
Silver_bullet
Recon
- 先執行看看程式,看起來像是一款遊戲
- 能夠執行的動作有四種
- 接著用 Ghidra 分析看看
- 建立子彈時會讀入
0x30
的空間,字串多長力量就多少
- 力量會被放在
bullet + 0x30
的位置
- 也就是
bullet
描述的後面一格
- 增強力量時,
bullet + 0x30
需要小於 0x30
- 讀入一個
0x30 - 目前力量
長度的字串
- 將該字串用
strncat
接到 bullet
上面
- 戰鬥就是將血量減去力量
- 離開就離開
- 另外,本程式的讀入是使用自己寫的
read_input
- 重點在於如何利用
strncat
的特性,在串接完之後會在後面補上一個 \0
來攻擊
- 由於
strncat
參數並沒有多留一格的空間放最後的 \0
,這導致當字串串接剛好塞滿時,會發生 overflow

- overflow 時,多出來的
\0
會剛好蓋在 bullet + 0x30
上面,這導致了判斷力量的依據也壞掉,可以無限寫入
- 最後,檢查一下程式的保護權限

- Full RELRO,因此考慮使用 Ret2libc
- 檢查一下是否存在 one_gadget
漏洞利用
- 整體思路是取得無限寫入之後,leak 出 libc Address,然後跳到 one_gadget 上
- 先利用
strncat
的漏洞讓 bullet
可以無限寫入
- 先輸入
47
個 a
- 再 power up
1
個 b
- 接著要覆蓋掉 return address,先用 GDB 確認一下 padding

- padding =
7
- 接著我們 leak 出 puts 的 GOT Address
- 預期 stack 長這樣
- padding(
7
格)
puts@plt
(return address)
main
(return address for puts
)
puts@got
(leak address)
- 先測試一下能否正確 leak 出 address
- 執行之後發現又回到選單了,回去檢查一下 Ghidra 後,發現只有打倒狼人時會觸發
return
- 但由於代表力量的那格空間我們也能覆寫,因此在 padding 時用大一點的數字即可
- 執行戰鬥之後可以發現
- 成功打倒狼人
- 成功回到 main 了
- 印出了 Address
- So far so good,我們已經取得 puts GOT address,能夠計算出 libc address,接著我們再重複一次剛剛的動作,這次將 return address 寫成 one_gadget 即可
- 再次測試
- 執行後失敗了,但用 GDB 檢查後,程式確實已經執行到想要的位置(
execl
)

- 這可能代表本機的 one_gadget 前置條件無法達成,直接遠端看看
- 將 libc 與 one_gadget 換成遠端版本後…

- 成功 get shell!
hacknote
Recon
- 先執行看看流程
- 執行後出現菜單
- 新增筆記
- 刪除筆記
- 印出筆記
- 離開
- 換到 Ghidra 看看
- 程式被 strip 過,從 entry 找到
main
後開始分析
main
的整體架構跟上一題 silver_bullet
有點像
- 新增筆記的部分
- 最多存在
5
個筆記,超過就會印出 Full
- 如果沒有宣告過空間,建立
8
的空間在 DAT_NOTE + i * 4
的位置
- 前四 bytes 放入一個
puts
的 function address
read
筆記大小 size
,最多 8
個 bytes
read
一個 size
大小的空間,放到後四 bytes
- 在後四 bytes 這裡
read
內容
- 刪除筆記的部分
read
一個 index
- 如果
DAT_NOTE + i * 4
含有資料,free
掉前四 bytes 和後四 bytes 宣告的空間
- 沒有清空資料,沒有修改筆記大小
- 印出筆記的部分
read
一個 index
- 呼叫 note 前面的
puts
印出後面的內容
- 刪除不會修改筆記數量,因此我們最多只能 add note
5
次
- 刪除沒有清空資料,因此有機會可以做 UAF
漏洞利用
- 攻擊思路
- 先利用 UAF,修改
puts
後面的資料使其 leak 出 libc address
- 再利用一次 UAF,修改
puts
address 去 system
- 首先創造兩個筆記,創造時包含了 puts_addr 與 content,因此共有四個 chunk
- puts_addr 的 chunk 大小固定為
0x08
- content 的大小要故意與
0x08
不同,這邊使用 0x20
- 接著將兩個筆記都刪除,因此四個 chunk 進入 bin 中
- 兩個是 puts_addr 的(
0x08
),兩個是 context 的(0x20
)
- 再次新增筆記,這次大小故意使用
0x08
,分配 chunk 時就會將原本是 puts_addr、內容沒有刪除乾淨的 chunk 分配過來
0x08
的第一塊一樣被拿去當 puts_addr 了,另一塊是存放 content
- 因為是 fast bin 是 LIFO,所以第一塊的 index 會是
1
,第二塊會是 0
- 而我們可以在這邊寫入
puts
的 GOT(其實也是可以用其他的 GOT,但習慣用 puts
了)
- 記得前面的 puts_addr 要放回去,這樣 printNote 的時候才會觸發
- 印出筆記
0
,取得 GOT,計算進而得出 libc base address
- 因為
0x08
的 chunk 被我們用完了,我們二次攻擊前要再刪除一次筆記
- 直接刪掉最後新增的筆記
2
,因為他的兩個 chunk 都是 0x08
,且因為 free 的順序,具有 puts_addr 是第二個
- 然後再寫入一次
0x08
的筆記,內容為利用 libc base address 計算出的 system()
,並在後面接上 ||sh
得到 shell
- 寫入的地方變成原本放 puts_addr 的地方了
- 最後再次印出筆記
0
,得到 shell
Tcache Tear
- 想說上一題寫了一個 heap 相關的,這次也來一題連著寫
Recon
- 執行程式看看流程
- 要求輸入名稱
- 出現選單
- Malloc
- Free
- Info
- Exit
- 改用 Ghidra 分析
- 程式被 Strip
read
0x20
的大小到 DAT_name
read
名稱的部分使用自己寫的 function
read
選單的不是也是另外的 function
- 讀入
0x17
的資料,轉成 long long
- 後面寫成
readll
,以跟前面的區分
Malloc
的部分
size
也是用 readll
讀入
- 當
size
< 0x100
才會繼續
- 先分配
size
的空間到 DAT_allo
- 這導致了每次呼叫
Malloc
時,DAT_allo
會指向不同地址
- 用
read
讀入 size
- 0x10
的空間
Free
的部分
- 將
DAT_allo
free
掉
- 最多只能
Free
7
次
Info
的部分
exit
的部分
- 比較明顯的漏洞是
Free
的部分完全沒有任何防護或判斷,因此存在 double free 的漏洞
- 另外,清空 pointer 後沒有清空,因此可以做 UAF
漏洞利用
- 攻擊思路為先 allocate 一次空間後,
free
兩次,使其在 bin 中指向自己
- 接著再次要空間後修改 fd,使之後拿到的空間為指定的空間,達到任意空間寫入
- 執行後再輸入
3
看 Info,可以發現名稱已經被改為 ouo
了,代表任意寫入成功
- 值得注意的是,在本機因為 libc 版本過新,會跳出 double free 的錯誤

- 可以透過在中間 free 其他塊來 bypass
- 我們已經能夠任意寫入了,但存在一些限制
- 因為
free
只能 7
次,因此寫入次數是有限制的
- 再來,我們的目標是建構出 RCE 的流程
- 拿到 libc address
- 取得 shell
- 對於 libc address,由於我們整個程式可控的 output 只能透過
info
,因此我們的目標是使 DAT_name
存在能洩漏 libc 的資訊
- 而這個 libc 的資訊,我們利用 bin 中的 linked-list 會指向 arena 的特性來取得
- bin 我們使用 unsorted bin 來利用,因為其他是單向的
- 也就是說,我們希望將一個 chunk 故意丟入 unsorted bin 之中,然後我們讀取 chunk 中的 arena address 來計算 libc address
- 要將 chunk 故意丟入 unsorted bin 之中,使用到 house of spirit 的技巧,這必須符合幾個條件
- fake chunk 位於
DAT_name
上面,這樣我們才能利用 info 把資料印出來
- chunk size >
0x408
,這樣才不會掉到 tcache 或是 fastbin
- next chunk 的 prev_inuse 必須為
1
,才不會被合併
- 為了達成以上條件,我們理想中的 heap 應該長成這樣
- chunk 1 – DAT_name
- 0
- 0x501 (size + prev_inuse)
- chunk 2 – DAT_name + 0x500
- 0
- 0x21 (size + prev_inuse)
- 0
- 0
- chunk 3 – 反正就是接在 chunk 2 且長的一樣
- 最後我們再針對
DAT_name + 0x10
的位址再寫入一次,將指標移到上面方便我們 free 掉預先做好的 fake chunk
- 如此一來,
DAT_name
應該就會被我們丟到 unsorted bin 之中,我們可以呼叫 info 將 arena 印出來
- arena addr 可以透過 ghidra 分析 libc 中的
malloc_trim
得到
- 現在已經得到 libc address 了,最後一部是進一步從 libc 之中呼叫到 shell
- 然而我們沒辦法使用 RET,因此沒辦法 Ret2lib
- 這時候就該使用 hook 了
- 常見的有
__malloc_hook
和 __free_hook
- 我們只要去修改這些 hook function 的值,就能夠在呼叫到對應 function 時去替換功能
- 先檢查一下是否存在 one_gadget,如果有就直接將
free
hook 到 one_gadget 即可
- 修改
__free_hook
的方式一樣使用 doublefree 即可
問題
- 比較奇怪的是,我查看 libc 的 Arena offset 應該為
004ed8e0
,但是一直不成功
- 最後看了別人的 write-up,卻發現 offset 為
0x3ebca0
,這部分待確認
applestore
Recon
- 執行程式看看狀況
- 執行後會有 Menu 出現,可以做幾件事情
- 看商品
- 加到購物車
- 移出購物車
- 列出購物車
- 結帳
- 離開
- 一樣換到 Ghidra
- 沒有 strip
main
的地方先分配了 0x10
的空間
- Menu 出現後,讀取了
0x15
的資料作為選擇輸入,並轉為 int
read
的部分有另外寫 function,會將最後一個字設為 \0
- 如果輸入
1 ~ 6
以外的數字,有做處理
- 列出商品的部分是打印出固定字串,沒有利用空間
- 加入購物車的部分
- 讀入
0x15
的 int
輸入
- 根據輸入的數字,
create
不同的商品
- 這邊的重點在於需要還原
Cart
struct 會較好分析
- 名稱、價錢、上一個商品、下一個商品


create
會新增一個 Cart
實體,將名稱、價錢設定,並且將 linked-list 的前和後指向 NULL
- 接著將該
Cart
用 insert
加到 linked-listed myCart
之中
myCart
的開頭是空的,myCart.next
才是第一個商品
- 最後將商品名稱印出來
- 移出購物車的部分
- 讀入
0x15
的 int
輸入
- 如果購物車沒東西,直接 return
- 接著遍歷
myCart
,如果到指定的數字,就將前後商品的 next
與 prev
接到對的位置
- 沒有進行
free
,也沒有清空指標等等
- 列出購物車的部分
- 讀入
0x15
的輸入,如果第一格是 y
才會繼續往下
- 遍歷
myCart
的 linked-list
- 每次印出目前的編號、商品名、價錢
- 計算總和,最後回傳
- 結帳的部分
- 呼叫列出購物車的 function,將總價
sum
紀錄下來
- 如果總價等於
7174
,自動將 iPhone 8 1$
加到購物車
- 離開的部分
- 在這邊明顯可利用的地方在於總額達到 7174 的時候放入的 iphone cart,並不是另外呼叫
create
創造出來的實體,而是直接使用 stack 上的 cart

- 這導致放入後在
myCart
的鍊上存在 stack 的資料,我們便有機會利用該資料 leak 出 libc 的 address
漏洞利用
- 先利用
z3-solver
求出如何剛好達到 7174
元
- 由於 iphone 8 的商品是 stack 上的內容,因此離開該 function 後,鏈上的資料就會壞掉
- 可以在 iphone 8 加入之後,再呼叫查看購物車來驗證

- 進一步觀察,可以看到這筆資料是在
ebp - 0x24
的位置
- 由於新增、刪除、列出購物車等等的 function,都和結帳位於
handle
function 的下一層,因此 stack frame 是同一的位址
- 檢查一下,是否有辦法修改到
ebp - 0x24
的 function
- 在列出購物車的
cart
function 中,負責儲存使用者輸入的 input_choice[22]
這個變數是 ebp - 0x26
的位址

- 向上
22
格的空間是我們可控的,這就包含到 ebp - 0x24
的位址
- 而
input_choice
在該 function 的檢查只有開頭等於 y
會印出資訊,而後面的欄位我們可以任意修改
- 我們將後面修改為
puts
的 GOT,藉此 leak libc address
- 執行後成功取得 libc address

- 可以在 GDB 輸入
info proc mappings
來比對結果是否正確

- 再來我就有點沒想法了,偷偷查了一下別人的 write-up 後才知道接下來的思路
- 洩漏 stack address
- 修改 GOT 表
- 洩漏 stack 的部分比較單純,但我知識量不足
- 在 glibc 之中有個叫做
environ
的全域變數,用於儲存環境變數
- 而程式執行時,環境變數就放在 stack 上面
- 因此該變數其實儲存的就是 stack 上的某個 address(還是存在 offset)
- 這邊看別人 write-up 的時候順便學會
libc.address
這種設定方式,後面就不用自己寫 + libc_addr
之類的
- 上面的 exp 其實還不是正確的,因為我們沒有處理 offset
- 這邊使用 GDB 確認一下印出來的值和實際的 ebp 差多少
- 我暫停在
cart
function 之中,然後查看這層 stack frame 的 ebp 與目前的值差多少


0xffec30cc - 0xffec2fa8
為 0x124
- 再來這邊就更難一點了,我們要如何呼叫到 shell?
- 我們可以利用
delete
內 unlink 的部分進行攻擊,改寫 GOT
- 先看一下
delete
function 的 code

- 可以看到第
30
行後面是在將 temp_cart
這個商品移除時,前後商品的 link 重新設定(unlink)
- 而當輸入是
27
時,temp_cart
在 stack 上且可控,因此這邊的 next_temp
與 prev_temp
通通可控
- 我們就可以將其中一個改寫成 GOT 中想要修改的 function,另一個改為 ebp 的值,使他們互相覆寫資料
- 需要注意的是,根據
cart
的資料結構,next
和 prev
都有一段 offset,需要計算過後才能設計出預想的攻擊
- 那麼要修改什麼值呢?我們回到
handle
看看

- 可以看到第
15
行使用到 atoi
,且上面的 my_read
可以幫助我們寫入資料去控制 stack 的內容
- 因此我們可以嘗試建構出一個攻擊思路
- 將對應到
input_choice
的 stack 區段改寫為 got['atoi'] - offset
- 這裡的 offset 為
input_choice
與 ebp 的距離
- 再利用
my_read
將其改寫為 system()
- 遠端的部分需要修改幾個地方
- ENVIRON_OFFSET 會改變
我可能得找個時間研究 elfpatch
- 搞定了,請查看最上面
環境
的部分
- 更換之後確實 offset 改為
0x104
了
- 另外一點是
system(';/bin/sh\x00')
會無法使用
- 最後腳本
babystack
Recon
- 先使用 pathelf 把提供的 libc 更換上去
- 記得是 64-bits 的程式,ld 也要用 64-bits 的
- 執行一下程式
- 沒有 menu,直接要求輸入
1
的話會要求輸入密碼
2
會離開程式
- 換 Ghidra 分析
- 程式被 stripped,從 entry 找到
main
main
的開頭先讀了 Linux 中產生亂數的檔案並儲存起來
- 接著印出
'>'
的字串後讀 0x10
的輸入
- 如果輸入開頭是
1
,進入 login
- 如果是
2
,離開程式
- 如果是
3
,進入 copy
login
的參數為最開始讀入的 random
- 讀入
0x7f
的輸入 input_pwd
,輸入長度為 n
- 比較
input_pwd
與 random 參數的前 n
個字是否一樣,一樣的話將 DAT_login
設為 1
- 離開程式時,如果
DAT_login
為 1
才會觸發 return
,否則直接 exit
- return 之前會檢查 random 是否被修改過,如果被修改過則發生錯誤
- 如果
DAT_login
為 1
才能進入 copy
- 傳入的參數為
copy_dest
,是一個 char *
- 先讀入
0x3f
的輸入,再使用 strcpy
將輸入複製到參數中
漏洞利用
- 第一步我們得先想辦法成功 login,否則什麼也不能做
- 要 login 很簡單,由於只比較輸入字串的長度,因此直接輸入空字串或是使用
\x00
截斷字串就能繞過判斷
- 登入成功後就可以使用
copy
,那這個 function 要如何利用呢
- 首先,我們得知道
strcpy
會直接把整個 source 的內容複製到 copy,即便 dest 空間不夠
- 然而
copy_dest
空間有 64(0x40)
,copy 時只能讀入 0x3f
的資料,看起來似乎無法造成 overflow
- 但仔細觀察 copy 和 login 兩個 function,可以發現兩者接收輸入的地方在 stack 中與 ebp 的 offset 是一樣的
input_pwd
與 str

- 再加上兩個 function 都位於
main
的下一層,因此兩者位於 stack 是同樣的位址
- 因此我們可以在登入時預寫資料,而在 copy 時將預寫的資料複製過去,造成 overflow
- 舉個例子,先使用
\x00 + a*99
等等的 payload 進行登入,在 copy 時隨便使用 b
進行複製,就會讓 copy_dest
後面的資料被蓋成一堆 a
- 因此我們可以從
copy_dest
一路覆蓋掉 main 的 return address
- 以下是一個驗證腳本,證明我們有能力覆蓋掉
random
導致密碼改變
- 我們現在有能力造成 overflow,但我們仍需要解決兩個問題
- 洩漏 libc address,使我們能夠 return to one_gadget
- 洩漏 random,使我們 copy 時不會修改到 canary
- 先解決 random 的問題,因為比較單純
- 回去看一下 login 可以發現,比較的長度是根據使用者輸入而改變
- 因此能夠利用
0x00
作截斷,一次只比較一個 byte,而不比較整個 random
- 那麼我們就能夠使用逐字爆破法,一個一個 byte 將整個
random
爆出來
- libc address 的部分,我們可以看看 stack 上是否存在可利用的 libc function address,想辦法洩漏出來之後再算出 offset
- 我將 GDB 設斷點在
strcpy
的地方,以下是 stack 的樣子
- 可以看到 stack+0x58 的地方有個
0x007ffff7878439 → <_IO_file_setbuf+9>
,看起來就很像是 libc 中的 function address
- 這邊可以做點驗證
- 我們知道
libc_addr + _IO_file_setbuf_offset + 9
= _IO_file_setbuf_addr + 9
- 從上圖可以知道
_IO_file_setbuf_addr + 9
= 0x007ffff7878439
- 使用 vmmap 看到
libc_addr
= 0x007ffff7800000
- 然後利用 pwntools
print(hex(libc.sym['_IO_file_setbuf']))
可以得到 _IO_file_setbuf_offset
= 0x78430
- 帶回去式子成立,代表的確利用該 function address 可以算出 libc_address
- 我們現在知道 libc_addr 可以利用 stack 中的資料取得了,問題是該如何 leak 出來
- 再回去看一下 login 可以發現,比較的長度是根據使用者輸入而沒有針對
random
去做 0x10
的上限控管
- 因此使用者輸入即便超過了
0x10
,依然會將 stack 上 random
後面的資料作為密碼繼續比較
- 而我們可以利用 copy 將前面的資料全部覆蓋成
a
,順便將想要 leak 的資料 copy 到 main
的 stack 上面
- 然後到要洩漏的資料時再一樣使用逐字爆破把資料印出
- 最後一步,我們需要再次使用 copy 的功能,將 leak 出來的 random 覆蓋回去,並且一路蓋到 return address 的地方,將 one_gadget address 蓋回去,即可拿到 shell
main
stack 布置
- copy_dest: 第一格
0x00
,剩餘隨意 0x40
- random: leak 出來的值放回去
0x10
- padding: 隨意
0x18
- target addr: one_gadget address
- 值得注意的是,如果逐字爆破時遇到原本的值就等於
0x00
的話就會爆破失敗,因此有可能需要多跑幾次腳本
- 再加上遠端測試的時間差,可能整個跑完要五到十分鐘
- 整個跑完後再輸入
2
觸發 return 即可得到 shell