Writeup CONFidence CTF 2015

14

Uczestniczyliśmy (@msm oraz @Rev ) jako p4 w CONFidence CTF (w sumie głównie przez/dzięki @Gynvael Coldwindowi) i wrzucamy opis tego co udało nam się zrobić - tym razem dużo tego nie ma, całe 5 zadań. Był to swoja droga pierwszy CTF offlinowy (na miejscu, nie przez internet) w którym braliśmy udział.

Tym razem (i następne będa) z konta @p4, żeby móc spokojnie pisać rozwiazania bezosobowo.

A, i ten writeup przeleżał (jako googledoc) dobre kilka miesięcy zanim go odgrzebaliśmy i wrzuciliśmy w końcu. No ale lepiej późno niż wcale.

<font size="6">Turbo CrackMe (100p)</span>

Najbardziej standardowe zadanie z tych które rozwiązaliśmy na CTFie, i najprostsze (dla nas).

Nazwa sugeruje że program był napisany w Turbo Pascalu. W sumie nie weryfikowaliśmy tego, ale Pascal Stringi w binarce popierają tą teorię.

Po uruchomieniu pokazuje się następujący widok (animowanego smoka niestety screen nie oddaje):

a71438d0c1.png

Szybkie przejrzenie źródła i miejsca gdzie smok jest rysowany na ekran pozwoliło zlokalizować funkcję sprawdzającą podane hasło. Był to długi, ale bardzo prosty w rewersowaniu ciąg opcodów, idący mniej więcej tak:

                xor     ax, ax
                mov     [bp+valid_chars], ax
                mov     [bp+var_1], 0
                cmp     [bp+var_102], 19
                jz      short loc_106B7
                jmp     loc_109B2
loc_106B7:
                mov     al, [bp+char0]
                xor     al, 'D'
                or      al, al
                jnz     short loc_106C5
                inc     [bp+valid_chars]

loc_106C5:
                mov     al, [bp+char0]
                xor     al, [bp+char1]
                cmp     al, '6'
                jnz     short loc_106D5
                inc     [bp+valid_chars]

loc_106D5:
                mov     al, [bp+char1]
                xor     ah, ah
                mov     dx, ax
                mov     al, [bp+char0]
                xor     ah, ah
                sub     ax, dx
                cmp     ax, -46
                jnz     short loc_106EE
                inc     [bp+valid_chars]

loc_106EE:
                mov     al, [bp+char1]
                xor     al, [bp+char2]
                cmp     al, 15h
                jnz     short loc_106FE
                inc     [bp+valid_chars]

loc_106FE:
                mov     al, [bp+char2]
                xor     ah, ah
                mov     dx, ax
                mov     al, [bp+char1]
                xor     ah, ah
                sub     ax, dx
                cmp     ax, 0Bh
                jnz     short loc_10717
                inc     [bp+valid_chars]

loc_10717:
                mov     al, [bp+char2]
                xor     al, [bp+char3]
                cmp     al, 9
                jnz     short loc_10727
                inc     [bp+valid_chars]

loc_10727:
                mov     al, [bp+char3]
                xor     ah, ah
                mov     dx, ax
                mov     al, [bp+char2]
                xor     ah, ah
                sub     ax, dx
                cmp     ax, 0FFF9h
                jnz     short loc_10740
                inc     [bp+valid_chars]

Dla niemówiących po asemblerowemu, to odpowiednik mniej więcej takiego C:

if (pass[0] == 'D') { valid_chars++; }
if (pass[0] ^ pass[1] == 0x36) { valid_chars++; }  // 0x36 == '6'
if (pass[1] ^ pass[2] == 0x15) { valid_chars++; }
if (pass[2] ^ pass[3] == 0x9) { valid_chars++; }
// ...

Na końcu było porównanie czy valid_chars == 19, i jeśli tak, hasło było uznane za prawidłowe.

Idąc znak po znaku i xorując sobie ręcznie uzyskaliśmy flagę:

DrgnS{TextModeFTW!}

Autor zadania: gynvael coldwind

<font size="6">Encryption (200p)</span>

Ciekawe zadanie w którym dostajemy zaszyfrowany plik (o którym wiemy że jest obrazem png) i mamy oczywiście zadanie przywrócenia go do oryginalnej postaci.

Kod za pomoca którego zaszyfrowano obraz wyglądał tak (po drobnych moich modyfikacjach, nie zmieniających działania):

def split_by(data, cnt):
    return [data[i : i+cnt] for i in xrange(0, len(data), cnt)]

def str_to_int(s):
    return int(s.encode('hex'), 16)

def int_to_str(num, bytes):
    s = '{0:x}'.format(num)
    while len(s) < bytes*2:
            s = '0' + s
    return s.decode('hex')

