VC下提前注入进程的一些方法2——远线程带参数

        在前一节中介绍了通过远线程不带参数的方式提前注入进程,现在介绍种远线程携带参数的方法。(转载请指明出处)

1.2 执行注入的进程需要传信息给被注入进程

        因为同样采用的是远线程注入,所以大致的思路是一样的,只是在细节上要注意一些处理。总体来说分为以下几个步骤:

        1 将需要传递的信息写入被注入进程的地址空间。

        2 将远线程函数体写入被注入进程的空间。

        3 在被注入进程中执行该远线程函数,让该线程利用我们之前写入的参数完成任务。

        在被注入进程的地址空间中写入“需要传递”的信息不存在什么问题,因为该信息是”死的“”数据“,我们写入什么内容就是什么内容,它就是二进制数据。但是写入函数执行体就存在一定的问题。首先我们要考虑用什么语言来写这个函数?我是VC程序员,当然优先选择C++/C。可是使用这些语言往往会存在问题,因为我们不知道编译器对我们的代码可能做了什么手脚。下面我来验证下

    typedef struct _RemoteThreadRountineParam_
    {
        //LPLoadLibrary lpLoadLibraryW;
        //LPGetProcAddr lpGetProcAddress;
        WCHAR wszDllPath[MAX_PATH];
        CHAR szFuncName[MAX_FUNCNAMELENGTH];
        HANDLE hEvent;
    }RemoteThreadRountineParam,*pRemoteThreadRountineParam;
DWORD WINAPI RemoteThreadRoutine_Error( LPVOID lpParam )
    {
        if ( NULL == lpParam )
        {
            return 0;
        }
        pRemoteThreadRountineParam lpRmtParam = (pRemoteThreadRountineParam) lpParam;

        if ( NULL == lpRmtParam->lpLoadLibraryW )
        {
            return 0;
        }

        if ( NULL == lpRmtParam->lpGetProcAddress )
        {
            return 0;
        }

        HMODULE hHookDll = LoadLibraryW(lpRmtParam->wszDllPath);
        if ( NULL == hHookDll )
        {
            return 0;
        }

        LPExportFun lpExportFunAddr = (LPExportFun)GetProcAddress( hHookDll, lpRmtParam->szFuncName );
        if ( NULL == lpExportFunAddr )
        {
            return 0;
        }

        lpExportFunAddr( lpRmtParam->hEvent );

        return 0;
    }

        回想前一节中,我们将DLL的绝对路径写入被注入进程的空间作为远线程的唯一参数,而本节的远线程需要很多参数,所以我们要定义一个结构体RemoteThreadRountineParam。它包含的成员是:要注入的DLL的绝对路径、这DLL中的导出函数名,以及这个导出函数需要的参数——Event句柄。远线程执行的函数体是RemoteThreadRoutine_Error,其参数是一个指向RemoteThreadRountineParam结构体对象的一个指针,正如其名字——它是Error的。其执行的逻辑也是很简单的:加载DLL,寻找导出函数和执行导出函数。之后我们所有带参数的注入逻辑都将采用这个最基本的处理流程,只是细节处理上存在一定的区别。

