這次 TMCTF 中最高分的逆向題是個蠻有趣的題目,比起包裝成逆向題的數學跟密碼學問題,我個人還是比較喜歡這種類型的逆向題目
0x00 - Challenge
題目是個加了 VMProtect
的 64bit PE,從 linker version 也可以看出是 Delphi
寫的
覺得 TMCTF 總是會看到
VMProtect
呢... -_-
程式首先會將滑鼠游標移至其中一個按鈕上 幾秒後將按鈕座標打亂,再將游標移至另一個按鈕上 根據標題上的數字來看,會重複 41216 輪
嘗試記下前幾輪中游標下的字元,發現是 7z 的 header:
$ unhex 377ABCAF271C0004264201C0D64F > 1 ; file 1
1: 7-zip archive data, version 0.4
很明顯的題目是希望我們記錄下每一輪的結果並組成檔案
0x01 - Unpack
由於程式執行的速度非常慢,決定先嘗試脫殼分析 首先透過 Scylla
來 dump 出 binary 和預料之中的一樣,無法自動修正 IAT,加上有開 VM 保護,沒辦法完整修正脫殼後的程式
但由於是 Delphi
開發的程式,加上許多關鍵部分被 VM,純靜態分析其實並沒有很大的幫助,因此決定配合動態分析
0x02 - Dynamic Analysis
事實上我還沒有處理過加了 VMProtect
的 64bit PE 就我所知比較適合用的工具大概只有 x64dbg
不能用 OllyDBG 真是太痛苦了... 再怎麼改,x64dbg 還是不像 OllyDBG 一樣順手
x64dbg
雖然可以 Attach,但沒辦法下斷點 嘗試裝了 ScyllaHide
跟其他幾個 Plugin 還是沒辦法快樂的 debug 最後決定不用 Debugger,直接透過 Hook 來處理
0x03 - Hook
根據經驗,就算 VMProtect
加了 VM 保護,在呼叫 Windows API 時還是需要暫時離開 VM 因此可以直接 Hook 做 Windows API 來記錄程式行為
視窗程式要改變控制項的位置時需要呼叫 SetWindowPos
改變滑鼠游標位置需要呼叫 SetCursorPos
寫個 DLL 來 Hook 這兩個 API,記錄下每一輪各個按鈕以及滑鼠的座標,並在 SetCursorPos
中計算當下距離滑鼠座標最近的是哪一個按鈕
雖然可以透過
WindowFromPoint
來取得滑鼠座標下的視窗/控制項的 HWND,但題目的滑鼠游標似乎不一定會在按鈕上
順帶一提,Visual Studio x64 compiler 不支援 inline assembly,hook 寫起來蠻麻煩的...
雖然可以記錄座標,但程式執行起來還是很慢,最後發現 Hook 掉 SleepEx
可以加速
#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>
#include <stdio.h>
#include <Shlwapi.h>
#include <intrin.h>
#include <math.h>
#pragma comment(lib, "Shlwapi.lib")
#pragma intrinsic(_ReturnAddress)
#define LOG_FILE ".\\log.txt"
extern "C" BOOL WINAPI RealSetCursorPos(int X, int Y);
extern "C" BOOL WINAPI RealSetWindowPos(HWND hWnd, HWND hWndInsertAfter, int X, int Y, int cx, int cy, UINT uFlags);
HWND hWindow;
POINT button_pos[16];
char dump[41217];
void Log(const char* format, ...)
{
va_list valist;
FILE *fp;
if (fopen_s(&fp, LOG_FILE, "a"))
return;
va_start(valist, format);
vfprintf(fp, format, valist);
va_end(valist);
fclose(fp);
}
double dis(double x, double y, double a, double b)
{
return sqrt(double((x - a) * (x - a) + (y - b) * (y - b)));
}
DWORD WINAPI MySleep(DWORD dwMilliseconds)
{
// Log("Sleep(%d) Return: 0x%x\n", dwMilliseconds, _ReturnAddress());
return TRUE;
}
BOOL WINAPI MySetCursorPos(int x, int y) // RCX RDX R8 R9
{
static int i = 0;
POINT p = { x, y };
ScreenToClient(hWindow, &p);
//Log("SetCursorPos(X=%d (%d), Y=%d (%d)), Return: 0x%x\n", x, p.x, y, p.y, _ReturnAddress());
int min_idx = -1;
double min = 9999;
for ( int j = 0; j < 16; ++j ) {
double d = dis(p.x, p.y, button_pos[j].x + 12.5, button_pos[j].y + 12.5);
if ( d < min ) {
min = d;
min_idx = j;
}
}
dump[i++] = "0123456789ABCDEF"[min_idx];
if ( i >= 41216 )
Log("%s", dump);
//RealSetCursorPos(x, y);
return TRUE;
}
BOOL WINAPI MySetWindowPos(HWND hWnd, HWND hWndInsertAfter, int X, int Y, int cx, int cy, UINT uFlags)
{
char buf[256];
GetWindowTextA(hWnd, buf, 256);
//Log("SetWindowPos(hWnd=0x%08x (%s), 0x%08x X=%d, Y=%d, cx=%d, cy=%d) Return: 0x%x\n", hWnd, buf, hWndInsertAfter, X, Y, cx, cy, _ReturnAddress());
if ( buf[0] && !buf[1] ) {
int idx = strtol(buf, NULL, 16);
button_pos[idx].x = X;
button_pos[idx].y = Y;
}
return RealSetWindowPos(hWnd, hWndInsertAfter, X, Y, cx, cy, uFlags);
}
void Hook(DWORD64 dwSource, LPVOID lpTarget, UINT uNops = 0)
{
DWORD OldProtect, OldProtect2;
VirtualProtect((LPVOID)dwSource, 12 + uNops, PAGE_READWRITE, &OldProtect);
*(LPWORD)dwSource = 0xB848; // mov rax
*(PDWORD64)(dwSource + 2) = (DWORD64)lpTarget;
*(LPWORD)(dwSource + 10) = 0xe0ff; // jmp rax
memset((LPVOID)(dwSource + 12), 0x90, uNops);
VirtualProtect((LPVOID)dwSource, 12 + uNops, OldProtect, &OldProtect2);
}
void Go()
{
char buf[MAX_PATH];
GetModuleFileNameA(NULL, buf, 256);
*(StrRChrA(buf, NULL, '\\')) = 0;
SetCurrentDirectoryA(buf);
DeleteFileA(".\\log.txt");
HMODULE hUser32 = GetModuleHandleA("user32.dll");
DWORD64 SetCursorPos = (DWORD64)GetProcAddress(hUser32, "SetCursorPos");
Hook(SetCursorPos, MySetCursorPos);
DWORD64 SetWindowPos = (DWORD64)GetProcAddress(hUser32, "SetWindowPos");
Hook(SetWindowPos, MySetWindowPos);
HMODULE hKernelBase = GetModuleHandleA("kernelbase.dll");
DWORD64 SleepEx = (DWORD64)GetProcAddress(hKernelBase, "SleepEx");
Hook(SleepEx, MySleep);
while ( hWindow == NULL ) {
hWindow = FindWindowA("TMainForm", NULL);
Sleep(100);
}
Log("hWindow = %x\n", hWindow);
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
if ( fdwReason == DLL_PROCESS_ATTACH )
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Go, NULL, NULL, NULL);
return TRUE;
}
/* syscall.asm */
public RealSetCursorPos
public RealSetWindowPos
_TEXT$00 segment para 'CODE'
ALIGN 16
RealSetCursorPos PROC
movsxd rdx, edx
movsxd rcx, ecx
mov r8d, 74h
mov r10, rcx
mov eax, 102ah
syscall
ret
RealSetCursorPos ENDP
RealSetWindowPos PROC
mov r10, rcx
mov eax, 1024h
syscall
ret
RealSetWindowPos ENDP
_TEXT$00 ENDS
END
最後透過 Xenos
或其他 DLL Injector 來 Inject DLL 記錄下的 Byte 可以組成一個 7z 壓縮檔,會解出一個 stub.exe
0x04 - Reverse
分析過後得知程式會讀取 16 個 byte 的輸入,經過一連串運算之後要等於 0p3n5354m3...=.=
接著會將輸入的內容當成 RC4 Key,解出 Flag
透過 Hexrays + KLEE 可以直接解出 Input 要是 g00dm0rn1n9^_^!!
#include <klee/klee.h>
unsigned char xkey[17] = { 0x6E, 0xC2, 0xE1, 0x2D, 0x05, 0xF8, 0x68, 0x71, 0xAF, 0x76, 0x68, 0xFD, 0xF8, 0x76, 0xA3, 0x82, 0x00 };
int seed = 0x1337;
int rand()
{
seed = 214013 * seed + 0x269EC3;
return (seed >> 16) & 0x7FFF;
}
unsigned int rol(unsigned char a1, char a2)
{
return (a1 << a2) | ((unsigned int)a1 >> (8 - a2));
}
int ror(unsigned int a1, char a2)
{
return (a1 >> a2) | (a1 << (32 - a2));
}
int cmp(char* s1, char* s2)
{
for ( int i = 0; i < 16; ++i ) {
if (s1[i] != s2[i])
return 0;
}
return 1;
}
int main(int argc, char *argv[])
{
unsigned char input[16];
klee_make_symbolic(input, sizeof(input), "input");
for ( int i = 0; i < 16; ++i )
input[i] ^= xkey[i];
int v6 = 0x12345678;
for ( int i = 0; i < 13; ++i ) {
input[i] = rol(input[i], 3);
*(unsigned int*)&input[i] ^= v6;
int v8 = ror(v6, 5);
v6 = rand() % 13371337 ^ v8;
}
if ( cmp("0p3n5354m3...=.=", input) )
klee_assert(0);
return 0;
}
$ LazyKLEE.py ./sol.c
=== LazyKLEE ===
[+] Creating container...
[+] Compiling llvm bitcode...
[+] Running KLEE...
klee ./out.bc
[+] ASSERTION triggered!
ktest file : './klee-last/test000017.ktest'
args : ['./out.bc']
num objects: 1
object 0: name: b'input'
object 0: size: 16
object 0: data: b'g00dm0rn1n9^_^!!'
[+] Removing container...
Flag: TMCTF{https://www.youtube.com/watch?v=q6EoRBvdVPQ}
覺得越來越常遇到 Windows 64bit PE 了呢,跟以前在 32bit 胡搞瞎搞還是有蠻多差別的
期待相關工具能夠越來越成熟啊...