最近一直忙于Opencv图像处理方面的学习,以及工作,没有更新C/C++专栏方面的博客了,所以今天就给大家写个应用层方面的编程代码,可用于参考学习,本篇博客将运用WindowsSDK库所提供的API来编写一个修改其他进程里变量值的程序。
在开始实际编写代码之前,先给大家介绍一下所需函数:OpenProcess、VirtualProtectEx、ReadProcessMemory、WriteProcessMemory,FindWindow,GetWindowThreadProcessId
一.函数介绍
1. OpenProcess
函数原型:
HANDLE OpenProcess(DWORD dwDesiredAccess,BOOL bInheritHandle, DWORD dwProcessId);
参数介绍:
DWORD dwDesiredAccess, //渴望得到的访问权限(标志) BOOL bInheritHandle, // 是否继承句柄 DWORD dwProcessId // 进程标示符
返回值:
如成功,返回值为指定进程的句柄。 如失败,返回值为空,可调用GetLastError函数获得错误代码。
2.VirtualProtectEx
函数原型:
BOOL VirtualProtectEx(HANDLE hProcess,LPVOID lpAddress,DWORD dwSize,DWORD flNewProtect,PDWORD lpflOldProtect);
参数介绍:
HANDLE hProcess, // 要修改内存的进程句柄 LPVOID lpAddress, // 要修改内存的起始地址 DWORD dwSize, // 页区域大小 DWORD flNewProtect, // 新访问方式 PDWORD lpflOldProtect // 原访问方式 用于保存改变前的保护属性
返回值:
如果成功,返回非零。失败返回零。即TRUE(1)与FALSE(0)
3.ReadProcessMemory
BOOL ReadProcessMemory(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead);
参数介绍:
hProcess [in]远程进程句柄。 被读取者 pvAddressRemote [in]远程进程中内存地址。 从具体何处读取 pvBufferLocal [out]本地进程中内存地址. 函数将读取的内容写入此处 dwSize [in]要传送的字节数。要写入多少 pdwNumBytesRead [out]实际传送的字节数. 函数返回时报告实际写入多少返回值:
如果成功,返回非零。失败返回零。即TRUE(1)与FALSE(0)
4.WriteProcessMemory
函数原型:
BOOL WriteProcessMemory(HANDLE hProcess,LPVOID lpBaseAddress,LPVOID lpBuffer,DWORD nSize,LPDWORD lpNumberOfBytesWritten);
参数介绍:
hProcess 由OpenProcess返回的进程句柄。 如参数传数据为 INVALID_HANDLE_VALUE 【即-1】目标进程为自身进程 lpBaseAddress 要写的内存首地址 再写入之前,此函数将先检查目标地址是否可用,并能容纳待写入的数据。 lpBuffer 指向要写的数据的指针。 nSize 要写入的字节数。返回值:
如果成功,返回非零。失败返回零。即TRUE(1)与FALSE(0)
5.FindWindow
函数原型:
FindWindow,LPCTSTR lpClassName,LPCTSTR lpWindowName);
参数介绍:
lpClassName 指向一个以NULL字符结尾的、用来指定类名的字符串或一个可以确定类名字符串的原子。如果这个参数是一个原子,那么它必须是一个在调用此函数前已经通过GlobalAddAtom函数创建好的全局原子。这个原子(一个16bit的值),必须被放置在lpClassName的低位字节中,lpClassName的高位字节置零。 如果该参数为null时,将会寻找任何与lpWindowName参数匹配的窗口。 lpWindowName 指向一个以NULL字符结尾的、用来指定窗口名(即窗口标题)的字符串。如果此参数为NULL,则匹配所有窗口名。
返回值:
如果函数执行成功,则返回值是拥有指定窗口类名或窗口名的窗口的句柄。 如果函数执行失败,则返回值为 NULL 。可以通过调用GetLastError函数获得更加详细的错误信息。
6.GetWindowThreadProcessId
函数原型:
DWORD GetWindowThreadProcessId(HWND hWnd,LPDWORD lpdwProcessId);
参数介绍:
hWnd[in] (向函数提供的)被查找窗口的句柄. lpdwProcessId[out] 进程号的存放地址(变量地址) Pointer to a variable that receives the process identifier. If this parameter is not NULL, GetWindowThreadProcessId copies the identifier of the process to the variable; otherwise, it does not. (如果参数不为NULL,即提供了存放处--变量,那么本函数把进程标志拷贝到存放处,否则不动作。)
返回值:
返回线程号,注意,lpdwProcessId 是存放进程号的变量。返回值是线程号,lpdwProcessId 是进程号存放处。
二.开始编写代码
这里我们就拿一个Windows默认提供的一个游戏做测试:蜘蛛纸牌,我们通过编写代码来修改纸牌分数!
第一打开Windows自带的蜘蛛纸牌:
可以看到分数默认分数为500,那么接下来我们要编写代码让其分数发生改变,改成我们想要的任意值!
1.首先我们打开任务管理器查看进程的窗口名:
2.通过FindWindow获取窗口句柄:
//得到窗口句柄 HWND hWnd = FindWindow(NULL, _T("蜘蛛")); if (hWnd == NULL){ //如果无法获取句柄则报错 printf("无法获取进程句柄,请检查进程是否存在!"); getchar(); return -1; }
3. 用GetWindowThreadProcessId函数通过窗口句柄获取进程ID:
//GetWindowThreadProcessId获取进程ID的变量类型是DWORD,但是GetWindowThreadProcessId在MSDN中并没有明确表示返回值得成功与失败 //所以我们定义一个DWORD类型变量赋值为0,在调用GetWindowThreadProcessId之后看一下ID是否还为0如果为0则失败,一般来说不会失败! DWORD Process_ID = (DWORD)0; //获取进程ID GetWindowThreadProcessId(hWnd, &Process_ID); //PID这里传址 if (Process_ID == (DWORD)0){ //判断是否与原值相同 printf("无法获取进程ID"); }
//打开进程对象,并获取进程句柄 HANDLE hPro = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, calcID); if (hPro == 0){ printf("无法获取进程句柄"); }
当我们已经拥有了该进程句柄时,就可以对该进程下的内存做各种操作了,但是操作时需要拥有权限!
但是操作前,我们需要知道对哪块地址进行读写操作,不然乱写会操作进程数据混乱,运行出错,所以为了确保正确性,我们用for循环的方式遍历内存!
在操作系统当中,进程最大被分配栈大小为4092字节,所以我们定义一个for循环循环次数为4092,可以通过IDE,修改编译产生的PE文件头!
DWORD dwOldProtect; //用于存储之前的权限 int pid_p = 0x01000000; //PE文件映射到内存的映像数据段首偏移地址为0x01000000 BOOL pid_bol; for (int i = 0; /*无需确定PE文件大小*/; ++i){ //for循环 pid_bol = VirtualProtectEx(hPro, (LPVOID)pid_p, 4, PAGE_READWRITE, &dwOldProtect); //循环获取权限 if (pid_bol == FALSE){ //基址错误 printf("已经没有可用地址空间..."); getchar(); return -1; } else{ //访问内存值看一下是否与我们要查找的数值一致 int date = 0; int new_date = 0; ReadProcessMemory(hPro, (LPVOID)pid_p, &date, 4, NULL); if (date == 500){ printf("找到第一个地址:%0x,数值匹配,请输入要写入的值:", pid_p); scanf("%d", &new_date); //写入新的分数值 DWORD dwNumberOfBytesRead; //存储之前的访问权限 int write_ = WriteProcessMemory(hPro, (LPVOID)pid_p, &new_date, 1, &dwNumberOfBytesRead); //写入内存 if (write_ == 0){ printf("写入内存值失败,请检查权限!"); getchar(); return -1; } getchar(); } } pid_p+=4; //递增地址 printf("地址:%0x\n", pid_p); }
写完之后我们运行一下看看效果如何!
成功在内存中找到第一个与数值匹配的内存地址空间,这个时候写入新的值看一下游戏里的分数有没有发生变化!
当你写完之后会发现有发现了一个与我们要查找的数值对应的内存地址空间!
这个时候不用管,先不要写入值,因为有的时候你在编写程序时难免会有两个或多个变量拥有一样的数值!
我们先看一下游戏里的分数是否发生了变化
会发现分数并没有发生变化,注意这个时候不要立即去向下一个内存地址空间写入,我们先将游戏最小化后额最大化一次,惊奇的发现
数值改变了,是因为当你写入新的值之后窗口显示的那块显存区域并没有发生任何改变,我们需要发送一个窗口绘图消息让Windows将新的绘图数据写入显存中才可以!
你可以使用SendMessage函数来实现这个功能!
SendMessage函数原型:
LRESULT SendMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM IParam) 参数 hWnd:其窗口程序将接收消息的窗口的句柄。如果此参数为HWND_BROADCAST,则消息将被发送到系统中所有顶层窗口,包括无效或不可见的非自身拥有的窗口、被覆盖的窗口和弹出式窗口,但消息不被发送到子窗口。 Msg:指定被发送的消息。 wParam:指定附加的消息特定信息。 IParam:指定附加的消息特定信息。 返回值:返回值指定消息处理的结果,依赖于所发送的消息。
这里我就不写额外的代码了,如有需要可以自己行增加绘图消息
绘图消息宏:
WM_PAINT
十六进制:
0x000F无附加参数
很好,意味着0x0/*符号位*/1012f60这个PE文件在内存中映射的地址就是存储分数的变量首地址,那么剩下的地址就无需再写入了,有时候遇到重复的值没有办法确定哪一个才是正确的地址空间时,建议先玩两盘让你的分数发生改变,在去寻找,这样大大减少撞变量的几率,你可以将查找数值改成scanf用户键入,这样不至于写死,我这里只是做测试所以写死了。
注意当你运行时会发现查找速度很慢,这是因为你在查找期间又向I/O流里输出了东西,而此时CPU需要向GPU里写东西,而GPU又要从显存里将数据读取出来,然后通过CRT绘制到屏幕上,所以速度上会有所减慢!
这个时候可以把这句代码注释掉:
printf("地址:%0x\n", pid_p);
这样的话基本上就是秒查找
下面来解释一下这个代码,+=4是因为CPU寻址的方面的问题,我的CPU寻址是32位的,游戏也是32位程序,所以位宽字节为4字节,详细可以查看我的这篇博客:详解C语言内存对齐, 深度理解“CPU内部寻址方式”
pid_p+=4; //递增地址
即使是LONG LONG8字节的类型在32位CPU中也是分开来寻址的,分两次。
这里就不说更多的关于底层方面的信息,如果想了解的话可以去本博客下的CPU内部工作原理/C/C++底层代码实现里查看更多相关文章!
完整代码如下:
//得到窗口句柄 HWND hWnd = FindWindow(NULL, _T("蜘蛛")); if (hWnd == NULL){ //如果无法获取句柄则报错 printf("无法获取进程句柄,请检查进程是否存在!"); getchar(); return -1; } //GetWindowThreadProcessId获取进程ID的变量类型是DWORD,但是GetWindowThreadProcessId在MSDN中并没有明确表示返回值得成功与失败,所以我们定义一个DWORD类型变量赋值为0,在调用GetWindowThreadProcessId之后看一下ID是否还为0如果为0则失败,一般来说不会失败! DWORD Process_ID = (DWORD)0; //获取进程ID GetWindowThreadProcessId(hWnd, &Process_ID); //PID这里传址 if (Process_ID == (DWORD)0){ //判断是否与原值相同 printf("无法获取进程ID"); } //打开进程对象,并获取进程句柄 HANDLE hPro = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, Process_ID); if (hPro == 0){ printf("无法获取进程句柄"); } DWORD dwOldProtect; //用于存储之前的权限 int pid_p = 0x01000000; //PE文件映射到内存的映像首偏移地址为0x01000000 BOOL pid_bol; for (int i = 0; /*无需确定PE文件大小*/; ++i){ //for循环 pid_bol = VirtualProtectEx(hPro, (LPVOID)pid_p, 4, PAGE_READWRITE, &dwOldProtect); //循环获取权限 if (pid_bol == FALSE){ //基址错误 printf("已经没有可用地址空间..."); getchar(); return -1; } else{ //访问内存值看一下是否与我们要查找的数值一致 int date = 0; int new_date = 0; ReadProcessMemory(hPro, (LPVOID)pid_p, &date, 4, NULL); if (date == 500){ printf("找到第一个地址:%0x,数值匹配,请输入要写入的值:", pid_p); scanf("%d", &new_date); //写入新的分数值 DWORD dwNumberOfBytesRead; //存储之前的访问权限 int write_ = WriteProcessMemory(hPro, (LPVOID)pid_p, &new_date, 4, &dwNumberOfBytesRead); //写入内存 if (write_ == 0){ printf("写入内存值失败,请检查权限!"); getchar(); return -1; } getchar(); } } pid_p+=4; //递增地址 printf("地址:%0x\n", pid_p); } getchar(); return 0;
你可以基于本列子加上进程快照做一个进程间的内存值遍历,遍历整个windows进程下的所有内存数值,看一下是否有符合你要求的数值地址内存,并位于哪个进程上,该进程的哪个地址空间下!
同理你也可以通过该列子来修改其他软件中的UI界面!列如标题,按钮标题等!
最后再给大家介绍两个函数virtuallallocEx,FlushInstructionCache
1.virtuallallocEx
函数原型:
LPVOID VirtualAllocEx(HANDLE hProcess,LPVOID lpAddress,SIZE_T dwSize,DWORD flAllocationType,DWORD flProtect);
参数介绍:
hProcess: 申请内存所在的进程句柄。 lpAddress: 保留页面的内存地址;一般用NULL自动分配 。 dwSize: 欲分配的内存大小,字节单位;注意实际分 配的内存大小是页内存大小的整数倍 flAllocationType 可取下列值: MEM_COMMIT:为特定的页面区域分配内存中或磁盘的页面文件中的物理存储 MEM_PHYSICAL :分配物理内存(仅用于地址窗口扩展内存) MEM_RESERVE:保留进程的虚拟地址空间,而不分配任何物理存储。保留页面可通过继续调用VirtualAlloc()而被占用 MEM_RESET :指明在内存中由参数lpAddress和dwSize指定的数据无效 MEM_TOP_DOWN:在尽可能高的地址上分配内存(Windows 98忽略此标志) MEM_WRITE_WATCH:必须与MEM_RESERVE一起指定,使系统跟踪那些被写入分配区域的页面(仅针对Windows 98) flProtect可取下列值: PAGE_READONLY: 该区域为只读。如果应用程序试图访问区域中的页的时候,将会被拒绝访 PAGE_READWRITE 区域可被应用程序读写 PAGE_EXECUTE: 区域包含可被系统执行的代码。试图读写该区域的操作将被拒绝。 PAGE_EXECUTE_READ :区域包含可执行代码,应用程序可以读该区域。 PAGE_EXECUTE_READWRITE: 区域包含可执行代码,应用程序可以读写该区域。 PAGE_GUARD: 区域第一次被访问时进入一个STATUS_GUARD_PAGE异常,这个标志要和其他保护标志合并使用,表明区域被第一次访问的权限 PAGE_NOACCESS: 任何访问该区域的操作将被拒绝 PAGE_NOCACHE: RAM中的页映射到该区域时将不会被微处理器缓存(cached) 注:PAGE_GUARD和PAGE_NOCHACHE标志可以和其他标志合并使用以进一步指定页的特征。PAGE_GUARD标志指定了一个防护页(guard page),即当一个页被提交时会因第一次被访问而产生一个one-shot异常,接着取得指定的访问权限。PAGE_NOCACHE防止当它映射到虚拟页的时候被微处理器缓存。这个标志方便设备驱动使用直接内存访问方式(DMA)来共享内存块。
返回值:
执行成功就返回分配内存的首地址,不成功就是NULL。
2.FlushInstructionCache
函数原型:
BOOL WINAPI FlushInstructionCache(__in HANDLE hProcess,__in LPCVOID lpBaseAddress,__in SIZE_T dwSize);
参数介绍:
hProcess是进程句柄。 lpBaseAddress是写入cpu指令高速缓存里去内存的开始地址。 dwSize是写入cpu指令高速缓存里去内存的大小。
返回值:
如果成功,返回非零。失败返回零。即TRUE(1)与FALSE(0)
注释:
一般的程序都是在运行前已经编译好的,因此修改指令的机会比较少,但在软件的防破解里,倒是使用很多。当修改指令之后,怎么样才能让CPU去执行新的指令呢?这样就需要使用函数FlushInstructionCache来把内存里的数据写入cpu指令高速缓存里去,让CPU加载新的指令,才能执行新的指令。
你可以通过virtuallallocEx来在进程开辟一块空间在通过FlushInstructionCache函数来运行该指令,达到进程附加的功能!
具体使用方法我这里就不列出了,可以自己去尝试一下!
尾言:
如有不懂的地方可以举例出来。