遊戲安全入門-掃雷分析&遠端執行緒注入

蚁景网安实验室發表於2024-08-13

前言

無論學習什麼,首先,我們應該有個目標,那麼入門windows遊戲安全,腦海中浮現出來的一個遊戲 -- 掃雷,一款家喻戶曉的遊戲,雖然已經被大家分析的不能再透了,但是我覺得自己去分析一下還是極好的,把它作為一個小目標再好不過了。

我們編寫一個妙妙小工具,工具要求實現以下功能:時間暫停、修改表情、透視、一鍵掃雷等等。

本文所用工具:

Cheat Engine、x32dbg(ollydbg)、Visual Studio 2019

掃雷遊戲分析

遊戲資料在記憶體中是地址,那麼第一個任務,找記憶體地址

開啟CE修改器

修改時間->時間暫停

計數器的時間是一個精確的值,所以我們透過精確數值掃描出來,遊戲開始之前計數器上的數是0,所以我們掃描0。

image.png

時間在變化,選擇介於什麼數值之間再次掃描

image.png

可得 0x100579c --- winmine.exe+579C

image.png

我們發現這個資料都是直接透過基址 + 固定偏移能直接得到的。

然後我們對資料去找 是什麼改寫了這個地址,得到一個指令和指標:

image.png

時間:0x100579c

修改表情 - 沒啥用

修改表情這個功能怎麼搞我覺得還是很容易想到的,這個按鈕的作用是重新開始遊戲,開始遊戲,遊戲勝利,遊戲失敗。

(表情的狀態被分成了兩個變數(4byte)來控制)

所以它是一種狀態,所以我們透過0和1進行掃描,遊戲進行狀態輸入1進行掃描,還原遊戲之後輸入0進行掃描。

首先是遊戲進行狀態,輸入1進行掃描

image.png

再點選表情,將遊戲還原,輸入0開始掃描

image.png

如此反覆進行掃描,得到表情的記憶體地址

0x1005164 -- winmine.exe+5164

image.png

但是嘞,修改成2或者3,表情沒有心得反應,所以控制遊戲勝利和遊戲失敗的是其他的地址,我們知道,一般來說,一個功能的程式碼在記憶體中基本上都是連續的,(就像你修改一個遊戲的血量,瀏覽血量記憶體塊,你可以發現怒氣,藍量等記憶體地址)

所以,我們瀏覽記憶體

image.png

image.png

0x1005164-4 = 0x1005160

修改為3,發現出現了戴墨鏡的表情(遊戲勝利)

但是這個勝利知識一個狀態,並不能說明掃雷完成.

image.png

表情:0x1005160與0x1005164

【----幫助網安學習,以下所有學習資料免費領!加vx:dctintin,備註 “部落格園” 獲取!】

 ① 網安學習成長路徑思維導圖
 ② 60+網安經典常用工具包
 ③ 100+SRC漏洞分析報告
 ④ 150+網安攻防實戰技術電子書
 ⑤ 最權威CISSP 認證考試指南+題庫
 ⑥ 超1800頁CTF實戰技巧手冊
 ⑦ 最新網安大廠面試題合集(含答案)
 ⑧ APP客戶端安全檢測指南(安卓+IOS)

透視 - 顯示雷區

思考遊戲結束的時候會自動顯示所有的雷,因此我們動態除錯,看看在哪個函式呼叫之後會顯示所有的雷

image.png

image.png

經過幾次的動態除錯之後發現:0x2F80函式是我們要找的結果。

image.png

一鍵掃雷

透過透視,我們玩一把遊戲,使得遊戲勝利(點完最後一個)

image.png

image.png

然後後兩個函式,是破紀錄跟英雄榜的函式

image.png

image.png

ret來到了這兒,遊戲通關了,來到了這兒,可以知道,這個0x347c就是判斷輸贏的函式

並且透過除錯發現由一個引數 0 1 來控制,所以跟透視差不多,帶個引數執行緒回撥就完了

image.png

編寫妙妙小工具

怎麼實現這個工具呢,當然是選擇DLL注入

那麼dll 怎麼注入進去呢,這裡選擇遠端執行緒注入

這裡先簡單介紹下什麼是遠端執行緒注入

前置知識-動態呼叫dll

主要就是這幾個個 API:

LoadLibraryA

載入指定 DLL 並返回模組控制代碼,引數為字串,就是 dll 的路徑。

GetProcAddress

獲取指定 dll 的匯出函式的地址。

第一個引數是模組控制代碼,第二個引數是模組函式,返回值為函式的地址。

