65.9K
CodeProject 正在变化。 阅读更多。
Home

为 MS Solitaire 添加高分功能

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.70/5 (15投票s)

2007 年 7 月 25 日

CPOL

14分钟阅读

viewsIcon

45356

downloadIcon

769

一个通过读取和写入 Solitaire 内存来管理 MS Solitaire 高分的应用程序。

Screenshot - solitairehighscore.gif

目录

前言

这场对决来得太迟了。我们大概在 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。

现在我们必须决定读取进程内存的方法。我不喜欢强迫决定。这会导致错误,错误会导致混乱,混乱会导致恐惧,恐惧会导致黑暗面。

我选择了 ReadProcessMemoryWriteProcessMemory 函数,因为它们提供了一种简单且易于实现的解决方案。我也选择它,因为我必须决定何时读取分数。我决定让用户来做。就像我说的,我不喜欢强迫决定。其他选项可能是挂钩或尝试将 DLL 加载到进程内存空间并运行一个线程来读取您想要的地址。以下是你为什么不应该使用 ReadProcessMemoryWriteProcessMemory 函数的原因: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;
}

设计应用程序

现在我们知道了如何获取分数,我们需要决定新应用程序的目标

  1. 从应用程序内部以一致的方式启动 Solitaire,即每次都以相同的方式启动。
  2. 找到分数的地址并保存它。
  3. 管理高分列表。
  4. 读取分数。
  5. 加载一个分数,以便您可以从中断的地方继续游戏。

由于这是为了好玩,我添加了以下软件请求

  1. 该应用程序不需要主窗口,所以我们将它作为一个托盘图标应用程序运行。
  2. 我希望它是一个小型快速的应用程序。
  3. 我希望它不使用当前系统中没有的 DLL,例如各种 MFC。这意味着静态链接到 MFC。
  4. 我将尝试将模块分离成静态库,以便它们可以轻松地在其他项目中重用。天哪,我们为此要付出代价。
  5. 应用程序将数据保存在注册表中。
  6. 应用程序将加密保存数据。由于它在注册表中,我们不希望聪明的用户干涉我们的数据。
  7. 我喜欢 C# 风格的事件;它们非常易于使用。我认为我们需要使用事件来处理新高分等事情。
  8. 如果已经有代码在做我想要的事情并且我可以使用它,我就会用。

当我们将其分解为模块时,我们将需要以下内容

  1. 一个高分模块。添加、删除等等。
  2. 一个事件模块。
  3. 一个进程处理模块。我们需要通过名称找到 Solitaire,因为这是我们所知道的。我们需要读写内存。
  4. 带有加密模块的注册表。
  5. 一个 UI 模块。我们需要一个列表来显示高分。我们需要一个带有链接等的关于窗口。
  6. 我们需要一个在托盘中运行的应用程序,并且我们只需要它运行一次。

事件

对于事件模块,我使用了现有的代码。请参阅 参考文献 部分。此代码允许您定义 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 的一名守法公民,我在此声明:

我保留了我使用的代码的所有版权,我是在哪里找到的。我没有更改任何版权。在制作此应用程序的过程中,部分代码有所更改,但我保证它没有受到损害。您可以以任何您喜欢的方式使用此处提供的代码,只要它不是针对我的。如果您喜欢该应用程序或文章,那太好了。如果不喜欢,我并没有写其中的任何一部分;那是我一个不太熟的远房亲戚。他请我在这篇文章上签我的名字,因为我是成员,等等,我勉强同意了。此外,请不要声称此应用程序是您的创作;这不好。

参考文献

历史

  • 2007/07/24 - 首次发布。
© . All rights reserved.