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

FreeCell & Hearts,幕后揭秘

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (121投票s)

2003年1月29日

Ms-PL

14分钟阅读

viewsIcon

283718

downloadIcon

5066

本文介绍了 Freecell 和 Hearts 游戏的内部机制,其中使用了读取和写入另一个进程内存的库。

Sample Image - freecellreader.jpg

Sample Image - freecellreader1.jpg

引言

是的,又来了。对于喜欢我第一篇文章《扫雷,幕后揭秘》的读者,我在这里呈现它的自然续集。这个想法源于我的一位朋友读了之前的文章后和我开玩笑说,“现在做个 Hearts 吧……”,所以我做了,这篇文章就是最终结果。在此,我想感谢一些帮助过我的朋友,Itay Langer、Michael Kuperstein 和 Yoav Sion(按字母顺序排列)。

两周前,当我写第一篇文章时,我解释了如何使用 C# 中的 API 函数(通过 P/Invoke)来读取另一个进程的内存。我还写了一个示例,说明如何使用这个类库来读取扫雷游戏的雷区地图。在那篇文章中,我曾声称(现在仍然坚持)文章的目标不是如何破解扫雷。文章的目标是为读者提供一个简单的 C# 类库,让他们能够读取另一个进程的内存。令我惊讶的是,许多读过这篇文章的人对我的示例非常感兴趣。因此,我不能声称这篇文章的目标是学习如何使用一些新的 API 函数来实现 C# 不支持的功能。这篇文章的目标是看看微软的这两款纸牌游戏:Freecell 和 Hearts 的幕后发生了什么。尽管如此,我的良心不允许我发布一篇没有新增编程特性的文章,所以我增强了这个库,现在它也支持写入另一个进程的内存。我在其中一个示例中甚至使用到了这个函数。

主要目标

  1. FreeCell 幕后花絮以及开发一个利用这些知识的酷炫应用程序。
  2. Hearts 幕后花絮以及开发一个利用这些知识的非常酷炫的应用程序。
  3. 使用 WriteProcessMemory API 函数写入另一个进程的内存。

注意:前两部分使用了调试器来调查游戏,我个人最喜欢的调试器是 Olly Debugger v1.08,但任何调试器都可以。

第一部分:FreeCell,它是如何工作的?

我猜你想知道 FreeCell 有什么好破解的?你已经看到了所有的牌!问题在于理解它做了什么以及如何得到这些牌。另外,假设你能读取牌,你就可以编写一个能自己玩 Freecell 的程序……但你必须有一种方法来读取 Freecell 的内存。

第一步,和往常一样,是在调试器中打开文件。在这种情况下,我们需要 freecell.exe 文件,它位于 windows\system32 文件夹中。打开文件后,我们需要找到一个有趣的起始点。当你查看文件开头时,你会看到一个从 dll 导入的函数列表,如下所示:

01001188 DD USER32.GetDlgItemInt
0100118C DD USER32.SetWindowTextW
01001190 DD USER32.wsprintfW
01001194 DD USER32.GetSysColor
01001198 DD USER32.GetWindowDC
0100119C DD USER32.IsIconic
010011A0 DD 00000000
010011A4 DD msvcrt._except_handler3
010011A8 DD msvcrt._controlfp
010011AC DD msvcrt.__set_app_type
010011B0 DD msvcrt.__p__fmode
010011B4 DD msvcrt.__p__commode
010011B8 DD OFFSET msvcrt._adjust_fdiv
010011BC DD msvcrt.__setusermatherr
010011C0 DD msvcrt._initterm
010011C4 DD msvcrt.__getmainargs
010011C8 DD OFFSET msvcrt._acmdln
010011CC DD msvcrt.exit 
010011D0 DD msvcrt._cexit 
010011D4 DD msvcrt._XcptFilter
010011D8 DD msvcrt._exit
010011DC DD msvcrt._c_exit
010011E0 DD msvcrt.isdigit
010011E4 DD msvcrt.time
010011E8 DD msvcrt.srand
010011EC DD msvcrt.rand

这只是列表的最后一部分,但如果你仔细看,你会发现最后列出的导入函数是来自 msvcrt dll 的 rand 函数,当然,这是微软 Visual C++ 运行时 dll 的随机化函数。所以我决定查找这个函数的使用位置。

