NC3 CTF 2022 2022

Sparenissen [200 pts]

Sparenissen har været på spil, hvad har han egentlig gang i?


nc3{sparenissen-synes-det-er-enormt-hyggeligt-og-lidt-sjovt-med-et-laaaangt-flag-i-en-lille-kort-fil}


tl;dr Den binære fil er compressed, og konverteres først til bits. Den komprimerede bitstring består af to typer datafelter, der enten starter med 0 eller 1.
Starter feltet med 0, er de efterfølgende 7 bits en ny ASCII-værdi. Denne skrives ned og appendes til en liste over fundne chars.
Starter feltet med 1, er de efterfølgende 5 bits et index ind i den liste, og karakteren ved det index skrives ned.
På den måde bruges i alt 8 bits på hver ny karakter, men derefter kun 6 for hver gentagen brug.

Introduktion

Vi får givet ZIP-filen sparenissen.zip med de to filer sparenissen.txt og sparenissen. Tekstfilen indeholder et lille digt:


	Sparenissen skriver kort og trangt,
	hilser ikke først,
	og siger ej farvel,
	modsætter sig størst,
	at gentage sig selv,
	hvordan kan brevet blive stort og langt.

og sparenissen er en binær fil, vi kan køre xxd på for at få hexdump:

00000000: 6e63 337b 7370 6172 6581 a649 2880 b647  nc3{spare..I(..G
00000010: 9828 92a6 4a1d 2aa2 7aa8 81be 76db 6a68  .(..J.*.z...v.jh
00000020: ad9f 1a1b 29c6 daae c6ac a9b2 daa4 6ab9  ....).........j.
00000030: dada afa2 caa8 b6ac a69a 69a0 c6da 99b2  ..........i.....
00000040: 9b1a a9aa 882a ca9c b2a2 a6bb a7b6 ad69  .....*.........i
00000050: c9f4                                     ..

Vi ser her først starten på et flag som almindelig ASCII-tekst, men herefter er resten umiddelbart ulæseligt.

Analyse

Digtet hinter meget til komprimering - teksten er “kort og trang”, men brevet skal blive “stort og langt”. “hilser ikke først og siger ej farvel” kunne hinte mod enten manglende header og footer eller mod at nogle unødvendige bits fjernes for at komprimere indholdet. F.eks. bruger ASCII chars kun 7 bits, men gemmes typisk i en 8-bit byte. Eller måske hinter det bare til, at resultatet ikke indeholder noget overflødigt.

Sidst kunne “modsætter sig størst at gentage sig selv” hinte til at samme karakter aldrig skrives to gange. Med mange komprimeringsteknikker skrives hver karakter kun første gang, den optræder i teksten. De resterende gange refereres til den første for at spare bits.

Denne hypotese passer umiddelbart godt på den komprimerede tekst, hvor vi i starten ser en række ASCII-karakterer, der hver optræder første gang. En fornuftig antagelse er, at næste bid af flaget er nissen, så det starter med nc3{sparenissen. Dette kan stadig passe fint, da næste karakter så er n, og da den allerede er brugt, er det ikke nødvendigt at skrive den igen. Men hvordan refereres der så til den første forekomst?

Vi kan starte med at konvertere filen til bits, så vi bedre kan se, hvad der foregår. Dette gøres nemt med Python:

with open("sparenissen", "rb") as f:
    data = f.read()

bits = "".join([bin(c)[2:].zfill(8) for c in data])

Koden konverterer hver byte til 8 bits og concatenater dem, så vi får et samlet output. Printer vi det, får vi et stort output:

 01101110011000110011001101111011011100110111000001100001011100100110010110000001
 10100110010010010010100010000000101101100100011110011000001010001001001010100110
 01001010000111010010101010100010011110101010100010000001101111100111011011011011
 01101010011010001010110110011111000110100001101100101001110001101101101010101110
 11000110101011001010100110110010110110101010010001101010101110011101101011011010
 10101111101000101100101010101000101101101010110010100110100110100110100110100000
 11000110110110101001100110110010100110110001101010101001101010101000100000101010
 11001010100111001011001010100010101001101011101110100111101101101010110101101001
 1100100111110100

Vi ved, hvad de første bytes er og noterer dette:

 01101110 : n
 01100011 : c
 00110011 : 3
 01111011 : {
 01110011 : s
 01110000 : p
 01100001 : a
 01110010 : r
 01100101 : e
 10000001101001100100100100101000100000001011011001000111100110000010100010010010
 10100110010010100001110100101010101000100111101010101000100000011011111001110110
 11011011011010100110100010101101100111110001101000011011001010011100011011011010
 10101110110001101010110010101001101100101101101010100100011010101011100111011010
 11011010101011111010001011001010101010001011011010101100101001101001101001101001
 10100000110001101101101010011001101100101001101100011010101010011010101010001000
 00101010110010101001110010110010101000101010011010111011101001111011011010101101
 011010011100100111110100

Hvis vores antagelse holder stik, skal de næste bits referere til det første n, og herefter bør vi have 8 bits, der svarer til ASCII-værdien for i, altså 01101001 i bits. Den sekvens er nem at identificere, og vi ser faktisk, at nemlig de bits optræder kort efter:

 01101110 : n
 01100011 : c
 00110011 : 3
 01111011 : {
 01110011 : s
 01110000 : p
 01100001 : a
 01110010 : r
 01100101 : e
 100000   : n?
 01101001 : i
 10010010010010100010000000101101100100011110011000001010001001001010100110010010
 10000111010010101010100010011110101010100010000001101111100111011011011011011010
 10011010001010110110011111000110100001101100101001110001101101101010101110110001
 10101011001010100110110010110110101010010001101010101110011101101011011010101011
 11101000101100101010101000101101101010110010100110100110100110100110100000110001
 10110110101001100110110010100110110001101010101001101010101000100000101010110010
 10100111001011001010100010101001101011101110100111101101101010110101101001110010
 0111110100

Vi ved stadig ikke, hvordan n decodes mellem e og i, men vi kan se, der bruges seks bits på det. De næste to karakterer bør begge være s, og det passer perfekt, da de næste to sekvenser af seks bits er ens. Vi ved også at de to efterfølgende karakterer er e og n, der begge har været brugt før, så vi antager at de også er seks bits, og ser på de sekvenser, vi har identificeret:

 01101110 : n
 01100011 : c
 00110011 : 3
 01111011 : {
 01110011 : s
 01110000 : p
 01100001 : a
 01110010 : r
 01100101 : e
 100000   : n?
 01101001 : i
 100100   : s?
 100100   : s?
 101000   : e?
 100000   : n?
 00101101100100011110011000001010001001001010100110010010100001110100101010101000
 10011110101010100010000001101111100111011011011011011010100110100010101101100111
 11000110100001101100101001110001101101101010101110110001101010110010101001101100
 10110110101010010001101010101110011101101011011010101011111010001011001010101010
 00101101101010110010100110100110100110100110100000110001101101101010011001101100
 10100110110001101010101001101010101000100000101010110010101001110010110010101000
 101010011010111011101001111011011010101101011010011100100111110100

Vi ser her, at begge instanser af n encodes med 100000, e med 101000 og s med 100100. Måske kan du nu spotte et mønster: nye karakterer starter med 0, efterfulgt af de syv bits, der udgør ASCII-værdien. Når en karakter optræder igen, starter den i stedet altid med 1, efterfulgt af fem bits. Så første bit i hver sekvens afgør, hvilken type det er.

De fem resterende bits er 00000 = 0 for n, 01000 = 8 for e og 00100 = 4 for s. Det passer præcis med den rækkefølge, karaktererne forekommer i:

0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8
n | c | 3 | { | s | p | a | r | e

Så når en karakter forekommer anden gang bruges indekset på den første forekomst i listen over brugte karakterer. Dvs. hver gang f.eks. p forekommer, vil vi forvente at se sekvensen 100101 - et 1-tal efterfulgt af 00101 = 5. Det sparer på den måde to bits på hver eneste gentagen karakter. Hver gang en ny karakter forekommer, lægger vi den til listen over brugte karakterer, så vi kan referere til dens indeks senere. F.eks. vil i placeres som næste element på indeks 9.

Vi tjekker om dette passer med de næste bits - når vi ser et 0 har vi en ny 7-bit karakter, og et 1 har vi en reference til en foregående:

 01101110 : n
 01100011 : c
 00110011 : 3
 01111011 : {
 01110011 : s
 01110000 : p
 01100001 : a
 01110010 : r
 01100101 : e
 100000   : n  (0)
 01101001 : i
 100100   : s  (4)
 100100   : s  (4)
 101000   : e  (8)
 100000   : n  (0)
 00101101 : _
 100100   : s  (4)
 01111001 : y
 100000   : n  (0)
 101000   : e  (8)
 100100   : s  (4)
 101010   : _  (10)
 01100100101000011101001010101010001001111010101010001000000110111110011101101101
 10110110101001101000101011011001111100011010000110110010100111000110110110101010
 11101100011010101100101010011011001011011010101001000110101010111001110110101101
 10101010111110100010110010101010100010110110101011001010011010011010011010011010
 00001100011011011010100110011011001010011011000110101010100110101010100010000010
 10101100101010011100101100101010001010100110101110111010011110110110101011010110
 10011100100111110100

Vi har _ som næste karakter, der altså får index 10 i listen over fundne karakterer:

0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
n | c | 3 | { | s | p | a | r | e | i | _

Og rigtig nok ser vi kort tid efter en reference til 10, som passer med _, og vi har nc3{sparenissen_synes_ som start på flaget.

Dekomprimeringen automatiseres nemt med Python:

with open("sparenissen", "rb") as f:
    data = f.read()

bits = "".join([bin(c)[2:].zfill(8) for c in data])


flag = ""
chars = []
i = 0
while i < len(bits):
    if bits[i] == "0":
        # Bit er 0 = næste 7 bits er ny char
        char = chr(int(bits[i:i + 8], 2))
        flag += char
        chars.append(char)  # Gem i listen over brugte chars
        i += 8
    else:
        # Bit er 1 = næste 5 bits er index i listen over brugte chars
        idx = int(bits[i + 1:i + 6], 2)
        flag += chars[idx]
        i += 6

Køres dette får vi hele flaget dekomprimeret. Dette er givetvis en velkendt komprimeringsalgoritme, men jeg har ikke forsøgt at finde den efterfølgende.

nc3{sparenissen-synes-det-er-enormt-hyggeligt-og-lidt-sjovt-med-et-laaaangt-flag-i-en-lille-kort-fil}

____

13 December 2022
Tags: <forensics/> <compression/>