赞
踩
经过上一篇文章大家可能对外挂有了一个初步的认识,之前的内容比较简单,应该大家很容易掌握。我写作过程中我发现三个篇幅可能写不完,后续还可能要多加几个篇幅,这篇我们就先以内存挂为主吧。话不多说了,让我们开始吧。
文章中代码github地址:
https://github.com/Diamonds-ZhaoYu/hz_example
环境要求:
系统:Windows
语言:Windows C++
开发工具:Visual Studio 2012
咱们讲内存外挂之前先要掌握一些基础知识,主要分为几个部分:
Windows内存原理
内存分布情况
系统中每个进程都有自己的虚拟地址空间,对于一个32位进程的应用来说。这个地址的寻找空间等于2的32次方,也就是刚好4GB。32位指针地址空间是从0x00000000到0xFFFFFFFF之间任意一个值。对于一个64位程序来说,由64位指针可以表示从0x00000000'00000000到0xFFFFFFFF'FFFFFFFF之间任意一个值,因此地址空间大小为16EB。每个程序都有自己的专有空间,当这个程序每个线程运行时它只能访问到自己的地址空间。
每个进程都有自己的内存分区,下图是一个32位程序的分区情况:
由于地址空间分区依赖于系统底层实现,所以windows内核的不同分区略有不同
分区名称 | X86 32位 Windows系统 | X64 64位 Windows系统 | IA-64 64位 Windows系统 |
空指针赋值分区 | 0x00000000 ~ 0x0000FFFF | 0x00000000'00000000 ~ 0x0000000'0000FFFF | 0x00000000'00000000 ~ 0x00000000'0000FFFF |
用户模式分区 | 0x00010000 ~ 0x7FFEFFFF | 0x00000000'00010000 ~ 0x000007FF'FFFEFFFF | 0x00000000'00010000 ~ 0x000006FB'FFFEFFFF |
64K禁入分区 | 0x7FFF0000 ~ 0x7FFFFFFF | 0x000007FF'FFFF0000 ~ 0x000007FF'FFFFFFFF | 0x000006FB'FFFF0000 ~ 0x000006FB'FFFFFFFF |
内核模式分区 | 0x80000000 ~ 0xFFFFFFFF | 0x00000800'00000000 ~ 0xFFFFFFFF'FFFFFFFF | 0x000006FC'00000000 ~ 0xFFFFFFFF’FFFFFFFF |
空指针赋值分区:
进程中空指针赋值分区段又称为闭区间,该分区是帮组给程序捕捉空指针使用。如malloc分配内存失败,就会返回NULL。如果一个程序对该分区做写入操作会触发违规操作。
用户模式分区:
Windows中所有的exe和动态链接库都是加载到这个区域,同时会把该进程的可以访问的资源都加载到这个区域。
64K禁入分区:
64K禁入区的作用很明显是隔离了用户和内核空间;防止用户程序跨越到内核空间中。与内核交互会涉及到SSDT表,后续破解驱动保护部分会讲到。
内核模式分区:
内核模式分区顾命思意,内核模式是对操作系统的代码驻地。与线程,内存管理,文件系统,网络支持以及驱动相关程序的代码都会载入这个区域。这一部分的代码是所有的进程都共有的。
程序内存分布情况
对于一个程序来说逻辑上我们可以把它分为三个区域段:动态分配段、代码段、静态数据段。
动态分配段一般就是“堆栈”,栈(stack)和堆(heap)是两种不同的动态数据区。栈是一种线性数据,堆一种链式数据。进程的线程每一个都有自己私有的“栈”,所以每个线程虽然代码一样,但是互相不干扰。一个堆栈可以通过“基地址”和“栈顶地址”来描述。全局变量和静态变量分配在静态数据区,本地变量分配在动态数据区,即堆栈中。程序通过堆栈的基地址和偏移地址来访问本地变量。
找基地址和偏移地址
根据以上的描述我们知道本地变量分配在动态数据中,需要通过堆栈的基地址+偏移地址访问本地变量。我们今天会使用到一个工具上一篇文章提到过,就是CE。我电脑是装的虚拟机,不能开太大的游戏,我就拿一个植物大战僵尸入手吧,也方便大家学习。
先看两张图,比对一下,这个是我启动两次的结果,定位这个游戏的阳光地址:
两次的金币的地址果然不一样,第一次是0x12D63AC8,第二次是0x12D29DF8,说明我们这个阳光是分配在动态区域。按照这样说的话,我们需要找一下基地址和偏移地址。
接下来我们来说说CE,CE去找游戏中的地址的时候特别容易,可以多次扫描之后就能快速定位。第一次扫描是点击新的扫描,后面的时候都点再次扫描,每次扫描的时候改动一下你想扫描的值,比如这个值是血量,或者是阳光等等。
第一步:
得到扫描结果。以上图的0x12D29DF8为结果。
第二步:
以上步骤可以得到访问的内存的代码段,可以根据汇编看到 。 add eax ,[exd+00005560]得到我们的第一个偏移地址0x5560。然后还有一个基地址,这个基地址是指针数值: 0x12D24898我们可以把这个数值保存下来。
在图中我们还看到 call PlantsVsZombies.exe+0x1B980 这样的一堆数据,其实这个数据是我们的入口点+偏移地址得到的。咱们现在看到的这个地址段就属于代码段,代码段是静态数据区,不会改变。PlantsVsZombies.exe对应的是0x0400000这个是程序的入口点,我们一个exe程序在windows加载中是通过PE结构去映射他们入口点以及相关的导入表导出表,这个篇幅不够写,后续有兴趣的我可以写相关的内容,我们继续。
第三步:
我们继续扫描,用之前的0x12D24898地址进行十六进制扫描,得出一堆结果。我们发现第一个结果中第一个数据老是动,这种数据一般不会是我们想要的数据地址,所以我们选择第二个,点击加入到保存区域。
继续查看访问该数据地址,我们发心大量到访问数据都是mov esi,[edi + 00000768],说明可能0x768是一个偏移地址。
然后查看数据指针数值发现数值地址0x018B2910,继续用018B2910试试扫描。
扫描后发现结果中有几个颜色的和其他颜色不一样这个可能就是基地址,我们拿一个测试测试,复制出来是PlantsVsZombies.exe+2A9EC0(6A9EC0),说明这个也是一个静态数据区域数据不会改变。现在我们的阳光地址可以得到一个公式:
根据上图我们可以验证一下:
关掉游戏重新开始一局,这个数值以10进制显示150,看来我们是找对了。
找游戏基地址是一个很枯燥的活,要反复尝试。或者通过od查看汇编下断点也可以。不过也不是所有游戏都可以找,因为大部分网游是驱动保护的,所以我们要知道怎么破解。
编程实现
编程前我们要学习两个函数ReadProcessMemory和WriteProcessMemory,刚说到每个程序的内存是自己管理自己的虚拟内存,所以这两个函数都有一个进程句柄。当然这个只是入门后续写内存还有很多中方式,比如说通过shellcode或者是dll注入等形式,大部分的会使用dll注入形式唤出一个程序。
ReadProcessMemory
函数原型:
BOOL ReadProcessMemory( HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfBytesRead );
hProcess:
由OpenProcess返回的进程句柄。
具有正在读取的内存的进程的句柄。句柄必须具有对进程的PROCESS_VM_READ访问权限
lpBaseAddress:
指向要从中读取的指定进程中的基地址的指针。在进行任何数据传输之前,系统会验证基址和指定大小的内存中的所有数据都可以访问以进行读取访问,如果无法访问,则该功能将失败。
lpBuffer:
指向缓冲区的指针,该缓冲区从指定进程的地址空间接收内容。
nSize:
从指定进程中读取的字节数。
返回:
非零值代表成功。可用GetLastError获取更多的错误详细信息。
查看官方文档:
https://docs.microsoft.com/zh-cn/windows/win32/api/memoryapi/nf-memoryapi-readprocessmemory
WriteProcessMemory
函数原型:
BOOL WriteProcessMemory( HANDLE hProcess, LPVOID lpBaseAddress, LPCVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfBytesWritten );
hProcess:
由OpenProcess返回的进程句柄。
如参数传数据为 INVALID_HANDLE_VALUE 目标进程为自身进程。
lpBaseAddress:
要写的内存首地址
在写入之前,此函数将先检查目标地址是否可用,并能容纳待写入的数据。
lpBuffer:
指向要写的数据的指针。
nSize:
要写入的字节数。
lpNumberOfBytesWritten:
指向变量的指针,该变量接收传输到指定缓冲区的字节数。如果lpNumberOfBytesRead为NULL,则忽略该参数。
返回:
非零值代表成功。可用GetLastError获取更多的错误详细信息。
查看官方文档:
https://docs.microsoft.com/zh-cn/windows/win32/api/memoryapi/nf-memoryapi-writeprocessmemory
看一下我们的代码吧:
- int main(void)
- {
-
- DWORD Pid; //进程PID
- HANDLE hProcess=0; //进程句柄
-
- //获得窗口句柄
- HWND hWnd = ::FindWindow(NULL,TEXT("植物大战僵尸中文版"));
- if( hWnd == NULL )
- {
- printf("\n未发现游戏进程 请先运行游戏\n");
- }
- else if ( hWnd != NULL )
- {
- //获得进程pid
- GetWindowThreadProcessId(hWnd,&Pid);
-
- //打开进程 获取所有操作权限
- hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,Pid);
- if ( hProcess == NULL )
- {
- printf("\n打开游戏进程失败\n");
- _getch();
- return -1;
- }
- else
- {
- printf("\n成功打开游戏进程,窗口句柄:0x%x,进程PID:%d\n",hWnd,Pid);
- }
-
-
- change_sunny_number(hProcess); //修改阳光
-
- }
- _getch();
- return 0;
- }
主方法内我们做了一件事,就是通过FindWindow拿到窗口句柄,通过窗口句柄调用GetWindowThreadProcessId获得进程pid,然后通过进程pid调用OpenProcess打开进程句柄。打开后调用
change_sunny_number方法,我们来看看change_sunny_number实现:
- #define SUNNY_NUMBER_ADDR 0x006A9EC0 //基地址
- /**
- * 修改阳光方法
- */
- void change_sunny_number(HANDLE hProcess)
- {
- DWORD sunny;
-
- DWORD baseAddr = SUNNY_NUMBER_ADDR;
- DWORD stackAddr;
- DWORD dataAdrr;
-
- 阳光基址读取读基地址
- ReadProcessMemory (hProcess,(void *)baseAddr,&stackAddr,4,0);
- printf("\n一级偏移基地址: 0x%x\n",stackAddr);
-
- //阳光基址 + 一级偏移 = 一级基址
- stackAddr = stackAddr + 0x768 ;
- printf("\n一级基地址: 0x%x\n",stackAddr);
-
- //读二级偏移基地址
- ReadProcessMemory(hProcess,(void *)stackAddr,&dataAdrr,4,0);
- printf("\n一级偏移基地址: 0x%x\n",dataAdrr);
-
- //二级偏移 + 二级偏移 = 阳光地址
- dataAdrr = dataAdrr + 0x5560 ;
- printf("\n二级基地址: 0x%x\n",dataAdrr);
-
- //写入阳光
- printf("\n您需要多少阳光?:");
- scanf_s("%d",&sunny);
- DWORD res = WriteProcessMemory(hProcess, (void *)dataAdrr, &sunny, 4, 0);
-
- if ( res == NULL )
- {
- printf("\n修改失败\n");
- }
- else
- {
- printf("\n修改成功\n");
- }
-
- }
该方法调用SUNNY_NUMBER_ADDR读取到变量基地地址,变量基地地址+0x768读取到二级偏移基地址,二级及地址+ 0x5560得到我们的阳光变量地址,进行写入操作,看一下我上面画的图,特别容易理解。
结尾:
这章就先到这里了,字数有限制。后面在多加几个章节。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。