我找到了两个使用这个函数的地方。然后我在两个地方都设置了断点并运行了程序。开始新游戏后,程序在第一个断点处停了一次,在第二个断点处停了 52 次。这时就需要动脑筋了,我的分析是,第一个断点用于随机化游戏编号,第二个断点用于随机化每张牌的位置(或者每个位置的牌)(记住,你有 52 张牌……)。

于是,我做了一个小测试来证明我的理论。我运行程序直到第一个断点,然后单步执行 rand 函数,并查看 eax 寄存器的值,该寄存器的值为 0x00006A26,然后我继续运行程序,窗口标题栏上显示的游戏编号是 25254…… bingo,0x6A26 = 25254。现在我知道我是对的,我需要找到 Freecell 存储这个值和牌值的位置,这样我以后就可以用我的程序来读取它们了。我开始新游戏,现在我调查第二个断点。在断点前几条指令,我发现了一段非常有趣的代码。我找到的代码将一个值存储在内存中的特定位置,然后格式化字符串“FreeCell Game #%d”以及这个数字,所以这就是它存储游戏编号的地方……代码如下所示:

010031D0  MOV DWORD PTR DS:[100834C],EAX
010031D5  PUSH ESI
010031D6  PUSH EDI
010031D7  PUSH 80                                  ; /Count = 80 (128.)
010031DC  MOV ESI,freecell.01007880                ; |
010031E1  PUSH ESI            ; |Buffer => freecell.01007880
010031E2  PUSH 12F            ; |RsrcID = STRING "FreeCell Game #%d"
010031E7  PUSH DWORD PTR DS:[1007860]              ; |hInst = 01000000
010031ED  CALL DWORD PTR DS:[<;&USER32.LoadStringW>>; \LoadStringW
010031F3  PUSH DWORD PTR DS:[100834C]
010031F9  PUSH ESI                                 ; |Format => ""
010031FA  MOV ESI,freecell.01007820   ; |UNICODE "Cards Left: %u"
010031FF  PUSH ESI                                 ; |s => freecell.01007820
01003200  CALL DWORD PTR DS:[<;&USER32.wsprintfW>]  ; \wsprintfW
01003206  ADD ESP,0C
01003209  PUSH ESI                                 ; /Text
0100320A  PUSH DWORD PTR SS:[EBP+78]               ; |hWnd
0100320D  CALL DWORD PTR DS:[<;&USER32.SetWindowTex>; \SetWindowTextW

正如你所见,第一行是将 eax 寄存器的值(这是第一个断点的随机化数字)存储在内存地址 0x0100834C。顺便说一句,从代码本身无法得知 eax 的值是多少,但在运行时使用调试器时,你可以看到这与第一个 rand 函数得到的值是相同的。

回到第二个断点。正如我前面所说,第二个断点被触发了 52 次,这正好是我们拥有的牌的数量。现在我感兴趣的是这段代码在哪里存储随机化的牌。让我们看看相关代码:

010032D3  PUSH EAX                                 ; seed
010032D4  CALL DWORD PTR DS:[<;&msvcrt.srand>]      ; [srand]
010032DA  POP ECX                                  ;  ebx = 52 (from earlier)
010032DB  XOR ESI,ESI                              ;  esi = 0
010032DD  /CALL DWORD PTR DS:[<;&msvcrt.rand>]      ; [rand]: eax = randNumber
010032E3  |XOR EDX,EDX                             ;  edx = 0
010032E5  |DIV EBX                                 ;  edx = randNumber % ebx
010032E7  |MOV ECX,ESI                             ;  ecx = iteration #
010032E9  |AND ECX,7                               ;  ecx = ecx AND 7
010032EC  |IMUL ECX,ECX,15                         ;  ecx = ecx * 21
010032EF  |DEC EBX                                 ;  decrease iteration #
010032F0  |LEA EAX,DWORD PTR SS:[EBP+EDX*4-60]
010032F4  |MOV EDX,ESI                             ;  edx = esi
010032F6  |SHR EDX,3                               ;  edx = edx / 8
010032F9  |ADD ECX,EDX                             ;  ecx = ecx + edx
010032FB  |MOV EDX,DWORD PTR DS:[EAX]              ;  load randCardNum
010032FD  |MOV DWORD PTR DS:[ECX*4+1007554],EDX    ;  save in memory
01003304  |MOV ECX,DWORD PTR SS:[EBP+EBX*4-60]
01003308  |INC ESI                                 ;  esi = esi +1
01003309  |CMP ESI,34                              ;  check loop iterations
0100330C  |MOV DWORD PTR DS:[EAX],ECX
0100330E  \JB SHORT freecell.010032DD              ;  go to start of the loop

所以,在这段代码中,我们调用了 srand 函数,以之前随机化的游戏编号作为种子。这一点很重要。Freecell 有游戏编号,你可以选择一个特定的游戏,那么为什么游戏是随机化的,但如果你反复选择相同的游戏编号,你会得到相同的牌呢?答案在于代码的前两行,“随机化”的牌是以游戏编号作为种子数来随机化的。如果 rand 函数在 srand 函数中用相同的种子初始化,它生成的随机数将是相同的。

第 5 行是一个循环的开始,这个循环会迭代 52 次,每次迭代都随机化一张牌的编号,并将其放在内存的下一个位置。行 010032FD 是将随机化后的牌号保存在内存中的地方。在用调试器跟踪这些地址几次后,我可以告诉你,有 8 个牌的数组,第一次迭代将牌号作为第一个数组的第一个元素,接下来的迭代将牌号放在第二个数组的第一个元素,依此类推。当到达第 9 张牌时,它会重新开始,将牌号放在第一个数组的第二个元素……下面的图应该能更好地帮助你理解:

那么这 8 个数组有什么用呢?嗯,我们得到 8 个数组的原因是因为 Freecell 有 8 列牌。所以每个数组就是一列牌。再次,在跟踪循环几次后,我发现存储牌的地址的计算公式是:CardAddress = BaseAddress + 0x54*I + 4*J,其中 BaseAddress 是 0x1007554(在行 010032FD 中提到),I 是列号(0..7),J 是行号。

现在,我们得到了地址,就可以继续创建 Freecell Memory Reader 应用程序了。窗体包含 8 个列表框控件,每个列表框对应一列。我们首先要做的是检查我们使用的是哪个操作系统,我检查的 Freecell 在 XP 上,我也搜索了 Win2K 的地址,但无法测试程序,所以如果它在 Win2K 上不起作用,请留下评论,我会修复它。无论如何,我检查操作系统,并根据它设置稍后要使用的地址。然后我使用静态方法 Process.GetProcessesByName() 搜索 Freecell 进程,然后打开我的 ProcessMemoryReader 类的实例。这个类在我之前的文章中已经介绍过,请查看它以获得关于该类使用方法的详细解释。打开进程后,我从游戏编号地址读取游戏编号。然后我遍历牌的数组,对于数组中的每个位置,我读取其内存,将牌号转换为普通牌号,并将其放置在适当的列表视图中(根据其列)。

“转换牌号”?好吧,这里我需要解释牌是如何存储在内存中的。之前随机化的数字是 1 到 52 之间的数字,所以我需要将其转换为牌的编号(1 到 13)和牌的花色(1 到 4)。起初,我以为转换很简单,我认为它是这样组织的:

花色 | 编号 | 原始编号

1  - 1 - 1
1  - 2 - 2
1  - 3 - 3
1  - 4 - 4
...
2 - 1 - 14
2 - 2 - 15
...
4 - 12 - 51
4 - 13 - 52

事实证明并非如此……他们没有按顺序指定一种花色的所有牌,然后是下一张花色,依此类推(1..13,1..13,1..13,1..13),而是指定了牌号的集合。(1..4,1..4,1..4,重复 13 次),像这样:

花色 | 编号 | 原始编号

1 - 1 - 1
2 - 1 - 2
3 - 1 - 3
4 - 1 - 4
1 - 2 - 5
2 - 2 - 6
...
3 - 13 - 51
4 - 13 - 52

希望现在清楚了,这就是为什么需要进行转换。最后要提的是,当我从内存中读取游戏编号时,我得到的是一个 4 字节数组,我需要将其转换为 Int32。有比我的方法更好的方法,但我为了简单起见保留了我的方式。所以代码如下:

IntPtr GameNumAddress;
IntPtr CardsAddress;

// check if version is win2k or winXP
if (Environment.OSVersion.Version.Major == 5)
{
    if (Environment.OSVersion.Version.Minor == 0)    // win2K
    {
        GameNumAddress = (IntPtr)0x010071F8;
        CardsAddress = (IntPtr)0x01007f74;
    }
    else    // winXP
    {
        GameNumAddress = (IntPtr)0x0100834C;
        CardsAddress = (IntPtr)0x01007554;
    }
}
else
{
    MessageBox.Show("Sorry, only winXP and win2K are supported!");
    return;
}

// Search the Hearts Process
Process[] pArray = Process.GetProcessesByName("freecell");
if (pArray.Length == 0)
{
    MessageBox.Show("No Freecell process!");
    return;
}

// Create memory reader
ProcessMemoryReaderLib.ProcessMemoryReader pReader = 
  new ProcessMemoryReaderLib.ProcessMemoryReader();

// Take the first process found
pReader.ReadProcess = pArray[0];

pReader.OpenProcess();

for(int i=0 ; i<8 ; i++)
    listArray[i].Items.Clear();

int iGameNum;
int readBytes;
byte[] buffer;
int CardNum, CardKind;
string ItemString = "";
bool bAddCard;

buffer = pReader.ReadProcessMemory(GameNumAddress,4,out readBytes);
iGameNum = buffer[0] + 256*buffer[1] + 256*256*buffer[2] + 
  256*256*256*buffer[3];
txtGameNum.Text = iGameNum.ToString();

for (int i=0 ; i<8 ; i++)
{
    for (int j=0 ; j<21 ; j++)
    {
        buffer = pReader.ReadProcessMemory((IntPtr)(
           (int)CardsAddress + 0x54*i + 4*j),4,out readBytes);
        CardNum = (buffer[0] / 4) + 1;    
        CardKind = (buffer[0] % 4) + 1;
        bAddCard = true;

        switch (CardNum)
        {
            case 11:
                ItemString = "J";
                break;
            case 12:
                ItemString = "Q";
                break;
            case 13:
                ItemString = "K";
                break;
            case 64:
                bAddCard = false;
                break;
            default:
                ItemString = CardNum.ToString();
                break;
        }

        if (bAddCard)
            listArray[i].Items.Add(ItemString ,CardKind -1);
    }
}

pReader.CloseHandle();

接下来是……

第二部分:Hearts,它是如何工作的?

这款游戏的调试比其他游戏要困难得多,因为它在内存中存储牌的方式非常复杂。而且示例仅在 XP 上运行,因为我在 Win2K 系统上找不到 Hearts,所以无法搜索对应的地址。由于其复杂性,我将保持解释简洁明了,不会深入细节。

那么,有多少人知道我如何开始?没错,我们在调试器中打开程序(mshearts.exe),然后在文件中搜索著名的 rand() 函数。我们会找到两个地方,然后我们在每个地方设置断点。然后我们运行程序。一旦开始游戏,第一个断点就会触发一次,第二个断点会触发 52 次……我想我们已经找到了有趣的部分。所以,像往常一样,我们会查看代码并搜索存储值的内存地址。这里是第二个断点的代码:

01007FB6  /CALL DWORD PTR DS:[<;&msvcrt.rand>]      
01007FBC  |CDQ
01007FBD  |IDIV DWORD PTR SS:[EBP-18]
01007FC0  |MOV EAX,DWORD PTR SS:[EBP-14]
01007FC3  |PUSH 0D
01007FC5  |POP EDI
01007FC6  |PUSH 4
01007FC8  |POP EBX
01007FC9  |MOV ECX,EDX
01007FCB  |CDQ
01007FCC  |IDIV EDI
01007FCE  |SUB EAX,DWORD PTR DS:[ESI+140]
01007FD4  |MOV EDI,EDX
01007FD6  |ADD EAX,4
01007FD9  |CDQ
01007FDA  |IDIV EBX
01007FDC  |LEA EAX,DWORD PTR SS:[EBP+ECX*4-110]
01007FE3  |MOV EBX,DWORD PTR DS:[EAX]
01007FE5  |SHL EDI,4
01007FE8  |LEA ECX,DWORD PTR DS:[ESI+EDX*4+130]
01007FEF  |MOV EDX,DWORD PTR DS:[ECX]
01007FF1  |MOV DWORD PTR DS:[EDI+EDX+1C],EBX
01007FF5  |MOV ECX,DWORD PTR DS:[ECX]
01007FF7  |XOR EBX,EBX
01007FF9  |DEC DWORD PTR SS:[EBP-18]
01007FFC  |INC DWORD PTR SS:[EBP-14]
01007FFF  |CMP DWORD PTR SS:[EBP-14],34
01008003  |MOV DWORD PTR DS:[EDI+ECX+28],EBX
01008007  |MOV ECX,DWORD PTR SS:[EBP-18]
0100800A  |MOV ECX,DWORD PTR SS:[EBP+ECX*4-110]
01008011  |MOV DWORD PTR DS:[EAX],ECX
01008013  \JL SHORT mshearts.01007FB6

是的,我知道,很多丑陋的代码……但我加粗了我们感兴趣的部分,第一行是我们设置断点的地方,调用了 rand() 函数。第二行加粗的代码(01007FF1)是将随机化值存储在内存中的一行。请注意,这是第一类操作:MOV <内存地址>,<值>,所以我仔细检查了这个命令。在跟踪了几次之后,我发现用于存储值的地址具有一定的逻辑。前 13 次迭代(顺便说一句,这里提供的代码是一个循环)访问内存的一个区域,我们称之为区域 A,接下来的 13 次迭代访问另一个区域,区域 B,然后是接下来的 13 次迭代,以及最后 13 次迭代。所以我们得到什么?52 次迭代,每 13 次我们切换到一个不同的内存区域。我还发现每个集合中的内存地址之间的差值为 16。为了简化,我做了一个小图表,以便更好地理解结构:

所以,你看,我们有 4 个主要的基地址,每个都是一个数组,第一个元素在 ZoneA + 0,第二个在 ZoneA + 16,依此类推。根据图示,这 4 个基地址存储在一个数组中,这是正确的。我发现有一个包含 4 个地址的数组,该数组的每个元素都指向另一个包含 13 个元素的数组。这一切都很棒,但我仍然有一个问题,所有涉及这个结构的地址都是动态的,意味着它们在每次运行时都会改变。我需要找到一个地址,每次程序启动时,它始终是一个指向基地址的静态指针(基地址是 4 个指针数组的地址)。我怎么知道这种静态指针确实存在?嗯,这涉及到解释在堆栈上创建变量时会发生什么,以及在堆上创建变量时会发生什么(我指的是非托管代码)。所以当你创建堆栈上的变量时,它的地址会写入文件,因此在运行不同实例时不会改变;但如果你动态创建变量,它的地址在设计时是未知的,因此无法写入文件。现在假设你有一个指向数组的指针,那么数组的大小可以是任意的,你不能动态创建它,等等,但是指针本身是静态的,并且声明在堆栈上,所以总需要至少一个静态指针来保存结构,并且这个静态指针始终具有相同的地址。所以现在我知道它存在,我该如何找到它?嗯,我刚刚做了一个搜索。我运行了一个程序实例,并查看了我的基地址(指向地址数组的地址),然后我在内存中搜索,找到这个基地址存储在哪里,并且只找到了一个这样的地址。bingo。现在,诀窍在于知道在哪个内存区域查找,因为你怎么知道你正在查看的内存是动态内存还是静态内存?嗯,内存区域在 0x0100D000 和 0x0100E000 之间。这仅适用于此特定文件。每个文件都有自己的 .data 段,这是存储所有静态内存的段。

如果你没有理解所有细节也没关系,你只需要知道我们有一个静态地址 = 0x0100d514,这个静态地址是指向另一个指针,我们称之为 PointerArrayAddress,PointerArrayAddress 是指向一个包含 4 个指针的数组的指针。数组中的每个指针都指向一个包含 13 个元素的数组。每个元素包含一个牌号。每个牌的数组就是一个玩家。现在我只需要读取这个结构并在屏幕上显示它。

那么我们的代码做了什么?首先我们找到 Hearts 进程,然后读取静态指针,然后读取数组的 4 个指针,然后从数组中读取 52 个元素,对于每个元素,我们将它的牌号转换为标准的表示。如果你还记得上一节关于牌的转换,这里的转换也是一样的。最后,我们将它添加到屏幕上相应的列表(我们有 4 个列表)。代码如下:

// Search the Hearts Process
Process[] pArray = Process.GetProcessesByName("mshearts");
if (pArray.Length == 0)
    return;

// Create memory reader
ProcessMemoryReaderLib.ProcessMemoryReader pReader =
  new ProcessMemoryReaderLib.ProcessMemoryReader();

// Take the first process found
pReader.ReadProcess = pArray[0];

pReader.OpenProcess();

int i;
int readBytes;
byte[] buffer;

IntPtr StaticAddress = (IntPtr)0x0100d514;
buffer = pReader.ReadProcessMemory(StaticAddress,4,out readBytes);
IntPtr PointerArrayAddress = (IntPtr)(
        buffer[0] + 
        256*buffer[1] + 
        256*256*buffer[2] + 
        256*256*256*buffer[3] + 0x130);

IntPtr[] aArray = new IntPtr[4];
buffer = pReader.ReadProcessMemory(
  (IntPtr)PointerArrayAddress,16,out readBytes);
for (i=0 ; i<4 ; i++)
    aArray[i] = (IntPtr)(
        buffer[4*i] + 
        256*buffer[4*i + 1] + 
        256*256*buffer[4*i + 2] + 
        256*256*256*buffer[4*i + 3] + 0x1c);
    
for (i=0 ; i<4 ; i++)
    listArray[i].Clear();

int CardNum, CardKind;
string ItemString = "";
bool bAddCard;

for (int j=0; j<13 ; j++)
{
    for (i=0 ; i<4 ; i++)
    {
        buffer = pReader.ReadProcessMemory(
          (IntPtr)((int)aArray[i] + (16 * j)),1,out readBytes);
        CardNum = (buffer[0] / 4) + 1;    
        CardKind = (buffer[0] % 4) + 1;
        bAddCard = true;

        switch (CardNum)
        {
            case 11:
                ItemString = "J";
                break;
            case 12:
                ItemString = "Q";
                break;
            case 13:
                ItemString = "K";
                break;
            case 64:
                bAddCard = false;
                break;
            default:
                ItemString = CardNum.ToString();
                break;
        }

        if (bAddCard)
            listArray[i].Items.Add(ItemString ,CardKind -1);
    }
}
pReader.CloseHandle();

在下一节,我将给出一个使用 Hearts 游戏向进程内存写入的示例。

第三部分:WriteProcessMemory 奖励

正如我开头所说,我不能写一篇没有新功能的文章,所以我增强了这个类库的功能,现在它也可以写入另一个进程的内存。这是通过 API WriteProcessMemory 实现的。这个函数接受一个字节数组、一个地址和一个进程,并将字节数组写入进程的地址。很简单。它的常规头文件如下:

BOOL WriteProcessMemory(
    HANDLE hProcess,                // handle to process
    LPVOID lpBaseAddress,           // base of memory area
    LPCVOID lpBuffer,               // data buffer
    SIZE_T nSize,                   // count of bytes to write
    SIZE_T * lpNumberOfBytesWritten // count of bytes written
    );

所以,它的 C# 等效代码是:

[DllImport("kernel32.dll")]
public static extern Int32 WriteProcessMemory(
    IntPtr hProcess, 
    IntPtr lpBaseAddress,
    [In, Out] byte[] buffer, 
    UInt32 size, 
    out IntPtr lpNumberOfBytesWritten);    
为了让这个函数正常工作,你首先需要以 PROCESS_VM_WRITE PROCESS_VM_OPERATION 的权限打开进程。

这里有一个使用这个函数的示例,该示例再次打开 Hearts 进程,但这次的目标是让我玩一局好牌。代码如下:

// Search the Hearts Process
Process[] pArray = Process.GetProcessesByName("mshearts");
if (pArray.Length == 0)
    return;

// Create memory reader
ProcessMemoryReaderLib.ProcessMemoryReader pReader =
  new ProcessMemoryReaderLib.ProcessMemoryReader();

// Take the first process found
pReader.ReadProcess = pArray[0];

pReader.OpenProcess();

int i;
int readBytes;
byte[] buffer;

IntPtr StaticAddress = (IntPtr)0x0100d514;
buffer = pReader.ReadProcessMemory(StaticAddress,4,out readBytes);
IntPtr PointerArrayAddress = (IntPtr)(
        buffer[0] + 
        256*buffer[1] + 
        256*256*buffer[2] + 
        256*256*256*buffer[3] + 0x130);

IntPtr[] aArray = new IntPtr[4];
buffer = pReader.ReadProcessMemory(
  (IntPtr)PointerArrayAddress,16,out readBytes);
for (i=0 ; i<4 ; i++)
    aArray[i] = (IntPtr)(
        buffer[4*i] + 
        256*buffer[4*i + 1] + 
        256*256*buffer[4*i + 2] + 
        256*256*256*buffer[4*i + 3] + 0x1c);
    
int writtenBytes;
buffer = new byte[1];

for (i=0 ; i<4 ; i++)
{
    for (int j=0; j<13 ; j++)
    {
        buffer[0] = (byte)(j*4 + i);
        pReader.WriteProcessMemory(
          (IntPtr)((int)aArray[i] + (16 * j)),buffer,out writtenBytes);
    }
}
pReader.CloseHandle();

btnReadMemory_Click(sender,null);

注意:现在游戏可能没那么有趣了。就是这样。希望你喜欢,别忘了投票。

© . All rights reserved.