22.04.2014 | 07:02

Adam Haertle

Rozwiązanie konkursu CTF – szczegółowy opis krok po kroku

Kilka dni temu zorganizowaliśmy konkurs, w którym można było wygrać wejściówkę na konferencję Honeynet Project Workshop. Konkurs startował wieczorem i nie spodziewaliśmy się, że zwycięzca pojawi się jeszcze tego samego dnia.

Trzy zadania opublikowaliśmy o godzinie 21, a krótko przed północą Konrad Kuźmiński przesłał nam prawidłowe rozwiązania wszystkich z nich. Pokonując je jak burza zasłużył w pełni na wejściówkę na konferencję, a do tego przygotował dla Was szczegółowy opis rozwiązanych zadań. Wszystkim uczestnikom konkursu dziękujemy za udział i zapraszamy do lektury opracowania Konrada.

Zadanie 3

Po zapoznaniu się z zadaniem, odruchowo zacząłem przeglądać kod pięknej symulacji spadających literek jednak nie znalazłem tam nic nadzwyczajnego. Zacząłem więc analizować co właściwie robi cała symulacja. Litery wpisane do pola tekstowego spadają swobodnie w kolejności wpisywania, a następnie układają się w „naturalny” sposób w predefiniowanym kontenerze. Rozwiązanie to układ liter w kontenerze odczytywanych wiersz po wierszu zaczynając od dołu.

Po kilku próbach dla różnych przypadkowych danych upewniłem się, że litery spadają za każdym razem w ten sam sposób, niezależnie od danych wejściowych. Wyciągnąć można z tego jeden prosty wniosek: mamy doczynienia z trywialną funkcją przyporządkowującą, która wstawia litery z wejścia na określone pozycje w ciągu wyjściowym. Wprowadzając dowolny ciąg unikalnych 28 znaków można łatwo zbadać jak działa nasze przyporządkowanie, a następnie zastosować odwrócone przekształcenie dla rozwiązania podanego w podpowiedzi:

char result[] = "ro05412qpa638b79dfcehjgilkmn";
char input[] = "1234567890abcdefghijklmnoprq";
int map[28];
for (int i=0; i<28; i++)
 map[i] = strchr(input, result[i])-input;
char solved[29] = {0};
char s[] = "FIXZhZm9SZ3xKhtY2ycNX0aBlRRX";
for (int i=0; i<28; i++)
 solved[map[i]] = s[i];
printf("%s\n", solved);

Otrzymane rozwiązanie to: „ZmxhZ3tKYXZhc2NyaXB0RlRXISF9”, ale zgodnie z podpowiedzią to jeszcze nie wszystko. Mamy 28 bajtów danych, 28 dzieli się przez 4, a dane to same litery i cyfry. Uznałem to za wystarczającą przesłankę aby wrzucić nasze dane do dekodera base64. Opłacało się bo w ten sposób zdobyłem swoją pierwszą flagę: flag{JavascriptFTW!!}

Zadanie 1

Żadnych podpowiedzi. Sam na sam ze ścianą tekstu o podejrzanie małej entropii. Dość długo nie mogłem znaleźć żadnego punktu zaczepienia. Dane zawierają tylko 11 różnych liter oraz cyfrę cztery. Wiele małych podciągów się powtarza, czasem nawet całe linie. Myślałem że może to być jedna i ta sama linia, obrócona o określoną liczbę pozycji, które stanowią zakodowaną wiadomość. Niestety eksperymenty praktyczne oraz ostatnia linia (któtsza niż inne) obaliły tę tezę.

Postanowiłem ponownie skorzystać z dekodera base64 co ku mojemu zaskoczeniu opłaciło się. Moim oczom ukazało się mnóstwo krótkich sekwencji kropek i myślników w oddzielnych liniach. Charset jest raczej nie do pomylenia i bardziej niż oczywistym było, że jest to alfabet Morse’a. Po zdekodowaniu otrzymałem coś takiego:

2B2B2B2B2B2B2B2B5B3E2B3E2B2B3E2B2B2B3E2B2B2B2B3E2B2B2B2B2B3E2B2B2B2B2B2B3E2B2B2B
…dużo tekstu…
E2B2B2B3C3C3C3C3C3C3C3C3C3C3C3C3C3C3C3C2E0A?

Według instrukcji konwertera znak „?” oznacza że znaku nie udało się zdekodować. Wikipedia już spieszy z wyjaśnieniem, że tajemniczy znak …-.- to komenda specjalna oznaczająca koniec kontaktu. Znak ten można więc pominąć. Ciąg do złudzenia przypomina klasyczny hex text, a do tego na końcu widać 0A czyli line feed. Po wklejeniu tekstu jako bajty w hex edytorze moim oczom ukazał się kolejny tekst, który wyglądał bardzo znajomo:

