单机游戏修改器——从计算机基础学科出发
游戏修改器——从计算机基础学科出发
qq:aaa@qq.com
主要针对单机游戏。从操作系统层面对如何做进行了说明。
-
认识内存
图1所示的为某一代内存条的硬件示意图,任何与CPU打交道的硬件部分都通过其自身的一个控制芯片进行通信完成读写操作。图1引用了 https://www.cnblogs.com/mikewolf2002/archive/2012/11/13/2768804.html中的示意图,具体细节可以参考该引用。
图1 .某一代内存条的硬件示意图
该内存条会作为一种资源被操作系统接管,并且会被分成若干页,也称页框。页框可以装载不同的页帧。一段代码被编译成可执行文件K后,K文件结构体中会包含所需的所有段的信息。被OS加载到内存时,OS为其配置相关资源,这些资源中就包括被分配到的页框信息,此时就有了图2、图3(可参考https://blog.csdn.net/lvyibin890/article/details/82217193)。
关于虚拟内存与物理内存的联系
图2.虚拟内存与物理内存的联系
页表的工作原理如下图
图3.页表的工作原理
进程控制块与可执行文件
进程控制块,具体可以参考:https://blog.csdn.net/qq_38499859/article/details/80057427,该链接博文的第2部分的介绍。
可执行文件,以exe文件为例子。exe文件比较复杂,属于一种多段的结构,是DOS最成功和复杂的设计之一。要了解exe文件,首先需要了解exe文件的文件头结构。每个exe文件包含一个文件头和一个可重定位程序的映像。文件头包含MS-DOS用于加载程序的信息,例如程序的大小和寄存器的初始值。文件头还指向一个重定位表,该表包含指向程序映像中可重定位段地址的指针链表。详细可以参考https://wenku.baidu.com/view/04965066581b6bd97f19eae9.html。
//摘自:https://blog.csdn.net/qq_38499859/article/details/80057427
//linux eg.
struct task_struct
{
long state; /*任务的运行状态(-1 不可运行,0 可运行(就绪),>0 已停止)*/
long counter;/*运行时间片计数器(递减)*/
long priority;/*优先级*/
long signal;/*信号*/
struct sigaction sigaction[32];/*信号执行属性结构,对应信号将要执行的操作和标志信息*/
long blocked; /* bitmap of masked signals */
/* various fields */
int exit_code;/*任务执行停止的退出码*/
unsigned long start_code,end_code,end_data,brk,start_stack;/*代码段地址 代码长度(字节数)
代码长度 + 数据长度(字节数)总长度 堆栈段地址*/
long pid,father,pgrp,session,leader;/*进程标识号(进程号) 父进程号 父进程组号 会话号 会话首领*/
unsigned short uid,euid,suid;/*用户标识号(用户id) 有效用户id 保存的用户id*/
unsigned short gid,egid,sgid; /*组标识号(组id) 有效组id 保存的组id*/
long alarm;/*报警定时值*/
long utime,stime,cutime,cstime,start_time;/*用户态运行时间 内核态运行时间 子进程用户态运行时间
子进程内核态运行时间 进程开始运行时刻*/
unsigned short used_math;/*标志:是否使用协处理器*/
/* file system info */
int tty; /* -1 if no tty, so it must be signed */
unsigned short umask;/*文件创建属性屏蔽位*/
struct m_inode * pwd;/*当前工作目录i 节点结构*/
struct m_inode * root;/*根目录i节点结构*/
struct m_inode * executable;/*执行文件i节点结构*/
unsigned long close_on_exec;/*执行时关闭文件句柄位图标志*/
struct file * filp[NR_OPEN];/*进程使用的文件表结构*/
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
struct desc_struct ldt[3];/*本任务的局部描述符表。0-空,1-代码段cs,2-数据和堆栈段ds&ss*/
/* tss for this task */
struct tss_struct tss;/*本进程的任务状态段信息结构*/
};
//windows eg.
typedef struct _EPROCESS
{
KPROCESS Pcb; //KPROCESS被内核用来进行线程调度使用
EX_PUSH_LOCK ProcessLock;//ProcessLock域是一个推锁(push lock)对象,用于保护EPROCESS中的数据成员。用来对可能产生的并行事件强制串行化。
LARGE_INTEGER CreateTime; //这两个域分别代表了进程的创建时间和退出时间
LARGE_INTEGER ExitTime;
EX_RUNDOWN_REF RundownProtect; //RundownProtect域是进程的停止保护锁,当一个进程到最后被销毁时,它要等到所有其他进程和线程已经释放了此锁,才可以继续进行,否则就会产生孤儿线程
HANDLE UniqueProcessId; //UniqueProcessId域是进程的唯一编号,在进程创建时就设定好了,我们在"任务管理器"中看到的PID就是从这个域中获取的值
LIST_ENTRY ActiveProcessLinks; //ActiveProcessLinks域是一个双链表节点(注意是双链表中的一个节点),在windows系统中,所有的活动进程都连接在一起,构成了一个链表。
SIZE_T QuotaUsage[PsQuotaTypes];//QuotaUsage和QuotaPeak域是指一个进程的内存使用量和尖峰使用量
SIZE_T QuotaPeak[PsQuotaTypes];
SIZE_T CommitCharge; //CommitCharge域中存储了一个进程的虚拟内存已提交的"页面数量"
SIZE_T PeakVirtualSize;//PeakVirtualSize域是指虚拟内存大小的尖峰值。
SIZE_T VirtualSize;//VirtualSize域是指一个进程的虚拟内存大小。
LIST_ENTRY SessionProcessLinks;//SessionProcessLinks域是一个双链表节点,当进程加入到一个系统会话中时,这个进程的SessionProcessLinks域将作为一个节点(LIST_ENTRY在内核中很常见)加入到该会话的进程链表中。
PVOID DebugPort; //DebugPort和ExceptionPort域是两个句柄(指针),分别指向当前进程对应的调试端口和异常端口。
PVOID ExceptionPort;
PHANDLE_TABLE ObjectTable; //ObjectTable域是当前进程的句柄表。
EX_FAST_REF Token; //Token域是一个快速引用,指向该进程的访问令牌,用于该进程的安全访问检查。
PFN_NUMBER WorkingSetPage; //WorkingSetPage域是指包含进程工作集的页面
KGUARDED_MUTEX AddressCreationLock;//AddressCreationLock域是一个守护互斥体锁(guard mutex),用于保护对地址空间的操作。
KSPIN_LOCK HyperSpaceLock;//HyperSpaceLock是一个自旋锁,用于保护进程的超空间
struct _ETHREAD *ForkInProgress;//ForkInProgress指向正在复制地址空间的那个线程,仅当在地址空间复制过程中,此域才会被赋值,在其他情况下为NULL。
ULONG_PTR HardwareTrigger;//HardwareTrigger用于记录硬件错误性能分析次数
PMM_AVL_TABLE PhysicalVadRoot;//PhysicalVadRoot域指向进程的物理VAD的根。它并不总是存在,只有当确实需要映射物理内存时才会被创建。
PVOID CloneRoot;//CloneRoot指向一个平衡树的根,当进程地址空间复制时,此树被创建,创建出来后,一直到进程退出的时候才被销毁。CloneRoot域完全是为了支持fork语义而引入。
PFN_NUMBER NumberOfPrivatePages;//指进程私有页面的数量
PFN_NUMBER NumberOfLockedPages;//指进程被锁住的页面的数量
PVOID Win32Process;//Win32Process域是一个指针,指向由windows子系统管理的进程区域,如果此值不为NULL,说明这是一个windows子系统进程(GUI进程)
struct _EJOB *Job;//对于job域,只有当一个进程属于一个job(作业)的时候,它才会指向一个_EJOB对象。
PVOID SectionObject;//SectionObject域也是一个指针,代表进程的内存去对象(进程的可执行映像文件的内存区对象)
PVOID SectionBaseAddress;// SectionBaseAddress域为该内存区对象的基地址
PEPROCESS_QUOTA_BLOCK QuotaBlock;//QuotaBlock域指向进程的配额块,进程的配额块类型为: EPROCESS_QUOTA_BLOCK
PPAGEFAULT_HISTORY WorkingSetWatch;//WorkingSetWatch域用于监视一个进程的页面错误,一旦启用了页面错误监视功能(由全局变量PsWatchEnabled开关来控制),则每次发生页面错误都会将该页面错误记录到WorkingSetWatch域的WatchInfo成员数组中,知道数组满为止。
HANDLE Win32WindowStation;//Win32WindowStation域是一个进程所属的窗口站的句柄。由于句柄的值是由每个进程的句柄表来决定的,所以,两个进程即使同属于一个窗口站,它们的Win32WindowStation也可能不同,但指向的窗口站对象是相同的。窗口站是由windows子系统来管理和控制的。
HANDLE InheritedFromUniqueProcessId;//InheritedFromUniqueProcessId域说明了一个进程是从哪里继承来的,即父进程的标识符。
PVOID LdtInformation;//LdtInformation用来维护一个进程的LDT(局部描述符表)信息。
PVOID VadFreeHint;//VadFreeHint域指向一个提示VAD(虚拟地址描述符)节点,用于加速在VAD树中执行查找操作。
PVOID VdmObjects;//VdmObjects域指向当前进程的VDM数据区,其类型为VMD_PROCESS_OBJECTS,进程可通过NtVdmControl系统服务来初始化VDM。
PVOID DeviceMap;//DeviceMap域指向进程使用的设备表,通常情况下同一个会话中的进程共享同样的设备表。
PVOID Spare0[3];//Spare0域是一个备用域
union //页表项
{
HARDWARE_PTE PageDirectoryPte;
ULONGLONG Filler;
};
PVOID Session;//Session指向进程所在的系统会话,实际上它是一个指向MM_SESSION_SPACE的指针。\base\ntos\mm\mi.h 中相关的结构体定义
UCHAR ImageFileName[ 16 ];//ImageFileName域包含了进程的映像文件名,仅包含最后一个路径分隔符之后的字符串,不超过16字节。
LIST_ENTRY JobLinks;//JobLinks域是一个双链表节点,通过此节点,一个job中的所有进程构成了一个链表。在windows中,所有的job构成了一个双链表,其链表头为全局变量PspJobList。每个job中的进程又构成了一个双链表。
PVOID LockedPagesList;//LockedPagesList域是一个指向LOCK_HEADER结构的指针,该结构包含了一个链表头,windows通过此链表来记录哪些页面已被锁住(这里所谓的锁住和Mdll中的映射机制有关,本质上就是把用户空间下的内存地址锁定到内核空间中以便访问)
LIST_ENTRY ThreadListHead; //ThreadListHead域是一个双链表的"头结点",该链表中包含了一个进程中的所有"线程"。
PVOID SecurityPort; //SecurityPort域是一个安全端口,指向该进程域lsass.exe进程之间的跨进程通信端口。
PVOID PaeTop; //PaeTop域用于支持PAE内存访问机制。
ULONG ActiveThreads;//ActiveThreads域记录了当前进程有多少活动线程。当该值减为0时,所有的线程将退出,于是进程也退出。
ACCESS_MASK GrantedAccess;//GrantedAccess域包含了进程的访问权限,访问权限是一个"位组合"。 public\sdk\inc\ntpsapi.h 中的宏 PROCESS_XXX
ULONG DefaultHardErrorProcessing;//DefaultHardErrorProcessing域指定了默认的硬件错误处理,默认为1
NTSTATUS LastThreadExitStatus; //LastThreadExitStatus域记录了刚才最后一个线程的退出状态。
PPEB Peb; //Peb域是一个进程的"进程环境块
EX_FAST_REF PrefetchTrace;//PrefetchTrace域是一个快速引用,指向与该进程关联的一个"预取痕迹结构",以支持该进程的预取。
LARGE_INTEGER ReadOperationCount;//ReadOperationCount,WriteOperationCount记录了当前进程NtReadFile和NtWriteFile系统服务被调用的次数,OtherOperationCount记录了除读写操作以外的其他IO服务的次数(文件信息设置.)
LARGE_INTEGER WriteOperationCount;
LARGE_INTEGER OtherOperationCount;
LARGE_INTEGER ReadTransferCount;//ReadTransferCount,WriteTransferCount记录了IO读写操作"完成"的次数,OtherTransferCount记录了除读写操作以外操作完成的次数。
LARGE_INTEGER WriteTransferCount;
LARGE_INTEGER OtherTransferCount;
SIZE_T CommitChargeLimit;
SIZE_T CommitChargePeak;
PVOID AweInfo; //AweInfo域是一个指向AWEINFO结构的指针,其目的是支持AWE(Adress Windowing Extension 地址窗口扩展)
SE_AUDIT_PROCESS_CREATION_INFO SeAuditProcessCreationInfo;//SeAuditProcessCreationInfo域包含了创建进程时指定的进程映像全路径名
MMSUPPORT Vm;//Vm域是windows为每个进程管理虚拟内存的重要数据结构成员,其类型为MMSUPPORT, \base\ntos\inc\ps.h 中有相关定义
LIST_ENTRY MmProcessLinks; //MmProcessLinks域代表一个双链表节点,所有拥有自己地址空间的进程都将加入到一个双链表中,链表头是全局变量MmProcessList
ULONG ModifiedPageCount; //ModifiedPageCount域记录了该进程中已修改的页面的数量,即"脏页面数量",这和缓存的读写有关。
ULONG JobStatus; //49. ULONG JobStatus
JobStatus域记录了进程所属job的状态。
union //Flags域包含了进程的标志位,这些标志位反映了进程的当前状态和配置。 \base\ntos\inc\ps.h 中的宏定义 PS_PROCESS_FLAGS_XXX
{
ULONG Flags;
struct {
ULONG CreateReported : 1;
ULONG NoDebugInherit : 1;
ULONG ProcessExiting : 1;
ULONG ProcessDelete : 1;
ULONG Wow64SplitPages : 1;
ULONG VmDeleted : 1;
ULONG OutswapEnabled : 1;
ULONG Outswapped : 1;
ULONG ForkFailed : 1;
ULONG Wow64VaSpace4Gb : 1;
ULONG AddressSpaceInitialized : 2;
ULONG SetTimerResolution : 1;
ULONG BreakOnTermination : 1;
ULONG SessionCreationUnderway : 1;
ULONG WriteWatch : 1;
ULONG ProcessInSession : 1;
ULONG OverrideAddressSpace : 1;
ULONG HasAddressSpace : 1;
ULONG LaunchPrefetched : 1;
ULONG InjectInpageErrors : 1;
ULONG VmTopDown : 1;
ULONG ImageNotifyDone : 1;
ULONG PdeUpdateNeeded : 1; // NT32 only
ULONG VdmAllowed : 1;
ULONG SmapAllowed : 1;
ULONG CreateFailed : 1;
ULONG DefaultIoPriority : 3;
ULONG Spare1 : 1;
ULONG Spare2 : 1;
};
};
NTSTATUS ExitStatus;//ExitStatus域包含了进程的退出状态,从进程的退出状态通常可以获知进程非正常退出的大致原因。反映退出状态的一些宏定义位于 public\sdk\inc\ntstatus.h
USHORT NextPageColor;//NextPageColor域用于物理页面分配算法。
union
{
struct
{
UCHAR SubSystemMinorVersion;
UCHAR SubSystemMajorVersion;
};
USHORT SubSystemVersion;
};
UCHAR PriorityClass;//PriorityClass域是一个单字节值,它说明了一个进程的优先级程度
MM_AVL_TABLE VadRoot;//VadRoot域指向一个平衡二叉树的根,用于管理该进程的虚拟地址空间。
ULONG Cookie;//Cookie域存放的是一个代表该进程的随机值,当第一次通过NtQueryInformationProcess函数获取此Cookie值的时候,系统会生成一个随机值,以后就用此值代表此进程
} EPROCESS, *PEPROCESS;
游戏修改器原理
从PCB的代码段来看,一个进程的所有相关信息均被记录在该结构体对象里。如果进程间要能相互访问,需要OS提供相应的方法来让不同进程之间能够相互访问。游戏修改器对游戏进行修改,可以理解为:一个进程对另一个进程的内存空间访问以及修改,因此只需要知道操作系统提供的进程数据访问方法便可以进行数据修改,具体如图4所示,改图1-6表示了大概的数据访问和修改的步骤。
图4单机游戏修改器修改数据原理示意图
示意代码:
//--------------------------------------------------------------------------//
HWND hWnd =::FindWindow("CRHClass",NULL); //得到窗口句柄
if(hWnd ==FALSE)
MessageBox("游戏没有运行!");
else
{
GetWindowThreadProcessId(hWnd,&hProcId); // 从窗口句柄得到进程ID
HANDLE nOK =OpenProcess(PROCESS_ALL_ACCESS|PROCESS_TERMINATE|PROCESS_VM_OPERATION|PROCESS_VM_READ|
PROCESS_VM_WRITE,FALSE,hProcId); //打开进程并得到读与权限
if(nOK ==NULL)
MessageBox("打开进程时出错");
else
{
//0047EB17
WriteProcessMemory(nOK,(LPVOID)0x0047EB17,&A1,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0047EB18,&A2,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0047EB19,&A3,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0047EB1A,&A4,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0047EB1B,&A5,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0047EB1C,&A6,1,NULL);
//00506950
WriteProcessMemory(nOK,(LPVOID)0x00506950,&B1,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x00506951,&B2,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x00506952,&B3,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x00506953,&B4,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x00506954,&B5,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x00506955,&B6,1,NULL);
//第二句
WriteProcessMemory(nOK,(LPVOID)0x00506956,&C1,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x00506957,&C2,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x00506958,&C3,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x00506959,&C4,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0050695A,&C5,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0050695B,&C6,1,NULL);
//最后一句
WriteProcessMemory(nOK,(LPVOID)0x0050695C,&D1,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0050695D,&D2,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0050695E,&D3,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x0050695F,&D4,1,NULL);
WriteProcessMemory(nOK,(LPVOID)0x00506960,&D5,1,NULL);
CloseHandle(nOK); //关闭进程句柄
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//读取并修改内力值
DWORD hProcId;
HWND hWnd =::FindWindow("CRHClass",NULL);
if(hWnd ==FALSE)
MessageBox("No");
else
{
GetWindowThreadProcessId(hWnd,&hProcId);
HANDLE nOK =OpenProcess(PROCESS_ALL_ACCESS|PROCESS_TERMINATE|PROCESS_VM_OPERATION|PROCESS_VM_READ|
PROCESS_VM_WRITE,FALSE,hProcId);
if(nOK ==NULL)
MessageBox("ProcNo!");
else
{
DWORD buf1;
DWORD write;
BOOL OK=ReadProcessMemory(nOK,(LPCVOID)0x00506961,(LPVOID)&buf1,4,NULL); //读取我们保存EDX中的基础
if(OK ==TRUE)
{
write =buf1+0x000003F4; //得到内力值的地址
DWORD Writeed =0x00; //要修改的数值
BOOL B =WriteProcessMemory(nOK,(LPVOID)write,&Writeed,1,NULL);
if(B==FALSE)
MessageBox("WriteNo");
}
}
CloseHandle(nOK);
}
其他游戏修改器种类
1、动作式,所谓动作式,就是指用API发命令给窗口或API控制鼠标、键盘等,使游戏里的人物进行流动或者攻击。可以直接通过脚本软件开发。非常简单。
2、加速式,这种外挂修改时钟频率达到加速的目的。 具体待补充。
3、封包式,这种外挂先截取封包,应用层协议分析,后修改,再转发,适用于大多数网络游戏,像WPE及一些网络游戏外挂都是用这种方式写成的,涉及apihook技术,winsock技术等。
推荐阅读