Šiais metais NKSC (Nacionalinis kibernetinio saugumo centras) organizavo tarpmokyklines CTF varžybas Cyber maratonas. Šiose varžybose dalyvavo mūsų komanda ZeroDayTeam ir po šių varžybų norėčiau pasidalinti savo patirtimi.
Varžybose iš komandos dalyvavo du dalyviai: V1lkius ir zerodaygym.
Apie rungtynes
“Cyber maratonas” buvo 3 ciklų CTF varžybos, tai reiškia, kad užduotys yra paleidžiamos atskirai į kiekvieną ciklą, o pertraukos tarp ciklų yra tarp 2-3 mėnesių. Kiekviename cikle nugalėtojas yra išrenkamas pirmiausiai pagal komandos taškus, paskui pagal užduoties išsprendimo laiką. Varžybų nugalėtojas yra renkamas pagal sudėtinį visų ciklų komandinių taškų kiekį.
Patirtis
Pirmasis ciklas
Pirmasis ciklas, kaip mūsų komanda ir tikėjosi, buvo ganėtinai paprastas ir užduotys buvo nesunkiai išsprendžiamos. Kadangi jau turėjome gana nemažai patirties CTF užduočių sprendime, visas užduotis, išskyrus vieną, išsprendėme per ~40 min. Paskutinė likusi užduotis vadinosi „Gilaus tinklo slaptažodžių grupių himnas“ ir buvo verta 80 taškų (vidutinis užduoties sunkumas).
Užduoties aprašymas:
Ieškant informacijos giliausiuose NeoVilniaus tinklo kampuose randate nutekintus Jono Avilio buvusius slaptažodžius. Išsiaiškinus jo dabartinį slaptažodį būtų galima bandyti „praplėsti“ informacijos šaltinių lauką…
Randate du Jono buvusius slaptažodžius (tekstas0.txt.flag ir tekstas1.txt.flag) ir tris užkoduotus slaptažodžiams tekstus(tekstas0.txt.msg ir tekstas1.txt.msg ir tekstas2.txt.msg), du (tekstas0.txt.msg ir tekstas1.txt.msg) yra slaptažodžiams, kuriuos dabar turi, o trečias yra dabartinio slaptažodžio (tekstas2.txt.msg).
Matai jog kiekviena užkoduota žinutė yra pakartota tris kartus tekste ir kodavimas dar yra papildomai sumaišęs žinutės eilučių eiliškumą.
Taip pat matai kad duomenų bazėje yra paminėta, kad tekstas0.txt.msg failo atkoduoto teksto kiekvienos eilutės pirmi simboliai sudaro slpatažodį tekstas0.txt.flag. Analogiškai yra ir su tekstas1.txt.msg tekstu ir tekstas1.txt.flag slaptažodžiu.
Vadinasi tavo tikslas yra dekoduoti tekstas2.txt.msg , teisingai sudėliotų eilučių sąrašą ir kaip atsakymą pateikti pirmus visų eilučių simbolius - kokie jie bebūtų. Duomenų bazėje taip pat rašoma: P.S. vėliavos atsakyme reikėtų naudoti tik lotyniško raidyno raides ą → a, č → c ir t.t.
Užduoties autorius: Linas Bukauskas @ Vilniaus universitetas
Nekreipdami dėmesio į labai neaiškiai suformuluotą užduotį (kurios aprašymas po kelių dienų buvo patobulintas organizatorių), mes šią užduotį intensyviai sprendėme 3 dienas, be jokių hint’ų (organizatoriai suteikia hint’us paskutinę ciklo dieną). Paskutinę dieną sulaukėme hint’o ir beveik iškart išsprendėme užduotį. Paaiškėjo, kad sprendimas buvo paprasčiausiai apsukti tekstą ir uudekoduoti. Sprendimas pasirodė pakankamai logiškas ir nors mums ir nepatiko ši užduotis dėl jos paprastumo, mes supratom, kad, kaip komandai, mums reikia patobulėti ties kriptografija.
Pirmąjį ciklą iškarto pastebėjome, kad užduotys nėra standartiškos (nebuvo reguliarių kategorijų, kaip PWN ar Web) ir didžioji dalis jų tebuvo kriptografija arba steganografija. Tačiau to ir tikėjomės iš CTF, kuris buvo ruošiamas moksleiviams, kurie, dažnu atveju, nėra susipažinę su kibernetinio saugumo pasauliu.
Pirmąjame cikle mūsų komanda užėmė 2 vietą, VL (Vilniaus Licėjaus) komandai atlikus paskutinę likusią užduotį keliomis valandomis greičiau už mus.
Antrasis ciklas
Antrasis ciklas buvo, taip pat, ganėtinai paprastas. Didžiąją dalį užduočių išsprendėme gana greitai, ir paskutinę likusią rev tipo užduotį išsprendėme antros dienos pradžioj. Antrajame cikle užduočių monotoniškumas pasireiškė dar stipriau. Užduočių tipai buvo tokie patys, kaip ir pirmame cikle: labai daug steganografijos, kriptografijos ir tik viena pasirodžiusi įdomesnė rev tipo užduotis.
Labai aiškiai buvo matomi organizatorių bandymai sukurti užduotis, kurios nebūtų išsprendžiamos AI agentų, ką aš gerbiu ir tikiu, kad tai turėtų būti skatinama. Tačiau jau šiame etape pasimatė ir AI unsolvable užduočių kūrimo minusas - užduočių suprastėjusi kokybė. Mūsų komanda, kaip ir kelios kitos su kuriomis bendravome, tiek pirmo, tiek antro etapo užduotimis, jau tuo metu, buvo pakankamai nusivylusi. Atrodė, kad užduotys buvo sukurtos be idėjos, užduotyse buvo rodomi tik keli triukai, kuriuos buvo sunku ne exploit'int, o tiesiog surasti. Tikriausiai daug kas su manimi gali nesutikti ir sakyti, kad CTF tikslas ir yra surasti kelią, kaip išspręsti užduotį. Tačiau jeigu užduotis turi vieną neaiškų sprendimą, kurio tiesiog reikia ieškoti per spėliojimą, nežinant ar eini teisingu keliu, ar esi visiškame rabbit hole, kuris niekur neveda, tai nėra gera užduotis.
Trečias ciklas ☠️
Trečias etapas žadėjo daug, ypač dėl naujos „Web“ kategorijos, tačiau organizatoriai nepasimokė iš savo klaidų. Logiškos užduotys buvo išspręstos žaibiškai. Galiausiai mes ir komanda „Kvantai“ atsidūrėme situacijoje, kai iki finišo trūko vos trijų užduočių.
Pirmoji iš likusių užduočių
Pirmoji iš likusių 3 užduočių vadinosi Agento kišenė. Joje buvo duoti du failai: tekstinis failas Priminimas.txt ir paveiksliukas, kuris buvo agento kortelės logotipas.
Užduoties aprašymas:
Vieną dieną Jus besekantį agentą pavyko apsukti gatvėse pasinaudojus LocalNet triukus, kuriuos dar išmokote vaikystėje.
Kol agento dėmesys buvo nukreiptas pasinaudojant greitomis rankomis ir pačio agento pasimetimu, jam iš kišenės pavyko ištraukti atminties kortelę.
Iš pirmo kortelės sluoksnio, kuris nėra apsaugotas, pavyko iškasti nuotrauką ir priminimą… Naudojant juos pabandyk gauti prieigą prie likusio atminties kortelės turinio.
Panašu, kad slaptažodžio langui reikia įvesti 6 skirtingus simbolius.
Naują logo galima rasti Discord’e > maratono-info!
Atsakymo formatas ctf_cm{xxxxxx} Užduoties autorius: Povilas @ NKSC
Paveiksliukas iš išvaizdos atrodė visiškai paprastai:
Tačiau paveiksliuko metadatoj buvo užslėpta daug skirtingų duomenų, kas atrodė kaip užuomenos:
Antrame tekstiniame faile buvo matoma, kas iš pradžių atrodė kaip paprastas tekstas, tačiau įsigilinus ir apžiūrėjus visus failo baitus buvo matomi užslėpti specialūs non ascii-readable baitai:
Iš pradžių tai atrodė kaip įdomi užduotis, visa komanda pradėjo labai intensyviai ieškoti informacijos apie užslėptus baitus, tačiau šios užduoties mums nepavyko išspręsti. Tikėjomės, jog atsakymas yra kažkas sudėtingo ir įdomaus, tačiau sužinoję atsakymą labai nusivylėme. Pasirodo, paveikslėlyje užslėpta informacija, tekstiniame faile užslėpti specialūs baitai tebuvo klaidinanti informacija, viskas ką reikėjo padaryti tebuvo kaip flag'ą pateikti raudonos spalvos hex kodą, kuri buvo paveikslėlyje.
Atsakymas: ctf_cm{dc0906}
Organizatoriai paaiškino, jog iš tekstinio failo reikėjo suprasti Raudona, žalia, mėlyna, kaip RGB (kas buvo aišku nuo pat pradžių), ir todėl reikėjo paimti hex kodą spalvos, buvusios paveiksliukyje.
Nors ir šis paaiškinimas yra iš dalies logiškas, bet tie nieku nevedantys rabbit hole'ai, su kuriais susidūrėme ne mes vieni ir kurie nieko nepridėjo prie edukacinės vertės, bei flag'o formato nebuvimas sukėlė daug klausimų, koks išvis buvo šitos užduoties tikslas.
Antroji iš likusių užduočių
Antroji iš likusių užduočių vadinosi Paint paslaptis. Joje buvo duotas ~19mb didumo painter.exe failas. Tai buvo piešimo programėlė sukurta Windows operacinei sistemai.
Užduoties aprašymas:
Po visų įvykių pavyko atlikti ganėtinai sudėtingą operaciją, kurios metu vienas iš agentūros tyrėjų apsimesdamas valytoju gavo prieigą prie Vilko namų
kompiuterių.
Greito sistemos nuskaitymo metu daug anomalijų nebuvo aptikta, bet programėlė „Paint“, kurią jie naudoja, buvo kažkaip modifikuota.
Pasidarius greitą programėlės kopiją, tyrėjui pavyko sėkmingai palikti patalpas…
Dabar laikas išanalizuoti, kas yra paslėpta už šios programėlės.
Mano komandos narys Vilkius labai didelę dalį laiko praleido analizuodamas programos kodą, jį reverse engineerindamas ir aiškindamasis, kaip programa veikia. Po daug laiko jis išsiaiškino, jog programa iš tikrųjų nieko kenksmingo nedaro ar net nieko neslepia, vienintelis dalykas, kur jis kažką rado tai buvo cm{1 simbolius vienoje iš programos ikonėlių, kas atrodė kaip ctf_cm{...}flag'o formato pradžios dalis. Šioje vietoje jis neklydo ir mes pradėjom labiau gilintis į programos ikonėles, kas buvo teisingas kelias. Tačiau net 4 dienas sėdėdami ir bandydami aiškintis skirtingus būdus rasti ir atkoduoti skirtingas flag'o dalis, geriausia, ką mums pavyko atkoduoti buvo ctf_cm{1c0n5_uplMkhad!cr3u}, kas atrodė kaip pradžia, bet tikrai ne galutinis flagas. Baigusis CTF’ui Kvantų komanda, kuriai pavyko išspręsti šią užduotį, draugiškai pasidalino užduoties sprendimu. Jie mums pasakė, kad po labai daug token'ų, jų AI išsprendė šią užduotį ir davė sprendimą. Su dirbtinio intelekto pagalba paredagavau jų šį jų pasidalintą sprendimą ir pateikiau jį žemiau.
Užduoties sprendimas:
Summary
The flag was hidden across the icon resources inside painter.exe, not just in the exported icon_*.png files.
The crucial realization was that the icons had to be analyzed as original Windows RT_ICON resources:
some sizes were stored as PNG resources
some sizes were stored as DIB icon bitmaps with XOR and AND masks
some resource PNGs contained extra chunks or trailing bytes that were lost in normal exports
Once the original resource blobs were inspected size by size, each icon contributed one fragment of the flag.
The final reconstructed flag is:
ctf_cm{1c0n5_h1d3_53cr375}
Normalized out of leetspeak, this reads:
ctf_cm{icons_hide_secrets}
Why The Exported PNGs Were Not Enough
At first, the exported icon files looked like the whole dataset:
icon_16x16.png
icon_24x24.png
icon_32x32.png
icon_48x48.png
icon_64x64.png
icon_96x96.png
icon_128x128.png
icon_256x256.png
But after dumping the original RT_ICON resources from painter.exe, it became clear that several exported files were not byte-identical to the source resources:
96x96 and 256x256 matched exactly
32x32, 48x48, 64x64, and 128x128 had hidden structure-level data in the original resource blobs
16x16 and 24x24 were not PNG resources at all; they were DIB icon bitmaps with masks
This matches the organizer hint perfectly: the data was hidden in deeper layers such as bits, extra channels, and file structure.
Resource Layout
painter.exe contains one RT_GROUP_ICON with 8 icon entries:
16x16
24x24
32x32
48x48
64x64
96x96
128x128
256x256
Each size used a different hiding method.
Extraction By Size
16x16 →ctf_
The 16x16 resource is a DIB icon, not a PNG.
Inside the XOR bitmap, there are 17 fully transparent pixels with RGB (0,0,1) in the first two rows. Interpreting those near-black pixels as a binary bitmap gives:
row 0: 01100011 01110100row 1: 01100110 01011111
ASCII:
01100011 = c01110100 = t01100110 = f01011111 = _
Fragment:
ctf_
24x24 →cm{1
The 24x24 resource is also a DIB icon.
The important part was not the visible image, but the AND mask. The start of the AND-mask region contains the literal bytes:
63 6d 7b 31
ASCII:
cm{1
This was the missing proof that cm{ was real and not just an invented XOR artifact.
32x32 →c0n
The original 32x32 icon resource is a PNG with an extra tEXt chunk that does not exist in the exported file.
The chunk contents are:
Comment\0p0a
Taking the hidden payload p0a and applying ROT13:
p0a -> c0n
Fragment:
c0n
48x48 →5_h
The original 48x48 icon resource is a PNG with 8 extra bytes after IEND:
# NV9o
Taking the payload NV9o and base64-decoding it:
NV9o -> 5_h
Fragment:
5_h
64x64 →1d3
The original 64x64 icon resource contains a hidden zTXt chunk:
Description\0\0...
After decompression, the hidden text is:
cyZx
Base64-decoding it gives:
cyZx -> s&q
Then XORing each byte with 0x42 gives:
s&q XOR 0x42 -> 1d3
Fragment:
1d3
96x96 →_53
The 96x96 icon contains 16 pixels with alpha = 1 on the top row. Reading the top row as bits gives:
0001110101110111011100010100001
Padding it to 32 bits on the right:
00011101011101110111000101000010
Hex:
1d777142
Bytes:
1d 77 71 42
Using the same XOR key as the 64x64 fragment, 0x42:
1d 77 71 42XOR 42 42 42 42=5f 35 33 00
ASCII:
_53\0
Ignoring the terminating null byte gives:
_53
128x128 →cr3
The original 128x128 resource contains an iCCP chunk that does not exist in the exported file.
Its profile name is:
637233
Interpreting that as hex:
63 72 33 -> cr3
Fragment:
cr3
256x256 →75}
The 256x256 icon contains three fully transparent pixels with nonzero RGB:
Concatenating the extracted fragments in icon-size order:
ctf_ + cm{1 + c0n + 5_h + 1d3 + _53 + cr3 + 75}
This gives:
ctf_cm{1c0n5_h1d3_53cr375}
Leetspeak normalization:
1 -> i0 -> o5 -> s3 -> e7 -> t
So the phrase reads:
ctf_cm{icons_hide_secrets}
Final Answer
Raw reconstructed flag:
ctf_cm{1c0n5_h1d3_53cr375}
Normalized reading:
ctf_cm{icons_hide_secrets}
If the platform expects the exact extracted leetspeak form, submit:
ctf_cm{1c0n5_h1d3_53cr375}
Perskaitęs užduoties sprendimą pakalbėjau su komanda (tokią nuomonę turėjo ir kitos komandos) ir mes visi pritarėm, jog ši užduotis yra visiškai absurdiška ir neišsprendžiama žmogui. Flag'o dalys buvo ištraukiamos iš visiškai atsitiktinių vietų programoje ir tarp jų nesimatė jokios logiškos sąsajos. Net organizatoriai nesugebėjo paaiškinti šio sprendimo kelio ir kodėl flag'o dalys buvo būtent taip išmėtytos. Ties šia užduotimi praleidome labai daug laiko, tačiau galiausiai likome tikrai nusivylę jos sprendimu.
Trečioji iš likusių užduočių
Ši užduotis vadinosi Iš ko statomi Vilko namai?, tai buvo web kategorijos užduotis, kurioje buvo pateikta svetainė.
Užduoties aprašymas:
Patekus į Xitacho tech gamyklines patalpas, naudojant kortelę su gamykline prieiga galite pasiekti vidinę gamykloje naudojamą sistemą, kurioje, panašu, yra visa gamyklinė informacija.
Gal pavyks surasti sistemos spragų, kurios galės pateikti daugiau informacijos apie medžiagas, kurios buvo naudojamos Vilko namų statyboje.
Užduoties atsakymą pateikite ctf_cm{} formatu.
Užduoties autorius: Adas @ NKSC
Ties šia užduotimi praleidau daugiausiai laiko. Pakankamai greitai išsiaiškinau, kad visi svetainės endpoint'ai buvo statiniai apart vieno /api/proxy. Šis endpoint turėjo path parametrą su kuriuo serveris darė HTTP GET requestus į nurodytą kelią. Kas blokavo vartotoją nuo paprašymo visiškai, bet kokio kelio buvo reikalavimas, kad kelias prasidėtų su /assets/ ir, taip pat, visi path traversal bandymai tokie kaip ../, bandant pabėgti iš leidžiamo kelio buvo neleistini. Pakankamai greitai, jau pačią pirmą dieną išsiaiškinau, kad šį blokavimą galima apeiti su trigubu URL kodavimu ties .. simboliais, taigi /api/proxy?path=/assets/%25252e%25252e/index.html suteikė serverio užklaustą /index.html puslapį. Ties šia vieta užstrigau, kadangi iš serverio nesugebėjau gauti jokių naujų failų. Tris dienas praleidau ties šita vieta: naudodamas labai daug bruteforce su skirtingais wordlist'ais ieškojau naujų kelių, ieškojau kitų bug'ų, kuriuos sujungęs galėčiau kažką naujo pasiekti (radau, kad serveris buvo pažeidžiamas CL.0 HTTP Request Smuggling, cache deception bei cache poisoning, tačiau tai niekur nenuvedė). Per šias tris diena kelis kartus prašiau organizatorių patikrinti, ar tikrai užduotis yra išsprendžiama, ar WAF (Web Application Firewall neblokuoja jokių užklausų, tačiau du kartus buvau patikintas, kad užduotis yra išsprendžiama. Galiausiai varžyboms baigusis užduoties kūrėjas atsiuntė šios užduoties sprendimą. Pats pirmas žingsnis buvo:
Ties pirmu žingsniu man iš karto kilo klausimas, kaip reikėjo nežinant gauti /internal/registry/current kelią. Testuojant, net teisingas atspėtas directory nebuvo niekaip skirtingas nuo paprasto neegzistuojančio kelio. Peržiūrėjęs visus savo bruteforce sąrašus supratau, kad nebuvo jokių logiškų būdų atspėti šį kelią. Paklausus organizatorių jie pirmiausiai kovojo, jog tai buvo galima išmąstyti, kadangi /registry/current kelias yra pakankamai dažnas docker aplinkoje, tačiau, kaip atspėti /internal/ jie patys negalėjo atsakyti. Galiausiai pasigilinę jie suprato, jog ši užduotis yra neišsprendžiama ir parašė atsiprašymą dalyviams.
Rekomendacijos organizatoriams
Kadangi su komanda esame dalyvavę ne viename CTF’e ir matę pačių įvairiausių varžybų formatų, turime kelis pastebėjimus bei rekomendacijas organizatoriams. Visų pirma, siūlytume įvesti privalomą testavimą su žmonėmis, kurie tų užduočių nekūrė - tai padėtų išvengti situacijų, kai vietoje techninių žinių tenka tiesiog spėlioti autoriaus mintis bei padėtų sumažinti ar pilnai eliminuoti neišsprendžiamų užduočių kiekį. Taip pat vertėtų atsisakyti painių mįslių, kurios neturi nieko bendro su kibernetiniu saugumu, ir kurti iššūkius, paremtus realiomis pažeidžiamumų paieškos situacijomis. Galiausiai, labai svarbu suskirstyti užduotis į kategorijas, įsitikinti, kad užduotys atitiktų savo kategorijas, o po renginio būtų dalinamasi išsamiais oficialiais sprendimais (write-ups), iš kurių būtų galima pasimokyti.
Apibendrinimas
Apibendrinant mūsų komandos patirtį visuose trijuose „Cyber maratono“ etapuose, tenka pripažinti, kad varžybos paliko dvejopą įspūdį. Aš tikrai gerbiu Nacionalinį kibernetinio saugumo centrą (NKSC/CERT-LT) už iniciatyvą ir bandymą įtraukti Lietuvos jaunimą į kibernetinio saugumo sritį, tačiau tenka pripažinti, kad pačiu šių CTF varžybų formatu bei kokybe esu labai nusivylęs.
Nors pasitaikė viena kita įtraukianti užduotis, daugelis jų atrodė realiame gyvenime nepritaikomos ir sukurtos tik tam, kad užpildytų varžybų turinį. Didžioji dalis užduočių neturėjo gilesnės idėjos ir pademonstravo tik pavienes, neišskirtines technikas. Užduotims smarkiai trūko loginės sekos, sprendimus tekdavo aklai spėlioti, o dažnu atveju susidarė įspūdis, jog jos buvo tiesiog nekokybiškai sugeneruotos dirbtinio intelekto. Trečiajame etape, praleidus daugybę laiko prie „Iš ko statomi Vilko namai?“ užduoties ir net kelis kartus gavus organizatorių patikinimus dėl jos išsprendžiamumo, galiausiai paaiškėjo apmaudi tiesa – su turima informacija jos išspręsti buvo neįmanoma.
Bendra šių varžybų kokybė mūsų komandą labai nuvylė. Užduotys buvo silpnos ir paviršutiniškos, o iš tokios institucijos kaip NKSC tikėjomės gerokai aukštesnio, profesionalesnio ir į realių įgūdžių ugdymą orientuoto lygio. Tikimės, kad organizatoriai atsižvelgs į šią kritiką ir ateities kibernetiniai maratonai bus daug labiau apgalvoti.