def egcd(a,b):
    xa,xb = 1,0
    ya,yb = 0,1
    while ya*a + yb*b > 0:
        cnt = (xa*a + xb*b) / (ya*a + yb*b)
        xa -= ya*cnt
        xb -= yb*cnt
        xa,ya = ya,xa
        xb,yb = yb,xb
    return xa, xb

def inv_mod(x, mod):
    return (egcd(x, mod)[0]) % mod

def encrypt(data, key_start, key_base, BLOCK_SIZE):
    mod = 1 << BLOCK_SIZE*8
    k = key_start
    res = []
    for block in split_by(data, BLOCK_SIZE):
            int_block = str_to_int(block)
            assert inv_mod(k, mod) * k % mod == 1
            int_block = ((int_block * k % mod) ^ k) * inv_mod(k, mod) % mod
            res.append(int_to_str(int_block, BLOCK_SIZE))

        k = (k * key_base) % mod

    return ''.join(res)

def main():
    if len(sys.argv) != 3:
            print 'Usage: %s [input file] [output file]' % sys.argv[0]
            return

    with open(sys.argv[1], 'rb') as in_file, open(sys.argv[2], 'wb') as out_file:
            data = in_file.read()
            out_file.write(encrypt(data, KEY_START, KEY_BASE, BLOCK_SIZE))

    print 'Done!'

Jak się do tego zabrać? Rozpoczęliśmy od zauważenia, że header png jest zawsze stały (pierwsze ~~16 bajtów, może trochę więcej). Zapisaliśmy więc sobie pierwsze bajty plain- i ciphertextu:

const_hdr = '89504E470D0A1A0A0000000D49484452'.decode('hex')
encr_hdr = 'A75EF713A0460B235AAD1C91C5493C60'.decode('hex')

Szyfr jest blokowy (nie znamy długości bloku, jeszcze), i każdy blok jest szyfrowany niezależnie - piewszy blok z kluczem k, drugi z km, trzeci z km*m, itd.

Więc pierwszy krok który zrobiliśmy to było odzyskanie k. Patrząc na to jak szyfrowane są bloki:

for block in split_by(data, BLOCK_SIZE):
    int_block = str_to_int(block)
    int_block = ((int_block * k % mod) ^ k) * inv_mod(k, mod) % mod
    res.append(int_to_str(int_block, BLOCK_SIZE))

mamy równanie modularne w rodzaju
CIPH_BLOCK === ((PLAIN_BLOCK * k) ^ k) * (1/k) (mod m)

albo zapisując inaczej

CIPH_BLOCK * k === (PLAIN_BLOCK * k) ^ k (mod m)

Niestety, równanie to jest (wg. mojej wiedzy) w ogólności nierozwiązywalne w żaden sensowny sposób (jakiś matematyk tutaj mnie wyprowadzi z błędu?), nawet mimo tego że jedyną niewiadomą tutaj jest 'k'.

Z drugiej strony, 'm' nie jest takie zupełnie dowolne - wiemy że jest potęgą dwójki.

Jeśli przyjrzymy sie jeszcze raz temu równaniu:

CIPH_BLOCK * k === (PLAIN_BLOCK * k) ^ k (mod m)

można zauważyć że dla danego plain_block, na najniższy bit ciph_block wpływa tylko najniższy bit k. Później na drugi najniższy bit ciph_block wpływają tylko dwa najniższe bity k, etc, etc. W ten sposób możemy odzyskać k idąc od najniższych bitów:

def nth_bit(x, n):
    return int(('0' * 256 + bin(x)[2:])[-1 - n])

def get_k_when_a_times_k_is_b_times_k_xor_k_mod_m(a, b, n):
    result = []
    reverse(0, a, 0, b, n, 0, '', result)
    return result

def reverse(current_a, a, current_b, b, n, bit, k, result):
    if bit == n:
        ki = int(k, 2)
        if inv_mod(ki, (1 << n)) * ki % (1 << n) == 1:
            if (a * ki) % (1 << n) != ((b * ki) ^ ki) % (1 << n):
                        print ':('
            result.append(ki)
    else:
        # sprawdz dla bit = 1
        current_a2 = current_a + (a << bit)
        current_b2 = current_b + (b << bit)
        if nth_bit(current_a2, bit) == nth_bit(current_b2, bit) ^ 1:
            reverse(current_a2, a, current_b2, b, n, bit + 1, '1' + k, result)
        # sprawdz dla bit = 0
        if nth_bit(current_a, bit) == nth_bit(current_b, bit):
            reverse(current_a, a, current_b, b, n, bit + 1, '0' + k, result)

Ta funkcja szczerze mówiąc bardzo mnie zaskoczyła, bo zadziałała już za pierwszym razem.

