readprocessmemory使用方法_记一次对某游戏使用Hotpatch锁定数据

本文介绍了如何使用ReadProcessMemory寻找并Hotpatch游戏内存中存储油量的地址,通过修改代码实现拖拉机油量永不减少,提升游戏体验。详细步骤包括模糊搜索内存地址、定位减少油量的代码、实现Hotpatch技术。
摘要由CSDN通过智能技术生成

0x00 开头废话


最近突然想玩一款农场类的游戏,就随便找了一个休闲种地的游戏玩了玩。
在玩的时候发现游戏还挺不错,挺养生的。但是这游戏有一个缺点,就是它的拖拉机太虚了,油箱小得跟个可乐罐似的,动不动就要跑去加油。

0ee2c2f95908c12295c978279623e99e.png


Nobody knows 拖拉机 better than me. 拖拉机的油箱不可能这么小。
虽然没油了可以去加油站加油,但是这就严重影响了游戏体验。种地种一半,没油了,就得满地图跑加油站加油。那这还是养生游戏吗?
所以我打算给游戏打个Hotpatch,让油量不会减少,这样就能开心地种地了。而且只锁定一个油量,不改其它东西,也不会影响游戏体验。

0x01 寻找存储油量的地址和减少油量的代码

对这种没啥保护的游戏,首先考虑用CE。
由于无法知道当前油量的精确数值,所以先模糊搜索,得到所有可能为Data的地址。

5ac579725d713311139d021d0bd4a2e5.png


得到一大堆地址。没事,游戏里走两步墨迹一会(注意不要让油量发生变化),然后再筛出那些未变化的数值。

6c1b75765f91a7d4324a7e5e40c37a1b.png


可以看到还是又很多地址。然后,随便干些事情,让油量减少一些。

772713847ac056bf6e8195859d37ee93.png


然后筛出那些减小的数据。

d9ae0ff61c393bb32a97e24dc592570d.png


然后再闲逛一下,筛出不变的数据。如此往复,就能筛掉大部分不符合条件的地址。

da5288b3c8db9f4a3c07718a896a0fa0.png


最后几十个可能会比较难筛。这时候可以跑去加加油,然后筛出增加的数据,再筛几次减少的,就能找到存储油量的地址了。

76bcb6113ca019bc33fe2471a8a78a21.png


最后能够筛出个位数的地址。这时候就比较好找了。三个数据,很明显前两个不可能是用来存储油量的,那就是第三个地址0x2EBD41FD8D4存储的是油量。
看一下这块内存:

9e941c2495e5bbc49b346d4f3811ef0d.png


能看到游戏作者用了32位的int来存储油量。
接着,把CE的Debugger挂上游戏进程,用内存断点来看看谁在写这块内存。
挂上Debugger之后,随便做点降油量的事情,然后看结果:

cc425b04927a6d575f4eddf9d5bd80c2.png


能看到是0x2EB8526E42D这个地址的代码写了这块地址。看看这个函数的反汇编:

2a7b192eb9719e59ae08d0660d46d41b.png


能看出来,这个函数有两个参数。一个参数是存有油量的结构体,油量是它一个4 bytes的成员,偏移为0xDC。另一个参数是要减少的油量。
先把剩余油量取出来:

2EB8526E410 - 8B 87 DC000000        - mov eax,[rdi+000000DC] 

然后跟需要减少的油量比较:

2EB8526E416 - 3B C6                 - cmp eax,esi

如果油量够的话,就用剩余油量减去需要扣除的油量,然后重新存回结构体中:

2EB8526E420 - 8B 8F DC000000        - mov ecx,[rdi+000000DC]
2EB8526E426 - 2B CE                 - sub ecx,esi
2EB8526E428 - 3B C1                 - cmp eax,ecx
2EB8526E42A - 0F42 C1               - cmovb eax,ecx
2EB8526E42D - 89 87 DC000000        - mov [rdi+000000DC],eax

那么,这就简单了。我们只需要把油量存回的那条指令给NOP掉,就可以快乐地玩耍了:

2EB8526E42D - 89 87 DC000000        - mov [rdi+000000DC],eax

先看一下这块内存是在哪个section中。我们先拿到这一块的基地址0x2EB8526E000:

fc4ba0c3bb861443b5d64c3fe147fafc.png


顺便得到了这块内存的保护状态。目前是PAGE_EXECUTE_READWRITE,看起来应该是动态分配的地址,而且为了动态写入代码,把所有的权限都加上了。这样的话,改起来就简单了。
把Debugger挂上去,看一下这块内存映射到了哪里:

