为 MS Solitaire 添加高分功能






4.70/5 (15投票s)
一个通过读取和写入 Solitaire 内存来管理 MS Solitaire 高分的应用程序。
目录
前言
这场对决来得太迟了。我们大概在 7、8 年前就见面了。他的名字叫 Score。Solitaire Score。他因为缺乏高分机制而嘲笑我。我试着用 Spy++ 对付他,但他只是笑了:“你这个菜鸟,”他说,“你到底怎么了?”
“我没时间跟你打,”我说,“我还有工作要做。”
“等你不再那么嫩的时候再回来。我们出去解决我们的事情,”他说。
从那时起,我们偶尔会见面。他总是冲我眨眼,“好吧,小子,我们都越来越老了……”然后有一天我决定够了;不是你死就是我亡。我擦干净了我的 WinDbg,把它放回了枪套。我希望我们不用赤手空拳来解决这件事。我们在中午碰面,就像约定的那样。我们只站了一会儿,然后就拔枪了。我朝他开了几枪断点,看看他会怎么做。他向右又向左移动,但最后一枪击中了他,他突然僵住了。我能在他数据段里看到恐惧。他试图向我扔出令人困惑的汇编命令,但我太专注于目标了。突然,它就在那里了。我能看到了。我几乎激动得 cried out。他隐藏的地址暴露无遗。
我太早欢呼了。他还有最后一招。在我们调试的时候,他把地址藏在他的左袖子里。一旦我们回到正常运行,他就会把它换到右边。我拆穿了他的谎言,他的地址掉在了地上。他知道他输了。他用他那愤怒的错误处理看着我,说:“你花了整整 2 天,你这个混蛋!”
引言
好的。这是一个不错的故事。而且是真实事件改编的。一旦我意识到我无法用 Spy 来“窃取”分数,我就感到好奇了。我也想知道为什么没有内置的 Solitaire 高分机制。我在这里 找不到答案。然而,人们仍然以最奇怪的方式保存他们的 Solitaire 分数,例如,在这里 。显然,一旦你“知道”当前的分数是多少,你就可以管理一个高分列表。我至少可以想到最后这个问题的两个答案。你可以在 结论 部分找到它们。
寻找分数
老实说,刚开始的时候,我确信这部分会是最耗时的。结果并非如此,但即便如此,当你开始做某件事时,你并不确定它何时会完成,需要多长时间。我开始用 WinDbg 运行 Solitaire。请注意这些行
xecutable search path is:
ModLoad: 01000000 01010000 sol.exe
ModLoad: 7c900000 7c9b0000 ntdll.dll
ModLoad: 7c800000 7c8f5000 C:\WINXP\system32\kernel32.dll
ModLoad: 77c10000 77c68000 C:\WINXP\system32\msvcrt.dll
ModLoad: 77dd0000 77e6b000 C:\WINXP\system32\ADVAPI32.dll
ModLoad: 77e70000 77f01000 C:\WINXP\system32\RPCRT4.dll
ModLoad: 77f10000 77f57000 C:\WINXP\system32\GDI32.dll
ModLoad: 7e410000 7e4a0000 C:\WINXP\system32\USER32.dll
ModLoad: 6fc10000 6fc6b000 C:\WINXP\system32\CARDS.dll
ModLoad: 7c9c0000 7d1d5000 C:\WINXP\system32\SHELL32.dll
ModLoad: 77f60000 77fd6000 C:\WINXP\system32\SHLWAPI.dll
ModLoad: 773d0000 774d3000 C:\WINXP\WinSxS\
x86_Microsoft.Windows.Common-Controls_
现在我们知道了我们进程的虚拟地址空间。我们可以安全地假设 DLL 的地址空间不包含保存分数的变量。因此,我们可以将我们的精力集中在地址 0x01000000 和地址 0x01010000 之间。
现在我们寻找一个分数正在改变的地方。好吧,当我们把 Solitaire 的选项设置为 Vegas-Cumulative 时,分数在每次发牌时都会改变。我们需要找到确切的代码行。这并不容易。我所做的是在沿途设置断点,希望在分数实际改变之前就能捕获到发牌操作。你可以一边盯着 Solitaire 一边观察分数的改变。
一旦我找到了那个断点,0x010019ac,我一直跟踪它,直到我看到分数在哪里改变。起初,我跳过了每个 call 命令,看看是哪个 call 改变了分数。然后,下一次我进入了那个 call。最后我到达了这里
Address OpCode/Params Decoded instruction
------------------------------------------------------------
010030a1 014830 add dword ptr [eax+30h],
ecx ds:0023:000bc2f0=ffffff30
这就是我们加上值的地方——在本例中是 -208——从地址 0x000bc2f0 加到 ecx 寄存器中的值,ecx 在前一行中从地址 0x007fc60 加载为 -52 (ffffffcc)。成功了!
分数正在移动
现在我们需要编写一个程序来访问该地址并获取分数。在我们 all chirpy 之前,我将为你节省麻烦,并告诉你分数的位置会根据启动进程的方式而变化。通过双击 EXE 启动 Solitaire 与双击 EXE 的快捷方式不同。同一个 EXE 的两个不同快捷方式——例如,不同的描述——将产生不同的分数地址。我们还可以猜测,尝试在不同的计算机、Windows 版本、Solitaire 版本等上查找地址将导致分数位于不同的地址。我们的猜测是正确的;我已经尝试过了。现在显而易见,我们需要扫描地址,但如何扫描,在哪里扫描?
最重要的事情是记住,我们可以知道分数是多少。我们希望它是一个唯一的值,这样就容易查找了。我们还需要知道在哪里查找。在哪里很容易。回到 WinDbg,当进程运行时,我们可以看到数据段从地址 0x000a0000 开始,并持续一段时间。我们可以看到一些包含数据的节,大量的字符串,但我找到的最高地址是 0x000bc2f0。出于预防起见,我们将在此范围内扫描:0x000a000 和 0x000bffff。
现在我们必须决定读取进程内存的方法。我不喜欢强迫决定。这会导致错误,错误会导致混乱,混乱会导致恐惧,恐惧会导致黑暗面。
我选择了 ReadProcessMemory
和 WriteProcessMemory
函数,因为它们提供了一种简单且易于实现的解决方案。我也选择它,因为我必须决定何时读取分数。我决定让用户来做。就像我说的,我不喜欢强迫决定。其他选项可能是挂钩或尝试将 DLL 加载到进程内存空间并运行一个线程来读取您想要的地址。以下是你为什么不应该使用 ReadProcessMemory
和 WriteProcessMemory
函数的原因:The old new thing。
我们还是要使用它们。这篇文章描述了一个安全问题,即两个进程没有相同的权限。如果我们有多用户混乱,我们愿意接受这意味着我们有问题。这是一段代码,它扫描数据段以查找值 -104,我发现这个值在数据段中非常独特且易于获得。我稍后会详细解释。
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
#pragma warning (disable:4312)
int _tmain(int argc, _TCHAR* argv[])
{
long pid;
HANDLE hProcess;
HWND hWnd = FindWindow(TEXT("Solitaire"), NULL);
if (hWnd != NULL)
{
GetWindowThreadProcessId(hWnd, (LPDWORD)&pid);
hProcess = OpenProcess(PROCESS_VM_OPERATION|
PROCESS_VM_READ|
PROCESS_VM_WRITE|
PROCESS_QUERY_INFORMATION|
PROCESS_TERMINATE, FALSE, pid);
if (hProcess != NULL)
{
//long lAddr = 0x000bc2f0;
long lPtr = 0x000a0000;
long lAddrEnd = 0x000Bffff;
long lVal = 0;
DWORD dwBytesTx = 0;
while (lPtr < lAddrEnd)
{
ReadProcessMemory(hProcess,
(void*)lPtr, &lVal, sizeof(long), &dwBytesTx);
if (-104 == lVal)
{
printf("at address %d found value -104\n", lPtr);
lVal = 1;
dwBytesTx = 0;
WriteProcessMemory(hProcess,
(void*)lPtr, &lVal, sizeof(long), &dwBytesTx);
// send message to the solitiare window to
// refresh the score view
SendMessage(hWnd, WM_SIZE, SIZE_MINIMIZED, 0);
ReadProcessMemory(hProcess,
(void*)lPtr, &lVal, sizeof(long), &dwBytesTx);
printf("at address %d the value is %d\n", lPtr, lVal);
break;
}
lPtr+=4;
}
CloseHandle(hProcess);
}
}
getchar();
return 0;
}
设计应用程序
现在我们知道了如何获取分数,我们需要决定新应用程序的目标
- 从应用程序内部以一致的方式启动 Solitaire,即每次都以相同的方式启动。
- 找到分数的地址并保存它。
- 管理高分列表。
- 读取分数。
- 加载一个分数,以便您可以从中断的地方继续游戏。
由于这是为了好玩,我添加了以下软件请求
- 该应用程序不需要主窗口,所以我们将它作为一个托盘图标应用程序运行。
- 我希望它是一个小型快速的应用程序。
- 我希望它不使用当前系统中没有的 DLL,例如各种 MFC。这意味着静态链接到 MFC。
- 我将尝试将模块分离成静态库,以便它们可以轻松地在其他项目中重用。天哪,我们为此要付出代价。
- 应用程序将数据保存在注册表中。
- 应用程序将加密保存数据。由于它在注册表中,我们不希望聪明的用户干涉我们的数据。
- 我喜欢 C# 风格的事件;它们非常易于使用。我认为我们需要使用事件来处理新高分等事情。
- 如果已经有代码在做我想要的事情并且我可以使用它,我就会用。
当我们将其分解为模块时,我们将需要以下内容
- 一个高分模块。添加、删除等等。
- 一个事件模块。
- 一个进程处理模块。我们需要通过名称找到 Solitaire,因为这是我们所知道的。我们需要读写内存。
- 带有加密模块的注册表。
- 一个 UI 模块。我们需要一个列表来显示高分。我们需要一个带有链接等的关于窗口。
- 我们需要一个在托盘中运行的应用程序,并且我们只需要它运行一次。
事件
对于事件模块,我使用了现有的代码。请参阅 参考文献 部分。此代码允许您定义 C# 风格的事件。事件总是很有用的。它们使您能够分离不属于一起的代码。我唯一遇到的问题是,我无法定义一个没有参数的事件。
高分
Highscore
模块由以下类组成
CAppSetting
:一个模板类,代表一个可以保存和加载的设置。我忍不住给一个类起了这个名字。App - setting。有趣……CHighscoreEntry
:一个由姓名、分数和时间组成的高分条目。CHighscoreManager
:管理高分的类。
进程
CProcessQuery
是执行所有与进程相关的操作的类,即所有读取、写入和按名称查找。我从这个文章的方法开始:如何通过名称获取任何正在运行进程的句柄。该方法从注册表中读取有关进程的信息。问题是,实际读取进程信息的代码行导致应用程序内存达到 20MB。
使用如此多内存的应用程序已不能算是小巧,因此我决定采用微软在这里推荐的 PSAPI 方法:枚举所有进程。可以认为 CProcessQuery
的函数都可以是静态的,因为它不保存特定进程的信息。我们称之为从 Win32 API 转向 OOP 的第一步。每次要求我们对 Solitaire 执行操作时,我们都会再次查找它。这样,我们就不介意 Solitaire 是否停止了,只要它通过我们的应用程序启动就行。
注册表
注册表代码到处都是。我使用了 CAESEncRegKey
注册表加密访问。有关此类的更多信息,请参阅 参考文献 部分。
用户界面和应用程序
由于我们(我)决定使用托盘应用程序,因此许多操作由应用程序类而不是隐藏窗口执行。这是我使用的代码
CWinAppEx
:单实例应用程序。我对另一个实例启动时发生的事情做了一些小的改动:引发了一个事件。处理该事件将在托盘图标上显示一个气球弹出窗口。哦,美妙的事件。CTrayNotifyIcon
(或NTray
):托盘图标的实现。CSortListCtrl
:组合了两个列表控件。一个是排序列表,另一个添加了文本颜色和图标。我开始编写一个日志记录器时很久以前就用过它。日志记录器从未完成,但视图很棒。CLabel
:用于分数窗口标题。CHyperLink
:用于关于窗口链接。
有关我使用的类的更多信息,请参阅 参考文献 部分。另一个值得一提的类是 CSettingsManager
类,它负责所有设置操作,如读取和写入。
构建应用程序
该应用程序是用 Microsoft Visual C++ 2005 编写和构建的。我不能保证(并且我非常怀疑)它能在任何其他版本上编译。不过,你永远不知道,直到你尝试。
构建应用程序的简单方法是下载解决方案存档和已编译的库。请参阅本文顶部出现的链接。将存档解压到相邻位置,即使用“全部解压”。打开解决方案并进行构建。如果需要,您可以下载并自己构建 Crypto++,包括 CPP 文件。您需要指定 Crypto++ 库输出文件的路径。因为我选择创建 LIB 文件并静态链接,所以遇到了一些链接问题。但是,在将 #include "Stdafx.h"
添加到 AESHelper.h 后,大多数(如果不是全部)链接问题都奇迹般地消失了。花了很长时间才找到。您应该可以轻松地在发行版或调试版中构建应用程序。
使用应用程序
Solitaire Highscore 附带一个帮助文件。您可以从本文顶部提供的链接下载带有帮助文件的应用程序发行版。我不会重复整个过程,因为我不想暴露我的指纹。简而言之,在首次运行时,SolitaireHighscore 会将 Solitaire 的设置更改为 Vegas-Cumulative。它会隐藏启动 Solitaire,向其发送一个 deal (F2) 消息,该消息会导致分数减少到 -104,并扫描 Solitaire 的内存以查找分数地址。
用户会收到扫描结果的通知。用户将不得不从 Solitaire Highscore 启动 Solitaire。此选项是默认设置,可以在 Solitaire Highscore 的设置中更改。Solitaire Highscore 的默认操作是检查高分。因此,双击托盘图标将检查 Solitaire 是否正在运行,如果是,则读取第一次运行时找到的地址。10 个高分列表和其余应用程序设置保存在注册表中。
预告:当高分窗口显示时——或其其他窗口,如消息框等——按下 CTRL+W,然后观察 Solitaire 行为的变化。
结论
所以,正如我承诺的那样,这里可能是 Solitaire 没有内置高分机制的一些原因
- Solitaire 更注重赢得当前这一局,而不是累积分数。
- 并非所有 Solitaire 选项都会增加或减少分数;您可以以时间为单位进行游戏。
- 我花了大约 10 天时间编写了这个小程序,不包括找到分数地址所需的 2 天以及其余的调试过程。我估计,当 Solitaire 被编写出来时,可能只需要 3-4 天。他们已经有了分数,注册表访问,而且他们不需要破解自己的进程。话虽如此,想象一下以下的对话:
MS 团队领导:嘿,伙计们,我们将在下周发布 3.11 版本。我希望一切都准备好了。
Solitaire 程序员:Solitaire 高分方面我还需要 3-4 天。
MS 团队领导:什么?! - 也可能是因为没有人关心。
另一个结论是:如果你需要查找一个你认为在数据段中的变量的地址,并且你可以保证变量的唯一值,那么最好编写一个程序来扫描整个 DS 一次,而不是调试汇编。
待办事项
以下是我预见永远不会实现的几个想法,除非我受到强烈的鼓励
- 记录最佳游戏时间。
- 创建一个网站来保存从 Solitaire Highscore 发送的高分。如果有人想接受这个挑战,我很乐意提供帮助。
- 将设置机制更改为通用机制。
- 让用户选择默认操作,即双击托盘图标时发生的操作。
免责声明
作为 Code Project 的一名守法公民,我在此声明:
我保留了我使用的代码的所有版权,我是在哪里找到的。我没有更改任何版权。在制作此应用程序的过程中,部分代码有所更改,但我保证它没有受到损害。您可以以任何您喜欢的方式使用此处提供的代码,只要它不是针对我的。如果您喜欢该应用程序或文章,那太好了。如果不喜欢,我并没有写其中的任何一部分;那是我一个不太熟的远房亲戚。他请我在这篇文章上签我的名字,因为我是成员,等等,我勉强同意了。此外,请不要声称此应用程序是您的创作;这不好。
参考文献
- [NTray] - 托盘图标实现。
- [Events] - 在标准 C++ 中模拟 C# 委托。
- [CAESEncRegKey] - AES 加密注册表类。
- [CWinAppEx] - 以 MFC 的方式限制应用程序只有一个实例。
- [HyperLink] - 超链接控件。
- [List Control 1, List Control 2] - 列表控件。
- [Crypto++] - Crypto++ 库是加密方案的免费 C++ 类库。
- [CLabel] - 一个属于工具集的标签控件。
历史
- 2007/07/24 - 首次发布。