声明:该教程写给有一定外挂编程基础且热爱编程技术的朋友共同学习。
说明:1.教程本身只是作为技术交流探讨,没有其他目的
2.以完美国际187版游戏为例,不具有任何针对意味
3.在广海里学了很多东西,希望在广海里留下一些什么
见解:1.虽然注入技术已经不再是什么“时尚”的技术,但是在编程应用领域,却依然得到很广泛的应用。虽然本文是以外挂编程为例来说明的,但是希望热爱编程的朋友能够以此作为引申,达到抛砖引玉的作用。
2.注入是一种技术,也是一种思想;注入的手段并不是一般说的三种方法,凡是能将"可执行代码"或者“指令”写到目标进程空间里执行,都可以称之为注入技术。
3.本文仅做远程注入和全局DLL注入技术的介绍
计算机发展历程从单道技术、多道技术,到后来的分时技术,为了解决资源的高效分配和CPU利用率的提高,引入进程的概念可以说是一个操作系统的一个里程碑。这里不多引申,主要是想说明,系统里各个进程的运行是相互独立的,每个进程独享自己的进程空间。在单CPU上,微观上各个进程串行运作,但是宏观上表现为各个进程是并行处理。进程只是 一个“容器”,它负责向系统申请资源,真正的执行操作的是线程。这里仅仅是简单的说说...
在进程加载的时候,代码被映射到进程的地址空间中,然后利用各种分页分段技术,在运行期将代码映射到具体的物理内存上,方可执行。Windows下进程的地址空间在逻辑上是相互独立的,而在物理上却是相互重叠的,所谓的重叠是指在一块内存区域可能被多个进程同时使用(共享内存便是这个原理)。
上面说了进程的地址空间是相互独立的,那么怎么使本进程的代码(比如一个CALL函数)在目标进程里运行呢,那就需要注入了。将代码利用一定的技术手段加载进目标进程空间,然后触发使之执行,这便是代码注入的雏形。当然,后来的技术做了很多“变种”,以至于这种技术被病毒技术得到了很好的利用。
明白了上面的原理,就可以自己进行我们下面的这个实例了。
我们调用完美游戏的CALL,必须要吧代码写进完美游戏进程地址空间,有关于这一系列的操作,Windows提供了相应的API,我也把它封装了成函数,中间过程的一些重要的细节等会解剖。
这是一种比较普遍的做法,但是也有着明显的缺点,频繁的注入比较容易产生内碎片的大量累积。当然,有改进的做法,比如固定申请的地址,一次申请,多次利用,这里不在多说。
这里仅作为技术探讨,列出主要的实现步骤。分二层实现,稍作了封装
我们以带参数的选怪CALL为例:
void CallSelectMonster (long MonsterSn) { DWORD dwAddr = 0x59B8B0; _asm{ pushad mov edi , MonsterSn push edi mov eax , dword ptr [0x950954] mov eax , dword ptr [eax] mov ecx , dword ptr [eax + 0x20] add ecx , 0xec call dwAddr popad } }
这里CALL存在问题,等会解决
1>. 第一层调用:选怪函数,里面封装了调用注入函数的操作。
void CSkill::SelectMonster(DWORD ProcessId, long MonsterSn) { long * m_pGuaiId = & MonsterSn ; //调用注入函数 InjectRemoteFunc(ProcessId,CallSelectMonster,m_pGuaiId,sizeof(*m_pGuaiId)); }
2>.封装注入代码并调用执行的操作。
<注:这个函数是师傅的,封装的比较好,当然写法不一,但步骤都一样:开辟空间,写入,然后调用执行之>
//封装远程注入的函数 //参数 1. 进程ID //参数 2. 被注入函数指针<函数名> //参数 3. 参数 //参数 4. 参数长度 BOOL InjectRemoteFunc(DWORD dwProcId,LPVOID mFunc, LPVOID pRemoteParam, DWORD ParamSize) { HANDLE hProcess; LPVOID ThreadAdd; LPVOID ParamAdd = NULL; HANDLE hThread = NULL; DWORD lpNumberOfBytes; BOOL BO; ThreadAdd = mFunc; hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwProcId);//打开被注入的进程 ThreadAdd = VirtualAllocEx(hProcess,NULL,4096,MEM_COMMIT,PAGE_EXECUTE_READWRITE); BO = WriteProcessMemory(hProcess,ThreadAdd,mFunc,4096, &lpNumberOfBytes); //写入函数地址 if(ParamSize!=0) { ParamAdd = VirtualAllocEx(hProcess,NULL,ParamSize, MEM_COMMIT, PAGE_READWRITE); BO = WriteProcessMemory(hProcess, ParamAdd, pRemoteParam, ParamSize, &lpNumberOfBytes); //写入参数地址 } hThread = CreateRemoteThread(hProcess, NULL,0,(LPTHREAD_START_ROUTINE)ThreadAdd, ParamAdd, 0,&lpNumberOfBytes); //创建远程线程 WaitForSingleObject(hThread, INFINITE);//等待线程结束 VirtualFreeEx(hProcess, ThreadAdd, 4096, MEM_RELEASE); if(ParamSize!=0) { VirtualFreeEx(hProcess, ParamAdd, ParamSize, MEM_RELEASE); //释放申请的地址 } CloseHandle(hThread); CloseHandle(hProcess); return TRUE; }
会C++的朋友可以马上要它运行起来,哪怕是控制台下,只要获得了窗口句柄,赋值一个比较合适的怪物ID,做个循环加,都可以实现选怪了。其他附加的代码我不贴出来了,前面说了,这是写给有一定基础的外挂编程的朋友学习讨论的。
我在前面留了一个小的bug,这也是我主要写这篇教程的原因之一。在处理这个问题的时候,我也郁闷了好一阵,但最后还是解决了。如果现在能看出来bug的朋友,并知道为什么会有这样bug的朋友,可以不用看了,呵呵。
1>为了说明这个问题,我想引入msdn里的一些说明:
首先,创建远程线程的函数原型:
HANDLE CreateRemoteThread( HANDLE hProcess, // handle to process LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD SIZE_T dwStackSize, // initial stack size LPTHREAD_START_ROUTINE lpStartAddress, // thread function LPVOID lpParameter, // thread argument DWORD dwCreationFlags, // creation option LPDWORD lpThreadId // thread identifier );
看第四个参数,也就是回调函数(由用户编写,但调用权限由操作系统主宰的函数) ,当调用了这个创建远程线程的函数,只要这个倒数第二个参数设置为0,也就是dwCreationFlags=0,那么回调函数将会立即执行。
现在看看回调函数的原型:
DWORD WINAPI ThreadProc( LPVOID lpParameter // thread data );
发现该函数只接收一个LPVOID类型的参数,那么如果这个参数不是指针行不行呢,当然并行了,呵呵,现在明白了吧,前面的带参数CALL相当于这个“回调函数”,只是样子变了一下而已,这个思想很重要 那么现在改下CALL原型,变成这样就OK了
void CallSelectMonster (long * MonsterSn) { long sn = *MonsterSn; DWORD dwAddr = 0x59B8B0; _asm{ pushad mov edi , sn push edi mov eax , dword ptr [0x950954] mov eax , dword ptr [eax] mov ecx , dword ptr [eax + 0x20] add ecx , 0xec call dwAddr popad } }
到这里,都明白了吧,前面CALL的原型就是错误的,也就是不匹配,当调用创建远程线程时,参数传递就出现问题了。
作为引申,我在说明一点,观察另外一个函数的原型:
HMODULE LoadLibrary( LPCTSTR lpFileName // file name of module );
比较一下
DWORD WINAPI ThreadProc( LPVOID lpParameter // thread data );
原型一样,那么可以用LoadLibrary来作这个“回调函数”,又一个变形的回调函数,那么就可以轻松的将一个DLL注入的目标进程了,并且在DLL初始化的时候做很多事情。
这里我不再贴代码,很分散,我只是想说下实现的步骤和思想。这种方式可以用 hook + DLL来实现:
1>.安装一个钩子,将钩子回调函数写在DLL里,在回调函数里new PerfectDialog() ,创建一个窗口,方便操作。
2>.安装这个钩子,消息触发回调函数,那么指定的进程就会加载这个DLL了,这个进程当然就是你所要操作的进程。这个时候,DLL里代码操作的数据就和目标进程空间里的数据通用了。貌似很爽,调用CALL就好像调用自己进程空间的CALL一样,不用在进程注入了。但是,也会引发一些其他的问题,应该就是对于异常的考虑。比如,用指针读相关数据,指针+偏移,在读相关数据,这些操作都是要进行异常判断。这里不在用ReadProcessMemory WriteProcessMemory函数,那么,这些操作你就是考虑自己封装,理论上就像是模拟一个自己进程空间的读写函数。呵呵,不是很难,但是也需要一些技巧。我写的就不贴了,因为我觉得还需要改进下。
就说这么多了,希望对各位朋友是个帮助。
ps:1.源码就不发了,这个只是工程文件的一部分代码,发了也会干扰视听。这里只想学习讨论这方面的技术而已
2.今天对我来说是个特殊的日子,所以写下此漏文。
3.我是个热爱技术的人,希望和大家今后多交流学习。