++++++++[>+>++>+++>++++>+++++>++++++>+++++++>++, itd.

Oczywiście jest to kod napisany w brainfucku. Po zinterpretowaniu kodu, zdobyłem swoją drugą flagę: flag{doYouLikeThisCode}

Zadanie 2

Znów bez podpowiedzi, ale tym razem mamy do czynienia z plikiem. Brak rozszerzenia więc typ trzeba zidentyfikować samodzielnie. Po załadowaniu pliku do hex edytora w nagłówku widzimy ciąg „ELF”, a więc plik wykonywalny. Nareszcie to co lubię najbardziej, czyli zadanie z RCE.

Nie miałem akurat odpalonego linuxa, a czas uciekał więc zabrałem się za deasemblację i analizę statyczną. Tutaj pierwsza miła niespodzianka i spore ułatwienie ze strony organizatorów, gdyż plik zawierał symbole. Sam program jest bardzo krótki, a dzięki symbolom jego analiza jest prosta do przeprowadzenia w rozsądnym czasie. Mamy tylko trzy interesujące funkcje: main, transform oraz xcode. Po krótkich oględzinach podzieliłem program na trzy części:

1) Input i podstawowe error checki
2) Zależności
3) Dekodowanie i output

1) Prosta pętla wczytująca dane znak po znaku (getchar), z których każdy jest przekazywany do funkcji transform a jej wynik zapisywany jest do bufora (max 0x400 znaków). Wczytanie znaku 0xFF kończy pętlę co rozpoczyna sprawdzanie 2 warunków:

if (suma_bajtow_inputu!=0x26A)
{
 printf("Bad sum 1 !");
 return;
}
if (suma_bajtow_transformowanego_inputu!=0x408)
{
 printf("Bad sum 2 !");
 return;
}

Nie ma tu nic ciekawego, funkcja transform to proste przekształcenia artmetyczne w oparciu o dwa stałe klucze. Nie warto się tym zajmować, a dlaczego to zaraz się okaże.

2) Ciekawy zestaw 4 warunków, jeśli wszystkie są niespełnione otrzymamy komunikat „Another hint : fail !”. Widać jakieś operacje na pierwszych bajtach bufora. Wrócimy do tego, jednak najpierw warto przyjrzeć się jak przebiega samo dekodowanie.

3) Pseudokod:

byte *newbuf = malloc(0x20);
memcpy(newbuf, secret, 0x20); //nasz hardkodowany, zakodowany sekret
xcode(newbuf, 0x20, buffer, sizeof(buffer)); //buffer to nasze transformowane dane z wejścia, stanowiące klucz do dekodowania
printf("Decoded secret: \n%s\n\n", newbuf);
return 0; //koniec całego programu

Teraz skupimy się na szybkiej analizie funkcji xcode. Mamy 4 argumenty jak powyżej, 2 zmienne lokalne (iteratory po buforach), 1 pętlę w której widać odczyt po bajcie z każdego bufora, xor i oczywiście zapis odkodowanego bajtu. Równoważny pseudokod (nie dokładnie taki jak w xcode, ale robiący to samo) wygląda więc następująco:

for (int i=0; i<sizeof(buf); i++) buf[i] ^= key[i%sizeof(key)];
Tak więc nasze crypto sprowadza się do prostego xora. Teraz musimy zgadnąć nasz klucz…

2) Z wiedzą z punktu trzeciego możemy wrócić do zestawu 4 warunków, które definiują zależności między bajtami bufora, czyli naszego klucza:

mov dword ptr [esp+20h], 6 
mov dword ptr [esp+24h], 61h
mov dword ptr [esp+28h], 1 ; 3 zmienne (m1, m2, m3) inicjalizowane na ustalone wartości
shl dword ptr [esp+28h], 1 ; m3 = 1<<1 = 2
mov eax, [esp+20h] ; EAX = 6
imul eax, [esp+28h] ; EAX = 6*2 = 0x0C
mov [esp+20h], eax ; m1 = EAX
movzx edx, ds:buffer ; pierwszy bajt klucza ...
movzx eax, ds:buffer+1 ; oraz drugi bajt klucza ...
xor eax, edx ; xorowane ze sobą ...
movsx eax, al
cmp eax, [esp+20h] ; mają dać 0x0C
jz short decrypt_secret

W podobny sposób można analizować pozostałe warunki określające:
– zależność miedzy key[0] i key[3] – czym może być key[5] – zależność miedzy key[2] i key[4]

