cscg ist super

This commit is contained in:
2026-04-10 03:31:12 +02:00
parent 7a9dfeda60
commit db0324c43d
99 changed files with 92358 additions and 0 deletions

41
2026/cscg/crypto/intro1/main.py Executable file
View 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()

View File

@@ -0,0 +1 @@
FLAG = "CSCG{i_love_my_mum}"

View 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
View 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()

View File

@@ -0,0 +1 @@
FLAG = "CSCG{i_love_my_mum}"

View 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())

View File

@@ -0,0 +1 @@
3.13

View 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()

View 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

View 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",
]

View 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
View 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" },
]

View 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

View 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()

View 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
}

View File

@@ -0,0 +1,8 @@
version: "3"
services:
zipper:
image: dach2026-cry-zipper:latest
build:
context: .
ports:
- "1337:1337"

View File

@@ -0,0 +1,4 @@
#!/bin/bash
cd /ctf
sage -python challenge.py