之前逆過XP下的掃雷程序,感覺XP下的掃雷很簡單,但是發現網上對於Win7下的掃雷逆向很少很少,於是就試着繼續逆一下Win7下的掃雷。這一逆發現難度提升了不只一個等級啊,經過兩天的努力,終於整個逆完了它的掃雷算法。
首先在Win7下的掃雷不再是像XP一樣在一開始就布置好雷區,這樣我們就可以在一開始就讀取雷區內存,比較坑的是win7下的掃雷是在你點擊第一塊兒方塊時才開始布置雷區。這樣我首先在rand函數下斷點,發現有好多地方會調用rand函數,我把每個調用rand函數的地方下了斷點,然后把一直在調用的rand函數的那幾個函數斷點給去掉,這樣我們就找到了程序的突破口。
不斷退出當前調用,並在上層函數的call調用處下斷點,直到找到了一個疑是算法入口的函數。
跟進函數,又發現一個call,繼續跟進
我發現這個函數便是調用rand函數的地方,估計核心就在此了,開干
我們要對每個call都倍加小心,需要都看一下,我們發現這里好像是一個申請數組空間並填充的操作
經過多次循環后,發現數組填充完畢,之后觀察一下申請的數組空間中存儲的東西
發現沒有了00 01 09 0A四個值。因為我是點擊的第一個方塊,我們可以重新調試,點擊其他方塊試一下,發現這個數組會將點擊方塊周圍的的9個值去掉(包括點擊方塊自己),這樣我們就理解了,程序不會在第一次點擊方塊周圍產生雷。
這時候我們估計就對這個程序有了一點點理解了,在點擊第一塊兒方塊的時候,程序開始申請內存。這里它會有一個結構體存儲了隨機雷數組已用大小和總空間,然后生成一個數組,並將各個雷進行編號存入數組中。之后rand函數產生的隨機雷就在這些數組中產生。接下來驗證我們的想法:
跟進下一個call,發現這里申請數組空間,並存儲隨機出來的雷值
繼續單步,發現有個小循環比較有意思
看一下rax存儲了什么?
這好像是存儲了多個數組的首地址啊,正好我們現在設定了9x9的雷區,這里正好9個地址,我們再跟進去這些地址看一下
這里+10處又存儲了一個地址,繼續觀察,發現有個byte數組,存儲了雷的狀態,有雷就是1,無雷就是0
這個時候我們就基本搞明白了這個Win7下掃雷是怎么布置的了。可是問題來了,最初的記錄雷區各個數組的地址從哪得啊?我們逆着代碼去溯源。我們發現這個值是rax+0x10處的存儲的值,而rax是rsi+0x58處存儲的值,這個rsi是rcx作為上層調用函數傳過來的參,我們走出這個函數看看這個參數從哪里得到。
我們找到了這樣一個值,在FFCFAA38中存儲了我們所想要的rsi的值。我們知道,這是一個全局變量,存儲了rsi地址。但是這個值由於RSLR機制而導致每次地址不一樣。我們有一種方法得到這個值,我們先看當前模塊加載基地址,然后用FFCFAA38(全局變量地址)-FFC500000(當前模塊加載地址)= AAA38(相對當前模塊偏移)。這樣我們可以用GetModuleHandle函數得到當前模塊加載基地址,然后加上這個偏移AAA38就得到了全局變量地址。
這樣我們就有了得到數組地址的方法:
Address = [[[[hModule+0xAAA38]+0x18]+0x58]+0x10]
這時Address就是存儲雷區數組的首地址,每個雷區地址+0x10處就是雷區列狀態數組(byte)地址。
其實,到這里我們也開始明白了,它所使用的應該是C++的vector,一個個push才產生這樣的內存空間的,不得不說,這C++功力已經爐火存青了,各種數據結構弄得頭都大了。
找到了雷區布置數組就可以進行下一步動作了,我們通過計算鼠標坐標值來獲得雷區格子,每個格子是17*17像素大小並加上1像素的邊,所以每個格子大小為18像素,雷區邊界為30像素。你問我這些怎么得到的?這些值肯定在某個內存存着,你可以下斷點在GetCursorPos處,在你移動鼠標時會觸發斷點,然后跳出函數,發現下邊有一個GetWindowRect函數,這個函數會傳遞窗口句柄,窗口句柄存儲在一個全局內存中,我們可以得到這個窗口句柄。但是我用了更簡單的方法,既然有窗口,我直接用工具測一下就知道每個格子大小了么。
這樣我們就得到了鼠標坐標轉換格子的公式:
int x = (xPos - 30) / 18; //列
int y = (yPos - 30) / 18; //行
最后,上輔助代碼:
WNDPROC g_oldProc = NULL; DWORD dwArrOffset = 0xAAA38; //這是重定向之前全局變量相對於模塊基地址的位置 DWORD64 dwMineAddress = 0; //雷區列數組位置 BYTE *pMineMap; //自定義一個數組存儲雷的布局,便於訪問 DWORD rows = 0; //行 DWORD cols = 0; //列 LRESULT CALLBACK WindowProc( _In_ HWND hwnd, _In_ UINT uMsg, _In_ WPARAM wParam, _In_ LPARAM lParam ) { if (uMsg == WM_MOUSEMOVE) { int xPos = GET_X_LPARAM(lParam); int yPos = GET_Y_LPARAM(lParam); int x = (xPos - 30) / 18; //列 int y = (yPos - 30) / 18; //行 int nWidth = 30 + 18 * cols; int nHight = 30 + 18 * rows; if (x>=0&&y>=0&&x<nWidth&&y<nHight) { if (pMineMap[x*rows + y] == (BYTE)0x01) { SetWindowText(hwnd, L"!!!有雷!!!"); } else { SetWindowText(hwnd, L"掃雷"); } } else { SetWindowText(hwnd, L"掃雷"); } } //F12鍵一鍵掃雷 if (wParam == VK_F12) { int i = 0; do { for (int j = 0; j < rows; j++) { if (pMineMap[i*rows + j] != (BYTE)0x01) { int x = (i * 18) + 30 + 9;//定位到格子中心 int y = (j * 18) + 30 + 9; LPARAM point = MAKELPARAM(x, y); //發送鼠標點擊消息 PostMessage(hwnd, WM_LBUTTONDOWN, NULL, point); PostMessage(hwnd, WM_LBUTTONUP, NULL, point); } } i++; } while (i!=cols); } return CallWindowProc(g_oldProc, hwnd, uMsg, wParam, lParam); } // CMineSweeperWaiguaApp 初始化 BOOL CMineSweeperWaiguaApp::InitInstance() { CWinApp::InitInstance(); OutputDebugString(L"已加載!\n"); //獲得雷區數組地址 HMODULE hModule = GetModuleHandle(NULL); // 雷區列數組獲得公式 [[[[hModule+0xAAA38]+0x18]+0x58]+0x10] DWORD64 v1 = *(DWORD64*)((DWORD64)hModule + dwArrOffset); //v2+0x8處存儲了雷總數(DWORD),v2+0x0C處存儲了行總數(DWORD),v2+0x10處存儲了列總數(DWORD) DWORD64 v2 = *(DWORD64*)(v1 + 0x18); //申請一個存儲雷區的數組空間 rows = *(DWORD*)(v2+0x0C); //行 cols = *(DWORD*)(v2+0x10); //列 pMineMap = (BYTE*)VirtualAlloc(NULL,rows*cols, MEM_COMMIT, PAGE_READWRITE); if (pMineMap ==NULL) { OutputDebugString(L"申請內存失敗!\n"); } OutputDebugString(L"申請內存成功!\n"); //v1處存儲了列總數(DWORD) v1 = *(DWORD64*)(v2 + 0x58); dwMineAddress = *(DWORD64*)(v1 + 0x10);//存儲雷區列數組地址,有多少列就有多少數組 //數組地址首位顯示的是行數 for (int i=0;i<*((DWORD*)(v2+0x10));i++) { //v1處前4字節(DWORD)儲了行總數 ,后邊兩個內容沒搞懂(10 10) v1 = *(DWORD64*)(dwMineAddress + i * 8); BYTE *v5 = (BYTE*)(*(DWORD64*)(v1 + 0x10)); for (int j = 0; j < *((DWORD*)(v2 + 0x0C)); j++) { //將雷區狀態賦值到數組中去 pMineMap[i*rows +j] = *v5; //第i列第j行 v5++; } } OutputDebugString(L"雷區賦值成功!\n"); HWND hWnd = FindWindow(NULL, L"掃雷"); if (hWnd == NULL) { OutputDebugString(L"未找到目標窗口!\n"); return FALSE; } //更改指定窗口的屬性 //返回值是之前的窗口函數 g_oldProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)WindowProc); return TRUE; }
到此,我們大功告成,需要注意的是每次要點擊一下一個格子再注入動態庫。不過,我的F12一鍵掃雷並沒成功有人知道是怎么回事么?希望不吝賜教幫我解決一下,嘻嘻~~