纸牌游戏的高分:一种更高级的方法






4.88/5 (27投票s)
本文介绍了如何为纸牌游戏制作一个显示高分表的插件。通过Codecave,纸牌游戏将自动执行此插件的DLL——无需用户干预!
目录
1.0.0 简介
很久以前,我一直在寻找一种方法来为Windows纸牌游戏添加高分功能。我甚至在这个网站上偶然发现了一篇文章。然而,没有一篇具有我想要的功能。我想要的是一种能够与实际的纸牌游戏集成的东西。这样就不需要通过另一个进程运行纸牌游戏,也不需要任何需要用户操作的东西。经过几分钟的搜索,我意识到这是徒劳的——所以我决定自己动手。在本文中,我将详细介绍制作这样一个方便的插件的过程。需要对C++ Win32 API编程有扎实的了解。其中一个部分会涉及少量的x86汇编,但它足够简单,不需要任何真正的汇编知识就能理解。
1.0.1 所需工具
本文将使用TSearch和OllyDbg v1.10。
2.0.0 开始
也许,整个项目中最重要的就是获取分数。毕竟,如果无法获取高分,制作高分表有什么用呢?获取分数有几种方法,主要是长路和短路。长(且痛苦)的方法是其他纸牌高分程序的作者所做的那样。你可以选择逐段检查数以百计(数以千计?)的行,在有趣的部分设置断点,希望能找到那个难以捉摸的地址(那个可怜的家伙就是这么做的)。或者,你可以走捷径,找到静态指针并跟踪它到内存中的分数(这需要一两分钟)。那么,走捷径听起来更好?
2.0.1 获取当前分数
这种方法就是TSearch发挥作用的地方。要开始,我们需要打开TSearch,点击“打开进程”按钮(左上角),然后找到我们的纸牌进程(最有可能的是SOL.EXE)。我们要做的就是在内存中搜索我们的分数。现在TSearch已经聚焦于纸牌,我们可以开始了。我们注意到纸牌的起始分数是零。对于内存搜索来说,零不是一个好数字。为了节省时间,我们需要将分数提高到其他值。回到纸牌窗口,我只是点击了一两张A(Ace),露出了几张牌,得到了30分。我回到TSearch,搜索这个值(作为四字节类型),得到了74个结果。74仍然很多,那么接下来怎么办呢?我们可以等几秒钟,因为纸牌每十秒会将总分数减少两分。一旦分数再次降低,你就可以搜索一个新值,点击“搜索下一个”按钮(在“搜索”旁边,一个带...的放大镜)。这将搜索这74个地址以找到我们的新值。我的分数恰好降到了24,所以我搜索了24。最终我得到一个地址。
注意:如果你按照这个方法操作,但仍然得到多个地址,那么就找出哪个地址随着纸牌分数的改变而改变。
我们看到分数存储在地址0xAA268(你的可能不同)。如果我们这样做的话
ReadProcessMemory(hProcess, (LPVOID)0xAA268, &score, sizeof(int), 0);
分数将是24。但是,让我们重新开始游戏,看看这是否对所有实例都有效。重新启动纸牌并再次执行该行——分数是……0?如果我们重复TSearch过程,我们会发现分数现在位于一个不同的内存地址。
2.0.2 纸牌有DMA?真的吗?纸牌?
嗯,结果发现纸牌使用了动态内存分配(DMA)。这样,分数的值每次都会位于不同的内存地址。那么,我们接下来该怎么办?我们需要找到一个称为静态指针的东西,我们将看到,它将分数存储在一个寄存器中。幸运的是,TSearch使这个过程变得容易。我们通过单击“AutoHack”菜单项,然后选择“启用调试器”来开始。完成此操作后,右键单击地址窗口中地址前的空白区域会弹出一个上下文菜单——从中选择“AutoHack”。
现在我们已经将调试器附加到了纸牌,并且可以通过单击“AutoHack”菜单项,然后选择“AutoHack窗口”来查看正在发生的事情。一旦我们的分数发生变化,我们应该会看到类似这样的内容
再一次,我们很幸运,因为发生的事情真的很简单。唯一发生的是,EAX
被移动到了[ESI+0x30]
。我们可以假设EAX
保存着我们的分数,并且它被移动到了[ESI+0x30]
,其中ESI+0x30
是我们的地址(在我的例子中是0xAA268)。知道了这一点,我们就可以开始追踪静态指针。如果我们找到了ESI
的值,我们就可以看到它指向的地址,加上0x30,然后读取该新地址的值。但是,如何找到ESI
的地址呢?我们需要看看是什么指向它。所以,让我们从0xAA268(ESI+30
)地址减去0x30来得到ESI
。0xAA268 - 0x30 = 0xAA238。转换为十进制是696888。现在,我们知道了ESI
,所以我们来搜索它。
我得到了四个结果。在内存中,搜索那个不寻常的通常是最好的选择。你可以尝试所有四个,或者听我的建议,相信0x1007170是ESI
所在的位置。好了,这就是我们的静态指针。现在,要随时获取分数的值,我们执行我之前提到的操作。我们读取0x01007170处的地址,加上0x30,然后读取该地址的值。基本上,就像这样
ReadProcessMemory(hProcess, (LPCVOID)(0x01007170), &val, sizeof(int), 0);
//val would hold 696888 (0xAA238)
val += 0x30;
//Now it holds (0xAA268)
ReadProcessMemory(hProcess, (LPVOID)val, &score, sizeof(int), 0);
//Read value at 0xAA268 (our score)
就这样。通过读取0x01007170处的静态指针并对其进行操作,我们可以随时获取纸牌分数。无需逐步调试,无需扫描内存——我们直接访问分数。
3.0.0 制作插件
现在我们已经获得了分数,我们可以着手制作高分插件了。对此有什么方法呢?我所做的是制作一个DLL(它将被纸牌自动加载),该DLL继承纸牌窗口以添加一个高分菜单。我还制作了一个对话框,在游戏开始前弹出,询问玩家的名字(类似MS Hearts风格,稍后详细介绍)。
3.0.1 继承纸牌窗口
我为此项目采取的方法是继承纸牌窗口。
HWND hWnd = FindWindow("Solitaire", "Solitaire");
HMENU hMenu = GetMenu(hWnd);
HMENU hNewMenu = CreateMenu();
AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT_PTR)hNewMenu, "High &scores");
AppendMenu(hNewMenu, MF_STRING, 1234, "&Show high scores");
AppendMenu(hNewMenu, MF_STRING, 1235, "&Add current score");
AppendMenu(hNewMenu, MF_STRING, 1236, "&Clear high scores");
DrawMenuBar(hWnd);
SolitaireOrigProc = SetWindowLongPtr(hWnd, GWL_WNDPROC,
(LONG_PTR)SolitaireNewProc);
我找到了纸牌窗口,获取了菜单句柄,然后添加了我自己的项。SolitaireNewProc
负责处理我希望处理的所有重要消息。在这种情况下,它是1234/1235/1236,对应于菜单选项的ID。
从SolitaireNewProc
case WM_CLOSE:
case WM_QUIT:
if(saveScore == TRUE)
SaveHighScore(SAVE);
break;
case WM_COMMAND:
switch(LOWORD(wParam))
{
case 1234:
DialogBoxParam(g_hDLL, MAKEINTRESOURCE(IDD_DLGSCORE),
NULL, HighScoreProc, NULL);
break;
case 1235:
SaveHighScore(ADD);
break;
case 1236:
DeleteFile("SOLScores.txt");
break;
}
对于WM_QUIT
/WM_CLOSE
,我将其设置为在退出时保存高分,以防有人意外关闭游戏(他们的老板在附近)。事件1234,对应于显示高分,会弹出负责显示分数的对话框。事件1235,允许用户动态地将分数添加到高分表中,调用一个执行此操作的函数。事件1236删除高分文件,从而清除高分表。
3.0.2 处理高分:一个类
处理存储/检索/显示高分是这个程序的大部分工作。因此,为了处理所有这些相关操作,我决定编写一个类。
功能视图
关系视图
这个类通过读写高分文件(SOLScores.txt)来工作。下面是函数的简要概述,因为有些将在后面的代码片段中调用
void LoadHighScores(void);
//Loads the fileContents vector from the high score file
void AddHighScore(char* name, int score);
//Appends a high score to the file
void ParseParts(void);
//Parses the individual name/score/date components of fileContents
void SortScores(void);
//Selection sorts the score to display from highest to lowest
void MakeProperSpacing(void);
//Further performs operations on the name string
//so it is displayed correctly in the high score table
int getLowestHighScore(void);
//Returns the lowest score of the top 10
string getFullTable(void);
//Returns the top 10 in a string so they can be displayed
//by the edit control
高分文件的格式如下
name|x|x|score|x|x|date
|x|x| 用作分隔符,因此解析文件和查找重要位会更容易一些。我想概述一下即将出现的处理重要对话框的代码片段的函数。如果你对函数内部的代码感兴趣,那么所有内容的源代码都已附带——并带有注释。
3.0.3 - 欢迎对话框
我们需要一种方法来获取玩家的名字。我认为一个询问玩家名字的欢迎对话框(类似MS Hearts的那个)是最好的方法。为了玩游戏,玩家必须输入他们的名字,或者使用“默认”名字。稍后,当我们让DLL自动加载时,这个对话框将在游戏加载之前弹出。消息处理相当简单,最重要的一部分是获取玩家的名字。这是通过GetDlgItemText(hDlg, IDC_NAME, playerName, 32);
实现的。32个字符的最大限制是在WM_INITIDIALOG
消息中通过SendDlgItemMessage(hDlg, IDC_NAME, EM_SETLIMITTEXT, 32, NULL);
设置的。
3.0.4 高分对话框
最后,我们一直期待的主要对话框
case WM_INITDIALOG:
{
FileHandler* DisplayScores = new FileHandler();
DisplayScores->LoadHighScores();
string Table = DisplayScores->getFullTable();
SetDlgItemText(hDlg, IDC_SCORETABLE, Table.c_str());
SendDlgItemMessage(hDlg, IDC_CHKSAVE, BM_SETCHECK, BST_CHECKED, NULL);
delete DisplayScores;
}
加载后,它会获取高分文件中所有重要信息,并将其显示在编辑控件中。它还(默认情况下)选中“退出时保存分数”选项。FileHandler
类完成了文件读取和解析的所有繁重工作,所以剩下要做的就是显示它。
4.0.0 让我们的DLL自动加载
这个插件几乎完成了。但是,如果每次启动游戏都要加载它,那它真的算不上插件,对吧?所以,需要做的是让它在纸牌启动时自动加载。但是,怎么做呢?我选择的方法是硬编码一个CodeCave。
4.0.1 Codecave方法
这就是OllyDbg发挥作用的地方。打开OllyDbg,从菜单中选择“文件”,然后选择“打开”(或按F3)。找到纸牌(在WinXP上是C:\WINDOWS\system32\SOL.EXE)并打开它。模块加载后,按Ctrl+A让OllyDbg清理分析中的一些内容。我们要做的就是修改这段汇编代码来加载我们的DLL。
4.0.2 寻找位置
在我们做任何事情之前,我们需要在程序中硬编码我们的DLL名称。为此,我们需要一个有很多零的地方,因为我们也将把CodeCave指令放在附近。如果你滚动到模块底部,大约在0x01006D2D到0x01006FFF之间,有一个空隙。这是CodeCave的理想位置。选择空隙中的一条指令,然后向下滚动15条或更多指令(高亮区域将是灰色的)。我选择了从01006D32到0x01006D3E开始的值。一旦这些被高亮显示,我们就可以开始硬编码我们的DLL名称了。按Ctrl+E,我们会看到一个要求输入ASCII/UNICODE/Hex的对话框。在ASCII字段中,我们将写入DLL名称,在我这里是DialogDLL.dll。
完成后,我们可以点击“确定”。如果你真的在跟着操作,你会注意到,而不是文本,OllyDbg会产生一些不寻常的指令。要修复这个问题,选择该区域,然后按Ctrl+A重新分析。完成此操作后,多条指令应该会合并为一行,其中包含DLL名称。
01006D32 . 44 69 61 6C 6F>ASCII "DialogDLL.dll",0
字符串现在位于0x01006D32,这个位置稍后将用于调用LoadLibraryA
。现在字符串已经硬编码了,我们需要制作实际的CodeCave。CodeCave通常所做的就是劫持程序的流程,在返回正常之前执行额外的指令。我们可以使用汇编JMP
助记符来做到这一点。但首先,我们需要一个跳转的起点。我们需要一段代码,在游戏加载时至少执行一次,最好在卡牌和其他东西显示之前。这段代码对我来说看起来很有趣
01001468 /$ 837C24 04 00 CMP DWORD PTR SS:[ESP+4],0
0100146D |. 74 1B JE SHORT SOL.0100148A
0100146F |. 6A 00 PUSH 0 ; /timer = NULL
01001471 |. FF15 FC110001 CALL DWORD PTR DS:[<&msvcrt.time>] ; \time
01001477 |. 25 FF7F0000 AND EAX,7FFF
0100147C |. 50 PUSH EAX ; /seed
0100147D |. A3 44730001 MOV DWORD PTR DS:[1007344],EAX ; |
01001482 |. FF15 00120001 CALL DWORD PTR DS:[<&msvcrt.srand>] ; \srand
01001488 |. 59 POP ECX
01001489 |. 59 POP ECX
0100148A |> A1 70710001 MOV EAX,DWORD PTR DS:[1007170]
0100148F |. 6A 00 PUSH 0
01001491 |. FF7424 0C PUSH DWORD PTR SS:[ESP+C]
01001495 |. 6A 08 PUSH 8
为什么?因为srand
肯定会在程序开始时至少被调用一次(可能只是一次)。我们的JMP
指令需要5个字节,所以看起来有些东西会被覆盖。让我们从选择以下行开始
0100147C |. 50 PUSH EAX ; /seed
在OllyDbg中。这将是CodeCave的起点。我建议高亮显示0x01001376前后大约5条指令,并将结果复制/粘贴到记事本中。按Ctrl+Space会弹出一个菜单,可以在该地址汇编指令。让我们在文本附近创建一个(暂时的)空区域。我选择了01006D42。
4.0.3 编写CodeCave
你会注意到,一旦你这样做,就会有一个NOP
(无操作)指令在跳转之后。这是因为为了满足CodeCave的5字节要求,我们不得不覆盖另一个指令。现在,我们转到0x01006D42。高亮显示新的JMP
行,然后按Enter,或者按Ctrl+G,然后输入0x01006D42。OllyDbg现在应该指向空白区域。我们的CodeCave将执行以下操作:
- 保存寄存器(将它们压栈)
- 将我们的字符串加载到
EAX
寄存器 - 将
EAX
寄存器压栈作为LoadLibraryA
的参数 - 调用
LoadLibraryA
- 从栈中弹出
- 执行被覆盖的指令
- 跳回原始函数
将其转换为汇编代码
01006D42 > 60 PUSHAD
01006D43 . B8 336D0001 MOV EAX,SOL.01006D32 ; ASCII "DialogDLL.dll"
01006D48 . 50 PUSH EAX ; /FileName => "DialogDLL.dll"
01006D49 . E8 2DB07F7B CALL kernel32.LoadLibraryA ; \LoadLibraryA
01006D4E . 61 POPAD
01006D4F . 50 PUSH EAX
01006D50 . A3 44730001 MOV DWORD PTR DS:[1007344],EAX
01006D55 .^E9 28A7FFFF JMP SOL.01001482
寄存器已保存,我们执行我们想要的操作,然后恢复寄存器并执行被覆盖的指令。就好像什么都没发生过一样。要保存更改,请选择CodeCave中的两条或更多行,然后右键单击。当上下文菜单弹出要求保存所有内容时,选择“复制到可执行文件”,然后选择“所有修改”。
应该会弹出另一个菜单,有四个选项,“复制全部”是我们想要的,因为除了底部高亮显示的部分之外,还有更多修改过的代码。OllyDbg现在应该会显示新程序的内存转储。要进行更改并保存新文件,请关闭此窗口,您将看到另一个消息框。
选择“是”最终完成了保存更改的任务。现在,如果一切都正确完成,并且编译好的DLL与修改后的纸牌游戏在同一个目录中,那么游戏加载时应该会弹出欢迎对话框。就这样。通过继承和CodeCave,我们现在拥有了一个支持高分的纸牌客户端。无需做任何额外的工作,只需像往常一样运行纸牌。我个人建议更改开始菜单中的纸牌快捷方式,使其指向其他位置,而不是替换system32中的SOL.EXE。Windows使用文件保护,如果没有麻烦禁用它,它会将其简单地替换回原始副本。
5.0.0 其他
附带的源代码包含了编译与纸牌交互的DLL所需的一切。在可执行的ZIP文件中,我包含了一个补丁,它将一个普通的纸牌游戏打造成一个支持高分的版本。这只是为了方便分发,因为否则就必须发送整个SOL.EXE文件。此外,版权问题或其他类似问题可能会阻止我上传我修改后的纸牌游戏。
历史
- 2008.09.28 - 文章提交。