远程注入和全局DLL注入

声明:该教程写给有一定外挂编程基础且热爱编程技术的朋友共同学习。

说明:1.教程本身只是作为技术交流探讨,没有其他目的

2.以完美国际187版游戏为例,不具有任何针对意味

3.在广海里学了很多东西,希望在广海里留下一些什么

见解:1.虽然注入技术已经不再是什么“时尚”的技术,但是在编程应用领域,却依然得到很广泛的应用。虽然本文是以外挂编程为例来说明的,但是希望热爱编程的朋友能够以此作为引申,达到抛砖引玉的作用。

2.注入是一种技术,也是一种思想;注入的手段并不是一般说的三种方法,凡是能将"可执行代码"或者“指令”写到目标进程空间里执行,都可以称之为注入技术。

3.本文仅做远程注入和全局DLL注入技术的介绍

1 一点引申

计算机发展历程从单道技术、多道技术,到后来的分时技术,为了解决资源的高效分配和CPU利用率的提高,引入进程的概念可以说是一个操作系统的一个里程碑。这里不多引申,主要是想说明,系统里各个进程的运行是相互独立的,每个进程独享自己的进程空间。在单CPU上,微观上各个进程串行运作,但是宏观上表现为各个进程是并行处理。进程只是 一个“容器”,它负责向系统申请资源,真正的执行操作的是线程。这里仅仅是简单的说说...

在进程加载的时候,代码被映射到进程的地址空间中,然后利用各种分页分段技术,在运行期将代码映射到具体的物理内存上,方可执行。Windows下进程的地址空间在逻辑上是相互独立的,而在物理上却是相互重叠的,所谓的重叠是指在一块内存区域可能被多个进程同时使用(共享内存便是这个原理)。

2 注入的原理

上面说了进程的地址空间是相互独立的,那么怎么使本进程的代码(比如一个CALL函数)在目标进程里运行呢,那就需要注入了。将代码利用一定的技术手段加载进目标进程空间,然后触发使之执行,这便是代码注入的雏形。当然,后来的技术做了很多“变种”,以至于这种技术被病毒技术得到了很好的利用。

明白了上面的原理,就可以自己进行我们下面的这个实例了。

我们调用完美游戏的CALL,必须要吧代码写进完美游戏进程地址空间,有关于这一系列的操作,Windows提供了相应的API,我也把它封装了成函数,中间过程的一些重要的细节等会解剖。

3 远程注入

这是一种比较普遍的做法,但是也有着明显的缺点,频繁的注入比较容易产生内碎片的大量累积。当然,有改进的做法,比如固定申请的地址,一次申请,多次利用,这里不在多说。

这里仅作为技术探讨,列出主要的实现步骤。分二层实现,稍作了封装

我们以带参数的选怪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; 
} 

4 分析

会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初始化的时候做很多事情。

5 又一种注入DLL的方式,全局DLL注入

这里我不再贴代码,很分散,我只是想说下实现的步骤和思想。这种方式可以用 hook + DLL来实现:

1>.安装一个钩子,将钩子回调函数写在DLL里,在回调函数里new PerfectDialog() ,创建一个窗口,方便操作。

2>.安装这个钩子,消息触发回调函数,那么指定的进程就会加载这个DLL了,这个进程当然就是你所要操作的进程。这个时候,DLL里代码操作的数据就和目标进程空间里的数据通用了。貌似很爽,调用CALL就好像调用自己进程空间的CALL一样,不用在进程注入了。但是,也会引发一些其他的问题,应该就是对于异常的考虑。比如,用指针读相关数据,指针+偏移,在读相关数据,这些操作都是要进行异常判断。这里不在用ReadProcessMemory WriteProcessMemory函数,那么,这些操作你就是考虑自己封装,理论上就像是模拟一个自己进程空间的读写函数。呵呵,不是很难,但是也需要一些技巧。我写的就不贴了,因为我觉得还需要改进下。

就说这么多了,希望对各位朋友是个帮助。

ps:1.源码就不发了,这个只是工程文件的一部分代码,发了也会干扰视听。这里只想学习讨论这方面的技术而已

2.今天对我来说是个特殊的日子,所以写下此漏文。

3.我是个热爱技术的人,希望和大家今后多交流学习。