Bardzo częstym błędem, który popełniają początkujący (i który NIE JEST tematem tego postu), jest nazywanie kodowania Base64 "szyfrowaniem". Base64 oczywiście szyfrowaniem nie jest, co świetnie wyjaśniła dr Iwona Polak w swoim najnowszym wpisie na LI. To powiedziawszy, Base64 i niektóre jego implementacje mają kilka ciekawych cech, i to właśnie o nich będzie ta seria wpisów.
Posty w serii:
- Base64 poza kodowaniem – steganografia i kanoniczność (cz. 1)
- Base64 poza kodowaniem – steganografia i kanoniczność (cz. 2)
W dużym skrócie, Base64 jest kodowaniem zapisującym 3 bajty za pomocą 4 znaków tekstowych, co jest bardzo przydatne, jeśli trzeba zapisać lub przekazać dane binarne przez medium tekstowe. Dobrym, współcześnie stosowanym przykładem jest "protokół" data:, który umożliwia m.in. użycie Base64 do umieszczenia binarnych plików graficznych bezpośrednio w tekstowym kodzie HTML:
<img
src="data:image/png;base64,
iVBORw0KGgoAAAANSUhEUgAAAAUAAAAEAQMAAAB8/WcDAAAABlBMVEUAAAD
///+l2Z/dAAAAEElEQVQI12NYwfCDoYChAwALUAKZCQz4kwAAAABJRU5Erk
Jggg=="
style="width: 120px; height: 100px; image-rendering: pixelated;">
Efekt działania:
Base64, w różnych swoich wariantach, można znaleźć praktycznie wszędzie – od technologii webowych, przez kryptografię (przechowywanie kluczy binarnych, JWT), po protokoły sieciowe (takie jak SMTP czy IMAP – do kodowania załączników). Co za tym idzie, mamy do czynienia z wieloma różnymi implementacjami, które nie zawsze są ze sobą zgodne i nie zawsze 100% trzymają się specyfikacji (RFC 4648) (choć trzeba dodać, że czasem wynika to z nadrzędnej specyfikacji, która wprowadza wymusza zmiany).
Na marginesie...
Jeśli interesują Was formaty binarne, ale nie mieliście czasu lub okazji przy nich przysiąść, to w połowie listopada rusza druga edycja naszej serii "Pliki i protokoły binarne". Pierwsze szkolenie z serii jest w cenie "płać ile chcesz", więc nie ma powodu, żeby się nie zapisać :)
Steganografia w nieużywanych bitach
Zacznijmy od, moim zdaniem, najciekawszej cechy Base64 – czyli potencjalnych nieużywanych bitów w ostatnim znaku kodownia.
Skąd w ogóle biorą się "jakieś nieużywane bity"? Jak wspomniałem wcześniej, Base64 koduje (mapuje) 3 bajty do 4 znaków. Konkretniej – wchodząc w "matematykę bitową", 24 bity wejściowe, oryginalnie upakowane w 3 bajty – po 8 bitów każdy (co obecnie jest standardem, ale nie zawsze było), są zapisywane za pomocą 4 znaków, po 6 bitów na każdy (3 * 8 = 4 * 6).
Co jeśli danych jest mniej? Tj. 1 bajt albo 2 bajty? Policzmy:
1 bajt == 8 bitów » to daje nam 1 znak (6 bitów)
i jeszcze 2 bity w drugim
... oraz 4 bity nieużywane?
2 bajty == 16 bitów » to daje nam 2 znaki (12 bitów)
i jeszcze 4 bity w trzecim
... oraz 2 bity nieużywane?
Jeśli operacje na bitach Was trochę przerażają, to za bardzo niedługo rozpoczniemy rejestracje na nową edycję serii "Pliki i protokoły binarne", w tym 3-dniowe darmowe szkolenie z operowania na bitach i bajtach :)
Specyfikacja mówi (sekcja 3.5. Canonical Encoding), że te nieużwane bity muszą być wyzerowane. Czy jednak popularne dekodery to sprawdzają? Nie, ponieważ nie muszą (słowo klucz poniżej: MAY):
> In some environments, the alteration is critical and therefore
> decoders MAY chose to reject an encoding if the pad bits have not
> been set to zero.
Jak to przetestować? Najprościej jest zakodować jeden lub dwa bajty zerowe – dostaniemy wtedy kanonicznie kolejno AA==
oraz AAA=
. Teraz wystarczy dowolny z ostatnich znaczących znaków (tj. ostatnie A
) zamienić na np. B
, tak, abyśmy dostali: AB==
oraz AAB=
(co de facto zamienia ostatni z nieużywanych wyzerowanych bitów na 1
).
Tak spreparowane ciągi Base64 możemy wrzucić do dekoderów, np.:
// PHP
base64_decode("AB=="); // Nie zgłosi błędu, zwróci \0.
base64_decode("AB==", /*strict=*/true); // Również nie zgłosi błędu.
# Python
base64.b64decode("AB=="); # Nie zgłosi blędu, zwróci \0.
base64.b64decode("AB==", validate=True); # Również nie zgłosi błędu.
# Node.js
Buffer.from("AB==", "base64"); // Nie zgłosi błędu, zwróci \0.
I faktycznie, nawet w trybie strict
(PHP) czy z ustawionym validate=True
(Python) domyślne dekodery nie mają problemu z niewyzerowanymi nieużywanymi bitami.
Co nas sprowadza do steganografii (ukrywania informacji), tj. można te nieużywane bity wykorzystać do ukrycia czterech lub dwóch bitów danych.
Oczywiście 2 lub 4 bity to bardzo bardzo mało, dlatego w przypadku stosowania tej metody steganografii ukrywane dane są rozbijane na bardzo dużo 2- lub 4-bitowych pakietów, które następnie są ukrywane w bardzo wielu odrębnych ciągach zakodowanych Base64.
Hint dla graczy CTFowych: Jeśli dostaniecie bardzo dużo malutkich plików zakodowanych Base64 lub jeden duży plik z masą odrębnych ciągów Base64, to najpewniej macie do czynienia właśnie z tą opisaną wyżej techniką sztuczką steganograficzną.
Oczywiście, takie rzeczy najlepiej od razu przećwiczyć, więc... powodzenia!
TG9yZW1= aXBzdW0= ZG9sb3K= c2l0 YW1ldCw= Y29uc2VjdGV0dXJ= YWRpcGlzY2luZ5== ZWxpdC5= UGVsbGVudGVzcXVl c29kYWxlc3== YY== bmlzbE== ZWdldB== YWNjdW1zYW4u TW9yYml= Z3JhdmlkYSz= ZWxpdL== YWN= Z3JhdmlkYQ== Y29udmFsbGlzLF== bWFnbmE= YW50ZS== dWx0cmljZXN= YXJjdSy= dGVtcG9y dHJpc3RpcXVl ZW5pbZ== cHVydXN= ZWdldN== dXJuYS4= U2Vk dHJpc3RpcXVl dGluY2lkdW50 YXVndWUs dmVs aW1wZXJkaWV0 ZXJhdC5= UHJhZXNlbnQ= Y29uc2VjdGV0dXL= dWx0cmljaWVz ZXN0 YXT= bW9sbGlzLm== QWVuZWFu ZXR= cGxhY2VyYXS= bnVsbGEu U3VzcGVuZGlzc2V= dGluY2lkdW50 dGVtcG9y cXVhbSw= c2Vk dGVtcHVz cXVhbS== c2NlbGVyaXNxdWV= YS6= RG9uZWM= dmVzdGlidWx1bSx= ZWxpdH== YXQ= cnV0cnVt c29kYWxlcyz= dHVycGlz bGVjdHVz aGVuZHJlcml0 bmVxdWUs cXVpc2== ZWdlc3Rhc1== bG9yZW0= cXVhbd== b3JuYXJl dmVsaXQu TW9yYmm= bG9ib3J0aXM= YWNjdW1zYW7= cGVsbGVudGVzcXVlLk== U2Vk ZWdldH== c2FwaWVu dXT= bGlndWxh c2VtcGVy cG9ydHRpdG9yLk==
Z Base64 związanych jest jeszcze kilka ciekawostek, ale ponieważ ten post i tak wyszedł dość długi, zostawmy je na kolejny dzień (mamy RSS i Newsletter żeby nic nie przegapić).
Następny post w serii: