Posts FCSC 2025
Post
Cancel

FCSC 2025

Intro challenges

Smölkkey

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
from gmpy2 import powmod as pow
from Crypto.PublicKey import RSA

class Smolkkey:
	def __init__(self):
		k = RSA.generate(2048, e = 3)
		self.pk = (k.n, k.e)
		self.sk = (k.n, k.d)

	def encrypt(self, m):
		n, e = self.pk
		c = pow(m, e, n)
		return int(c)

	def decrypt(self, c):
		n, d = self.sk
		m = pow(c, d, n)
		return int(m)

# Generate a key
E = Smolkkey()

# Encrypt the fag
flag = open("flag.txt", "rb").read()
flag = int.from_bytes(flag, "little")
c = E.encrypt(flag)

# Test decryption
assert flag == E.decrypt(c)

# Output public values
n, e = E.pk
print(f"{n = }")
print(f"{e = }")
print(f"{c = }")



n = 20828609401338794038836680655046788059251524928933537772275737490132096798900518851229365799426251400151127719543434160180496659560792762761336988343332946920310984844136554433346165529108260963140451576722579583104830933409454682160084747257400706214980238995436388944310800852033141986598424966358149711167942491331040747300866718813771865768701794983365111208518863175847678947437360554933091347604616653687980177405805542214635577758515398014710929022135522835744517328114492844837858920033569071591971676487452812830920525469634367387593309067794509735740140745616085618489218115494716811261406227449967579233657
e = 3
c = 6317668510138686569655374990729607736156413707292408158720036346854309670467296052918552527575331589363290061240725095262980389263184520673983411112154423282089471021996509038472493779143273789325774414352608726252566350689111876373836913240644190951995980896093509379920452743478551321978067299216590452459233562642920123055978471365092000347562228787318105538018723376505390423730687522026043802357456368003656219942603097205774742385485995835519133581552096067468551713114231926639878045212204590071768

This challenge is used vanila RSA with some weird values. In this case, we can clearly see that:

\[c << n\]

In such cases, it can happen that:

\[c^e < n\]

and thus we can just take the root e straight away using regular math.

Solve

1
2
3
4
5
from gmpy2 import iroot
c = 6317668510138686569655374990729607736156413707292408158720036346854309670467296052918552527575331589363290061240725095262980389263184520673983411112154423282089471021996509038472493779143273789325774414352608726252566350689111876373836913240644190951995980896093509379920452743478551321978067299216590452459233562642920123055978471365092000347562228787318105538018723376505390423730687522026043802357456368003656219942603097205774742385485995835519133581552096067468551713114231926639878045212204590071768
print(bytes.fromhex(hex(iroot(c, 3)[0])[2:])[::-1])

# FCSC{30f7c4b2fa7f0fb48bfbd9bbd413491c0a6da660764961b862fe38a83b4bc00f}

Carrote radis tomate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

key = os.urandom(32)
print("carotte = ", int.from_bytes(key) % 17488856370348678479)
print("radis   = ", int.from_bytes(key) % 16548497022403653709)
print("tomate  = ", int.from_bytes(key) % 17646308379662286151)
print("pomme   = ", int.from_bytes(key) % 14933475126425703583)
print("banane  = ", int.from_bytes(key) % 17256641469715966189)

flag = open("flag.txt", "rb").read()
E = AES.new(key, AES.MODE_ECB)
enc = E.encrypt(pad(flag, 16))
print(f"enc = {enc.hex()}")

For this challenge, the name is a clear hint to use the Chinese Reminder Theorem (CRT). Using sage math we can find that the key is 58537804506201655097879135670024677446002384165465965481902712238119765745741

The rest is trivial

1
2
3
4
5
6
7
8
9
10
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Util.number import long_to_bytes

key = 58537804506201655097879135670024677446002384165465965481902712238119765745741
E = AES.new(long_to_bytes(key, 32), AES.MODE_ECB)
enc = bytes.fromhex("2da1dbe8c3a739d9c4a0dc29a27377fe8abc1c0feacc9475019c5954bbbf74dcedce7ed3dc3ba34fa14a9181d4d7ec0133ca96012b0a9f4aa93c42c61acbeae7640dd101a6d2db9ad4f3b8ccfe285e0d") 
print(E.decrypt(enc).decode())

