原文链接:Re - 逆向扫雷
前几天Mz师傅发了一个逆向扫雷写挂的视频,给我看得热血沸腾从床上猛地一下跳起来,然后翻了个身,首先找mz师傅要了一份扫雷的exe文件
 
主要就三个玩意,扫雷主程序直接点根本点不开,于是尝试开IDA看一看  
在Main里面有这么一个IsGamePlayable,如果返回0的话就直接退出了,怀疑可能是这个东西的原因,于是尝试用ollygdb看看是不是在这个地方出的问题 这里就牵扯到一个寻找函数入口的问题,Ollydbg中进入以后是真正的函数入口,而不是winMain函数,但是我们的关键代码在winMain里,所以就需要先找到真正的入口再说 但是这个扫雷是开了PIE的,我们并不方便直接定位代码,所以我们可能需要先找程序基址,但是其实我们真正需要的事情是找一个特征,这些特征可以从代码中找,我们查找调用winMain的函数  
注意到这个ImageBase,正好是程序基址,其是winMain的第一个参数,而winMain的第一个参数按道理来讲应该是hInstance,那么是不是说明hInstance正好就是程序基址呢?带着好奇的心情搜了一下  
Win32-HINSTANCE和HWND理解 - 白帽安全技术复现 - 博客园 (cnblogs.com) 这位大佬把源码翻了一通,找了一下定义,发现这个玩意是一个指向ImageBase的指针,那么与我们这里调用WinMain的代码就不谋而合了,下面就是看这个玩意的值到底是个啥 熟悉Linux的同学应该知道,因为页表效率的问题,一个程序基址或者so基址的低12位是肯定为0的,那么也就是说基址一定是0xxxxxx000这个样子的,而Windows也类似,那么ImageBase是一个指针,其地址为程序基址,也长0xxxxxx000这样子 我们再看看其汇编代码  
这是不是意味着当快要到程序入口的时候栈上就会出现类似于0xxx000这样的数据?类似的,如果是64位的程序的话,rdi寄存器应该就会变为这个数据 带着这个猜想我们打开od试试,刚进来长这样  
然后一路步进F8,盯着栈的数据,直到出现下面这个  
这是在开启PIE的情况下的做法,在没有开启PIE的时候有个更方便的做法,在Windows PE头中,有一个建议装载地址,Windows会将程序加载到这个建议装载地址上,这种情况下直接计算就行了 到现在就可以F7了   到这个位置,应该就是IsGamePlayable了,因为前面push了一个Shell-InBoxGames-Minesweeper,这玩意就是我们需要的,现在F8一下   发现执行完成以后是0,那么不符合jnz的判断,于是继续执行到了下面xor eax, eax的地方,最后jmp到ret处直接ret,从而执行失败   那么程序不能启动原因也就是这个了,可能因为一些不知道什么的原因导致了这个IsGamePlayeable的返回值为0,那么我们改一改就好,将jnz改为jz,或者这一串全部改为nop,这里可以使用pwntools和十六进制编辑器进行改写   nop的16进制为0x90 找到要修改的代码,从push开始改,下面全改成nop  
从IDA里看是这个地方  
到这个地方前  
在FlexHEX中搜索68 10 2c 00找到这条指令的位置然后给它用nop填充掉  
保存 于是程序就变成了这个样子  
isPlayable直接就没了 但是修改完以后发现还是打不开游戏=,=,难道真的not playable吗,我们再尝试将jnz改为jz试一试,万一是因为IsPlayable里还做了什么初始化呢 查看jz的十六进制  
然后跟之前一样直接改  
然后就会神奇地发现跑起来了!OVO   接下来就是写挂了OVO 进入IDA先静态逆一下逻辑,在InitializeEngine函数中可以发现消息循环逻辑,那么根据g_hWnd去寻找打开窗口的地方  
找到了CreateGameWindow函数  
CreateGameWindow函数调用了CreateWindowEx  
在CreateGameWindow以后就有这玩意,注册了一大堆事件,从函数名来看就是事件  
一个大胆的猜想,我们扫雷的时候按下去的时候并不会显示这个是不是雷,而在松开鼠标的时候会开始扫雷,那是不是意味着MouseRelease事件可能就涉及到雷区储存的位置 点进去看看是个什么玩意  
简单地很,但是后面逆了一段时间发现这玩意没啥用,它唯一的用处就是返回的result+16偏移处有事件编号,所以猜测这里只是简单地注册一个事件,::`vftable`是虚函数表
但总之在这里我们没有发现什么线索,后来找了很久也没什么线索,所以转换思路,我们直接猜函数名(主要这个玩意它没去掉调试符号),试一下Game、Player等关键词,然后就发现了下面这个牛马  
在这个函数里面我们又能看到这个调用  
这什么玩意啊,这看起来像重置游戏啊,但是进去看了以后发现什么线索都没有,就是delete掉一堆内存,但是从这个点我们想到,是不是这个Board类可能是储存雷区或者旗区的,那么我们找它的初始化部分是不是就可以找到雷区的地址了??  
然后就跟到了上面这个地方,调用了Board的构造函数,且传入的v50是一个this指针,继续跟  
果然找到了placeMines函数,这个玩意肯定就是放雷的了,继续跟  
我把我写的注释打了个马,先不看注释,大家能猜出这段代码是干什么的吗,反正一开始我是没看出来,向上面的width等变量名还是我改好了的,没改之前根本就不能看,现在我们还不清楚这里每个循环都有什么用,但是我们可以注意一下GetRandom函数,这很像是随机雷的代码,那么我们就尝试在这个while里下断点看一下,我们但是在这之前我们猜这个while的条件是雷的个数  
上面是这个循环的汇编,我们用x32dbg在这里下硬件断点 首先找到ImageBase 0x590000  
然后计算这个循环的VA 0x590000 + 0x2019d = 0x5b019d,然后下断点 mov dr0, 0x5b019d mov dr7, 0   随后摁F9让程序跑起来,由于第一次点击之前并不会生成雷区,所以现在断点是不会生效的,所以我们需要再点一下,生成雷区   于是现在断点就生效卡在这了 我们仔细观察   这里是将eax与 【esi + 4】做对别,现在是第一轮循环,eax为0,我们去看看这个【esi + 4】   0a即10,10个雷,正好达到我们的预期,且在后面的循环中eax会逐渐增加到10,但是这个过程并不是完全的累加,这个循环的Add和Remove函数是用来处理雷重复的情况的,而没有重复的话类的位置,即v9就会被储存到v8数组中,那么这个v9长啥样呢,我们猜测它的范围是 0 ~ 长 * 宽 - 1,在实际调试的过程中也证实了我们的猜想 接着我们看一看下面那个循环的代码  
这个v11是宽度,而v8是雷区数组,那么v12应该就是雷的y轴坐标,v13是x轴坐标,而在上面的那一长串指针操作应该就是给雷区赋值了  
稍微解释一下这个地方  
首先这里是获取到雷区的数组指针,它储存在this[68] + 12中  
获取到目标y轴数组指针储存的位置  
获取到目标y轴数组指针  
获取到目标坐标的指针 我们上dbg看一看  
   