48d88350e00262b080f5b098c6bff0a4.png


能看到,这块内存其实是从0x000002EB85220000开始分配的,大小为0x100000。这一整个大块内存的权限都为ERW。
我对Unity不是很熟悉,不太清楚这块内存是怎么分配的,但是大致能猜出来是先分配了一大块内存,然后把需要的代码分成模块载入。

0x02 实现Hotpatch

这块地址不太好通过基地址来找,所以考虑使用特征码搜索的方法来实现Hotpatch。


首先把特征码确定好。使用修改油量的指令加上后两条指令的机器码作为特征码:

89 87 DC000000      - mov [rdi+000000DC],eax
48 8B 87 A8000000   - mov rax,[rdi+000000A8]
48 85 C0            - test rax,rax

确定特征码为:

89 87 DC 00 00 00 48 8B 87 A8 00 00 00 48 85 C0

首先遍历一下进程,拿到游戏进程的句柄:

DWORD pid = FindProcess(PROCESS_NAME);
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);

FindProcess用来查找指令进程名的PID。遍历搜索进程PID的方法一搜一大把,就不赘述了。
接着在内存中搜索特征码:

const BYTE signature[] = { 0x89 ,0x87 ,0xDC ,0x00 ,0x00 ,0x00,0x48 ,0x8B ,0x87 ,0xA8 ,0x00 ,0x00 ,0x00, 0x48 ,0x85 ,0xC0 };
LPVOID targetAddr = SearchMem(hProc, signature, sizeof(signature));

SearchMem实现如下:

LPVOID SearchMem(HANDLE hProc, const BYTE signature[], DWORD sigSize)
{
    MEMORY_BASIC_INFORMATION info;
    BYTE* chunk = NULL;
    BYTE* current = 0;

    while (VirtualQueryEx(hProc, current, &info, sizeof(info)) == sizeof(info))
    {
        if ((info.State == MEM_COMMIT) && (info.Protect & PAGE_EXECUTE_READWRITE) && !(info.Protect & PAGE_GUARD))
        {
            current = (unsigned char*)info.BaseAddress;
            chunk = new BYTE[info.RegionSize];

            SIZE_T bytesRead;
            if (ReadProcessMemory(hProc, current, &chunk[0], info.RegionSize, &bytesRead))
            {
                for (size_t i = 0; i < (bytesRead - sigSize); ++i)
                {
                    if (memcmp(signature, chunk + i, sigSize) == 0)
                    {
                        return current + i;
                    }
                }
            }

            delete[] chunk;
        }

        current += info.RegionSize;
    }

    return NULL;
}

使用VirtualQueryEx来遍历所有的section。使用current来记录当前地址。每遍历完一个section就将current加上这个section的大小来遍历下个section。
如果当前section状态为MEM_COMMIT,权限标志为PAGE_EXECUTE_READWRITE(之前分析得到了这块内存是ERW权限,所以直接指定就行),并且PAGE_GUARD标志位没有置1,则把这块内存读出来搜索特征码。
使用一个叫chunk的buffer来存放这块内存。使用ReadProcessMemory将整个section读出来,然后对每一个byte进行遍历,使用memcpy来判断接下来的一块内存是否与特征码相等。如果找到特征码,则反汇其地址,否则返回NULL。
找到特征码之后,把 ``` mov [rdi+000000DC],eax ``` 这条指令覆盖成全NOP。这条指令一共6个bytes,全部覆盖成0x90即可。

const BYTE payload[] = { 0x90,0x90,0x90,0x90,0x90,0x90 };
WriteProcessMemory(hProc, targetAddr, payload, sizeof(payload), &dwBytesWritten);

由于这块内存本来就有WRITE权限,所以不需要使用VirtualProtect来改内存权限了。否则需要使用VirtualProtect增加写权限,写完之后再把权限改回去。
这样就实现好了。进游戏试试。进游戏之后先随便做些操作,消耗一些油量,目的是确保减少油量的相关代码已经载入内存中。为了好看,我每次Patch之前还会把油加满。
然后运行Patcher:

fe6d63df7c9a0d6c2a8e5befa4fd4403.png


游戏里试试:

9ee91951e111c1c8b23fb7796e0d3813.gif

能看到油量不会减少了。这下终于能愉快地玩耍了。之后每次启动游戏运行一下patcher就能使油量不减少。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值