# FCSC{2c4c4b3be7d86e1642ce6a8bf1bd75f33b9736e5943f51a49fb9327e248c3b6a}

Meme generator (not solved)

For this one, the challenge is to use a bot to recover the file from local storage.

After some look through the code we can see that there is 2 entries possible:

1
2
3
4
5
6
<?php if (isset($_GET['image']) && isset($_GET['text'])): ?>
            <div class="meme-container">
                <img src="img/<?php echo $_GET['image']; ?>" class="img-fluid">
                <div class="meme-text"><?php echo strtoupper($_GET['text']); ?></div>
            </div>
<?php endif; ?>

Using the image and text entries gives you the possibility to inject some JS code that can run and recover the flag from localstorage

Red herring

While both are possible, I focused mainly on the text one. Realized pretty soon that it had a glaring issue: echo strtoupper($_GET['text']). Unfortunately JS is sensitive to case and in this case we would need some way of encoding everything to bypass the strtoupper. One possibility is JS Fuck but unfortunately the payload we need would be too big for the bot and just fail.

Actual solution

Instead of using the text field you can use the image field and force JS code to execute since there is no formatting for that part of the code.

We know that we have access to all the console.log from the code of the bot so our goal is to execute the following payload: console.log(localStorage.getItem('flag'))

Since there is no formatting we can just add the payload right after the image source and just add "to get out of the <img> tag giving us:

http://meme-generator/?image=grumpy.jpeg"><script>console.log(localStorage.getItem('flag'))</script>&text=oops

This will get you the flag

Touillette

1
2
3
4
5
6
7
8
9
10
11
12
13
14
flag = open("flag.txt").read()
assert len(flag) == 64

x = "".join([
    flag[-8::-8],
    flag[-7::-8],
    flag[-6::-8],
    flag[-5::-8],
    flag[-4::-8],
    flag[-3::-8],
    flag[-2::-8],
    flag[-1::-8],
])
print(x[1::2] + x[0::2])

My solve

Here is actually tried to understand what was done and reverse it part by part. Pretty long to do

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
flag = """Z2yFm7bCjR6SMWOCSiw{wqKWoJxTtxP4Hf74mQZ4qmghcu1mdX9HND7u8oxF}JsR"""

x = "".join([
    flag[-8::-8],
    flag[-7::-8],
    flag[-6::-8],
    flag[-5::-8],
    flag[-4::-8],
    flag[-3::-8],
    flag[-2::-8],
    flag[-1::-8],
])
print(x[1::2] + x[0::2])

flag1 = flag[:32]
flag2 = flag[32:]

trueFlag = ""

for x, y in zip(flag1, flag2):
    trueFlag += y + x

print(trueFlag)

partition = 64/8

flag1 = trueFlag[0:partition]
flag2 = trueFlag[partition:partition*2]
flag3 = trueFlag[partition*2:partition*3]
flag4 = trueFlag[partition*3:partition*4]
flag5 = trueFlag[partition*4:partition*5]
flag6 = trueFlag[partition*5:partition*6]
flag7 = trueFlag[partition*6:partition*7]
flag8 = trueFlag[partition*7:64]

print(flag1)

ret = ""

for a, b, c, d, e, f, g, h in zip(flag1,flag2, flag3, flag4, flag5, flag6, flag7, flag8):
    ret += h + g +f  +e + d +c  +b +a

print(ret[::-1])

Better solve

A friend after the end made me realize that this is technically a basic substitution cipher so you can just go and find the index from the original alphabet and tada you get the reversed flag

1
2
3
4
5
6
7
8
9
10
11
12
etalon = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUWXYZ-_="
touilled_etalon = "Mwg0Nxh1Oyi2Pzj3QAk4RBl5SCm6TDn7UEo8WFp9XGqaYHrbZIsc-Jtd_Kue=Lvf"
touilled_flag="Z2yFm7bCjR6SMWOCSiw{wqKWoJxTtxP4Hf74mQZ4qmghcu1mdX9HND7u8oxF}JsR"
flag=["/"]*64

for i in range(64):
    curr_char = touilled_etalon[i]
    i_prime = etalon.find(curr_char)
    flag[i_prime] = touilled_flag[i]
print("".join(flag))

#FCSC{WT444hmHuFRyb6OwKxP7Zg197xs27RWiqJxfQmuXDoJZmjMSwotHmqcdN8}

Crypto challenges

La qu6ete de l’anneau

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
#!/usr/bin/env python3