Te informacje pozwalają lepiej szukać klucza, jednak ja przyjąłem nieco sprytniejszą taktykę. Z poprzednich zadań wiemy jaki jest format flagi, co pozwala założyć, że pierwsze 5 bajtów zdekodowanego sekretu to „flag{„. Za darmo dostajemy aż 5 bajtów klucza. Pozostaje problem długości klucza, gdyż żaden fragment kodu nie zdradza wprost jak długi może on być. Za dowód poszlakowy można przyjąć zestaw warunków, który opisuje zależności najdalej do 6 bajtu klucza. Sam sekret ma 32 bajty więc możemy spodziewać się że klucz ma 6-8 bajtów.

Na początek postanowiłem przyjąć 6 bajtów, a ten ostatni zwyczajnie zgadywać. 256-linijkowy output krótkiego programu można przecież bez problemu przejrzeć ręcznie:

char key[] = "\xAB\xA7\x9C\xC6\x9C\x00"; //5 zgadniętych bajtów i jeden dodatkowy
for (int j=0; j<256; j++)
{
 key[5] = j;
 char secret[] = "\xCD\xCB\xFD\xA1\xE7\xCB\xCE\xC4\xEE\xA3\xE8\xF5\xCE\xD4\xEF\xA7\xFB\xDD\xF3\xC8\xEE\xA3\xF8\xEF\xC2\xD3\xF4\x8D\xF9\xC1\xD6\xAD";
 for (int i=0; i<32; i++)
 secret[i] ^= key[i%6];
 printf("%d: %s\n", j, secret);
}
150: flag{]ecretcessagKXoredyithKeW}
151: flag{\ecretbessagJXoredxithKeV}
152: flag{SecretmessagEXoredwithKeY}
153: flag{RecretlessagDXoredvithKeX}
154: flag{QecretoessagGXoreduithKe[}
...
182: flag{}ecretCessagkXoredYithKew}
183: flag{|ecretBessagjXoredXithKev}
184: flag{secretMessageXoredWithKey}
185: flag{recretLessagdXoredVithKex}
186: flag{qecretOessaggXoredUithKe{}

Widać dwie sensowne opcje: flag{SecretmessagEXoredwithKeY} oraz flag{secretMessageXoredWithKey}. Warunek dotyczący 6 bajtu klucza (key[5]%2==0) nie pozwala jednoznacznie rozstrzygnąć, o którą flagę chodziło jednak opcja druga wydaje się bardziej prawdopodobna. Ostatnie zadanie zostało pokonane :)

Powrót

Komentarze

  • 2014.04.22 08:51 JackN

    Gratuluję fantastycznego i błyskawicznego rozwiązania!

    Odpowiedz
    • 2014.04.22 08:51 JackN

      A autorom ciekawego hackme :)

      Odpowiedz
  • 2014.04.22 15:50 Luke

    Elegancko.

    Odpowiedz
  • 2014.04.22 17:55 Tomek

    Flagi 2 nie trzeba brutforsowac, wystarczy troche RE (do odwracanie funkcji) i zalozenie ze wynikowy string zaczyna sie od 'flag{’. A tak mozna to zrobic w pytongu ;)
    #!/usr/bin/python

    def trans(c):
    return (c ^ 0x66) + 0x99

    def trans2(c):
    return (c – 0x99) ^ 0x66

    secret = [ 205, 203, 253, 161, 231, 203, 206, 196, 238, 163, 232,
    245, 206, 212, 239, 167, 251, 221, 243, 200, 238, 163,
    248, 239, 194, 211, 244, 141, 249, 193, 214, 173 ]

    out = [ 'f’, 'l’, 'a’, 'g’, '{’ ]
    key = []

    for i in range(len(out)):
    key.append( secret[i] ^ ord(out[i]) )
    keyok = [chr(trans2(x)) for x in key]

    print ”.join(keyok)

    keyok.append(’y’) #ostatnia litera narzuca sie sama

    print ”.join(keyok)

    out2 = []
    for i in range(len(secret)):
    j = i % len(keyok)
    out2.append( trans(ord(keyok[j])) ^ secret[i] )

    print ”.join(chr(x) for x in out2)

    Odpowiedz
  • 2014.04.22 20:34 m

    Gratulacje pięknie to rozpykałeś – takie lekturki lubie czytać!

    Odpowiedz
  • 2014.04.22 20:45 asdf

    gratuluję :)

    Odpowiedz
  • 2014.04.22 21:33 kurde

    potega, nie ogarniam tego rozumem:)

    Odpowiedz
  • 2014.04.23 01:58 widz

    Gratulacje :).
    Mam pytanie, bo zastanawiam się jak działa ta konkretna linijka kodu:
    map[i] = strchr(input, result[i])-input;

    Głównie to odejmowanie input, bo w wyniku działania funkcji strchr() dla i=0 otrzymamy
    26 (miejsce litery r w ciągu znakowym, bo tablica od 0, wiadomo) i co my później jeszcze odejmujemy? Cały string, który jest w input rzutowany niejawnie na int’a? Czy co i po co?

    Odpowiedz
    • 2014.04.23 20:15 Konrad

      strchr zwraca pointer na pierwsze wystąpienie zadanego chara w stringu input, a nas interesuje właśnie pozycja litery w stringu i po to właśnie jest to odejmowanie.

      Odpowiedz
      • 2014.04.24 16:10 widz

        Dzieki, doczytalem o funkcji strchr i juz kumam.

        Odpowiedz
        • 2014.04.24 17:25 kr0pek

          No właśnie, zamiast pisać „nie ogarniam i nie rozumiem” wystarczy poczytać o strchr :)
          Parafrazując Napoleona „Dla moich Polaków nie ma rzeczy niemożliwych, są tylko te o których jeszcze nie czytali :P”

          Odpowiedz

Zostaw odpowiedź

Jeśli chcesz zwrócić uwagę na literówkę lub inny błąd techniczny, zapraszamy do formularza kontaktowego. Reagujemy równie szybko.

Rozwiązanie konkursu CTF – szczegółowy opis krok po kroku

Komentarze