do {
                // 写入线程例程代码
                // 分配内存空间
                pBufferRemoteFun = VirtualAllocEx( hProcess, NULL, dwFunMemSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE );       
                if ( NULL == pBufferRemoteFun ) {
                    break;
                }
                // 将信息写入傀儡进程的内存地址空间
                if ( FALSE == WriteProcessMemory( hProcess, pBufferRemoteFun, pRmtRoutine, dwFunMemSize, NULL ) ) {
                    break;
                }

                do {
                    pBufferParam = VirtualAllocEx( hProcess, NULL, dwParamMemSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE );
                    if ( NULL == pBufferParam ) {
                        break;
                    }
                    if ( FALSE == WriteProcessMemory( hProcess, pBufferParam, &RmtThdRtParam, dwParamMemSize, NULL ) ) {
                        break;
                    }

                    do {
                        // 注入线程
                        pRemoteThread = CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pBufferRemoteFun, pBufferParam, 0, NULL );
                        if ( NULL == pRemoteThread ) {
                            break;
                        }

        这段逻辑分别对应于刚介绍的三个步骤。其中dwFunMemSize是我们定义的一个”足够大“的空间大小,因为我这儿没有计算准确的函数执行体大小(其实我也不知道怎么去计算这个大小)。因为我们的函数执行体代码是要执行的,所以我们申请的空间是具有EXECUTE属性的。pBufferRemoteFun是指向远线程函数执行体的在”远程“的空间。pRmtRoutine是指向远线程函数执行体的在”本地“的空间。其他没有什么好介绍的,我们将主要的注意力放在pRmtRoutine。

        最简单的方式是

char* pRmtRoutine = (char*)RemoteThreadRoutine_Error;

        但是我们debug的结果是 “‘0x000a432c’指令引用的‘0x0000a432c’内存。该内存不能为‘written’”。可以见得我们写入的远线程代码存在问题。现在我们用windbg调试下。在调试前,我们先调整下VC代码为

// 注入线程
                        pRemoteThread = CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pBufferRemoteFun, pBufferParam, 0, NULL );
                        if ( NULL == pRemoteThread ) {
                            break;
                        }
                        ResumeThread( hThread );
                        // 等待远线程激活事件
                        if ( WAIT_OBJECT_0 != WaitForSingleObject( hEvent, 10 * 1000 ) ) {
                            // 等待出错,退出进程
                            //::TerminateProcess( hProcess, 0 );
                        }
                        else {
                            // 等待成功,恢复进程
                            ResumeThread( hThread );
                        }

        注意此处,我不会在线程执行失败后立即TerminateProcess,否则我们windbg准备调试被注入进程时,被注入进程可能已经就被杀掉了。而且因为之前我们是以挂起方式创建被注入进程的,所以在执行完创建远线程后,要ResumeThread主线程。否则我们在远线程挂了后,windbg没法挂到任何一个线程上。调试的过程是:

       1 用VC在CreateRemoteThread上下断点,F5,断到这个函数执行前。记下pBufferRemoteFun的值。

       2 用windbg附加到被注入进程上。

       3 在VC中F5,让被注入进程出现错误,以让windbg捕获。

       4 在windbg中F5。

        会出现以下信息

(d20.8d8): Break instruction exception - code 80000003 (first chance)
eax=002d1eb4 ebx=7ffdb000 ecx=00000003 edx=00000008 esi=002d1f48 edi=002d1eb4
eip=7c92120e esp=0007fb20 ebp=0007fc94 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
_no_process_!DbgBreakPoint:
7c92120e cc              int     3

        看不出什么信息。但是我们可以借助pBufferRemoteFun的值(假设为0x000a0000 ),在windbg命令框中输入0x000a0000 0x000a0100。出现如下信息

000a0000 e927430000      jmp     000a432c
000a0005 e902360000      jmp     000a360c
000a000a e9ad6d0000      jmp     000a6dbc
……

        这是神马?这是debug环境下增量编译(incremental linking)的一种表现。这儿说说增量编译,增量编译如同在“调用”和“函数执行逻辑”之间插入一个“地址转换层”。比如我们有个函数A,我们调用A的汇编是Call 0x00ABCDEF,那么修改A函数代码后编译,这个0x00ABCDEF地址会发生改变。因为Call指令分为两步,其中第二步是jmp到A函数逻辑的入口点,jmp的偏移是需要计算的。于是我们频繁修改A的函数逻辑,会导致频繁的计算A函数逻辑偏移地址(想想整个PE文件中所有调用都要再算一次jmp偏移是不是很浪费)。于是一种解决方案是,调用A时就Call一个固定地址,该地址指令是jmp到一个固定的地址,这个地址保存的是真实调用A的代码。这样每次编译只用修改“转换层”中的jmp偏移即可。

        回到我们的问题,用u 0x000a432c可以发现这个内存空间不存在汇编代码。因为我们写错内容了,我们要写入的远线程函数的逻辑代码。那么我们将jmp过滤掉。

        // JMP过渡
        if ( (char)0xE9 == *pRmtRoutine ) {
            DWORD dwOffset = 0;
            memcpy_s( &dwOffset, sizeof(DWORD), pRmtRoutine + 1, sizeof(DWORD) );
            // 偏移,1为0xE9,4为dwoffset大小
            pRmtRoutine = pRmtRoutine + 1 + 4 + dwOffset;
        }

        再试一下我们程序。还是报错,这次错误是“'0x000a0053'指令引用的'0x00424788'内存。该内存不能为'read'”。使用刚才的调试方法,我们发现我们注入的代码如下

000a0000 55              push    ebp
000a0001 8bec            mov     ebp,esp
000a0003 81ece4000000    sub     esp,0E4h
000a0009 53              push    ebx
000a000a 56              push    esi
000a000b 57              push    edi
000a000c 8dbd1cffffff    lea     edi,[ebp-0E4h]
000a0012 b939000000      mov     ecx,39h
000a0017 b8cccccccc      mov     eax,0CCCCCCCCh
000a001c f3ab            rep stos dword ptr es:[edi]
000a001e 837d0800        cmp     dword ptr [ebp+8],0
000a0022 7507            jne     000a002b
000a0024 33c0            xor     eax,eax
000a0026 e983000000      jmp     000a00ae
000a002b 8b4508          mov     eax,dword ptr [ebp+8]
000a002e 8945f8          mov     dword ptr [ebp-8],eax
000a0031 8b45f8          mov     eax,dword ptr [ebp-8]
000a0034 833800          cmp     dword ptr [eax],0
000a0037 7504            jne     000a003d
000a0039 33c0            xor     eax,eax
000a003b eb71            jmp     000a00ae
000a003d 8b45f8          mov     eax,dword ptr [ebp-8]
000a0040 83780400        cmp     dword ptr [eax+4],0
000a0044 7504            jne     000a004a
000a0046 33c0            xor     eax,eax
000a0048 eb64            jmp     000a00ae
000a004a 8b45f8          mov     eax,dword ptr [ebp-8]
000a004d 83c008          add     eax,8
000a0050 8bf4            mov     esi,esp
000a0052 50              push    eax
000a0053 ff1588474200    call    dword ptr ds:[424788h]
000a0059 3bf4            cmp     esi,esp
000a005b e8c0bbffff      call    0009bc20
000a0060 8945ec          mov     dword ptr [ebp-14h],eax
000a0063 837dec00        cmp     dword ptr [ebp-14h],0
000a0067 7504            jne     000a006d
000a0069 33c0            xor     eax,eax
000a006b eb41            jmp     000a00ae
000a006d 8b45f8          mov     eax,dword ptr [ebp-8]
000a0070 0510020000      add     eax,210h
000a0075 8bf4            mov     esi,esp
000a0077 50              push    eax
000a0078 8b4dec          mov     ecx,dword ptr [ebp-14h]
000a007b 51              push    ecx
000a007c ff1504474200    call    dword ptr ds:[424704h]
000a0082 3bf4            cmp     esi,esp
000a0084 e897bbffff      call    0009bc20
000a0089 8945e0          mov     dword ptr [ebp-20h],eax
000a008c 837de000        cmp     dword ptr [ebp-20h],0
000a0090 7504            jne     000a0096
000a0092 33c0            xor     eax,eax
000a0094 eb18            jmp     000a00ae
000a0096 8bf4            mov     esi,esp
000a0098 8b45f8          mov     eax,dword ptr [ebp-8]
000a009b 8b8850020000    mov     ecx,dword ptr [eax+250h]
000a00a1 51              push    ecx
000a00a2 ff55e0          call    dword ptr [ebp-20h]
000a00a5 3bf4            cmp     esi,esp
000a00a7 e874bbffff      call    0009bc20
000a00ac 33c0            xor     eax,eax
000a00ae 5f              pop     edi
000a00af 5e              pop     esi
000a00b0 5b              pop     ebx
000a00b1 81c4e4000000    add     esp,0E4h
000a00b7 3bec            cmp     ebp,esp
000a00b9 e862bbffff      call    0009bc20
000a00be 8be5            mov     esp,ebp
000a00c0 5d              pop     ebp
000a00c1 c20400          ret     4

        我们查看之前报错的0x000a0053行call    dword ptr ds:[424704h],这个函数地址不是被注入进程空间的函数地址,像之后000a007c ff1504474200    call    dword ptr ds:[424704h]也是会报错的。那么这两个函数是啥?我在VC中Alt+8查看远线程函数的汇编代码,可以发现call    dword ptr ds:[424704h]对应于LoadLibraryW这个函数。0x00424704h保存的是0x7c80aeeb。我们用windbg加载并运行注入进程的PE文件,break后查看相关地址命令

0:002> u 0x7c80aeeb
kernel32!LoadLibraryW:
7c80aeeb 8bff            mov     edi,edi
7c80aeed 55              push    ebp
7c80aeee 8bec            mov     ebp,esp
7c80aef0 6a00            push    0
7c80aef2 6a00            push    0
7c80aef4 ff7508          push    dword ptr [ebp+8]
7c80aef7 e8f96bffff      call    kernel32!LoadLibraryExW (7c801af5)
7c80aefc 5d              pop     ebp
0:002> u 0x7c801af5
kernel32!LoadLibraryExW:
7c801af5 e9f2e7cf84      jmp     015002ec
7c801afa 807ce8d509      cmp     byte ptr [eax+ebp*8-2Bh],9
7c801aff 0000            add     byte ptr [eax],al
7c801b01 33ff            xor     edi,edi
7c801b03 897dd8          mov     dword ptr [ebp-28h],edi
7c801b06 897dd4          mov     dword ptr [ebp-2Ch],edi
7c801b09 897de0          mov     dword ptr [ebp-20h],edi
7c801b0c 897de4          mov     dword ptr [ebp-1Ch],edi

        可以想到,0x7c80aeeb是Kernel32.dll文件在该进程中LoadLibrary的函数入口地址。所以我们call    dword ptr ds:[424704h]时,被注入进程中424704h保存的是啥是不确定的。但是,如我在前一节介绍的,windows程序加载kernel32.dll的基地址一般是一样的,于是我们要是将0x7c80aeeb这个值直接传给远线程,应该就可以了。同样的问题存在于我们之前对GetProcAddress的调用,于是我们将这些函数地址以参数形式传入被注入进程。

RemoteThreadRountineParam RmtThdRtParam;
        RmtThdRtParam.lpGetProcAddress = GetProcAddress;
        RmtThdRtParam.lpLoadLibraryW = LoadLibraryW;
        wmemset( RmtThdRtParam.wszDllPath, 0, MAX_PATH );
        wcscpy_s( RmtThdRtParam.wszDllPath, MAX_PATH, lpDllPath );
        std::string strFuncName = "ExportFun";
        memset( RmtThdRtParam.szFuncName, 0 , MAX_FUNCNAMELENGTH );
        memcpy_s( RmtThdRtParam.szFuncName, MAX_FUNCNAMELENGTH, strFuncName.c_str(), strFuncName.length() );

        远线程的代码改为

        HMODULE hHookDll = (lpRmtParam->lpLoadLibraryW)(lpRmtParam->wszDllPath);
        if ( NULL == hHookDll )
        {
            return 0;
        }

        LPExportFun lpExportFunAddr = (LPExportFun)(lpRmtParam->lpGetProcAddress)( hHookDll, lpRmtParam->szFuncName );
        if ( NULL == lpExportFunAddr )
        {
            return 0;
        }

        lpExportFunAddr( lpRmtParam->hEvent );

        F5。还是报错。这次的错误是“'0x0009bc20'指令引用的'0x0009bc20'内存。该内存不能为'written'”。继续使用之前的调试方法,发现我们注入的代码中有如下一行

000a0053 8b4df8          mov     ecx,dword ptr [ebp-8]
000a0056 8b11            mov     edx,dword ptr [ecx]
000a0058 ffd2            call    edx
000a005a 3bf4            cmp     esi,esp
000a005c e8bfbbffff      call    0009bc20
000a0061 8945ec          mov     dword ptr [ebp-14h],eax
000a0064 837dec00        cmp     dword ptr [ebp-14h],0
000a0068 7504            jne     000a006e

        我们再在VS中查看我们的远线程反汇编代码有如下

00415A2C E8 BF BB FF FF   call        @ILT+1515(__RTC_CheckEsp) (4115F0h) 

        E8BFBBFFFF这条指令是引起被注入进程崩溃的原因,这指令是RTC检查函数,默认情况下VC会给我们的代码做些手脚,这个就是个例子。我们对远线程代码关闭RTC检查。

#pragma runtime_checks( "scu", off )
  DWORD WINAPI RemoteThreadRoutine_Error( LPVOID lpParam )
    {
      ……
    }
#pragma runtime_checks( "scu", restore )

        运行之,OK了。         这个过程很忐忑,但是如果不想研究这个,可以选择内嵌汇编方式。因为RTC检查不会在Release版本中做,所以我们可以将远线程函数本地执行一次,在函数的入口处int 3一下,然后用windbg或ollydbg启动之,断在函数入口点,然后我们把其汇编东东扒拉下来就行了。当然会写汇编的同学就直接动手写汇编代码就行了。

__declspec(naked) DWORD WINAPI RemoteThreadRoutineASM( LPVOID lpParam )
    {
        __asm
        {
            push esi
            // 检测指针参数
            mov     esi, [esp+8]
            pushad
            pushfd
            test    esi, esi
            jz      short End
            // 检测参数第一个成员
            mov     eax, [esi+4]
            test    eax, eax
            jz      short End
            // 检测参数第二个成员
            mov     eax, [esi]
            test    eax, eax
            jz      short End
            
            lea     ecx, [esi+8]
            push    ecx
            call    eax

            test    eax,eax
            jz      short End

            lea     edx, [esi+210h]
            push    edx
            push    eax
            mov     eax, [esi+4]
            call    eax

            test    eax,eax
            jz      short End

            mov     ecx, [esi+250h]
            push    ecx
            call    eax

End:
            xor     eax, eax
            popfd
            popad
            pop     esi
            retn    8

        }
        
    }

        一定要加__declspec(naked),否则起不来哦!         (转载请指明出处)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏C/C++基础

g++入门教程

g++是GNU开发的C++编译器,是GCC(GNU Compiler Collection)GNU编译器套件的组成部分。另外,gcc是GNU的C编译器。

1.6K30
来自专栏方亮

VC下提前注入进程的一些方法3——修改程序入口点

        前两节中介绍了通过远线程进行注入的方法。现在换一种方法——修改进程入口点。(转载请指明出处)

19630
来自专栏tkokof 的技术,小趣及杂念

编程小知识之switch语句

switch 语句大家都不陌生,平时在遇到较多相同的 if 语句判断时就会想到他,举个简单的例子:

11710
来自专栏MasiMaro 的技术博文

汇编debug与masm命令

汇编语言这块是我之前写在网易博客上的,不过那个账号基本已经作废了,所以现在抽个时间把当时的博客搬到CSDN上。 汇编命令(编译器masm命令):找到masm...

24040
来自专栏竹清助手

2019年如何成为现代化的后端开发者

当我们谈到语言时,你会有无数种选择。为了方便你做决定,我把它们分成几类。对于刚开始进入后端开发的初学者,我建议你随便选一门脚本语言去学,它们都有大量的需求,也能...

32810
来自专栏米扑专栏

C语言编译全过程剖析

C语言编译的整个过程是非常复杂的,里面涉及到的编译器知识、硬件知识、工具链知识都是非常多的,深入了解整个编译过程对工程师理解应用程序的编写是有很大帮助的,希望大...

24030
来自专栏陈树义

JVM规范系列第3章:为Java虚拟机编译

第一部分应该说的是 Javac 这个前置编译器,用于将Java源代码编译成字节码。第二部分是说 JIT 即时编译器,用于在JVM运行时进行进一步优化,将字节码编...

12920
来自专栏信数据得永生

飞龙的程序员书单 - 组原、OS、网络

简单介绍一下,这本书包括组成原理和操作系统两大部分知识。第二、三章学完之后,逆向就算是入门了。国内的教材很少有拿汇编和C语言对比教学的书籍,这样的教学方法很实用...

12330
来自专栏蓝天

x86_64汇编调试程序初步

掌握此基础,就可以用来修改无源代码的程序等,比如希望jstatd在指定的端口上监听,而不是一个值为0的随机端口号,请参见《防火墙内JVisualVM连接jst...

19820
来自专栏C/C++基础

C++动态联编实现原理分析

所谓动态联编,是指被调函数入口地址是在运行时、而不是在编译时决定的。C++语言利用动态联编来完成虚函数调用。C++标准并没有规定如何实现动态联编,但大多数的C+...

16530

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励