import json
import os
from Crypto.Util.number import *
from Crypto.Random.random import randrange
from math import gcd

class Cipher:

	def __init__(self, size = 512):
		self.s = 2**size + randrange(2**size)
		self.bs = size // 8

	def encrypt(self, m, data = False):
		assert len(m) % self.bs == 0, f"Error: wrong length ({len(m)})"
		C = []
		for i in range(0, len(m), self.bs):
			iv = randrange(self.s)
			while gcd(iv,self.s) != 1:
				iv = randrange(self.s)
			b = int.from_bytes(m[i:i + self.bs],"big")
			if not data:
				C.append({
					"iv" : iv,
					"c" : (b * iv) % self.s,
				})
			else:
				C.append({
					"m" : b,
					"iv" : iv,
					"c" : (b * iv) % self.s,
				})

		return C

	def decrypt(self, c):
		r = b""
		for d in c:
			m = d["c"] * pow(d["iv"], -1, self.s) % self.s
			r += int.to_bytes(m, self.bs,"big")
		return r

if __name__ == "__main__":

	flag = open("flag.txt", "rb").read().strip()
	assert len(flag) == 64, "Error: wrong flag length."

	E = Cipher()

	m = os.urandom(64).hex()
	data = E.encrypt(m.encode(), data = True)

	C = E.encrypt(flag)
	assert flag == E.decrypt(C), "Error: decryption test failed."

	print(json.dumps({
		"data" : data,
		"C" : C
	}, indent = 4))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
    "data": [
        {
            "m": 5300924613498218875069386136478898074006475860605696653663766789647491408111939951962787562515179231001576088241821257224379937942480792836286291708950580,
            "iv": 990478785346720639288503860820230515465392521155988529067551628642203679040736067705099478569806691727869997283427937397835972477847915536798446362176571,
            "c": 14608971898840483258729047186216694135901081092160417249500736213435666342621773315139756301160520793786438865525865528889455866097651720255856961635805732
        },
        {
            "m": 5100397453901829001001274202132069470845583300596122363556250272408483548979629877688935275217404969058564324787967275472360630765503385955263068863095345,
            "iv": 7475569552781515571825018823124971469978831837925573660203231293669196922174669333904855142066157502493624218193454674776997566021222129832543051527639435,
            "c": 7362760529141371598441777437669466021221530082696679574829167632394833775523119009628310509508552230962414538323390642388879749988667246066507482829109785
        }
    ],
    "C": [
        {
            "iv": 14065646473510331612652599339352010982436820046928032638519239485603018733626860766641543018671779202005925711911356379944986562755350855828351678555098061,
            "c": 8973029504100997651890859382562887986006499283456026995696294203177046779401655025542878703573157181581548171480134446908730999381542131074327319465448895
        }
    ]
}

For this challenge, the key resides in the fact that in cases where the message is too long we get more info:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for i in range(0, len(m), self.bs):
    iv = randrange(self.s)
    while gcd(iv,self.s) != 1:
        iv = randrange(self.s)
    b = int.from_bytes(m[i:i + self.bs],"big")
    if not data:
        C.append({
            "iv" : iv,
            "c" : (b * iv) % self.s,
        })
    else:
        C.append({
            "m" : b,
            "iv" : iv,
            "c" : (b * iv) % self.s,
        })

Thankfully for us, while the original message is short and only gives us one point of data, the rest is long and gives us 2 points. Thus we now have a way to solve this for the modulus:

We thus have:

\[b_1 * iv_1 \cong c_1\ [n] \newline \Leftrightarrow b_1 * iv_1 - c_1 \cong 0\ [n] \newline \Leftrightarrow b_1 * iv_1 - c_1 = n * k_1\]

For some random k. We have that for 2 points and we can assume that k will be significantly smaller than n. Thus finding the GCD will give us n. We can then use that to find what the original message m is by:

\[b * iv \cong c\ [n] \Leftrightarrow b \cong c * iv^{-1} [n]\]

Cocorico

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import os
import json
from zlib import crc32 as le_mac
from Crypto.Cipher import AES

