游戏安全的学习(2)—— 利用Windows API 操作游戏窗口,和扫雷游戏分析。
先定个小目标,实现以下功能:
句柄
句柄(handle)是Windows操作系统用来标识被应用程序所建立或使用的对象的整数,可以理解为内核有一张句柄表,我们打开获得的句柄得到的是句柄表的偏移值。
特定的,窗口句柄叫 HWND
。我们可以通过 VS 的工具选项中的 SPY++
去找到窗口的标题,句柄和类等等信息。
获取窗口句柄
使用 API FindWindow
即可,我们需要给定两个参数,第一个参数为 Class Name
,第二个参数为 Window name
,应该是标题。我们可以给第一个参数传 NULL
一样能成功获取窗口句柄。
设置窗口标题
使用 SetWindowText
就可以了,第一个参数为窗口句柄,第二个参数为要修改的标题。
然后我们来搞搞扫雷。
1 2 3 4 5 6 7 8
| #include<windows.h> #include<iostream>
int main() { HWND hwnd=FindWindow(NULL, L"扫雷"); printf("%x\n", hwnd); SetWindowTextA(hwnd, "扫雷 by xia0ji233"); }
|
获取窗口标题
1 2 3 4 5 6 7 8 9 10 11
| #include<windows.h> #include<iostream>
int main() { HWND hwnd=FindWindow(NULL, L"扫雷"); printf("%x\n", hwnd); char *s =(char*) malloc(0x20); GetWindowTextA(hwnd, s, 0x100); printf("%s", s); }
|
不过这玩意有点鸡肋啊,因为我就是通过标题找的窗口句柄,然后我又去 GetWindowText
了,大概是因为有别的方法去获得窗口句柄吧,咱也不知道。
扫雷游戏分析
对数据的分析(时间暂停)
首先我觉得最容易能分析的应该就是时间暂停了吧,用 CE 去找到数据相对于基址的偏移。抓准时机,搜到时间,看来就是四字节存储的。
然后对这个数据去找出 是什么改写了这个地址,得到一个指令和指针:
这个数据都是直接通过基址 + 固定偏移能直接得到的。
分析完成之后我们去写一下这个注入器和 DLL,这些都是之前学过的知识点。
注入器编写
就直接照着下面这么写:寻找进程,匹配,拿到进程 pid,然后用远程线程注入把指定的 DLL 注入到目标进程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| #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"winmine.exe",11)) { printf("Find winmine.exe Process %d\n", pe32.th32ProcessID); return pe32.th32ProcessID; } ret=Process32Next(hSnap,&pe32); } return 0; } void InjectModule(DWORD ProcessId,const char* szPath) { HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId); printf("进程句柄:%p\n", hProcess); LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); SIZE_T dwWriteLength = 0; WriteProcessMemory(hProcess, lpAddress, szPath, strlen(szPath), &dwWriteLength); HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryA, lpAddress, NULL, NULL); WaitForSingleObject(hThread, -1); VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE); CloseHandle(hProcess); CloseHandle(hThread); }
int main() { DWORD ProcessId = FindProcess(); while (!ProcessId) { printf("未找到扫雷程序,等待两秒中再试\n"); Sleep(2000); ProcessId = FindProcess(); } printf("开始注入进程...\n"); InjectModule(ProcessId,"C:\\Users\\xia0ji233\\source\\repos\\Game2\\Debug\\Mine.dll"); printf("注入完毕\n"); }
|
DLL编写
这里直接用 MFC 的动态链接库,静态编译的模式。
需要注意窗口加载的模式,我们创建窗体之后,需要自主生成一个类对象,在资源窗体,新建一个 Dialog 然后右键窗体,添加类,取个名。
这里需要注意了,我们在加载窗体的时候需要创建一个窗体类对象用它的 DoModal
方法去显示,但是直接写在 InitInstance
中初始化会卡住原进程,所以可以把它用线程回调的方式加载,像下面这样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| DWORD WINAPI DlgThreadCallBack(LPVOID lp) { MineDlg* Dlg; Dlg = new MineDlg(); Dlg->DoModal(); delete Dlg; FreeLibraryAndExitThread(theApp.m_hInstance, 1); return 0; }
BOOL CMineApp::InitInstance() { CWinApp::InitInstance(); ::CreateThread(NULL, NULL, DlgThreadCallBack, NULL, NULL, NULL); return TRUE; }
|
这里我们找到了它控制时间增加的指令之后就可以把它 NOP 掉,写两个按钮,创建下面的事件实现时间暂停开关。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| DWORD GetBaseAddr() { HMODULE hMode = GetModuleHandle(nullptr); return (DWORD)hMode; }
void MineDlg::OnBnClickedButton1() { 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() { 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); }
|
测试一下,成功执行:
对数据的分析(点雷不爆炸,修改表情)
这个呢一开始是没有什么头绪的,但是突然想到,里面应该有一个状态标识了现在的游戏状态:未开始,开始了,结束了,结束可能会分赢或输。反正就游戏开始的时候扫一下未知的值,然后故意点雷扫变化的值,再开始再扫变化的值,如此反复得到了两个跟游戏状态有关的数据:
游戏开始的时候,数值为这样,其它时候不变,输了的话变成 16 2
。赢的时候下面的值变成了 3
。
所以下面那个值应该标记了你当前的游戏状态。
直接改成 3 发现并没有赢得游戏,只是上面那个小人变化了,是赢的戴墨镜的样子。
所以这个数值应该也就是给自己看看的。正真决定的还是第一个值,不过修改小人的这个功能可以做做。
这里直接用一个下拉框把四种状态表示齐全就好了。
1 2 3 4 5 6 7 8
| void MineDlg::OnBnClickedButton3() { DWORD FaceStatusOffset = 0x5160; DWORD GameStatusOffset = 0x5000; int index=FaceSel.GetCurSel(); *(int*)(FaceStatusOffset + GetBaseAddr()) = index; }
|
但是还有另一个值需要分析,这个值应该决定了我们
是否能操作当前界面,如果我们点了一个雷之后,那个值变为 16,我们无法操作界面,但是我们可以改回 1 让它变得可操作,然后就把空白的地方点完就可以直接赢了。
然后我们创造一个复活键
1 2 3 4 5 6
| void MineDlg::OnBnClickedButton4() { DWORD GameStatusOffset = 0x5000; *(int*)(GameStatusOffset + GetBaseAddr()) = 1; }
|
而且目测好像改成偶数就无法操作,奇数则可以操作。
如果想要真正实现点雷不爆炸的话,可以把赋值 16 的语句给 NOP 掉。
跟之前差不多的套路。
点雷不爆炸:
1 2 3 4 5 6 7 8 9 10 11 12
| void MineDlg::OnBnClickedButton5() { auto BaseAddr = GetBaseAddr(); DWORD BoombOffset = 0x34D6; DWORD InsLen = 10; DWORD old; VirtualProtect((void*)(BaseAddr + BoombOffset), InsLen, PAGE_EXECUTE_READWRITE, &old); BYTE INS[] = { 0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90 }; memcpy((void*)(BaseAddr + BoombOffset), INS, InsLen); VirtualProtect((void*)(BaseAddr + BoombOffset), InsLen, old, &old); }
|
取消不爆炸:
1 2 3 4 5 6 7 8 9 10 11 12
| void MineDlg::OnBnClickedButton6() { auto BaseAddr = GetBaseAddr(); DWORD BoombOffset = 0x34D6; DWORD InsLen = 10; DWORD old; VirtualProtect((void*)(BaseAddr + BoombOffset), InsLen, PAGE_EXECUTE_READWRITE, &old); BYTE INS[] = { 0xC7,0x05,0x00,0x50,0x00,0x01,0x10,0x00,0x00,0x00 }; memcpy((void*)(BaseAddr + BoombOffset), INS, InsLen); VirtualProtect((void*)(BaseAddr + BoombOffset), InsLen, old, &old); }
|
透视+秒过关
这一步应该需要动态调试了,思考游戏结束的时候会自动显示所有的雷,因此我们在刚刚的基础上,往上面去找,看看哪个函数调用之后会显示所有的雷,经过几次的动态调试之后发现:0x2F80函数是我们要找的结果。
这里 Qfrost 师傅点醒我了,如果我想直接调用这个函数,那么我直接创建一个线程,让线程回调去调用这个函数即可,而且这玩意还有意外惊喜,一点直接就过关了哈哈哈,目标直接就结束了。
再经过一些调试分析的手段发现了实际调用的函数是 0x347c
并且根据函数的参数决定是输还是赢,这里可以直接秒杀。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void MineDlg::OnBnClickedButton7() { DWORD ESPOffset = 0x347C;
DWORD FuncAddr = GetBaseAddr() + ESPOffset; struct { int a; } s = { 0 }; CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)FuncAddr, &s, NULL, NULL); }
|
点击直接秒杀,跟透视一块做了感觉哈哈哈