Base64 poza kodowaniem – steganografia i kanoniczność (cz. 1)

Gynvael Coldwind

2024-08-15

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.

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).

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).

base64-explained-pl.png

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?

base64-unused-pl.png

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ć).

Nie przegap nic od HexArcana! Dodaj nasz blog do swojego czytnika RSS/Atom lub zapisz się na nasz newsletter poniżej.