class CocoRiCo_Chiffrement_AEAD:
	def __init__(self, la_clef):
		self.la_clef = la_clef

	def le_chiffrement(self):
		return AES.new(self.la_clef, AES.MODE_OFB, iv = b"\x00" * 16)
 
	def chiffrer_integre(self, le_message):
		le_tag = int.to_bytes(le_mac(le_message), 4)
		return self.le_chiffrement().encrypt(le_message + le_tag)

	def dechiffrer(self, le_chiffre):
		x = self.le_chiffrement().decrypt(le_chiffre)
		le_message, t = x[:-4], x[-4:]
		le_tag = int.to_bytes(le_mac(le_message), 4)
		if le_tag == t:
			return le_message
		else:
			return b""

try:
	la_clef = os.urandom(32)
	E = CocoRiCo_Chiffrement_AEAD(la_clef)

	for _ in "FCSC":

		print("0. Quit")
		print("1. Login")
		print("2. Logout")
		print("3. TODO")
		choice = int(input(">>> "))

		if choice == 0:
			break

		elif choice == 1:

			new = input("Are you new ? (y/n) ")
			if new == "y":

				name = input("Name: ")
				if name == "toto":
					print("Toto is one of our admin! Do not try to outsmart the system!")
					exit(1)

				d = json.dumps({
					"name": name,
					"admin": False,
				}).encode()

				c = E.chiffrer_integre(d)
				print(f"Welcome {name}. Here is your token:")
				print(c.hex())

				logged = 1

				print("This challenge is still under active developement, please come back in a few weeks to try it out!")
				# TODO: Add vulnerable code here

			elif new == "n":

				token = bytes.fromhex(input("Token: "))
				x = E.dechiffrer(token)
				d = json.loads(x)
				if d["name"] == "toto" and d["admin"]:
					print("Congrats! Here is your flag:")
					print(open("flag.txt").read().strip())
				else:
					print(f"Weclome back {d['name']}!")

		elif choice == 2:
			logged = 0

		elif choice == 3:
			print("This challenge is still under active developement, please come back in a few weeks to try it out!")
			# TODO: Add another vuln here

except:
	print("Please check your inputs.")

This challenge asks us to forge a token. Thankfully we can create our own. The 2 function that interest us are the following:

1
2
3
4
5
6
def le_chiffrement(self):
    return AES.new(self.la_clef, AES.MODE_OFB, iv = b"\x00" * 16)

def chiffrer_integre(self, le_message):
    le_tag = int.to_bytes(le_mac(le_message), 4)
    return self.le_chiffrement().encrypt(le_message + le_tag)

We can see that the iv is constant and the key as well. AES OFB only XORs the plain text with the iv after the later went through a round of AES. We just need to get that output in order to forge our message. The tag is trivial to find as it is just the CRC32 checksum of the message.

My solve is as follows:

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
#!/usr/bin/env python3

# Install using pip:
#    pip install pwntools --break-system-packages
from pwn import *
import json
from zlib import crc32

'''
To use this template, execute the script with python3:
$ python3 template.py

If you want to run it with a different server and port, use
$ python3 template.py HOST=localhost PORT=4000

If you want to debug and see the data going exchanged, add "DEBUG":
$ python3 template.py DEBUG
'''

# Define the remote connexion details: server address and port
HOST = args.HOST or "chall.fcsc.fr"
PORT = args.PORT or 2150

# Connect to the remote server
# The variable "io" will be used to send and receive text to the remove server
io = remote(HOST, PORT)
io.recvuntil(b">>>")


admin = "toto"
adminMsg = json.dumps({"name": "toto","admin": True,}).encode()
adminTag = int.to_bytes(crc32(adminMsg), 4, "big")
user = "test"
userMessage = json.dumps({"name": user,"admin": False,}).encode()
tag = int.to_bytes(crc32(userMessage), 4, "big")

io.sendline("1".encode())
io.recvuntil(b"Are you new ? (y/n) ")
io.sendline("y".encode())
io.recvuntil(b"Name: ")
io.sendline(userMessage)
io.recvline()


token = bytes.fromhex((io.recvline().decode()))
iv = xor(userMessage+tag, token)

print(token.hex())
print(iv.hex())

io.recvuntil(b">>> ")

forged = xor(adminMsg+adminTag, iv)[:-1]

io.sendline("1".encode())
io.recvuntil(b"Are you new ? (y/n) ")
io.sendline("n".encode())
io.recvuntil(b"Token: ")

io.sendline(forged.hex().encode())


io.recvuntil(b">>> ")
io.close()
This post is licensed under CC BY 4.0 by the author.