TMCTF 2017 - Reverse 400

這次 TMCTF 中最高分的逆向題是個蠻有趣的題目
比起包裝成逆向題的數學跟密碼學問題,還是比較喜歡這種類型的逆向題

0x00 - Challenge

題目是個加了 VMProtect 的 64bit PE,從 linker version 也可以看出是 Delphi 寫的

覺得 TMCTF 總是會看到 VMProtect 呢… -_-

執行之後視窗上會有 0-F 16 個按鈕:

程式首先會將滑鼠游標移至其中一個按鈕上
幾秒後將按鈕座標打亂,再將游標移至另一個按鈕上
根據標題上的數字來看,會重複 41216 輪

嘗試記下前幾輪中游標下的字元,發現是 7z 的 header:

root@ubuntu:~# unhex 377ABCAF271C0004264201C0D64F > 1 ; file 1
1: 7-zip archive data, version 0.4

很明顯的題目是希望我們記錄下每一輪的結果,組成檔案

0x01 - Unpack

由於程式執行的速度非常慢,決定先嘗試脫殼分析
首先透過 Scylla 來 dump 出 binary
和預料之中的一樣,無法自動修正 IAT,加上有開 VM 保護,沒辦法完整修正脫殼後的程式

但不能執行沒關係,能用 IDA Pro 分析就足夠了
再透過 IDAPython 簡單標記 Import Functions

但由於是 Delphi 開發的程式,加上許多關鍵部分被 VM
純靜態分析其實並沒有很大的幫助,因此決定配合動態分析

0x02 - Dynamic Analysis

事實上我還沒有處理過加了 VMProtect 的 64bit PE
但就我所知比較適合用的工具大概只有 x64dbg

不能用 OllyDBG 真是太痛苦了…
再怎麼改,x64dbg 還是不像 OllyDBG 一樣順手

x64dbg 雖然可以 Attach,但沒辦法下斷點
嘗試裝了 ScyllaHide 跟其他幾個 Plugin 還是沒辦法快樂的 debug

有人知道有什麼好 Plugin 請告訴我 QQ

最後最後決定不用 Debugger,直接透過 Hook 來處理

0x03 - Hook

根據經驗,就算 VMProtect 加了 VM 保護,在呼叫 Windows API 時,通常還是需要離開 VM,執行完 WinAPI 再回到 VM
因此可以直接 Hook 做 Windows API來記錄程式行為

視窗程式要改變控制項的位置時需要呼叫 SetWindowPos
改變滑鼠游標位置需要呼叫 SetCursorPos

寫個 Dll 來 Hook 這兩個 API,記錄下每一輪各個按鈕以及滑鼠的座標

x64 不能再直接插 JMP / CALL 來做 inline hook
需要改用 mov rax, target ; jmp rax 或是 push target ; ret

並在 SetCursorPos 中計算當下距離滑鼠座標最近的是哪一個按鈕

雖然可以透過 WindowFromPoint 來取得滑鼠座標下的視窗/控制項的 HWND,但題目的滑鼠游標似乎不一定會在按鈕上

順帶一提,Visual Studio x64 compiler 不支援 inline assembly,hook 寫起來蠻麻煩的…Orz

雖然可以記錄座標,但程式執行起來還是很慢,最後發現 Hook 掉 SleepEx 可以加速

// main.cpp
#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);

    //MessageBoxA(NULL, "", "Click to Exit", 0);
    //ExitProcess(0);
}

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

stub.exe 執行起來的畫面跟 HITCON Wargame 2013 某題一模一樣,果然又是大可出的XD

分析過後得知程式會讀取 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;
}
root@ubuntu:~/rev400# 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 胡搞瞎搞還是有蠻多差別的
期待相關工具能夠越來越成熟啊…