透過這兩個函式,我們可以拿到所有函式的地址,然後就能進行呼叫。

CreateThread - 遠端執行緒注入

裡面幾乎只有一個引數,那就是執行緒回撥函式,然後當然還有返回地址,返回執行緒 id 啥的,這裡我們都可以不用管,幾乎是與 Linux 的建立執行緒函式一致。

還有一個遠端版本的叫 CreateRemoteThread,它可以給別的程序建立一個執行緒並可以在本程序建立那個程序呼叫的回撥函式。我們可以在回撥函式中載入指定的 dll,在 dllmain 的入口當中,有一個 switch 的四個選項。

// dllmain.cpp : 定義 DLL 應用程式的入口點。
#include "pch.h"
​
BOOL APIENTRY DllMain( HMODULE hModule,//指向自身的控制代碼
                       DWORD  ul_reason_for_call,//呼叫原因
                       LPVOID lpReserved//隱式載入or顯式載入
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH://附加到程序上時執行
    case DLL_THREAD_ATTACH://附加到執行緒上時執行
    case DLL_THREAD_DETACH://從執行緒上剝離時執行
    case DLL_PROCESS_DETACH://從程序上剝離時執行
        break;
    }
    return TRUE;
}
​
​

我們可以在 DLL_PROCESS_ATTACH 的選項中加入程式碼,讓它在載入的時候呼叫執行。

那麼我們的步驟是:

  1. 開啟指定程序獲得控制代碼

  2. 開闢遠端程序的空間,分配可讀可寫段。

  3. 呼叫 WriteProcessMemory 將 dll 路徑寫入該記憶體區域。

  4. 建立遠端執行緒,回撥函式使用 LoadLibrary 載入指定 dll。

  5. 等待返回(loadLibrary返回)

  6. 釋放空間

  7. 釋放控制代碼

  8. 返回結果

demo:

void Inject(DWORD ProcessId, const char* szPath)
{
    //1.開啟目標程序獲取控制代碼
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);
    printf("程序控制代碼:%p\n", hProcess);
    //2.在目標程序體內申請空間
    LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    //3.寫入DLL路徑
    SIZE_T dwWriteLength = 0;
    WriteProcessMemory(hProcess, lpAddress, szPath, strlen(szPath), &dwWriteLength);
    //4.建立遠端執行緒,回撥函式使用 LoadLibrary 載入指定 dll
    HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryA, lpAddress, NULL, NULL);
    //5.等待返回(loadLibrary返回)
    WaitForSingleObject(hThread, -1);
    //6.釋放空間
    VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);
    //7.釋放控制代碼
    CloseHandle(hProcess);
    CloseHandle(hThread);
    //返回結果
    AfxMessageBox(L"完成");
}

編寫DLL注入器

#include<windows.h>
#include<iostream>
#include<time.h>
#include<stdlib.h>
#include<TlHelp32.h>
DWORD FindProcess() {
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32 pe32;
    pe32 = { sizeof(pe32) };
    BOOL ret = Process32First(hSnap, &pe32);
    while (ret)
    {
        if (!wcsncmp(pe32.szExeFile, L"mine.exe", 11)) {
            printf("Find winmine.exe Process %d\n", pe32.th32ProcessID);
            return pe32.th32ProcessID;
        }
        ret = Process32Next(hSnap, &pe32);
    }
    return 0;
}
void Inject(DWORD ProcessId, const char* szPath)
{
    //1.開啟目標程序獲取控制代碼
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);
    printf("程序控制代碼:%p\n", hProcess);
    //2.在目標程序體內申請空間
    LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    //3.寫入DLL路徑
    SIZE_T dwWriteLength = 0;
    WriteProcessMemory(hProcess, lpAddress, szPath, strlen(szPath), &dwWriteLength);
    //4.建立遠端執行緒,回撥函式使用 LoadLibrary 載入指定 dll
    HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryA, lpAddress, NULL, NULL);
    //5.等待返回(loadLibrary返回)
    WaitForSingleObject(hThread, -1);
    //6.釋放空間
    VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);
    //7.釋放控制代碼
    CloseHandle(hProcess);
    CloseHandle(hThread);
}
​
int main() {
    DWORD ProcessId = FindProcess();
    while (!ProcessId) {
        printf("未找到掃雷程式,等待兩秒中再試\n");
        Sleep(2000);
        ProcessId = FindProcess();
    }
    printf("開始注入程序...\n");
    Inject(ProcessId, "E:\\CODE\\wimine\\Mine\\release\\Mine.dll");
    printf("注入完畢\n");
​
}