Co teraz - uzbrojeni w funkcję odwracającą (o bardzo opisowej nazwie) atakujemy zaszyfrowany obrazek - musimy spróbować każdej możliwej długości bloku, bo nie wiemy w zasadzie jakie jest block_size:

for i in range(1, 16):
    cst = const_hdr[:i]
    enc = encr_hdr[:i]
    mod = 1 << (8 * i)
    a = str_to_int(cst)
    b = str_to_int(enc)
    ks = get_k_when_a_times_k_is_b_times_k_xor_k_mod_m(a, b, 8 * i)
    print i, ks

Okazało się że dla każdej długości bloku poza '7' nie ma żadnego pasującego k, więc jeden problem z głowy - wiemy że block_size jest równe 7, oraz mamy możliwe 'k'.

Drugi krok był podobny do pierwszego - robimy to samo, tylko dla drugiego bloku:

block_size = 7
cst = const_hdr[7:14]
enc = encr_hdr[7:14]
a = str_to_int(cst)
b = str_to_int(enc)
print a, b
ks = get_k_when_a_times_k_is_b_times_k_xor_k_mod_m(a, b, block_size * 8)
print ks

Oraz trzeci i ostatni krok - bruteforce po możliwych kluczach i sprawdzenie czy któryś z nich tworzy działający png. Jako że możliwych k w pierwszym i drugim kroku było więcej niż 1 a bliżej 100 (na 90% dało się wyeliminować większość z nich, ale okazało się to niekonieczne, bo sprawdzenie wszystkich ~~10k możliwości zadziałało równie dobrze):

possible1 = [...] # k z pierwszego kroku
possible2 = [...] # k z drugiego kroku
data = open('flag.png.enc', 'rb').read()
block_size = 7
mod = 1 << (8 * block_size)
for p1 in possible1:
    for p2 in possible2:
            key_start = p1
            key_base = p2 * inv_mod(p1, mod)
            dec = decrypt(data[:21], key_start, key_base, block_size)
            if 'IHDR' in dec:  # czy to poprawny png? (heurystyka)
                print 'ok'
                dec = decrypt(data, key_start, key_base, block_size)
                open('out.' + str(p1) + '.' + str(p2) + '.png', 'wb').write(dec)

0503c75fa2.png

Autor zadania: redford (pozdrawiamy!)

<font size="6">Internet of Booze (200p)</span>

Ciekawe zadanie (bo hardwarowe), i musiało być pracochłonne w tworzeniu (szacunek dla autora za chęci - swoja drogą kod został po CTFie udostępniony na githubie: https://github.com/q3k/internet-of-booze) - tym bardziej robiło wrażenie.

Otóż na stole organizatorów (dragonsectoru tzn) stała taka diabelska maszyna:

8896512a64.png

Działała tak, że przyjmowała banknoty 50 zł i wydawała wódkę (podobno, nikt z nas nie próbował zapłacić 50zł za kieliszek :P).

Ale dostaliśmy też firmware do zreversowania, i okazało się że maszyna przyjmuje SMSy (i nawet odpisuje na nie).

Zreversowaliśmy kod odpowiadający za czytanie odbieranych SMSów, i doszliśmy do czegoś takiego:

int main() {
    char *i32u89xn = "\xBC\xED\xE5\xB5"; // c29j

    char a2[] = "WYSŁANA WIADOMOSC";
    int v8 = strlen(a2 + 4);
    char *v7 = a2 + 4;
    char v22[20] = "000000000000000000000000";

    for (int i = 0; i != v8; ++i ) {
        char v15 = *(v7++);
        uint8_t v16 = ("93lf4s"[i % 6] + i) ^ v15;
        if ( i > 3 ) {
            printf("%x ", v16);
            *(v22 + i - 20) = v16;
        }
        else if ( v16 != "c29j"[i]) {
            puts("fail");
        }
    }
    if (v22[0] == 1) { DbgSendStatus(); } // ta linijka z pamięci
}

(+ sprawdzenie na początku czy wiadomość zaczyna się od CTRL).

Jak widać wiadomość była "deszyfrowana" poprzez xorowanie ze stałym napisem, pierwsze cztery bajty były dodatkowo sprawdzane przez porównanie z jakimś magic stringiem. Kiedy to rozwiązaliśmy, wystarczyło wysłać odpowiedni SMS i gotowe.

Była jeszcze druga część zadania której nie zdążyliśmy wykonać, a konkretnie zmuszenie maszyny do wydania darmowego napoju - sposób był bardzo podobny i byliśmy na dobrym tropie, ale nie starczyło nam czasu (mieliśmy trochę ponad 40 minut po skończeniu pierwszego etapu) - należało pójść dalej z SMSem, i za pomocą buffer overflowa nadpisać adres powrotu funkcji tak żeby skoczyła do miejsca odpowiedzialnego za nalewanie.

Autor zadania: q3k

<font size="6">PHP Core (300p)</span>

Nasze ulubione zadanie z całego CTFa (z tych które rozwiązaliśmy przynajmniej, na temat reszty nie mamy zdania). Tak bardzo nam się spodobało że aż (za namową autora) napisaliśmy o nim profesjonalny artykuł do "Programisty". Pdf z artykułem będzie wrzucony na forum jak tylko upewnimy się co do formalności.

Autor zadania: gynvael coldwind


Poza własnym kontem na 4programmers, p4 uzyskało też konto na githubie: https://github.com/p4-team/ctf

Zapraszamy do śledzenia jeśli ktoś jest chętny. Writeupy tworzymy teraz na githubie, więc jest dużo aktualniejszy (dwa writeupy ciągle zrestą oczekują na skład i wrzucenie na 4p). No i dzięki samozaparciu @Shaloma są nawet wersje angielskie ;).