会发现这里储存的雀食是一个指针数组,用来储存雷区数据 好了,现在我们基本上能确定雷区的位置是怎么来的了,下面就是把这个过程自动化 在Board类中,雷区位置是这么来的,*(Board指针 + 68)+ 12 = 雷区双重指针,现在我们去跟Board的初始化  
Board指针被储存在了Game指针 + 4(DWORD)的位置,所以Game指针 + 16 = Board,继续跟Game指针储存在哪  
在全局变量Game::G中(或者应该说是静态变量),那么看一看这个地方的地址  
好那接下来就是写注入了,但是我这里嫌麻烦就没写了,就暴力读内存了 下面是代码,因为偷了懒,没怎么注意代码格式,所以这里的代码很丑(,默认是16 * 16的棋盘,在初始化雷区以后将扫雷保持在桌面置顶,再启动脚本,就会自动去扫了,没有采用强制写入内存的方式,是移动鼠标的方式,视觉效果更好一点 - #include
- #include
- #include
- using namespace std;
- DWORD GetProcessIdEx(char windowName[]) {
- HWND hWnd = FindWindow(NULL, windowName);
- DWORD pid;
- GetWindowThreadProcessId(hWnd, &pid);
- return pid;
- }
- DWORD GetModuleInfo(
- DWORD pid,
- MODULEENTRY32* info,
- const char processName[]
- ) {
- HANDLE handle;
- handle = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid);
- //尝试获取进程信息
- Module32First(handle, info);
- do{
- if (strcmp(processName, info->szModule) == 0) {
- return TRUE;
- }
- } while (Module32Next(handle, info) != FALSE);
- return FALSE;
- }
- VOID ClickMineTile(HWND hWnd, RECT rect, int x, int y) {
- x = rect.left + 45 + x * 18;
- y = rect.top + 87 + y * 18;
- //::PostMessage(hWnd, WM_LBUTTONDOWN, MK_LBUTTON, MAKELONG(x, y));
- //::PostMessage(hWnd, WM_LBUTTONUP, MK_LBUTTON, MAKELONG(x, y));
- SetCursorPos(x, y);
- mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, x, y, 0, 0);
- mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, x, y, 0, 0);
- }
- BOOL GetWindowPosition(char windowName[], RECT *rect) {
- HWND hWnd = FindWindow(NULL, windowName);
- return GetWindowRect(hWnd, rect);
- }
- BOOL InjectDLL(const wchar_t* DllPullPath, const DWORD dwRemoteProcessId) {
- int pathSize = (wcslen(DllPullPath) + 1) * sizeof(wchar_t);
- //尝试获取远程进程句柄
- HANDLE rtProcessHandle = OpenProcess(
- PROCESS_ALL_ACCESS, FALSE, dwRemoteProcessId);
- if (rtProcessHandle == NULL) {
- cout << "获取远程句柄失败" << endl;
- return FALSE;
- }
- HANDLE hToken;
- if (OpenProcessToken(GetCurrentProcess(),
- TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken) == FALSE) {
- cout << "权限提升失败" << endl;
- return FALSE;
- }
- LUID luid;
- if (LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid) == FALSE) {
- cout << "权限信息查询失败" << endl;
- return FALSE;
- }
- TOKEN_PRIVILEGES tkp;
- tkp.PrivilegeCount = 1;
- tkp.Privileges[0].Luid = luid;
- tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; //允许特权
- if (AdjustTokenPrivileges(
- hToken, FALSE, &tkp, sizeof(tkp), NULL, NULL) == FALSE) {
- cout << "特权提升失败" << endl;
- return FALSE;
- }
- //在远程线程中申请空间
- LPVOID lpAddr = VirtualAllocEx(
- rtProcessHandle, NULL, pathSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
- if (lpAddr == NULL) {
- cout << "远程内存申请失败" << endl;
- return FALSE;
- }
- if (WriteProcessMemory(
- rtProcessHandle, lpAddr, DllPullPath, pathSize, NULL) == FALSE) {
- cout << "远程内存写入失败" << endl;
- return FALSE;
- }
- PTHREAD_START_ROUTINE pfnStartAssr =
- (PTHREAD_START_ROUTINE)GetProcAddress(
- GetModuleHandle("Kernel32.dll"), "LoadLibraryW");
- HANDLE hRemoteThread = CreateRemoteThread(
- rtProcessHandle, NULL, 0, pfnStartAssr, lpAddr, 0, NULL);
- if (hRemoteThread == NULL) {
- cout << "创建远程线程失败" << endl << GetLastError() << endl;
- VirtualFreeEx(rtProcessHandle, lpAddr, 0, MEM_FREE);
- return FALSE;
- }
- cout << "注入成功" << endl;
- WaitForSingleObject(hRemoteThread, -1);
- CloseHandle(hRemoteThread);
- VirtualFreeEx(rtProcessHandle, lpAddr, 0, MEM_FREE);
- return TRUE;
- }
- BOOL PeekMines(const DWORD dwRemoteProcessId,
- const DWORD dwRemoteImageBase) {
- //尝试获取远程进程句柄
- HANDLE rtProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE,
- dwRemoteProcessId);
- if (rtProcessHandle == NULL) {
- cout << "获取远程句柄失败" << endl;
- return FALSE;
- }
- cout << "获取远程句柄成功" << endl;
- HANDLE hToken;
- if (OpenProcessToken(GetCurrentProcess(),
- TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken) == FALSE) {
- cout << "权限提升失败" << endl;
- return FALSE;
- }
- cout << "权限提升成功" << endl;
- LUID luid;
- if (LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid) == FALSE) {
- cout << "权限信息查询失败" << endl;
- return FALSE;
- }
- cout << "权限信息查询成功" << endl;
- TOKEN_PRIVILEGES tkp;
- tkp.PrivilegeCount = 1;
- tkp.Privileges[0].Luid = luid;
- tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; //允许特权
- if (AdjustTokenPrivileges(
- hToken, FALSE, &tkp, sizeof(tkp), NULL, NULL) == FALSE) {
- cout << "特权提升失败" << endl;
- return FALSE;
- }
- cout << "特权提升成功" << endl;
- cout << "开始读取雷区数据" << endl;
- LPVOID GameAddr = (LPVOID)(dwRemoteImageBase + 0x868b4);
- cout << "Game指针地址:\t0x" << hex << GameAddr << endl;
-
- LPVOID GamePtr;
- ReadProcessMemory(rtProcessHandle, GameAddr, &GamePtr, 4, 0);
- cout << "Game指针:\t0x" << hex << GamePtr << endl;
- LPVOID BoardPtr;
- ReadProcessMemory(rtProcessHandle,
- (LPVOID)((DWORD)GamePtr + 16), &BoardPtr, 4, 0);
- cout << "Board指针:\t0x" << hex << BoardPtr << endl;
- LPVOID MinesTmpPtr;
- ReadProcessMemory(rtProcessHandle,
- (LPVOID)((DWORD)BoardPtr + 68), &MinesTmpPtr, 4, 0);
- cout << "雷区缓冲指针:\t0x" << hex << MinesTmpPtr << endl;
- LPVOID MinesPtr;
- ReadProcessMemory(rtProcessHandle,
- (LPVOID)((DWORD)MinesTmpPtr + 12), &MinesPtr, 4, 0);
- cout << "雷区指针:\t0x" << hex << MinesPtr << endl;
- BYTE map[16][32] = { 0 };
- for (int i = 0; i < 30; i++) {
- //获取雷区二级指针
- LPVOID MinesColPtr;
- ReadProcessMemory(rtProcessHandle,
- (LPVOID)((DWORD)MinesPtr + 4 * i), &MinesColPtr, 4, 0);
- ReadProcessMemory(rtProcessHandle,
- (LPVOID)((DWORD)MinesColPtr + 12), &MinesColPtr, 4, 0);
- for (int j = 0; j < 16; j++) {
- BYTE tmp;
- ReadProcessMemory(
- rtProcessHandle, (LPVOID)((DWORD)MinesColPtr + j), &tmp, 1, 0);
- map[j][i] = tmp;
- }
- }
- for (int i = 0; i < 16; i++) {
- for (int j = 0; j < 30; j++) {
- cout << (int)map[i][j] << " ";
- }
- cout << endl;
- }
- cout << "开始模拟点击" << endl;
- RECT windowRect;
- GetWindowPosition((char *)"扫雷", &windowRect);
- HWND hWnd = FindWindow(NULL, "扫雷");
- for (int i = 0; i < 16; i++) {
- for (int j = 0; j < 30; j++) {
- if (map[i][j] != 1) {
- ClickMineTile(hWnd, windowRect, j, i);
- Sleep(2);
- }
- }
- }
- }
- /*
- 0x868b4 = Game
- Game指针 + 16 = Board
- (*(Board指针 + 68)+ 12) == 雷区指针
- */
- int main() {
- MODULEENTRY32 modentry;
- DWORD pid = GetProcessIdEx((char *)"扫雷");
- cout << "目标进程ID:\t" << pid << endl;
- Sleep(2000);
- if (GetModuleInfo(pid, &modentry, "MineSweeper.exe")) {
- cout << "目标进程:\t" << modentry.th32ProcessID << endl;
- cout << "目标程序基址:\t0x" << hex << modentry.hModule << endl;
- PeekMines(modentry.th32ProcessID, (DWORD)modentry.hModule);
- }
- else {
- cout << "不存在的进程" << endl;
- }
- return 0;
- }
复制代码
|