Hackme CTF 練習站的一些解題心得與提示
這篇文章是用來記錄一些我解 Hackme CTF 上一些題目學到的一些東西,不會直接寫出完整的解法。主要是為了我以後能比較容易記憶起這些東西,當作個筆記而已。
內容會根據自己的解題進度慢慢更新,最後更新 2021/01/29。
Misc
flag
和提示說的一樣,用 Regex 找一下就好了。不過要注意一下
FLAG{...}
中括號裡面是不會有一些特殊符號的。
corgi can fly
就按照它的說明,用 stegsolve 去找就可以了。
television
打開 HxD 搜尋一下,或是直接 strings
尋找也可以。
big
用 file
檢查一下檔案格式,然後解壓縮。它一共兩層,第二層有
16GB,注意一下。最後的檔案一樣用 HxD 打開搜尋,或是 strings
也可以。
encoder
按照它給的 encoder.py
裡面的邏輯,把它反過來解就好了。這題用 Python 2
比較好解,因為可以直接複製它原本就寫好的函數小改一下就好了。
slow
這題回應速度很慢,不過經過不同的測試會發現它的速度會根據不同的輸入有差別,例如
FLAG{
和 ABCD{
相比前者會比較慢,所以這個可以用 timing attack:
1 | const { Socket } = require('net') |
暴力破解完需要花將近 10 分鐘的時間,就有耐心的等一等吧。
用 node.js 的原因是因為寫這種平行發 request 的程式比較容易
pusheen.txt
注意一下貓的顏色。
drvtry vpfr
這個看起來就是 substitution cipher
類的東西,但是一開始也沒什麼頭緒,所以就把題目名稱丟到 Google
上,結果得到的是查詢 secret code
的結果...
我就想說 Google 是有什麼奇怪的解碼功能嗎? 所以把加密過的 flag
也丟上去搜尋,卻沒有結果。所以我就想知道這到底是什麼 encoding
方法,但是在 Google 上都查不太到資料,因為都會被修正成
secret code
。所以就換使用 DuckDuckGo
搜尋一樣的關鍵字,就有看到一些比較有趣的結果,像是有整個使用這種方法寫的網站都有,還有一些人在各種論壇上使用這種語言來傳遞某些訊息。然後找了找就在這邊看到了這種編碼的原理了,就是打字時都往右按一個按鍵。例如輸入
star burst stream
時都按各個字母的右邊那個鍵,所以能得到
dyst nitdy dytrs,
。
所以這樣就能很容易的寫出解碼程式出來:
1 | ch = 'abcdefghijklmnopqrstuvwxyzABCDEFGHJIKLMNOPQRSTUVWXYZ{}' |
K nry upi eo;; frvpfr yjod ,rddshr
BZBZ
一進去它就告訴你 You must be a employee from
bilibili!,所以我們就想想看怎麼試著用員工的帳號登入。Google
了一下會知道他們的格式是
xxx@bilibili.com
,然後輸進去之後會告訴你下一步的提示說
Do you like golang? We use golang for our service and it was
opensourced.。這其實是在說之前 Bilibili
原始碼外洩的那個事件,所以就去找一份別人備份的原始碼 clone
下來,搜尋幾組帳號密碼去登入看看就好了。
我測試成功的 email 是 melloi@bilibili.com
。
Web
hide and seek
檢查原始碼
guestbook
就按照它給的提示,使用 sqlmap 輕鬆解決。
LFI
觀察一下它的網址,有沒有感覺像是讀檔案的感覺? 然後研究一下
php://filter
的用法就可以了。
homepage
console 是你的好朋友。
PS: 那個很奇怪的 JavaScript 檔案是由 aaencode 所編碼過的,不過這個訊息和解題毫無關係
ping
想一想 bash 哪些方法可以在參數中執行指令? $(cmd)
`cmd`
有哪些方法可以讀取檔案 print 出來? cat
head
tail
有哪些方法可以不用打出檔案的全名就能讀檔? *
scoreboard
HTML 原始碼中沒有藏 FLAG,那哪裡還有可能藏資訊呢? 可以想想 HTTP 有什麼東西。
login as admin 0
SQL Injection
它的原始碼告訴了你 '
會被轉換成
\'
,那有沒有什麼方法把 \'
前面那個礙眼的
\
處裡掉呢? 提示: \\
-> \
再來是可以想辦法用 ORDER BY
或是 LIMIT
去讓它選到 admin 的帳號就可以了。
login as admin 0.1
前一題的延伸,用 Union Query 就可以了。
可能用了到的幾個 MySQL 技巧:
1 | SELECT 1,1,column_name,1 FROM whatever_table; # 把 SELECT 的結果變成想要的格式 |
login as admin 1
在 SQL 中,OR 1=1
和 OR/**/1=1
是等價的。
login as admin 3
可以去研究一下 PHP 的 weak comparison 表格,"php" == 0
的結果是 true
。
然後也請記得 JSON 格式是有類型資訊存在裡面的。
login as admin 4
Redirect,秒殺
login as admin 6
extract()
這個函數會把你物件中的
key => value
賦值給 $key
變數,如果有原本就存在的變數的話也會直接覆蓋,像是下面這樣:
1 | $a = 1; |
login as admin 7
也是 php 的 weak comparison 問題,不過這次兩邊都是字串。
看過這個 stackoverflow 的問題後你應該就會了。
login as admin 8
它的原始碼告訴你說它 Session 的處裡不給你看,然後檢查一下 cookie
會看到有 login8cookie
和 login8sha512
兩個,cookie 的部份是 url encoded 的。解碼之後試著複製丟到其他的線上
sha512
時發現複製到的東西只有一小段令我很困惑,然後就仔細看了一下發現它預設的
cookie 裡面有個 %00
,decode 之後自然變成 null
byte,所以系統(Windows)的剪貼簿好像就把它當成結尾了...
我的解決方法是到 SHA512
Online 上面開 devtool,把 cookie 貼進去,然後看看
sha512(decodeURIComponent(cookie))
的值是否和它給的相同,結果也是不出意料之外是一樣的。所以應該只要能自己改
cookie 然後順便更新 sha512 上去就應該能破解了。
看了一下 decoded 的 cookie 的裡面有發現它有個
is_admin
,所以試著把它改成 1 之後更新 sha512
再刷新之後就看到 flag 了。
login as admin 8.1
上題最後得到的 flag 的內容中告訴了你一個關鍵字 object
injection,去查了一下發現原來 php 有一組函數叫
serialize
和
unserialize
,可以把一些資料用一種特別的格式編碼起來,然後那個格式正好就和
cookie 裡面的是一樣的。
所以再稍微讀一下那個 cookie 的內容會發現有個 debug
是個
boolean 變數,預設的值是 0,這就讓我想到一開始看到的 source code
裡面有個判斷 debug=1
的情況會讓它進入 session 的 debug
模式,但它原本卻都說
Debug mode is not enabled
,所以合理推測那個應該就是 debug
mode 的值。
一樣的方法改完 cookie 後順便在網址上加上 debug=1
後就成功進入了 debug mode,但是看一下卻發現內容根本和
show_source=1
的狀態一樣,不曉得是怎麼回事。
之後再繼續檢查一下 cookie 後就發現它還有個
debug_dump
,後面跟著 index.php
,感覺就像是指定
debug mode 要輸出的檔案,所以試著把 index.php
改成
session.php
後就看到原始碼了。
改字串的時候記得順便把字串的長度改一改,不然它 unserialize 會失敗,例如
s:9
->s:11
原始碼簡單看了一下會發現它只允許你讀那個目錄之下的檔案,然後裡面也沒有
flag,那我就再改一次讓它去讀
config.php
,就看到了上一題和這題的 flag 內容了。
dafuq-manager 1
guest 登入後可以下載到網站的 source
code,建議要讀一下,然後它的提示也告訴你了改 cookie,所以把 cookie
show_hidden
改成 yes
就能找到 flag 了。
內心 OS: 那個 source code 讀起來很痛苦... 各種全域變數...
dafuq-manager 2
上一題已經告訴你 flag 是要想辦法登入為 admin 才有的,所以要想辦法找到
admin 的密碼。而帳密實際上就存在 .config/.htusers.php
裡面,不過原始碼版本的沒有寫,所以需要去讀讀看那個檔案。
讀原始碼的 fun_edit.php
中的 edit_file
函數,會發現到關鍵在於 get_show_item
這個函數。不過你上一題改的 cookie 讓你的 $item
以
.
作為開頭,所以就能讀很多的檔案了。
接下來還要發現到原始碼裡面有個 data
資料夾,就能看出結構來,所以就在 edit 頁面給
item=../../.config/.htusers.php
就能讀到使用者資料了。取得
admin 密碼的 hash 之後去查表就能找到是
how do you turn this on
了,推薦 CrackStation。登入後就能得到 flag
了。
dafuq-manager 3
上一題告訴了你要去網頁資料夾的 flag3
找,還說要用 shell
才行,所以搜尋一下 exec
找到 fun_debug.php
就是這次的關鍵了。參數的 action
改成 debug
就能呼叫這個檔案。
do_debug
函數中有下方幾行,這個部分要靠
strcmp
在 $dir
不是字串的時候會回傳
0,所以傳個 dir[]=1
就搞定了。
1 | $dir = $GLOBALS['__GET']['dir']; |
接下來後面就是用 base64 和 hash 去執行指令了,用下方的 php 就能達成了。還有要記得 php 中字串是可以作為函數來呼叫的,所以可以繞過它的函數黑名單。
1 |
|
wordpress 1
在文章列表中可以找到一篇叫 "Backup File" 的文章,裡面有 source code
的 Dropbox 連結,先下載下來。接下來我用 vscode 打開,搜尋
flag
然後仔細找找看有沒有可疑的地方,然後也真的有。
你會在 wp-content/plugins/core.php
發現到可疑的東西。密碼的部分 md5 查表可以找到
cat flag
,不過輸進去之後了還是不行,因為它說只能從
127.0.0.1
瀏覽。但你可以檢查一下
wp_get_user_ip
這個函數,它會從
$_SERVER['HTTP_X_FORWARDED_FOR']
讀 ip,所以加個
X-Forwarded-For: 127.0.0.1
的 header 就破解得到 flag
了。
wordpress 2
上題的 flag 內容告訴了你範圍在主題裡面,所以也只能在裡面慢慢找有沒有可疑的地方。(我是有去查別人 writeup 的提示,因為懶...)
目標檔案是
wp-content/themes/astrid/template-parts/content-search.php
(搜尋頁面),裡面有個很可疑的一行:
1 | <!-- debug:var_dump($wp_query->post->{'post_'.(string)($_GET['debug']?:'type')}); --> |
這個相當於取得目前這個 post 的 post_*
內容,如果有
debug
參數就取 post_{debug}
,否則是
post_type
。
不過我們要怎麼知道要取得什麼呢?
如果你搜尋空白字串,可以在第二頁找到一篇加密的文章叫
FLAG2
,然後我們再參考一下官方的
post
物件就能發現有 post_password
這種東西。所以我們的目標 url 就是:
https://wp.hackme.inndy.tw/page/2?s=&debug=password
了。
取得密碼後輸入進去,就能取得 flag 了。
webshell
檢查一下原始碼,然後想辦法改一下 php 就能得到它後台的 php
原始碼了,然後就針對它的碼去生成對應的 query string
就好了。不過要注意一下 (a^b)^b=a
,以及它到底在 hash
的是哪個變數。
1 |
|
還有注意一下 flag 藏在隱藏檔裡面。
command-executor
首先就是先探索一下整個網站有哪些功能,然後很快的就會發現說網址的參數
func
似乎是和檔案名是有關連的,所以可以測試一個
func=ls
,然後也確實會有個方便的 ls
工具能方便你探索整個系統。然後在檔案系統裡面探索後,很快就能找到根目錄下有幾個和
flag 有關的檔案。只是我們目前只有列出檔案的權限,還沒有方法去讀檔。
其實倒退一步回到剛剛的 func
參數,合理懷疑它其實和
include 有關,例如 include("$func.php");
之類的。既然是這樣,可以用前面題目用過的 LFI 去試著讀檔看看。例如
func=php://filter/read=convert.base64-encode/resource=ls
可以讀到 ls.php
的內容,所以也能讀 index.php
的內容。
接下來看到 index.php
的內容裡面會先發現它有個
waf,專門擋 flag
和 ()\s*{\s*:;\s*};
這樣的東西。從這兩個其實可以看出大概是想擋 shell 的。
然後再往下會看到一個很關鍵的一段:
1 | foreach($_SERVER as $key => $val) { |
那個 putenv 看了就相當可疑,去搜尋一下 putenv php
exploit 這組關鍵字之後會找到一個名詞叫 ShellShock,可以看到說它是一個
bash 早期版本的
bug,然後會從環境變數中執行函數,而且維基百科那篇中也有
payload:
() { :;}; echo vulnerable
,可以發現到前面那一段和上面 waf
的那個符合。
接下來要執行 shell 就簡單了,因為上面的那個 regex
實際上蠻容易繞過的,像是我用 () { _:; };
可以繞過,所以送像是 X-C8763: () { _:; }; echo hello
的
header 就能成功執行 shell 指令。
有時如果執行指令沒有輸出時,可能是那個指令輸出到 stderr 了,加個
2>&1
就好再來不知為何像是
cat
的指令無法直接起作用,需要使用/bin/cat
才行
接下來當然是去讀讀看 /flag
檔案,只是會發現說權限有問題,只有 root 才能讀,所以讀讀看旁邊的
/flag-reader.c
,會得到一個 C 的程式。那個程式會隨機產生 16
bytes 的資料然後輸出出來,你需要把那 16 bytes
輸入進去,只要一樣的話它就會從 argv[1]
中用 root
權限(setuid
)讀檔後輸出。所以我們的想法很簡單,就是想辦法把
stdout pipe 回去 stdin。而這個做法也不難,就
program > file < file
就搞定了,還能把後來輸出的 flag
給寫到檔案裡面。
只是問題是會發現說沒辦法寫入檔案,因為沒有權限。這種時候如果熟悉點應該能很快的想到說
/tmp
或是 /var/tmp
比較有可能有寫入的權限,所以用前面的 func=ls
檢查一下就會發現說 /var/tmp
確實可寫入,還有其他人在這邊寫入過的紀錄。所以寫入之後把檔案讀出來就能看到
flag 了。
xssme
註冊完之後會收到一封來自 admin 的信說
I will read all your mails but never reply.
,這代表它真的會打開你寄給他的信來閱讀的,這就有觸發
XSS 的機會。(實際上應該是用模擬的,headless browser 之類的)
關於 XSS 的觸發 tag 建議參考一下這份資料,因為它會擋一些關鍵的單字如
<script
onerror
之類的,不過只要拿那裡面的其他 payload 就可以了,例如我測試到
<svg/onload="">
是可以的。
然後你可能還需要一個伺服器來接收 XSS 之後的 request,想辦法讀資料。我是用 codesandbox 提供的服務,在上面弄個簡單的 node.js express server 把收到的 path 給輸出出來。
然後之後就用 location.href='https://YOUR_SERVER/test'
之類的東西,就可以了。如果遇到它告訴你遇到禁止的字元的話就用 html
entities 轉譯處裡,例如這個。
而這題就只要取得 admin 的 cookie (document.cookie
)
就好了,所以就在 url 後面接上
document.cookie
,也能選擇性的把它編碼(btoa
encodeURIComponent
)過之後再傳。
得到 cookie 後就能直接在裡面找到 Flag 了。
xssrf leak
這題是 xssme 的延伸,要你想辦法從某個 php 的原始碼中找到 flag。
上題所得到的 cookie 中有包含
PHPSESSID
,但你實際上使用這個是無效的,因為它有限制只能從
localhost 存取 admin。就算你使用 X-Forwarded-For
去改也是無效的。
先再看一次題目名稱看看,xssrf
中的 ssrf
指的是 Server-side request
forgery,就是想辦法讓對方在內網中發送一些 request
然後取得內容。
以這題來說,我們可以透過 Ajax 去抓取它網站上的資料。不過它模擬 JS
的軟體應該是很舊的樣子,fetch
() => {}
XMLHttpRequest.prototype.onload
一個都不支援,所以只能像是下面那樣用古老的寫法來做 Ajax。
1 | xhr = new XMLHttpRequest() |
之後我們會先取得 admin
看到的信件頁面原始碼,會在裡面的選單發現到它有多出一些頁面,如
setadmin.php
request.php
之類的。然後就用一樣的方法去取得那些頁面的原始碼,然後猜測它的功能並測試。之後會發現
request.php
頁面似乎是個可以對外部發送 request
的頁面(可以自己測試看看),有可能是用 file_get_contents
實作的,所以搞不好可以拿來讀檔案。
不過這時會不曉得該讀哪個檔案,所以我只好去查了一下關於這題其他人的解法,發現到還有
/robots.txt
可以查看。然後就在裡面發現到原來還有個
config.php
,那八成就是我們的目標了。
然後我們要讀到檔案的方法可以讓它的 url
參數塞
./config.php
之類的,不過發現到沒有效果,那就還有
file://
格式的 uri
能用,不過這個格式只能為絕對路徑,所以得猜一下檔案目錄是在哪裡。而這個的解答實際上就是
/var/www/html/config.php
,算是個還蠻常見的目標(其他的題目好像也都放在同個名稱的目錄下)。讀取到那個檔案之後就在裡面找到
Flag 了,這題就這樣破解了。
PS: 你實際上還可以再讓它讀讀看
request.php
,發現到它原來是用shell_exec
curl
escapeshellarg
實作的,所以真的只能用file://
格式的才能讀到檔案。
Reversing
helloworld
把執行檔丟進反編譯的軟體裡面,找到 main 的地方之後很快的就能找到它的 magic number 了。
simple
ltrace
, Rot cipher
和
Ascii
。
passthis
一樣先丟進反編譯器裡,然後透過搜尋 output
找到它的主程式。接下來會發現它會讀 input,然後先檢查第一個字元是否為
0x46 (F)
,如果是的話就繼續按照某個奇怪的規則檢查下去。這時我就弄個個迴圈給他試下一個字元是什麼,結果就發現第二個字元是
L
,所以合理推斷可能是 FLAG{
作為開頭,輸入進去他也說是對的。所以就寫個簡單的 python
程式直接暴力枚舉出 flag 就好了。
1 | from pwn import * # pwntools |
pyyy
把下載的 pyc
檔丟給
decompiler,然後在要求輸入的地方稍微改一下,然後執行後就會輸出 FLAG
了。
accumulator
反編譯之後看一下會發現它先讀輸入,然後計算它的 sha512,然後分別對 hash 和原字串呼叫個檢查用的函數。而檢查函數中會和記憶體某塊的資料比較字串的前綴和,所以把那塊記憶體 dump 出來然後用相減的方法還原回去就好了。
Dump: dump binary memory data.bin 0x601080 0x601398
(gdb)
1 | import os |
這樣就能得到 sha512 的值和 flag 了,不過我倒是不曉得為什麼要 sha512,因為它明明就有明文的前綴和了。
GCCC
用 IDA 開啟後會看到它的一些函數名稱很有 C# 的樣子,所以推測是 C# 的程式。反編譯的話推薦使用 dotPeek,JetBrains 的產品。
下載完丟進去後就可以很清楚的看到它裡面的邏輯,讀某個值進來後做一些處裡,然後看看有沒有符合條件,最後生成 flag 出來,所以問題就剩下怎麼解那個值出來。
解這個可能有其他的方法,不過直接用 z3 上去解真的比較簡單:
1 | from z3 import * |
值解出來之後可以自己再用同樣的算法把 flag 算出來,或是直接打開那個程式把值輸入進去就有 flag 了。
ccc
反編譯之後會發現它有個 verify
函數,做的是就是計算
crc32(input[0:3])
, crc32(input[0:6])
一直到
crc32(input[0:42])
,然後每次都會和預先有的一個
hashes
陣列比較。因為每次只增加三個字元的緣故,可以暴力破解,每次枚舉三個字元就好了。大概
98^3*14
次 crc32
計算就能搞定。
1 | from binascii import crc32 |
有些時候搜尋範圍小的時候直接暴力比較簡單,像我還有試過用 z3 寫,結果弄不出來,還是用簡單的枚舉法。
bitx
這個程式一執行就叫你把 flag
當做參數傳進去,然後它會檢驗是不是對的。反編譯後也會看到它呼叫
verify(argv[1])
,而 verify 就是關鍵的檢驗函數。
verify
用 IDA 反編譯出來的結果如下:
1 | int __cdecl verify(int input) |
你會發現它會從一個奇怪的記憶體位置 134520896
讀取值,這個轉成 16 進位是
0x804a040
,然後查看那塊的記憶體就會看到有 42 byte
的資料在那邊,可以先存起來。
然後繼續看 loop 就會發現它一次讀一個字串,然後判斷你的 input
和那塊記憶體所存的資料做運算和比對。而那行判斷式如果用比較簡單的寫法就像這樣:
input[i]+9 != (((data[i] & 0xAA) >> 1) | (2*(data[i] & 0x55)))
,明顯是個簡單的條件。這應該是可以直接枚舉
input[i]
出來,然後能得到 flag,不過我是直接用 z3
下去算比較簡單:
1 | from z3 import * |
2018-rev
這個就想辦法用它的參數給就好了,像是 argv
envp
都好處理:
1 | const cp = require('child_process') |
然後它會跟你說需要在正確的時間執行才行,所以直接改時間就好了
sudo date -s '2018-01-01 00:00:00' && node 2018.js
。
我不知道為什麼 libfaketime 沒有效果,所以還是只能改時間
what-the-hell
執行後會發現要輸入某個 key
才能得到 flag,然後 Decompile
之後會發現這個程式的流程大概像是這樣:
1 | int main() { |
calc_key3
裡面先有個 if 判斷 p1
,
p2
是否符合一些條件,然後再開始計算 key,否則返回
0。而那些條件可以用 z3 快速解開:
1 | from z3 import * |
然後條件成立之後它會有個奇怪的迴圈慢慢算 count 次數,然後每次都會檢查
what(cnt) == p1
是否成立,成立的話則返回
cnt * p2 + 1
作為 key
。不過
what(int)
是個執行非常耗時的遞迴函數,但也不用擔心,因為實際上看一下
what
後會很輕易的發現到它其實是 fib
...
這邊我一開始還用 python 寫,不過算出來的 key 並不對,後來就改用 C 寫了,可能是 Overflow, Signed, Unsinged 等等的問題...
所以我最後把 calc_key3
改寫後再把前面算出來的
p1
, p2
和起來就能算出 key
了:
1 |
|
接下來取得 p1
, p2
, key
之後就要想辦法讓它執行了,但是 decrypt_flag
有點複雜,重寫搞不好問題還比較大,所以替代方案就是想辦法在原本的 binary
讓它按照我的值去執行,跳過 calc_key3
。只是要修改 binary
大概不是件簡單的事,至少我不會修改...
所以另一招就是使用 gdb,用 gdb 開啟後先在 calc_key3
的地方設置斷點,然後執行時按照格式輸入 $p1-$p2
就會進到斷點,這個時候根據 decompiler 的結果知道說
calc_key3
的回傳值是存在 eax
裡面,所以就
set $eax = key
然後再
ret
,接下來讓它繼續執行就會 print 出正確的 flag 了。
mov
打開來看會發現它一大堆 mov,我猜應該是用 movfuscator 之類的東西弄的,所以就找了 demovfuscator 來試著處裡,不過不知是什麼原因都一直回報錯誤,沒辦法成功 deobfuscate。不過其實它的輸入是只要是前幾個字元完全正確就會回 Good,不然就回 Bad,所以可以直接暴力破解:
這個程式和我破 passthis 那題的程式基本上是一樣的
1 | from pwn import * # pwntools |
a-maze
反編譯之後可以很容易的看出它會針對讀進來的 map 資料做一些運算直到遇到
-1 為止,而它最關鍵的一行是
LODWORD(val) = *(_DWORD *)(map_data + (val << 9) + 4LL * (*input & 0x7F));
。
這邊可以看出說它讀 data 的 index 的 2~8 bits 是 flag 的字元,然後 9
以上的 bit 都是值,所以我們可以讓它反過來做就好了。
不過有個需要注意的地方是它在讀取時是讀 DWORD
的,所以讀資料的時候要讀成 int,但是在處裡 index 的時候都要乘 4:
1 | import os |
esrever-mv
執行時它會問你 flag 是什麼,然後判斷 flag
是否正確。不過我在測試的時候發現了一個神奇的事,就是如果我輸入
abcde
,那程式會告訴你 flag
是錯的並且退出,**然後下一行則出現了 bcde
的 command
執行結果(command not found)。用 strace
檢查一下會發現它是用
read
函數來讀的一次只讀一個字元,只要錯誤就會直接退出,所以剩下的東西還留著,就自然變成
shell 的指令了。經過測試也發現說如果輸入
FLAG{asd
,它就會在退出並且出現 sd
的指令結果(command not found)。
既然是如此,我們只要輸入 flag 的 prefix,然後判斷程式有沒有退出就能知道這個 prefix 是不是對的了,然後再一個一個字母爆破就好了,輕鬆簡單。
1 | const cp = require('child_process') |
話說這題用 IDA 打開後我不知道怎麼處裡...
termvis
這題真的很難,花了我不少時間才解出來...
先執行一下會發現說他能在 terminal 中 print 出 png 圖片來,不過用他給的圖片測試會發現除了顯示以外它還能 print 額外的訊息,還能要求你輸入東西進去。所以合理推測那些圖片裡應該有藏小程式在裡面,然後 termvis 的本體則是有藏個簡單的 interpreter 來執行它的程式。
丟進 IDA 之後會看到 main 的最下面有兩個函數,一個應該是用來關 file
的,另一個點進去之後則會發現說它是個奇怪的函數,有個迴圈會從傳入的指標中讀
byte,然後把那個 byte & 7
,然後它的結果會根據是 0~7
的不同做出不同行為,例如移動某個指標、增加該指標所指的值、和讀取或輸出一個
char 等等的東西。
這個實際上對應到的是 Brainfuck,一個很小的
Turing Complete
語言,所以這樣一切就說了通了,它就是想辦法在圖片中的一個小區塊藏
Brainfuck 的 code,然後用那個函數來執行,所以想找 flag 的話就是想辦法把
checkflag.png
裡面的那個 code 找出來。
我用的方法是用 IDA 的 debugger 在函數的入口下斷點,然後因為參數是從
rdi
傳進的,就去 rdi
的值的那個記憶體位置看,就會看到有一連串的資料了,而第一個 byte 是
F8
。然後再來就慢慢用滑鼠把資料複製下來,還蠻多的,反正多複製的話就之後再清掉。
接下來自己寫個腳本把那些東西一個一個 & 7
再轉換成
brainfuck 就好了,而轉換表如下(可以根據反編譯的程式碼獲得):
1 | 0: > |
轉換完之後可以線上找 intepreter 去確定 syntax 有沒有 error,有的話就從後面開始刪,刪到沒問題為止,所以我就得到了下面的 code:
1 | >+++++++[<++++++++++>-]<.[-]>++++++++++[<++++++++++>-]<++++++++.[-]>+++++++++[<++++++++++>-]<+++++++.[-]>++++++++++[<++++++++++>-]<+++.[-]>++++++[<++++++++++>-]<+++.[-]>,>>+++++++[<++++++++++>-]<<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++[<++++++++++>-]<++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++[<++++++++++>-]<+++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++[<++++++++++>-]<+<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++++[<++++++++++>-]<+++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++[<++++++++++>-]<++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++[<++++++++++>-]<+++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++[<++++++++++>-]<+++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<+++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++[<++++++++++>-]<+++++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++[<++++++++++>-]<+++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++[<++++++++++>-]<+++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++[<++++++++++>-]<++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++[<++++++++++>-]<+<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++[<++++++++++>-]<<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++[<++++++++++>-]<++++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++[<++++++++++>-]<+<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++[<++++++++++>-]<+++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<+++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++++[<++++++++++>-]<+<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<+<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<+++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++[<++++++++++>-]<++++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++[<++++++++++>-]<+++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++[<++++++++++>-]<+++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++[<++++++++++>-]<+++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++++[<++++++++++>-]<+++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<>++++++++[<++++++++++>-]<+++.[-]>+++++++++++[<++++++++++>-]<++++++.[-]>+++++++++[<++++++++++>-]<+++++++.[-]>+++++++++++[<++++++++++>-]<++++++.[-]>+++++++++++[<++++++++++>-]<+++++++.[-]>+++++++++++[<++++++++++>-]<+++++.[-]>+++++[<++++++++++>-]<++++++++.[-]>+++[<++++++++++>-]<++.[-]<[>+>+<<-]>>[<<+>>-]<[->-<]+>[<->[-]]+<[>>>+++++++[<++++++++++>-]<+.[-]>+++++++++++[<++++++++++>-]<+.[-]>+++++++++++[<++++++++++>-]<+.[-]>++++++++++[<++++++++++>-]<.[-]<-<[-]]>[>>++++++[<++++++++++>-]<++++++.[-]>+++++++++[<++++++++++>-]<+++++++.[-]>++++++++++[<++++++++++>-]<.[-]<-]<>+++[<++++++++++>-]<++.[-]>++++++++++[<++++++++++>-]<++.[-]>++++++++++[<++++++++++>-]<++++++++.[-]>+++++++++[<++++++++++>-]<+++++++.[-]>++++++++++[<++++++++++>-]<+++.[-]++++++++++.[-][][]+++ |
這坨不知道要怎麼去分析,不過使用原本的程式去執行
checkflag.png
我們知道說它會讀取輸入,然後根據你的輸入判斷的 flag
是不是對的。然後也能經過測試發現說它會讀取 44
個字元後才會輸出結果,或是也能直接數 ,
得到 44
這個數字。這代表我們沒辦法用暴力破解的方法去算,只能想辦法分析這個程式了。
要分析 Brainfuck 說實在的也根本不知道怎麼下手,不過實際上它可以很簡單的對應到 C 的程式,所以可以寫個腳本去轉換它:
1 | code = '...' # brainfuck 程式 |
首先先簡單看一下會發現它最高只用到 d[4]
的空間,代表它在比較的時候大概是讀進一個字元就比較一次,沒有額外的去儲存,所以可以搜尋
getchar
的地方去研究它的程式。
在第一個 getchar
的地方會看到它先把 d[2]
初始化到某個值,然後計算 d[2]-=d[1]
(d[1]
是我們輸入的 char),然後後面再根據 d[2]
的值跑迴圈。從這邊其實就能猜到 d[2]
搞不好是 flag
的字元,所以可以在適當的地方插入個 putchar
看看,然後測試幾次之後也發現真的是 flag 的字元。
1 | d[1] = getchar(); |
所以只要改一下原先的轉換程式碼讓它自動在適當的地方插入
putchar(d[2]);
就應該能輸出 flag 了:
1 | ptr = 0 |
轉換後把它編譯好,然後輸入 44 個任意字元後 flag 就會自己出現了。
rc87cipher
這題首先經過觀察可以發現 binary 是 UPX 加殼過的,但是因為有被動過所以
upx -d
不能用。我用的是 [原创]ELF64手脱UPX壳实战
裡面的方法把殼成功脫掉的。
脫殼完成後放進 IDA,雖然什麼 symbol
都沒有,但是透過慢慢讀、重命名、觀察它的行為之後還是有辦法讀懂它在做什麼。它首先會從
/dev/urandom
中讀 8 個 bytes 作為 IV,而 IV
再經過固定的算法生成 SBOX。加密的部分是透過 key 和 SBOX
經過一些奇怪的運算之後出來的,不過輸出檔案的前 8 個 bytes 是
IV,後面的才是真正的 ciphertext。
之後可以利用它給的 rc87
和 rc87.enc
試著去一個一個 byte 爆破 key,稍微測試一下可以發現到它的 key 其實就是
flag,所以可以用 dfs 剪枝爆搜找出 key,用 pypy 只要大概 3 秒鐘即可。
1 | def gen_sbox(iv): |
Pwn
catflag
按照上面的指示 nc
過去,然後執行指令
cat flag
就好了,非常簡單。
homework
可以看它的 Source Code,發現到它有個函數會呼叫 shell,然後改 Array 的 Input 也完全沒有檢查,所以目標就是把 return address 改到那個函數去就好了。
用一些工具如 Ghidra 或是 IDA 能很容易的看到 arr
的記憶體位置,例如 Ghidra 告訴我它在 Stack[-0x38]
,所以
return address 就在 arr+56/4=arr+14
的地方。再來是找到函數
call_me_maybe
的位置在 0x080485fb
,所以改
arr[14]=134514171
就可以了。退出之後就會進到 shell,然後用
ls
, cat
把 flag 弄出來就好了。
ROP
NX 是開啟的,然後題目名稱也很明顯的告訴你要怎麼解了,而這題的
ropchain 其實直接 ROPgadget --binary ./rop --ropchain
就能得到了,然後直接放到 ret
的地方就能簡單解決了。不過既然是練習的話還是練習自己寫寫看 rop
比較有意義。
先檢查一下程式裡面有沒有 shell 之類的東西存在,像是找找看有沒有
/bin/sh
的字串,然後很快就會發現沒有,所以只能自己湊。接下來檢查一下有沒有
int 0x80
的 gadgets 存在
(ROPgadget --binary ./rop2 --only 'int'
),然後就發現有,所以可以透過
execve 的 syscall 來取得 shell。
關於 syscall 的表格我推薦這個: Linux System Call Table
之後透過反覆搜尋看看哪些 gadgets 存在之後就能自己寫個 ROP:
1 | from pwn import * |
ROP2
這題一樣是
ROP,會發現說它的讀取輸入和輸出的方法都和之前不一樣,是直接使用 syscall
函數去叫的,如 syscall(3, 0, buf, 1024)
等價於
read(0, buf, 1024)
(參考 syscall table)。至於
syscall,可以作為 ROP 最後一步的目標,用那個函數去叫 execve
就好了。如果想和前面一題一樣使用 int 0x80
去 syscall
的話是做不到的,因為 ROPgadget --binary ./rop2 --only 'int'
找不到東西,所以要 syscall 就要利用原本就有的。
disassemble 一下 overflow()
就能看到那個函數的呼叫法,使用的是 stack
的傳參,對我們來說也輕鬆很多,所以目標就是建構出這樣的 stack(左側是
stack 頂端): syscall_addr 11 binsh_addr 0 0
。然後 binsh
的地方就和上面那題一樣隨便找個位置用類似的方法寫入,然後最後 ret 到
syscall_addr 就好了。
1 | from pwn import * |
toooomuch
用工具直接反編譯,看到它的 passcode 檢查是
code+0x3039==0xd903
,所以輸入 43210
就能進入下一步的遊戲。那個遊戲就是很經典的猜數字,就簡單的手動二分搜就能過了,然後就能拿到
flag。
toooomuch-2
這題是上題的後續,在輸入 passcode 的地方會發現有 overflow
的問題,然後 checksec 檢查一下發現什麼保護都沒開,不過它沒有
system("/bin/sh")
的函數能讓你呼叫,所以自己用 shellcode
就好了。
首先先找到它的 buffer 大小,所以知道 ret 在 input 加上
0x18+4
的位置,然後因為它還有把 input 複製到
.bss
的某個地方,所以讓它 return 到那個位置再加上一些
offset 就好了,最後放上 shellcode 搞定。Payload:
b'a'*(0x18+4)+p32(0x8049C60+0x18+8)+shellcode
(0x8049C60
是複製到的那個位置)。
其實如果有
push esp; ret
的 gadget 的話應該也能直接讓它在 stack 上執行,不過我沒找到。
shellcode 的部份簡單的話可以用 asm(shellcode.sh())
直接產生就好了,不過為了練習還是可以自己寫寫看。像是如果有發現到它還有個
print_flag()
函數裡面有呼叫
system("/bin/cat fake_flag")
的話可以直接改參數去呼叫
system
,不用自己 syscall。
可能要注意的地方是 /bin/sh
的字串在 push 到 stack
的時候要反過來,因為是 little endian,所以會變成
0x68732f6e69622f
,然後拆兩半變成 0x68732f
和
0x6e69622f
。只是我發現到說 0x68732f
在
assemble 之後會產生 0x00
的 byte,然後它的輸入函數是用
gets
,所以會直接被截斷,所以可以考慮自己隨便補上兩個數字變成
0xff68732f
,然後再用 shifting 的方法把它變成 0,或是參考
shellcode.sh()
的作法,使用
system("/bin///sh")
。
1 | from pwn import * |
echo
這題的是個 32 bit 的 binary,反編譯一看它就直接把使用者輸入的字串傳到
printf()
的第一個參數去,所以有 format string
的問題在。
關於這種漏洞能做什麼,要怎麼利用的問題建議先閱讀此文章,且要整篇讀完。
接下來觀察說它呼叫個 system
來輸出 Goodbye
的訊息,所以會想說能不能改它的字串,不過發現到那個字串在
.rodata
之後就可以放棄這個想法了。
再來是檢查看看 GOT & PLT,發現到 system
和
printf
都在上面,所以可以把 printf
的 GOT 改到
system
的 PLT 上面,然後最後再自己輸入 /bin/sh
就能得到 shell 了:
1 | from pwn import * |
Offset 的部份直接輸入
aaaa %p %p %p %p %p %p %p %p
就能看到是第幾個了。
echo2
這題是 64 bit,且 PIE 有開。檢查一下之後發現程式的部份和第一題差不多,所以也是用修改 GOT & PLT 的方法去搞定。不過因為有 PIE 的緣故,它們的位置並不是固定的,所以要先找到一個基準點才行。
像是我是在 stack 上找找看有沒有位置和 printf
system
兩個函數接近的東西存在,然後就找到了
__libc_csu_init
,所以就算出它的 offset
把它洩漏出來,然後減掉它在 binary
本身的位置作為基準點,然後加上兩個函數的值就能得到對應的 address
了,然後用和前一題一樣的方法把 address 蓋過去:
1 | from pwn import * |
echo3
這題回到了 32 bit,沒有 PIE。反編譯之後發現它會先利用
alloca()
在 stack 上取得隨機大小的空間,然後進入到有 format
string exploit 的函數,只是會發現它的 buffer 位置不在 stack 上,而是在
.bss
上。這代表我們沒辦法再向之前一樣隨意的寫入任意記憶體位置了,但是還是能利用
stack 本來就有的 pointer(如果有的話),然後它的輸入次數是限制到 5
次的。
既然這題是前面的延伸,所以一樣是先檢查看 GOT &
PLT,結果發現這次裡面沒有 system
存在,所以需要想辦法去取得
libc 上的 system 位置。要做到這件事需要先去 hackme 的網站上下載他們的
libc,然後看看是要用 VM, docker 還是 patchelf 去改都可以,確保在 remote
的時候有一樣的結果。
之後用 gdb debug 幾次之後會發現說它的 alloca()
大小雖然是隨機的,但是實際上是有一定的範圍的,所以可以用暴力用找到一個
libc 的 address 在 stack 很上面的世界縣。之後還會看到說 stack
上有些的值是指回到 stack 的某些位置的,所以能利用那些 pointer
去修改被指到的位置的值想修改的記憶體位置,接下來再利用修改完的指標去修改真實記憶體的值,所以修改一次需要兩次動作才能完成。
所以 leak 出來之後可以用一樣的方法算出 printf
和
system
的位置,但是會發現 system
的 address
有點大,直接單純的改的話會讓它回傳過來的資料很大,也很花時間,所以只能把它切成各
2 byte 去修改了。只是會發現說前面 leak 用了 1 次,修改要花 2*2=4
次,那就沒辦法輸入 /bin/sh
了。這個實際上可以算出要改的值的差值,然後直接在第一次改的時候修改兩個
pointer,然後第二次就一次修改兩個的值,這樣一共只要 4
次,就不會超過範圍了:
1 | from pwn import * |
smashthestack
這題看了一下程式非常簡單,就有個 flag 是從檔案讀進來的,存在 bss 區,然後有個 overflow 的地方能讓你直接塞任何東西出來,並且有開 canary。所以這題和前面不太一樣,不用拿到 shell,只要把那塊記憶體洩漏出來就好了。
Google 一下 stack smashing 加上它給的提示中的 stderr
應該就能知道目標做法了,方法是利用 canary 檢查失敗的那個函數
__stack_chk_fail
會在失敗時輸出
*** stack smashing detected ***: {argv[0]} terminated
的訊息,只要修改掉 argv[0]
的值就可以了。
建議先用 patchelf 把 libc 和 intepreter 換掉,版本
2.23-0ubuntu3_i386
,用它提供的 libc 的 md5 找到的,要下載 glibc 推薦用 glibc-all-in-one 比較方便不用的話可能會和我有一樣的狀況,就是 exploit 對我的 libc 沒用...
然後就寫個 debug 用的函數和 gdb 顯示的 stack 兩邊比較出來就能找到
argv[0]
的 offset 了,然後給它蓋過那個位置就好了:
1 | from pwn import * |
onepunch
這題是個很有趣的題目,首先丟進 IDA 裡面看 code,就是讀一個 16 進位的位置進來以及一個 10 進位的數字,然後把數字寫入到那個位置去然後就這麼的結束,所以這代表你只能寫入一個 byte。只能寫入一個 byte 讓人想到的大概是想辦法修改 address 的某個 byte 讓它跳轉到另一個函數去之類的,所以就先找找看有沒有原本就有的 shell 函數能讓它跳轉,只是找不到這個目標。
相對之下,我反而發現到有個名為 _
的函數,裡面呼叫了
syscall ,針對 syscall table 看了一下之後發現是
mprotect(0x400000, 0x1000, 7)
,就是把
0x400000~0x401000
的記憶體區塊的保護模式設為
7,然後再去查了一下 Linux 這部分的 source code 看到 7 是 Read, Write,
Execute 三個 flag 的 or 結果,即代表把那塊區塊設成 rwx 的意思。之後用
gdb 裡面的 vmmap 檢查了一下也發現那段記憶體區塊確實是 rwx
的,所以大概是想讓我們修改這塊的記憶體。而這塊基本上包含了程式大部分的地方,所以很多東西都能改,但只可惜只能改一個
byte。
所以就會想到能不能讓它多讀幾個 byte
之類的,所以會想看看有沒有辦法把它 ret 或是 jump 的位置改掉,回到 scanf
的前面。檢查一下之後就會發現 0x400767
的位置的 jump
如果可以回到 scanf 的前方,也就是 0x40072c
就好了。所以接下來打開 IDA 的 Hex View 去看一下那個 jump 的 opcode 是
75 0A
,丟到一個可以幫你解碼的線上工具之後發現那代表的是
jne 0xc
,所以回去檢查了一下發現它也真的是把 eip 往前
12。
所以我們大概猜測 75
是 jne
的意思,然後後面的 0A
是 offset,只是為什麼 0A
會變成 0xc
又是一個問題了。所以去查了一下有這個
StackOverFlow 回答和這份資料有比較詳細的資訊,而結論就是
offset = dest - (source + instruction size)
,而 short jump
(eip relative jump) 的 opcode 長度是 2,所以
offset = dest - (source + 2)
。
因此我們就可以計算出
offset = 0x40072c - (0x400767 + 2) = -61(dec)
,而 -61 的
unsigned 表示法可以寫成 195,然後去程式中 input 看看
400768 -61
或
400768 195
,也確實會有重複輸入出現,然後再用
400768 10
試著把它恢復看看也確實沒問題。
既然已經能成功修改記憶體之後就隨便挑一塊記憶體寫入 shellcode 再 jump
過去就好了,例如在 0x400767 + 2
:
1 | from pwn import * |
這題還蠻好玩的,還順便學了一點 opcode 的知識
raas
看它的程式碼會知道說 do_del
和 do_dump
都完全不檢查資料在不在的,然後 free 的地方也沒有把指標設成
NULL
,所以可以靠自己去叫那個函數讓它達成 Use After Free
的目標。當你 free
掉比較小的 chunk 之後再次
malloc
一樣大小的 chunk 出來的話會發現它的 address
是一樣的,這很容易寫個程式驗證。
然後想 get shell,目標是改變程式的流程,所以目標就是看看能不能控制
eip,然後最明顯的控制法就是想辦法去修改 record
裡面的
print
或是 free
指針了。首先,record
的大小很明顯是 12bytes,然後它在 new
東西的時候也是會 malloc
一個 12bytes 的 chunk
出來,之後如果類別是 String 的話那它還會讓你自己輸入數字去
malloc,而那塊區域是我們可以寫入的。
所以若是先讓它 free 掉兩個 chunk,之後 new record 的時候先用掉後 free
的 chunk 所用的記憶體,然後我們再讓它 malloc(12)
不就能取得曾經用過的 chunk
的記憶體空間嗎?然後在透過自己寫入東西之後再讓它呼叫 print
或是 free
就能讓它 jump
到我們想要的地方了。所以先讓它創造出 1
2
兩個
chunk,然後依序釋放 1
2
之後再讓它新建一個
3
的 chunk,種類為 String
且大小為
12
,這樣就能寫入到 1
chunk
的內容去了。像是前四個 byte 填上 ask
函數的位置,然後讓它去產生
records[1]->print(records[1]);
的呼叫就 ok 了。
接下來看了一下程式裡面並沒有什麼直接 get shell 的函數,不過有
system
在 PLT 表中,所以可以讓它 jump
到那邊看看,不過這樣只會告訴你參數錯誤。不過眼尖的可能會注意到它輸出
command not found 的時候是所輸出的其實是 system
的地址,這是為什麼呢?看一下它在 do_del
和
do_dump
裡面是把 record
指標本身當作參數傳入的,而 system 會把它當成 string
解讀。所以我們其實可以在前四 byte 寫上 sh\0\0
,然後後四
byte 寫上 system
的地址,然後改呼叫 do_del
就成功 get shell 了:
1 | from pwn import * |
rsbo
首先是注意到 read_80_bytes
讀了 0x80
個字元,所以能夠 overflow,然後就依照題目的提示去 open
read
write
是一種解法,不過我是直接和第二題一起解的,詳見下題。
rsbo-2
它的 overflow 地方最多只能讓我們塞 5 個 dword 進去,對 ROP
來說這個是還蠻致命的問題,不過我們可以試著用 Stack
Pivoting 去把 esp
弄到 stack
上面。首先是決定個地方作為新 stack 的地點,而一般都是選
bss+0x800
的地方,因為如果是 bss+0
的時候在
call 一些 libc 裡面的函數會產生問題。接下來用找到 pop ebp
的 gadget 結合 leave
就能改 esp
了,改完就能成功的 pivot。
不過 pivot 完之後要怎麼拿到 shell 又是另一個問題,像是可以直接叫 libc
裡的 system 或是自己弄 syscall,不過很容易發現 binary 本身的 gadgets
不足讓我們直接去叫 syscall,所以一定需要拿到 libc 的 address。這部分就用
write
去把 GOT 上面的 write
函數位置洩漏出來就能拿到了,然後之後再利用那個 base address 看看是要用
system
還是自己 syscall 都是可以的。我用的方法是自己去
syscall:
1 | from pwn import * |
leave_msg
這題的保護除了 Canary
以外都沒開,而主程式的功能是讀一個字串,如果長度超過 8 就切斷(塞
\x00
),然後再讓使用者輸入一個 index(用
atoi
),接下來只要 index<=64 或是第一個字元不是
-
就把字串用 strdup
複製之後的指標放到一個 bss
上的一個陣列,整個流程一共做三遍。然後經過觀察會發現這題沒有 overflow
能用。
其實檢查那個 -
字元就是個提示了,因為你可以透過在前面加空格來躲過這個檢查,所以目標就是用負的
index 去把某個位置的值改成 heap 上的一個 address。稍微檢查一下之後會發現
GOT 在 bss 的前面,所以可以拿來改 GOT,然後文字內容就放 shellcode
就好了。
像是可以選擇改 strlen
puts
read
之類的函數到 shellcode
上面,不過會發現它會因為長度太長被切斷 (\x00
+
strdup
),所以沒有效果,需要想辦法繞過那個限制。我的做法是把
strlen
改成 xor eax, eax; ret
,它就永遠會回傳
0,然後第二次就能不管長度限制直接隨便寫入 shellcode 了。
1 | from pwn import * |
stack
我解這題時 Description 中給的原始碼連結已經失效了,不過我找到了個類似但不完全一樣的 Source Code: this and backup
這題雖然是防護全開,但是沒有想像中的困難,只要能突破 Canary 之後就是經典的 ret2libc 套路了。
記得也先把它 patchelf 一下,結果才會一致
首先測試一下就和普通的 stack 作業一樣,不過很容易發現到說它沒有檢查
stack 大小就直接 pop,所以如果結合 c
p
i $n
p
一起使用就能得到特定記憶體位置的值,所以 Canary
就能輕鬆的拿出來了,__libc_start_main
的位置也是一樣。
接下來 Overflow 後之後就算好位置讓它去叫 system
就好了,不過要注意一下 main 的 epilogue
有點不太一樣,所以還需要稍微調一下才能讓它正常的去 ret。
1 | from pwn import * |
very_overflow
這題是個能夠讓你紀錄 note 的服務,然後它有個簡單的 note
的結構來存資料。不過問題在於它 add_note
函數在記錄完東西之後會直接把 next
指到字串的
\0
之後的一個位置去。所以讓它記錄完之後再用
edit_note
去把 next
所指到的地方改成其他的
address,然後再存取下兩個 note 就能達成任意 address
的讀寫了。
所以可以把 printf
的位置讀出來算 libc 的位置,然後讀
vuln
函數的 ret
,而它指到了 main 的
ret
(用 gdb 的 info frame
發現的),所以直接寫入之後就能改 main
的
ret
,然後就直接用 libc 上的 gadget 去 syscall
就搞定了。
1 | from pwn import * |
notepad
這題簡單讀一下反編譯的程式碼之後能看出它的 Note 結構大概如下:
1 | struct Note { |
接下來發現到 notepad_open
和 menu
函數在呼叫 function pointer 的時候問題,因為 menu
裡面的檢查沒有處裡負的,所以就能試著讓它存取上一個 chunk 的 address
達成任意 call。例如這樣就能 call 它的那個假 bash 函數:
1 | from pwn import * |
不過這之後才是比較困難的地方,像是我有想過讓它去 call
printf
,然後用 format string 的方法去 leak libc 的
address,不過因為結構的問題,會被 \0
斷掉所以不太可行。然後後來去看別人的解法發現到了有個方法是用 unsorted
bin 會洩漏 main_arena
的地址做的,詳見此。
所以用這個方法之後得到 main_arena
的位置,然後用這個腳本找 libc
裡面的 main_arena
offset,然後用 OneGadget
找看看能用的 gadget 就完成了。
1 | from pwn import * |
petbook
在 IDA 自己用 struct 出來之後會比較好讀,整個題目的架構是 user 可以註冊與登入,登入狀態下可以新增與編輯 note,或是領養、改名、拋棄寵物。
有用到的幾個的 struct 大概如下:
1 | struct list_type { |
第一個可以注意到的漏洞是它在 user_edit_post
裡面有用到
realloc
,它的大小是使用者可以自己輸入的,所以只要第一次編輯
note 時輸入大小為 0,第二次再輸入 0 的時候能觸發 double
free,不過我這題沒用這個。
另一個漏洞在 user_create
裡面,它 malloc user
之後只有修改 uid
username
password
is_admin
的值而已,所以只要讓它在
malloc(0x218)
的時候回傳一個特定排好的 chunk
即可達成任意讀寫。不過寫的地方因為它會檢查 uid
是否符合一個隨機的 magic
值,所以要先 leak 出
magic
才能寫。
整套流程大概是重複利用新增長度超過 0x218
的
note,然後編輯時讓他呼叫 realloc(ptr, 0)
去
free,這樣下個註冊的 user 就會拿到原本的 chunk。偽造 chunk
的部份還需要用 global 的 current_user
先去 leak 出 heap
的地址,之後再想辦法偽造 chunk 出來即可。libc 的位置可以從 unsorted bin
的 fd 拿。達成讀寫後就寫入 __free_hook
之後讓他
free("/bin/sh")
即可。
1 | from pwn import * |
mailer
題目就一個讓你能創建 mail 的功能,和檢視郵件內容的功能而已。
mail 的 struct 如下:
1 | struct mail { |
它會先 malloc
mail 的大小,然後設置好
content_len
之後使用 gets
去讀取 title 和
content 與 overflow 的值。很明顯能使用 House of Force 去讓
malloc
回傳任意位置的值。
使用 House of Force 需要有 top chunk 的位置,所以要先 leak 出一個
heap 的 address,這邊先建一個 chunk 之後透過 overflow title
可以改掉 content_len
,然後再隨意建第二個 chunk
之後讓他輸出就能 leak 出第一個 chunk 的 address,加上一個 offset (用 gdb
找) 就能得到 top chunk 的 address。
接下來參考 house_of_force.c
裡面的方法就能成功了,不過不知為何有些的目標位置會有錯誤,有些就不會有,所以原本想直接蓋
GOT 上面的 fwrite
去 get shell
的計畫行不通。之後檢查一下會發現到這題其實是 NX 沒開的,所以 stack 是
executable,不過我還是測試了一下,把 GOT 中的 puts 蓋掉變成 heap 上的
shellcode。
接下來在 local 用 gdb 會發現它會失敗,因為 heap 不是 RWX,但是最神奇的是這邊直接放到 remote 執行就很神奇的成功了,不知道為什麼 remote 的 heap 好像是 executable 的,但是 local 不是...
1 | from pwn import * |
Crypto
easy
hexstring -> ascii
r u kidding
明顯是 rot-n 之類的 Cipher,但是因為我們知道 Flag 的格式是
FLAG{...}
,那個 n 就很明顯了,連暴力都不用。
not hard
查一下 python 的 base64
documentation,看一下裡面有什麼
base64 的變種能用。這題需要 decode 兩次。
classic cipher 1
就和題目說的一樣是 substitution cipher,不過因為我們知道 Flag 的格式,所以起碼知道四個字母的轉換。
可能用了到的工具: quipqiup
classic cipher 2
網路上找個 vigenere cipher 的解碼器就好了,例如這個。
PS: 注意這題的 key 有點長,如果限制太短可能會解不出來
easy AES
這題真的很簡單,只要懂非對稱加密就好了。根據它的 python
碼可以知道它的 input plain_text
是固定的,寫個程式反過來算就好了。
1 | #!/usr/bin/env python3 |
得到字串後重新執行一次,輸入進去然後打開產生的
output.jpg
,上面就有 flag 了。
one time padding
這題點開來就直接給你看程式碼了,然後會輸出 20 個 flag^padding 的結果,而 padding 都是隨機生成的。這樣乍看之下也想不到有什麼做法,不過注意到它有寫個註解說:
X ^ 0 = X, so we want to avoid null byte to keep your secret safe :)
然後回到上面生成 padding 的地方看一下,padding 的每個 byte 是從 1~255 中選的。所以既然 padding 裡面沒有零,代表 flag 的每個字元也不會出現在那個位置上,所以我們可以用排除法。
假設在 i
的位置上從來沒有出現過某個 byte,就代表
flag[i]
是那個
byte。所以我們只要大量取得它的資料就能建表出來破解了。
這個是範例的 Python 程式,大概跑到 125 次的時候它就找出 flag 了:
1 | import requests |
shuffle
這題的程式很清楚明白,就是把明文經過一個隨機的字元 mapping 後寫到
crypted.txt
,然後再把明文打亂順序後寫入到
plain.txt
。所以 crpyted.txt
擁有著原本的順序,而 plain.txt
擁有著原本的字元及其頻率。這題的關鍵就是在字元頻率上面,例如
a
出現 100 次,它變成 k
之後寫入
crypted.txt
後 k
的出現次數也是 100 次,然後
plain.txt
裡面的 a
的出現次數也是 100
次,所以只要根據出現頻率做個 mapping 就好了:
1 | from collections import Counter |
login as admin 2
這題和 login as admin 3
有些相似,一樣是要透過自己改
cookie 來突破,不過它一樣有 sig 需要處裡。不過提示已經跟你說了 length
extension attack,那就找個工具來用就好了。例如 HashPump。
再來是 PHP 在解讀 querystring 的時候如果遇到重複的 key
出現,後面的會把前面的值覆蓋掉。例如 a=1&a=2
=>
$_GET['a'] === '2'
。
還有請你注意一下 md5($secret)
的長度是多少,對破解這題有用。
passcode
這題就是給你一個使用 AES OFB 模式加密的
flag,和一堆加密過的隨機小寫字母字串。去查了一下 wiki
上關於這個模式的介紹,就是它的方法就是先用 key 和 iv
產生出一個加密的一組東西,我叫它 o
,然後和 plaintext 做 xor
就加密好了。想到這個題目,讓我想到了之前寫的
one time padding
題目,一樣是和 xor
有關且也給你一堆密文,所以搞不好能用暴力的。
首先是注意到它的加密是每次都創造出一個 AES 的
instance,所以它的加密流程不像 wiki 上關於 OFB
模式介紹的那張圖一樣,會把 o
當作下一次的 cipher
來用,而是每次都用固定的 key 和 iv,所以
o
,應該也不會變。自己測試了一下,每次用
encrypt(b'test')
產生出的值都是不會變的,所以這個想法沒錯。所以想一想我們這邊有的資訊有:
flag^o r_1^o r_2^o r_3^o ....
,其中的 o
應該要能符合 (r_1^o)^o_guess
也是個小寫字母所組成的隨機字串,所以這代表我們有暴力破解的可能性。
不過其實也不用那麼急的開始寫暴力破解的 code,注意到它其實是給你一堆
r_1^o r_2^o r_3^o ...
以及 r_1 r_2 r_3 ...
的一些 constraints,然後要你求解 o
。如果熟悉 z3
的話應該就能發現這個是 z3 很擅長求解的一類問題,所以用 z3 求
o
之後再和 flag^o
做 xor 就好了:
1 | from pwn import remote |
關於那個
r_n
到底要取得幾個就隨便猜了,反正多拿點代表可能符合的o
範圍會變小
login as admin 5
透過觀察原始碼你會發現到你能擁有的資訊有加密過 guest user 資料 json,以及那個 json 實際上長怎樣。那麼這個怎麼做呢,建議先複習一下 RC4 這個加密法是怎麼實作的。
1 | keyStream = RC4(key, length) |
既然我們已經有了 text
和 cipherText
後,我們可以得到 keyStream
。再來我們再生成的我們想要的 user
json 用 keyStream
加密回去就好了。
1 | b = decodeURIComponent('user5 cookie here') |
xor
研究一下 xortool 就能很簡單的破解了。
emoji
它給了你一個壓縮過的 js,不過我們看到一開始就是
eval
,所以就先把它移除掉看看要執行的程式碼到底是什麼,然後再把它丟到
beautifier 之類的工具比較好閱讀。
接下來除了加密的資料以外我們還可以很清楚的看到它的加密邏輯,可以簡化為下方的
code:
1 | const encrypt = (str) => |
這個加密怎麼破解呢?因為字元的範圍是 0~255,所以直接暴力破解就好了。
1 | ar = [] // encrypted byte array |
multilayer
這題是個多層加密,第一層是隨機的 substitution cipher,第二層是乘某個值然後取 mod,第三層有個隨機的很大的 key 會根據迴圈變動,然後和 flag 做一些位元運算,而最後一層是把 flag 切塊做 RSA。要解這個一次把全部的東西寫對是不切實際的,所以建議可以自己弄個假的 flag 出來,然後自己 print 其中各層輸出的結果,這樣就能一層一層處裡比較方便。
首先是第四層,雖然那個 RSA
聽起來很可怕,只是注意到它的分塊一次只取四個 16 進位的字元,所以所有的
input 只有 0000~ffff
,這樣每次直接暴力跑 65536
次就能解密了,不過要更快的話可以直接預先算好一個表,不用每解一個 block
都要全部跑過一遍。
之後第三層我是直接用 z3 解,不過一開始用 BitVec
的時候因為 bits 設太嚴所以經常出現 unsat 的問題,後來發現是它的
BitVec
運算預設都是 signed 的,所以把 bits
加大就能解了。然後第二層也是 z3
輕鬆處裡,不過其實可以直接整合到第三層裡面去就好了,不用多一個函數。
最後的第一層是單純的 substitution
cipher,雖然它有給原始字串的字母頻率,不過直接用之前解
classic cipher 1
用的工具 quipqiup 就好了,然後給它前面四個字元是
FLAG
的提示就能解開了。
1 | #!/usr/bin/env python |
slowcipher
一開始先自己測試一下會發現它的執行時間基本上和 input 長度有關,呈指數般的成長。
丟進 IDA 打開可以看到解密函數的
code,然後它主要分成兩個部分,第一個部分有個迴圈根據密碼去算一個奇怪的值,暫且叫做
key
(uint64)。然後後面有另一個迴圈會利用前面算出來的
key
做一些運算,然後根據加密或是解密會有點不太一樣。而拖慢程式的那個迴圈是第二部分的,因為它裡面還有個迴圈會根據某個特別的值去重複對
key
進行個相同的運算很多遍:
k = (7829367 * k + 12345) & 0x7FFFFFFFFFFFFFFFLL
,看了第一個迴圈也是有看到這個運算反覆的出現。所以我就懷疑該不會這個值是有什麼循環之類的,只是用
python 隨便測試了一下看來是沒有。
不過再仔細看一下的話會發現 key
在第二個迴圈中只要有使用到它來加密的地方都一定有和某個 int8 的值做
xor,所以代表它除了最底的 8bits 以外都沒什麼用,應該可以 mod
256,所以實際上第一個迴圈計算出來的結果不是很重要,因為到最後也只有 256
種可能。然後也可以寫個程式去試試看,對於 0~255 的初始值,反覆的對
key
做那個運算都必定會產生循環,且神奇的是它循環的大小一定都是
64,不知道是有什麼奇怪的數學在裡面。
反正既然如此的話我們就能把 key
的搜索空間找出來就能很快的讓它計算完成了。key 的部份直接 0~256
全部都試試看,然後看結果能不能轉換成
ASCII,若不行就放棄,而這個方法在我自己測試加密的文字檔是有效的,但是用在真正的
flag.enc
檔上卻沒用。後來去查了一下別人的解法才發現說那個檔案不是文字檔,而是
7z...,如果要盲猜 key 的話就要判斷一下檔案類型了。
1 | import magic |
有個可能要注意的地方是有些運算在 python 和 C 底下差很多,像是溢位等的問題,而這題的第二個迴圈底下的某個運算在 IDA 顯示出來的結果真的很怪,後來直接用 Ghidra 的反編譯結果比較容易看懂,然後在 python 重新模仿一次那些運算
ffa
這題提示給了個 finite field
arithmetic,查了一下也沒很清楚是什麼東西,不過應該大概是 a
b
c
出來,所以就要用上解這種東西的神器 z3。
下面還有個用數學的解法,不用 z3
用 z3 的解法
1 | from z3 import * |
接下來我們看到後面會看到
所以最後在加上一段程式碼把 f 算出來,轉換回原本的 string 就好了。關於
r
s
的找法使用的是擴展歐幾里得算法。
所以完整的程式碼如下:
1 | import gmpy2 |
這在我電腦上不用 1/10 秒就能跑完。
數學解
注意到它的 a
b
c
其實是
所以要求 a
b
c
就把反矩陣乘過去就好了,而 sympy
有提供計算
1 | import gmpy2 |
不過我跑這個所花的時間大概是 z3 版本的 2.5 倍,不過一樣不到一秒就能解出,果然 z3 有 magic。
Programming
fast
這題就要寫程式去回應它出的 10000 題四則運算,不過有個它沒說的點是它的運算是遵守 C 的 32 位元整數來運算的... 這讓我一開始用 node.js 寫弄的很麻煩還沒過,最後只好用 python。
Lucky
you-guess
這題的名稱就真的叫你猜,然後裡面的也是給你 sha512 的
hash,想破解也真的不切實際。不過猜也是有方法的,根據它裡面的
%s really hates her ex.
,密碼應該是個人名,所以搞不好字典檔裡面有。
下面的腳本就傳個字典檔的路徑作為參數進去,它就會把每個字都試過一次。然後如果我直接把真正的
password 公開出來其實和直接講 flag 沒兩樣,不過我能提示說我是從 hashcat
裡面的 example.dict
找到密碼的。
1 | import hashlib |
Forensic
easy pdf
從 pdf 中找出所有的字串,然後就能看到 flag 了
1 | pdftotext easy.pdf easy.txt |
this is a pen
從 pdf 中把圖片輸出出來,裡面有一張就是 flag
1 | pdfimages this-is-a-pen.pdf -all tmp/ |