mocactf
my first time to play CTF with camp…!
i guess i can’t never experience two times in my life, but i want to buy my camp goods…
hash oracle
chall
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
import hashlib
with open("flag.txt", "rb") as f:
FLAG = f.read().strip()
def hash(s, i):
msg = FLAG[:i] + s + FLAG[i:]
return hashlib.sha256(msg).digest()
def main():
print("Welcome to the hash challenge!")
while True:
print("Enter a string to hash:")
s = bytes.fromhex(input())
print("Enter an index to insert the string at:")
i = int(input())
h = hash(s, i)
print("The hash is: " + h.hex())
if __name__ == "__main__":
main()
solve
i use only this A + b + x + B == A + x + b + B
if x
equal to b
, hash(A + b + x + B ) == hash(A + x + b + B)
.so i can find the flag one one byte each.
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
#!/usr/bin/env python
from ptrlib import *
from tqdm import tqdm
io = process(["python3","chall.py"])
# io = remote("10.90.39.124 5555")
# io.debug = True
def sender(m, i):
io.sendlineafter(b":\n", m.hex())
io.sendlineafter(b":\n", i)
return io.recvline().decode().split(": ")[1]
len_SALT = 50
pt = b""
for k in range(len(pt), len_SALT):
for i in tqdm(range(0x20, 0x7f)):
a = sender(bytes([i]), k + 1)
b = sender(bytes([i]), k)
if a[:-1] == b[:-1]:
pt = pt + bytes([i])
print(bytes([i]))
break
print(pt)
b'pwnx{13ngth_3xt3n510n_4tt4ck_w1th_0r4cl3}'
ML
situlation
first, i have 3 files(ciphertext, test.py, model.onnx).
tst.py is only to “how to use model.onnx” so i omit. our goal is reverse and decrypt from cipher text.
my initial analysis is below.
- encrypt for each 16bytes
- definitive(not tweak)
then, i guess this onnx file has some encryption scheme. (because this challnege category is not rev and misc)
now i have two ideas, it is AES encryption and original encryption scheme.
analysis
so, i read this article TSG CTF 2020 onnxrev write-up (github.com) and parse to onnx.
![image-20240918194209927](assets/img/ctf/2024-09-18-moca/image-20240918194209927.png
i found this about 1000 lines code and these has similarly repeat part.
so, i guess to this encryption is BLOCK encryption.
At this point, it was too huge to write code to parse each processing result like TSGCTF2024, so I had to work on other challenges and explore moca2024 venue.
BUT,
HINT
ok, we got a hint remember, this is crypto! We never design our own crypto
, so i am convinced this encryption scheme is AES256.
So I decided to look for the part where the sbox is hard-coded to get an overview of the process, but after looking at all the constants, I ended up helplessly without anything that looked like an Sbox.
I had some free time on the plane on the way home, so I was doing some random searches and found How to add an output node to an ONNX model #Python3 - Qiita, which is a god function that adds a process just by writing a variable. I found a god function that adds a process to an ONNX model just by writing a variable. I’m so glad I found it!
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
import onnx
from util import *
from onnxruntime import InferenceSession
import numpy as np
from Crypto.Cipher import AES
def seach():
model = onnx.load("../model/model.onnx")
for node in model.graph.node:
if node.op_type == "Identity" or node.op_type == "Constant":
continue
yield node.output[0]
for out in seach():
input_path = '../model/model.onnx'
output_path = 'model.onnx'
input_names = ['input']
output_names = [out]
onnx.utils.extract_model(input_path, output_path, input_names, output_names)
sess = InferenceSession("./model.onnx")
inp = b"\x00"*16
inps = [inp[i:i+16] for i in range(0, len(inp), 16)]
inp = []
for i in inps:
inp += bin_list(i)
x = np.concatenate(inp).reshape(-1, 16*8)
outs = sess.run(None, {"input": x})
print(out, outs)
input()
I look at the nodes excluding constants and initialisation like this, suddenly the part appear not b‘\x00’*16
.
It turns out that this corresponds to Add Roundkey at the beginning of AES.
So for AES256, if we know roundkey[0] and roundkey[1], we also know the masterkey, so everything works.
If i continue to look at the process, i see the following, which shows the AES 1round. From here we can find the roundkey[1].
1
2
roundkey0 = b'p{ylwt_dnht_xeom'
roundkey1 = b'ecdt_o__1und_loo'
Just be aware that this AES is transposed, so the character b ‘pwnx{they_told_me_1_could_not_do’
is the AES masterkey.
1
2
3
4
from Crypto.Cipher import AES
aes = AES.new(b"pwnx{they_told_me_1_could_not_do",AES.MODE_ECB)
print(aes.decrypt(open("ciphertext","rb").read()))
# b'here the last piece of the flag and the rest is in the key :) _aes_with_ml_so_1_did_it_anyway}\x02\x02'
Therefore, combining the information from the plaintext and key.
get a flag. pwnx{they_told_me_1_could_not_do_aes_with_ml_so_1_did_it_anyway}