TSCCTF 2024 Official WriteUp

Chumy | Jan 5, 2024 min read

TSCCTF 2024 Official WriteUp

這次與 TSC 的夥伴們一起舉辦了一場 CTF 比賽,而我則負責 infra 的建置以及出一題 reverse 題目,雖說我似乎難度沒有控好,還在學習控難度中。

Just Shooting Game

image

基本上這題最大的難點有兩個:

  1. 取得 magic 值
  2. 通靈出 Transfer.exec 是某種 bytecode interpreter ,然後把解出來的 code 丟 IDA 之類的解析。

解題

這題 code 用 ILSpy 逆會比較完整,但是這邊 writeup 還是用 Dnspy 寫。

DnSpy 打開 ShootingGame_Data\Managed\Assembly-CSharp.dll 會發現有一些 Class 有一個叫 code 的 string variable 裡面塞了一坨奇怪的東西,decode 以後會發現這些都 emoji。

image

image

image

這些 code 會在該 Class 裡和一個叫 magic 的變數做 Transfer.etob 運算以後,跟一些參數一起跑 Transfer.exec

image

通過與 gunenemy 等等 Class 比對後,發現 GameManager 是送入一個字串 Call Transfer.exec 以後會依據 Call 完後所得到的 value variable 轉成 Boolean 並判斷如果為 True 就把 supermode 設為 True,觀察 gun class 會發現 supermode 會讓玩家進入無敵狀態並沒有攻擊速度限制,應該是一個打入作弊代碼會開啟作弊模式的東西。其他 Call Transfer.exec 的地方都是在做字串比對,可以判斷 Transfer.exec 會依據 codemagic 運算後的 byte array 做相應的事情,但是 Transfer.exec 到底做了甚麼還不清楚,同時因為只有 GameManager 不是字串比對,所以目標可能在這裡,猜測那個作弊代碼可能就是 flag。

image

image

image

觀察 Transfer.etob 以後會發現他會將輸入的字串的做 UTF32 decode 以後依照不同的 range 減掉特定的 offset 以後跟 magic 做 XOR,最後減掉 128512 後強轉成 byte 然後拼回 byte array 回傳。

image

解析 Transfer.exec 可以發現他的參數變數名稱有 codememspbp 等等疑似 memory、CPU register 的東西,但是因為他太長了要猜一下,直接整個 function 丟給 chatgpt 判斷一下,我是用 GPT4,但是 GPT 3.5 也可以,只是要分段送。

image

image

image

image

image

image

image

image

可以確認這確實是某種 bytecode interpreter,這邊就可以知道 codemagic 經過 Transfer.etob 以後會變成 bytecode 然後 Transfer.exec 再依據的命令執行 bytecode 的動作,首先我們要發 bytecode 弄出來,但是會發現我們沒有 magic 的值。

image

寫過 unity 的話就會知道 MonoBehaviour 可以附加在 GameObject 上面,並且直接在 GameObject 上面指定 public variable 的值,這些在 build 完會存成 asset,所以需要找可以解析 unity asset 的工具。

image

AssetRipper 解開並且 export 出來,一個一個找太慢所以直接對 export 完的資料夾下 grep -A 2 -B 2 -r magic .,他會把資料夾下所有檔案中含有 magic 字串的行跟他的上下兩行印出來,為了濾出正確的,需要觀察 magic 的上下兩行有沒有 GameManager 的其他 variable , 會發現 magic: 228 上有一個叫 m_nextLevel 的 variable,可以判斷 GameManagermagic 就是 228。

image

image

用 python 寫 unicodetoemoji.pyemojitobyte.py 把他解成 bytecode 並存成檔案。

接著把解出來的 bytecode 丟 Disassembler 的工具,這邊要猜一下是哪種指令集,這個 Online-Assembler-and-Disassembler 網站不錯用,或者直接 IDA 用 x64 解看看,我們會拿到這坨。

image

解析他可以得知,輸入的字串的位址在 rax,他會先把 rax 推到 stack 再來把比對目標也推到 stack,然後將輸入字串的每個字元依照特定順序做 input[a] = (input[a] ^ input[b]) +- num,最後將運算結果跟比對目標迴圈比對是否完全一樣,如果一樣就將 rax 設為 1,否則為 0。

image

image

image

因此只需要把運算反過來做,就可以得到 flag。

exploit

data = [0x545C30D14737AC30, 0x2B818E98088CED63, 0x0BAD50580945D3F0, 0x3823C33FF8E47955, 0x54018BFECBE91C93, 0x0F4B763093C586459, 0x10E088E4C7281]
data = b''.join([a.to_bytes(8, 'little') for a in data])
data = bytearray(data)

order   = [0x2f, 0x20, 0x36, 0x15, 0x24, 0x25, 0x2A, 0x17, 0x11, 0x1e, 0x0c, 0x32, 0x07, 0x04, 0x33, 0x27, 0x09, 0x02, 0x08, 0x22, 0x0f, 0x28, 0x2e, 0x31, 0x1b, 0x1f, 0x1a, 0x1c, 0x13, 0x0d, 0x0e, 0x30, 0x1d, 0x06, 0x10, 0x12, 0x2c, 0x16, 0x23, 0x35, 0x0b, 0x0a, 0x2b, 0x21, 0x34, 0x26, 0x01, 0x29, 0x18, 0x2d, 0x05, 0x14, 0x03, 0x00, 0x19]
xorwith = [0x19, 0x11, 0x12, 0x0d, 0x0e, 0x1a, 0x16, 0x03, 0x20, 0x24, 0x03, 0x09, 0x2c, 0x11, 0x35, 0x15, 0x14, 0x24, 0x2f, 0x1e, 0x05, 0x2d, 0x1d, 0x1d, 0x07, 0x02, 0x08, 0x13, 0x32, 0x0b, 0x14, 0x02, 0x2e, 0x19, 0x32, 0x28, 0x05, 0x12, 0x33, 0x24, 0x36, 0x2e, 0x1a, 0x03, 0x20, 0x03, 0x32, 0x34, 0x13, 0x17, 0x08, 0x34, 0x04, 0x0c, 0x32]
addsub  = [-0x55, 0x7, 0xc9, -0x81, 0x29, -0x4c, -0xa1, -0x6b, -0x2d, 0xe5, 0x25, -0x42, 0x1a, 0xa4, 0x50, -0xac, -0x80, 0x20, -0xff, 0xe5, 0x25, 0x2c, 0x2b, -0x2a, -0x08, -0xf2, -0x29, 0xdb, -0x4d, 0x29, 0x45, 0x75, 0x44, 0x2d, -0x67, -0x35, -0xfc, 0x6a, -0xa4, -0xa, -0x1a, -0xf0, -0xc4, 0xa6, 0x99, 0xf2, 0x7c, 0xf, -0xad, -0xfb, 0xf9, 0xfb, 0x3e, -0x30, 0x4e]

for i in range(len(order)):
    data[order[i]] = (((int(data[order[i]]) + 256*2 - addsub[i]) % 256) ^ (int(data[xorwith[i]])))

data = bytes(data)
print(data)

Flag

image

TSC{reV3R53_4md64_45m_w17H_3m0ji_1n_T3H_NeT_5oO0O_c00L}