編寫DLL

這裡我們採用MFC DLL 基於對話方塊 (dialog)的方式編寫(簡單),使用靜態編譯的方式

image.png

image.png

然後我們需要在資源窗體,新建一個 Dialog ,簡單包裝一個介面

image.png

這樣我們在載入窗體的時候需要建立一個窗體類物件用它的 DoModal 方法去顯示,用執行緒回撥的方式載入並且初始化InitInstance

DWORD WINAPI DlgThreadCallBack(LPVOID lp) {
    MineDlg* Dlg;
    Dlg = new MineDlg();
    Dlg->DoModal();
    delete Dlg;
    FreeLibraryAndExitThread(theApp.m_hInstance, 1);
    return 0;
}
// CMineApp 初始化
BOOL CMineApp::InitInstance()
{
    CWinApp::InitInstance();
    ::CreateThread(NULL, NULL, DlgThreadCallBack, NULL, NULL, NULL);
    return TRUE;
}

時間暫停

上面我們找到了它控制時間增加的指令,我們把它們全部 NOP 掉,就可以實現時間暫停

寫兩個按鈕,建立下面的事件實現時間暫停開關。

image.png

DWORD GetBaseAddr() {
    HMODULE hMode = GetModuleHandle(nullptr);
    //LPWSTR s = (LPWSTR)malloc(0x100);
    //wsprintf(s, L"基址:%p", hMode);
    //AfxMessageBox(s);
    return (DWORD)hMode;
}
​
void MineDlg::OnBnClickedButton1() // 時間暫停
{
    // TODO: 在此新增控制元件通知處理程式程式碼
    auto BaseAddr=GetBaseAddr();
    DWORD TimeOffset = 0x579C;
    DWORD TimeInsOffset = 0x2FF5;
    DWORD InsLen = 6;
    DWORD old;
    VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, PAGE_EXECUTE_READWRITE, &old);
    BYTE INS[] = { 0x90,0x90,0x90,0x90,0x90,0x90 };
    memcpy((void *)(BaseAddr + TimeInsOffset), INS, InsLen);
    VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, old, &old);
}
​
​
void MineDlg::OnBnClickedButton2() // 恢復位元組即可取消時間暫停
{
    // TODO: 在此新增控制元件通知處理程式程式碼
    auto BaseAddr = GetBaseAddr();
    DWORD TimeOffset = 0x579C;
    DWORD TimeInsOffset = 0x2FF5;
    DWORD InsLen = 6;
    DWORD old;
    VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, PAGE_EXECUTE_READWRITE, &old);
    BYTE INS[] = { 0xFF,0x05,0x9C,0x57,0x00,0x01 };
    memcpy((void*)(BaseAddr + TimeInsOffset), INS, 6);
    VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, old, &old);
}

測試

image.png

透視

經過上面動態除錯我們得出結論:0x2F80函式是踩雷函式。

我們如果呼叫這個函式,是不是就能夠實現透視了呢?

我們依舊採取執行緒回撥的方式

void MineDlg::OnBnClickedButton3()
{
    // TODO: 在此新增控制元件通知處理程式程式碼
    DWORD ESPOffset = 0x2f80;
    DWORD FuncAddr = GetBaseAddr() + ESPOffset;
    // 建立不帶引數的執行緒
    CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)FuncAddr, NULL, 0, NULL);
}

測試

image.png

一鍵掃雷

跟透視差不多,只不過建立帶引數的執行緒回撥

void MineDlg::OnBnClickedButton4()
{
    // TODO: 在此新增控制元件通知處理程式程式碼
    DWORD ESPOffset = 0x347C;
    DWORD FuncAddr = GetBaseAddr() + ESPOffset;
    //建立帶引數的執行緒
    struct { int a; } s = { 0 };
    CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)FuncAddr, &s, NULL, NULL);
​
}

測試

image.png

總結

透過這個小專案,對WIN遊戲安全有初步的認識,並且加強對軟體的逆向思維,增強動態除錯的能力,找到軟體關鍵的基地址,透過CE修改器,初步pojie軟體,瞭解軟體的狀態,修改時間(時間暫停等等),理解幾個重要的API,FindWindow獲取控制代碼,WriteProcessMemory寫入記憶體資訊,LoadLibraryA載入指定 DLL 並返回模組控制代碼,GetProcAddress,獲取指定 dll 的匯出函式的地址,CreateThread 執行緒回撥函式等等。多寫,多做,多調,多實驗,加油,互勉。

更多網安技能的線上實操練習,請點選這裡>>

相關文章