cscg ist super
This commit is contained in:
41
2026/cscg/crypto/intro1/main.py
Executable file
41
2026/cscg/crypto/intro1/main.py
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env pypy3
|
||||
|
||||
import os
|
||||
from pydoc import plain
|
||||
from sys import byteorder
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util import Counter
|
||||
import hashlib
|
||||
|
||||
# Create a secret.py file with a variable `FLAG` for local testing :)
|
||||
from secret import FLAG
|
||||
|
||||
secret_key = os.urandom(16)
|
||||
|
||||
def encrypt(plaintext, counter):
|
||||
m = hashlib.sha256()
|
||||
m.update(counter.to_bytes(8, byteorder="big"))
|
||||
|
||||
alg = AES.new(secret_key, AES.MODE_CTR, nonce=m.digest()[0:8])
|
||||
ciphertext = alg.encrypt(plaintext)
|
||||
|
||||
return ciphertext.hex()
|
||||
|
||||
|
||||
def main():
|
||||
print("DES is broken, long live the secure AES encryption!")
|
||||
print("Give me a plaintext and I'll encrypt it a few times for you. For more security of course!")
|
||||
|
||||
try:
|
||||
plaintext = bytes.fromhex(input("Enter some plaintext (hex): "))
|
||||
except ValueError:
|
||||
print("Please enter a hex string next time.")
|
||||
exit(0)
|
||||
|
||||
for i in range(256):
|
||||
print(f"Ciphertext {i:03d}: {encrypt(plaintext, i)}")
|
||||
|
||||
print("Flag:", encrypt(FLAG.encode("ascii"), int.from_bytes(os.urandom(1), byteorder="big")))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1
2026/cscg/crypto/intro1/secret.py
Normal file
1
2026/cscg/crypto/intro1/secret.py
Normal file
@@ -0,0 +1 @@
|
||||
FLAG = "CSCG{i_love_my_mum}"
|
||||
34
2026/cscg/crypto/intro1/solve.py
Normal file
34
2026/cscg/crypto/intro1/solve.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from pwn import context, args, process, remote, success, xor
|
||||
|
||||
context.update(arch='amd64', os='linux', log_level='info')
|
||||
|
||||
URL = 'iaet6burymdllg3rflxg4ym7vk-1024-intro-crypto-1.challenge.cscg.live'
|
||||
PORT = 443
|
||||
|
||||
def start():
|
||||
if args.REMOTE:
|
||||
return remote(URL, PORT, ssl=True)
|
||||
else:
|
||||
return process(['python3', './main.py'])
|
||||
|
||||
io = start()
|
||||
|
||||
payload_len = 512
|
||||
io.sendlineafter(b"Enter some plaintext (hex): ", (b"00" * payload_len))
|
||||
|
||||
keystreams = []
|
||||
for i in range(256):
|
||||
line = io.recvline()
|
||||
ks = bytes.fromhex(line.strip().split(b" ")[-1].decode())
|
||||
keystreams.append(ks)
|
||||
|
||||
io.recvuntil(b"Flag: ")
|
||||
flag_enc = bytes.fromhex(io.recvline().strip().decode())
|
||||
|
||||
io.close()
|
||||
|
||||
for ks in keystreams:
|
||||
candidate = xor(flag_enc, ks[:len(flag_enc)])
|
||||
if b"CSCG{" in candidate or b"flag{" in candidate:
|
||||
success(f"Found Flag: {candidate.decode()}")
|
||||
break
|
||||
115
2026/cscg/crypto/intro2/main.py
Executable file
115
2026/cscg/crypto/intro2/main.py
Executable file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from hashlib import sha1
|
||||
from base64 import b64encode, b64decode
|
||||
from secrets import token_hex
|
||||
|
||||
from secret import FLAG
|
||||
|
||||
|
||||
KEY = token_hex(16)
|
||||
|
||||
|
||||
def get_mac(data: bytes) -> str:
|
||||
return sha1(KEY.encode("latin1") + data).hexdigest()
|
||||
|
||||
|
||||
def parse_token(token: str) -> dict:
|
||||
# Decode token
|
||||
token = b64decode(token)
|
||||
|
||||
# Check the MAC
|
||||
token, mac = token.split(b"|mac=")
|
||||
if get_mac(token) != mac.decode("latin1"):
|
||||
return None
|
||||
|
||||
# Parse values
|
||||
values = dict()
|
||||
for part in token.decode("latin1").split("|"):
|
||||
key, value = part.split("=")
|
||||
values[key] = value
|
||||
return values
|
||||
|
||||
|
||||
def generate_token(values: dict) -> str:
|
||||
token = "|".join(f"{key}={value}" for key, value in values.items())
|
||||
secure_token = f"{token}|mac={get_mac(token.encode('latin1'))}"
|
||||
|
||||
return b64encode(secure_token.encode("latin1")).decode("latin1")
|
||||
|
||||
|
||||
def handle_register():
|
||||
name = input("What is you name? ")
|
||||
animal = input("What is your favorite animal? ")
|
||||
|
||||
token = generate_token(
|
||||
{
|
||||
"name": name,
|
||||
"animal": animal,
|
||||
"admin": "false",
|
||||
}
|
||||
)
|
||||
|
||||
print("Here is your access token:", token)
|
||||
|
||||
|
||||
def handle_show_animal_videos():
|
||||
user_data = parse_token(input("Enter access token: "))
|
||||
|
||||
if user_data is None:
|
||||
print("Invalid token.")
|
||||
return
|
||||
|
||||
print(
|
||||
f"\nHere are some {user_data['animal']} videos for you: https://www.youtube.com/results?search_query=funny+{user_data['animal']}+video+compilation"
|
||||
)
|
||||
|
||||
|
||||
def handle_show_flag():
|
||||
user_data = parse_token(input("Enter access token: "))
|
||||
|
||||
if user_data is None:
|
||||
print("Invalid token.")
|
||||
return
|
||||
|
||||
if user_data["admin"] == "true":
|
||||
print("The flag is", FLAG)
|
||||
else:
|
||||
print("You are not an admin.")
|
||||
|
||||
|
||||
def main():
|
||||
while True:
|
||||
# Show main menu
|
||||
|
||||
print(
|
||||
"""
|
||||
1. Register
|
||||
2. Show animal videos
|
||||
3. Show flag
|
||||
4. Exit
|
||||
"""
|
||||
)
|
||||
|
||||
try:
|
||||
choice = int(input("Enter your choice: "))
|
||||
except ValueError:
|
||||
print("Please enter a number next time.")
|
||||
continue
|
||||
except EOFError:
|
||||
break
|
||||
|
||||
if choice == 1:
|
||||
handle_register()
|
||||
elif choice == 2:
|
||||
handle_show_animal_videos()
|
||||
elif choice == 3:
|
||||
handle_show_flag()
|
||||
elif choice == 4:
|
||||
break
|
||||
else:
|
||||
print("Please enter a valid choice.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1
2026/cscg/crypto/intro2/secret.py
Normal file
1
2026/cscg/crypto/intro2/secret.py
Normal file
@@ -0,0 +1 @@
|
||||
FLAG = "CSCG{i_love_my_mum}"
|
||||
130
2026/cscg/crypto/intro2/solve.py
Normal file
130
2026/cscg/crypto/intro2/solve.py
Normal file
@@ -0,0 +1,130 @@
|
||||
from pwn import context, args, process, remote, success, xor
|
||||
from base64 import b64decode, b64encode
|
||||
from hashlib import sha1
|
||||
import struct
|
||||
|
||||
context.update(arch="amd64", os="linux", log_level="info")
|
||||
|
||||
URL = "jpielr6wv7xor5jf32vxasubni-1024-intro-crypto-2.challenge.cscg.live"
|
||||
PORT = 443
|
||||
|
||||
example_token="name=cato|animal=cat|admin=false|mac=b5d4ddd83235359176d107b7c98bc7f2979cfec3"
|
||||
|
||||
|
||||
class Sha1State:
|
||||
def __init__(self, hex_digest):
|
||||
# Extract the 5 internal 32-bit registers from the hex digest
|
||||
self.h = struct.unpack(">5I", bytes.fromhex(hex_digest))
|
||||
|
||||
def _left_rotate(self, n, b):
|
||||
return ((n << b) | (n >> (32 - b))) & 0xffffffff
|
||||
|
||||
def extend(self, data, total_len_bytes):
|
||||
"""
|
||||
Continues hashing from the current state.
|
||||
total_len_bytes must be the length of (secret + message + padding + extra_data)
|
||||
"""
|
||||
h0, h1, h2, h3, h4 = self.h
|
||||
|
||||
# SHA-1 processes 64-byte blocks
|
||||
for i in range(0, len(data), 64):
|
||||
block = data[i:i+64]
|
||||
if len(block) < 64:
|
||||
# This is the final block of the 'extra_data', it needs its own padding
|
||||
block += self.get_padding(total_len_bytes)
|
||||
|
||||
w = list(struct.unpack(">16I", block[:64]))
|
||||
for j in range(16, 80):
|
||||
w.append(self._left_rotate(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1))
|
||||
|
||||
a, b, c, d, e = h0, h1, h2, h3, h4
|
||||
|
||||
for j in range(80):
|
||||
if 0 <= j <= 19:
|
||||
f = (b & c) | ((~b) & d)
|
||||
k = 0x5A827999
|
||||
elif 20 <= j <= 39:
|
||||
f = b ^ c ^ d
|
||||
k = 0x6ED9EBA1
|
||||
elif 40 <= j <= 59:
|
||||
f = (b & c) | (b & d) | (c & d)
|
||||
k = 0x8F1BBCDC
|
||||
elif 60 <= j <= 79:
|
||||
f = b ^ c ^ d
|
||||
k = 0xCA62C1D6
|
||||
|
||||
a, b, c, d, e = (self._left_rotate(a, 5) + f + e + k + w[j]) & 0xffffffff, \
|
||||
a, self._left_rotate(b, 30), c, d
|
||||
|
||||
h0 = (h0 + a) & 0xffffffff
|
||||
h1 = (h1 + b) & 0xffffffff
|
||||
h2 = (h2 + c) & 0xffffffff
|
||||
h3 = (h3 + d) & 0xffffffff
|
||||
h4 = (h4 + e) & 0xffffffff
|
||||
|
||||
return '%08x%08x%08x%08x%08x' % (h0, h1, h2, h3, h4)
|
||||
|
||||
def get_padding(self, msg_len):
|
||||
padding = b"\x80"
|
||||
padding += b"\x00" * ((56 - (msg_len + 1)) % 64)
|
||||
padding += struct.pack(">Q", msg_len * 8)
|
||||
return padding
|
||||
|
||||
def get_sha1_padding(message_len_bytes):
|
||||
padding = b'\x80'
|
||||
|
||||
num_nulls = (56 - (message_len_bytes + 1)) % 64
|
||||
padding += b'\x00' * num_nulls
|
||||
|
||||
message_len_bits = message_len_bytes * 8
|
||||
padding += struct.pack('>Q', message_len_bits)
|
||||
|
||||
return padding
|
||||
|
||||
def forge_mac(orig_mac: str, key_len: int, orig_data: bytes, added_data: bytes) -> str:
|
||||
state = Sha1State(orig_mac)
|
||||
current_len = key_len + len(orig_data)
|
||||
glue_padding = get_sha1_padding(current_len)
|
||||
|
||||
new_total_len = current_len + len(glue_padding) + len(added_data)
|
||||
|
||||
return state.extend(added_data, new_total_len)
|
||||
|
||||
def start():
|
||||
if args.REMOTE:
|
||||
return remote(URL, PORT, ssl=True)
|
||||
else:
|
||||
return process(["python3", "./main.py"])
|
||||
|
||||
|
||||
def get_token(io):
|
||||
io.sendline(b"1")
|
||||
io.recvuntil(b"? ")
|
||||
io.sendline(b"cato")
|
||||
io.recvuntil(b"? ")
|
||||
io.sendline(b"cat")
|
||||
return io.recvline().split(b" ")[-1].strip()
|
||||
|
||||
def get_flag(io, token):
|
||||
io.sendline(b"3")
|
||||
io.recvuntil(b": ")
|
||||
io.sendline(token)
|
||||
return io.recvline()
|
||||
|
||||
|
||||
io = start()
|
||||
io.recvuntil(b": ")
|
||||
token = b64decode(get_token(io))
|
||||
parts = token.split(b"|mac=")
|
||||
data_to_pad = parts[0]
|
||||
original_mac = parts[1].decode()
|
||||
|
||||
extension = b"|admin=true"
|
||||
key_len = 32
|
||||
glue = get_sha1_padding(key_len + len(data_to_pad))
|
||||
new_mac = forge_mac(original_mac, key_len, data_to_pad, extension)
|
||||
malicious_payload = data_to_pad + glue + extension + b"|mac=" + new_mac.encode()
|
||||
final_token = b64encode(malicious_payload)
|
||||
io.recvuntil(b": ")
|
||||
print(get_flag(io, final_token).decode())
|
||||
|
||||
1
2026/cscg/crypto/intro3/.python-version
Normal file
1
2026/cscg/crypto/intro3/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.13
|
||||
27
2026/cscg/crypto/intro3/main.py
Normal file
27
2026/cscg/crypto/intro3/main.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import os
|
||||
|
||||
BITS = 56
|
||||
|
||||
FLAG = os.getenv("FLAG", "CSCG{TESTFLAG}")
|
||||
|
||||
A = int.from_bytes(os.urandom(BITS//8), "little")
|
||||
B = int.from_bytes(os.urandom(BITS//8), "little")
|
||||
SEED = int.from_bytes(os.urandom(BITS//8), "little")
|
||||
|
||||
def rng(x, size):
|
||||
return (x*A+B) & ((2**size)-1)
|
||||
|
||||
def gen_random(seed, bits, mask):
|
||||
state = seed
|
||||
while True:
|
||||
state = rng(state, bits)
|
||||
yield state & mask
|
||||
|
||||
def main():
|
||||
print("Here are some random numbers, now guess the flag")
|
||||
rng = gen_random(SEED, BITS, 0xFF)
|
||||
for i in range(len(FLAG)):
|
||||
print(next(rng) ^ ord(FLAG[i]))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
139
2026/cscg/crypto/intro3/msg.txt
Normal file
139
2026/cscg/crypto/intro3/msg.txt
Normal file
@@ -0,0 +1,139 @@
|
||||
Here are some random numbers, now guess the flag
|
||||
212
|
||||
222
|
||||
52
|
||||
234
|
||||
44
|
||||
156
|
||||
96
|
||||
149
|
||||
122
|
||||
84
|
||||
164
|
||||
111
|
||||
148
|
||||
46
|
||||
218
|
||||
43
|
||||
165
|
||||
239
|
||||
14
|
||||
239
|
||||
31
|
||||
175
|
||||
5
|
||||
149
|
||||
122
|
||||
68
|
||||
177
|
||||
123
|
||||
162
|
||||
44
|
||||
224
|
||||
55
|
||||
225
|
||||
238
|
||||
26
|
||||
157
|
||||
48
|
||||
155
|
||||
90
|
||||
129
|
||||
125
|
||||
105
|
||||
176
|
||||
20
|
||||
174
|
||||
4
|
||||
242
|
||||
43
|
||||
228
|
||||
215
|
||||
26
|
||||
232
|
||||
48
|
||||
155
|
||||
112
|
||||
171
|
||||
98
|
||||
87
|
||||
197
|
||||
21
|
||||
176
|
||||
31
|
||||
133
|
||||
84
|
||||
228
|
||||
215
|
||||
30
|
||||
239
|
||||
20
|
||||
174
|
||||
90
|
||||
171
|
||||
37
|
||||
111
|
||||
142
|
||||
111
|
||||
144
|
||||
47
|
||||
132
|
||||
5
|
||||
167
|
||||
238
|
||||
26
|
||||
148
|
||||
103
|
||||
132
|
||||
113
|
||||
167
|
||||
97
|
||||
111
|
||||
160
|
||||
123
|
||||
161
|
||||
4
|
||||
241
|
||||
39
|
||||
225
|
||||
239
|
||||
32
|
||||
251
|
||||
33
|
||||
132
|
||||
113
|
||||
187
|
||||
98
|
||||
108
|
||||
160
|
||||
119
|
||||
161
|
||||
46
|
||||
218
|
||||
93
|
||||
240
|
||||
216
|
||||
26
|
||||
148
|
||||
35
|
||||
151
|
||||
96
|
||||
213
|
||||
112
|
||||
95
|
||||
160
|
||||
99
|
||||
184
|
||||
47
|
||||
206
|
||||
47
|
||||
196
|
||||
239
|
||||
69
|
||||
156
|
||||
59
|
||||
175
|
||||
78
|
||||
172
|
||||
42
|
||||
112
|
||||
9
2026/cscg/crypto/intro3/pyproject.toml
Normal file
9
2026/cscg/crypto/intro3/pyproject.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[project]
|
||||
name = "intro3"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"z3-solver>=4.16.0.0",
|
||||
]
|
||||
52
2026/cscg/crypto/intro3/solve.py
Normal file
52
2026/cscg/crypto/intro3/solve.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from z3 import BitVec, Solver, sat
|
||||
|
||||
# Replace this with the actual numbers printed by the program
|
||||
with open("./msg.txt") as f:
|
||||
OUTPUTS = [int(line) for line in f.readlines()[1:]]
|
||||
|
||||
BITS = 56
|
||||
MASK = 0xFF
|
||||
|
||||
# Symbolic variables for the LCG parameters
|
||||
A = BitVec('A', BITS)
|
||||
B = BitVec('B', BITS)
|
||||
SEED = BitVec('SEED', BITS)
|
||||
|
||||
s = Solver()
|
||||
|
||||
# Define the state sequence
|
||||
states = [SEED]
|
||||
for i in range(len(OUTPUTS)):
|
||||
# Next state transition: s_i+1 = (s_i * A + B) & (2**56 - 1)
|
||||
# Z3 BitVec arithmetic handles the modulo 2^n automatically
|
||||
next_state = (states[i] * A) + B
|
||||
states.append(next_state)
|
||||
|
||||
# Known prefix: "CSCG{"
|
||||
known_prefix = "CSCG{"
|
||||
for i, char in enumerate(known_prefix):
|
||||
# output = (state_i+1 & MASK) ^ ord(FLAG[i])
|
||||
# Therefore: (state_i+1 & MASK) == output ^ ord(FLAG[i])
|
||||
s.add((states[i+1] & MASK) == OUTPUTS[i] ^ ord(char))
|
||||
|
||||
# If the flag ends with '}', add that constraint as well
|
||||
s.add((states[len(OUTPUTS)] & MASK) == OUTPUTS[-1] ^ ord('}'))
|
||||
|
||||
if s.check() == sat:
|
||||
model = s.model()
|
||||
print("Found LCG Parameters:")
|
||||
print(f"A: {model[A]}")
|
||||
print(f"B: {model[B]}")
|
||||
print(f"SEED: {model[SEED]}")
|
||||
|
||||
# Recover the flag
|
||||
flag = ""
|
||||
for i in range(len(OUTPUTS)):
|
||||
# Extract the lower 8 bits of the calculated state
|
||||
state_val = model.evaluate(states[i+1]).as_long()
|
||||
flag_char = chr((state_val & MASK) ^ OUTPUTS[i])
|
||||
flag += flag_char
|
||||
|
||||
print(f"Flag: {flag}")
|
||||
else:
|
||||
print("Unsatisfiable: Check if the prefix or output data is correct.")
|
||||
29
2026/cscg/crypto/intro3/uv.lock
generated
Normal file
29
2026/cscg/crypto/intro3/uv.lock
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "intro3"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "z3-solver" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "z3-solver", specifier = ">=4.16.0.0" }]
|
||||
|
||||
[[package]]
|
||||
name = "z3-solver"
|
||||
version = "4.16.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/93/3b/2b714c40ef2ecf6d8aa080056b9c24a77fe4ca2c83abd83e9c93d34212ac/z3_solver-4.16.0.0.tar.gz", hash = "sha256:263d9ad668966e832c2b246ba0389298a599637793da2dc01cc5e4ef4b0b6c78", size = 5098891, upload-time = "2026-02-19T04:14:08.818Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/5d/9b277a80333db6b85fedd0f5082e311efcbaec47f2c44c57d38953c2d4d9/z3_solver-4.16.0.0-py3-none-macosx_15_0_arm64.whl", hash = "sha256:cc52843cfdd3d3f2cd24bedc62e71c18af8c8b7b23fb05e639ab60b01b5f8f2f", size = 36963251, upload-time = "2026-02-19T04:13:44.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/c4/fc99aa544930fb7bfcd88947c2788f318acaf1b9704a7a914445e204436a/z3_solver-4.16.0.0-py3-none-macosx_15_0_x86_64.whl", hash = "sha256:e292df40951523e4ecfbc8dee549d93dee00a3fe4ee4833270d19876b713e210", size = 47523873, upload-time = "2026-02-19T04:13:48.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/e6/98741b086b6e01630a55db1fbda596949f738204aac14ef35e64a9526ccb/z3_solver-4.16.0.0-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:afae2551f795670f0522cfce82132d129c408a2694adff71eb01ba0f2ece44f9", size = 31741807, upload-time = "2026-02-19T04:13:52.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/2e/295d467c7c796c01337bff790dbedc28cf279f9d365ed64aa9f8ca6b2ba1/z3_solver-4.16.0.0-py3-none-manylinux_2_38_aarch64.whl", hash = "sha256:358648c3b5ef82b9ec9a25711cf4fc498c7881f03a9f4a2ea6ffa9304ca65d94", size = 27326531, upload-time = "2026-02-19T04:13:55.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/df/29816ce4de24cca3acb007412f9c6fba603e55fcc27ce8c2aade0939057a/z3_solver-4.16.0.0-py3-none-win32.whl", hash = "sha256:cc64c4d41fbebe419fccddb044979c3d95b41214547db65eecdaa67fafef7fe0", size = 13341643, upload-time = "2026-02-19T04:13:58.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/20/cef4f4d70845df24572d005d19995f92b7f527eb2ffb63a3f5f938a0de2e/z3_solver-4.16.0.0-py3-none-win_amd64.whl", hash = "sha256:eb5df383cb6a3d6b7767dbdca348ac71f6f41e82f76c9ac42002a1f55e35f462", size = 16419861, upload-time = "2026-02-19T04:14:03.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/18/7dc1051093abfd6db56ce9addb63c624bfa31946ccb9cfc9be5e75237a26/z3_solver-4.16.0.0-py3-none-win_arm64.whl", hash = "sha256:28729eae2c89112e37697acce4d4517f5e44c6c54d36fed9cf914b06f380cbd6", size = 15084866, upload-time = "2026-02-19T04:14:06.355Z" },
|
||||
]
|
||||
28
2026/cscg/crypto/zipper/Dockerfile
Normal file
28
2026/cscg/crypto/zipper/Dockerfile
Normal file
@@ -0,0 +1,28 @@
|
||||
FROM sagemath/sagemath:10.6@sha256:19995db6194f4a4bab18ce9a88556fd15b9ed5e916b4504fefe618a7796ddbdb
|
||||
|
||||
USER root
|
||||
|
||||
ADD --chmod=0755 --checksum=sha256:c125df9762b0c7233459087bb840c0e5dbfc4d9690ee227f1ed8994f4d51d2e0 \
|
||||
https://raw.githubusercontent.com/reproducible-containers/repro-sources-list.sh/v0.1.4/repro-sources-list.sh \
|
||||
/usr/local/bin/repro-sources-list.sh
|
||||
RUN repro-sources-list.sh
|
||||
|
||||
RUN apt-get update && apt-get install -y xinetd
|
||||
|
||||
RUN sage -pip install --no-cache-dir --upgrade pip
|
||||
RUN sage -pip install --no-cache-dir pycryptodome
|
||||
|
||||
RUN mkdir /ctf
|
||||
WORKDIR /ctf
|
||||
|
||||
RUN echo "Connection blocked." > /etc/banner_fail
|
||||
COPY ctf.xinetd /etc/xinetd.d/ctf
|
||||
|
||||
COPY run.sh .
|
||||
COPY challenge.py .
|
||||
|
||||
RUN chown -R root:sage /ctf && chmod -R 750 /ctf && chmod 777 /ctf/run.sh && chmod 770 /ctf
|
||||
|
||||
CMD [ "xinetd", "-dontfork" ]
|
||||
|
||||
EXPOSE 1337
|
||||
90
2026/cscg/crypto/zipper/challenge.py
Normal file
90
2026/cscg/crypto/zipper/challenge.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from sage.all import *
|
||||
from Crypto.Util.number import long_to_bytes
|
||||
from Crypto.Util.Padding import pad
|
||||
import gzip, base64, os, string, random
|
||||
|
||||
FLAG = os.getenv("FLAG", "dach2026{fake_flag}")
|
||||
REQUIRED_WINS = 25
|
||||
|
||||
p = 0x1337
|
||||
Fp = GF(p)
|
||||
|
||||
I = matrix.random(Fp, 4)
|
||||
|
||||
def hash(message: bytes):
|
||||
m = pad(message, 25)
|
||||
assert len(m) % 25 == 0
|
||||
|
||||
sections = []
|
||||
for i in range(0, len(m), 25):
|
||||
block = int(m[i:i+25].hex(), 16)
|
||||
|
||||
digits = []
|
||||
for _ in range(16):
|
||||
digits.append(block % p)
|
||||
block //= p
|
||||
|
||||
sections.append(matrix(Fp, [digits[:4], digits[4:8], digits[8:12], digits[12:]]))
|
||||
|
||||
A = I
|
||||
for S in sections:
|
||||
A *= S
|
||||
|
||||
hashsum = 0
|
||||
for el in sum([list(r) for r in A.rows()], [])[::-1]:
|
||||
hashsum *= p
|
||||
hashsum += int(el)
|
||||
|
||||
return long_to_bytes(hashsum, 25)
|
||||
|
||||
def random_hash():
|
||||
return hash(os.urandom(25))
|
||||
|
||||
def random_message(n):
|
||||
return "".join([random.choice(string.ascii_letters) for _ in range(n)])
|
||||
|
||||
def main():
|
||||
print("Welcome to ZIPPER!")
|
||||
|
||||
wins = 0
|
||||
|
||||
while True:
|
||||
target_message = random_message(32)
|
||||
target_hash = random_hash() # Make this game impossible ( ͡° ͜ʖ ͡°)
|
||||
|
||||
print(f"I am thinking of a gzip file that decompresses to '{target_message}' and has a hash of '{target_hash.hex()}'.")
|
||||
print("What file am I thinking of?")
|
||||
user_input = input("> ")
|
||||
|
||||
try:
|
||||
file = base64.b64decode(user_input)
|
||||
file_hash = hash(file)
|
||||
|
||||
if len(file) == 0:
|
||||
continue
|
||||
|
||||
if file_hash != target_hash:
|
||||
wins = 0
|
||||
print(f"Wrong hash: {file_hash.hex()} != {target_hash.hex()}.")
|
||||
continue
|
||||
|
||||
if gzip.decompress(file).decode() != target_message:
|
||||
wins = 0
|
||||
print("Wrong data!")
|
||||
continue
|
||||
|
||||
wins += 1
|
||||
print(f"CORRECT! You have {REQUIRED_WINS-wins} wins more to go to get a prize!")
|
||||
|
||||
if wins >= REQUIRED_WINS:
|
||||
print("Congratulations, you've outguessed me!")
|
||||
print("\n" + FLAG + "\n")
|
||||
wins = 0
|
||||
except KeyboardInterrupt:
|
||||
exit(0)
|
||||
except:
|
||||
print("Wrong **insert whatever here**!")
|
||||
wins = 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
19
2026/cscg/crypto/zipper/ctf.xinetd
Normal file
19
2026/cscg/crypto/zipper/ctf.xinetd
Normal file
@@ -0,0 +1,19 @@
|
||||
service ctf
|
||||
{
|
||||
disable = no
|
||||
socket_type = stream
|
||||
protocol = tcp
|
||||
wait = no
|
||||
user = sage
|
||||
type = UNLISTED
|
||||
port = 1337
|
||||
bind = 0.0.0.0
|
||||
server = /bin/sh
|
||||
server_args = /ctf/run.sh
|
||||
banner_fail = /etc/banner_fail
|
||||
passenv = FLAG HOME
|
||||
# safety options
|
||||
per_source = 10 # the maximum instances of this service per source IP address
|
||||
rlimit_cpu = 1 # the maximum number of CPU seconds that the service may use
|
||||
#rlimit_as = 1024M # the Address Space resource limit for the service
|
||||
}
|
||||
8
2026/cscg/crypto/zipper/docker-compose.yml
Normal file
8
2026/cscg/crypto/zipper/docker-compose.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
version: "3"
|
||||
services:
|
||||
zipper:
|
||||
image: dach2026-cry-zipper:latest
|
||||
build:
|
||||
context: .
|
||||
ports:
|
||||
- "1337:1337"
|
||||
4
2026/cscg/crypto/zipper/run.sh
Normal file
4
2026/cscg/crypto/zipper/run.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd /ctf
|
||||
sage -python challenge.py
|
||||
Reference in New Issue
Block a user