游戏安全的学习(2)

游戏安全的学习(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);
//SetWindowTextA(hwnd, "扫雷 by xia0ji233");
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;
}
// CMineApp 初始化
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);
//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);
}

测试一下,成功执行:

对数据的分析(点雷不爆炸,修改表情)

这个呢一开始是没有什么头绪的,但是突然想到,里面应该有一个状态标识了现在的游戏状态:未开始,开始了,结束了,结束可能会分赢或输。反正就游戏开始的时候扫一下未知的值,然后故意点雷扫变化的值,再开始再扫变化的值,如此反复得到了两个跟游戏状态有关的数据:

游戏开始的时候,数值为这样,其它时候不变,输了的话变成 16 2。赢的时候下面的值变成了 3

所以下面那个值应该标记了你当前的游戏状态。

直接改成 3 发现并没有赢得游戏,只是上面那个小人变化了,是赢的戴墨镜的样子。

所以这个数值应该也就是给自己看看的。正真决定的还是第一个值,不过修改小人的这个功能可以做做。

这里直接用一个下拉框把四种状态表示齐全就好了。

1
2
3
4
5
6
7
8
void MineDlg::OnBnClickedButton3()
{
// TODO: 在此添加控件通知处理程序代码
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()
{
// TODO: 在此添加控件通知处理程序代码
DWORD GameStatusOffset = 0x5000;
*(int*)(GameStatusOffset + GetBaseAddr()) = 1;
}

而且目测好像改成偶数就无法操作,奇数则可以操作。

如果想要真正实现点雷不爆炸的话,可以把赋值 16 的语句给 NOP 掉。

跟之前差不多的套路。

点雷不爆炸:

1
2
3
4
5
6
7
8
9
10
11
12
void MineDlg::OnBnClickedButton5()
{
// TODO: 在此添加控件通知处理程序代码
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()
{
// TODO: 在此添加控件通知处理程序代码
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()
{
// TODO: 在此添加控件通知处理程序代码
//DWORD ESPOffset = 0x3497;
//DWORD ESPOffset = 0x2913;
//DWORD ESPOffset = 0x26A7;
//DWORD ESPOffset = 0x272E;
//DWORD ESPOffset = 0x34AB;
//DWORD ESPOffset = 0x2f80;
DWORD ESPOffset = 0x347C;

DWORD FuncAddr = GetBaseAddr() + ESPOffset;
struct { int a; } s = { 0 };
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)FuncAddr, &s, NULL, NULL);
}

点击直接秒杀,跟透视一块做了感觉哈哈哈

文章目录
  1. 1. 句柄
  2. 2. 获取窗口句柄
    1. 2.1. 设置窗口标题
    2. 2.2. 获取窗口标题
  3. 3. 扫雷游戏分析
    1. 3.1. 对数据的分析(时间暂停)
    2. 3.2. 注入器编写
    3. 3.3. DLL编写
    4. 3.4. 对数据的分析(点雷不爆炸,修改表情)
    5. 3.5. 透视+秒过关
|