1

Ciesze sie, ze udalo wam sie rozwiazac IoB. To fakt, troche sie zagalopowalem z robieniem tego zadania, ale chyba warto bylo :). Jakby co, schemat i maski/projekty PCB tez sa na moim githubie, a tutaj moge sie podzielic odpowiedziami na pytania (jesli jakies beda).

0

Ciesze sie, ze udalo wam sie rozwiazac IoB. To fakt, troche sie zagalopowalem z robieniem tego zadania, ale chyba warto bylo :). Jakby co, schemat i maski/projekty PCB tez sa na moim githubie, a tutaj moge sie podzielic odpowiedziami na pytania (jesli jakies beda).

Fajne było (coś co można namacalnie zobaczyć, i nawet SMSy odbiera) :P. Żałujemy że tak późno się za nie zabraliśmy, bo AFAIR walczyliśmy z którymś webowym zadaniem (którego i tak nie rozwiązaliśmy ostatecznie) i dopiero 2/3h przed końcem zaczeliśmy IoB. Nawet znaleźliśmy miejsce z buffer overflowem (i wymyśliliśmy gdzie skoczyć, bo to się dość rzucało w oczy), ale AFAIR skacząc pomyliliśmy się o cztery bajty i nic z tego nie wyszło. Może jakbyśmy mieli więcej czasu.

A do githuba nawet zalinkowaliśmy, podziwiamy ilość pracy która poszła w jedno zadanie :P.

0
msm napisał(a):

Nawet znaleźliśmy miejsce z buffer overflowem (i wymyśliliśmy gdzie skoczyć, bo to się dość rzucało w oczy), ale AFAIR skacząc pomyliliśmy się o cztery bajty i nic z tego nie wyszło

To, albo zapomnieliście że żeby skoczyć w thumb mode trzeba zrobić redirect na adres+1 (w „środek” instrukcji). To był największy problem dla reszty teamów, zrozumiały z resztą jeśli nie robiły się nigdy wcześniej rzeczy ARMowych.

A, i tak, zadanie było tak naprawdę dość oczywiste. Chciałem je zrobic bardziej skomplikowane (chociazby coś bardziej skomplikowanego niż ten banalny buffer overflow który tam był, albo zrobić stripa wszystkich nie-FreeRTOSowych symboli), ale chyba jednak dobrze trafiłem. Pierwszą częśc rozwiązało ~6 teamów, drugą ~2-3... Może w przyszłym roku wykorzystam ponownie platformę i zrobię coś ciekawszego :).

0

To, albo zapomnieliście że żeby skoczyć w thumb mode trzeba zrobić redirect na adres+1 (w „środek” instrukcji). To był największy problem dla reszty teamów, zrozumiały z resztą jeśli nie robiły się nigdy wcześniej rzeczy ARMowych.

O, dokładnie tak. W sumie pod sam koniec pojawiła się podpowiedź z tym zwiazana, ale nie doszliśmy do tego na czas :P.

Pierwszą częśc rozwiązało ~6 teamów, drugą ~2-3... Może w przyszłym roku wykorzystam ponownie platformę i zrobię coś ciekawszego :).

Dla nas to "hardware" brzmiało strasznie, baliśmy się nawet poświęcać czas na poczatku. Za rok coś ambitniejszego na tej samej platformie by było fajne :P.

To że pierwsza cześć rozwiazało tak mało teamów, biorac pod uwagę że to po prosty xor był w zasadzie, jest naprawdę dziwne.

0

@q3k a skoro to był hardware to możliwe były czysto sprzętowe rozwiązania, takie jak zrobienie zwarć na płytce miedzy odpowiednimi punktami?

1 użytkowników online, w tym zalogowanych: 0, gości: 1