tony666 发表于 2019-11-26 17:03

代码注入初探 ,c++实现CE 的代码注入功能

1、背景
看论坛大佬的教程做了CS1.6的内存修改 ,其中用到了CE的代码注入功能,在CE里代码注入很好用,也很方便。我就想到,能否脱离CE直接自己实现CE的修改内存和代码注入功能,然后做一款CS的修改器。
其中修改内存的操作很简单,直接ReadProcessMemory、WriteProcessMemory就可以了,但是代码注入的功能似乎不太容易实现(对于我),接下来便记录了我的探索过程和最终实现的思路。


2、目标
本次实验的目标并不是直接对CS进行代码注入,而是自己写了一个MFC的累加器,代码注入这个程序,这样程序结构简单,也方便自己调试。
累加器程序长这个样子:
    (图1)

这个程序的逻辑很简单,有一个数字,这个数字每秒增加2,然后把这个数字转化成字符串显示到界面上。
我们要做的就是把每秒增加2的这个逻辑更改掉,改成每秒 加4 再 加5 (别问为什么不直接加9,这里只是代表两条指令,任何合理的指令都行)
另外 此次注入的代码不使用远程线程的方式启动,而是用原代码逻辑中嵌入跳转的方式启动。



3、思路


首先用CE找到注入点(一定要找到基址) ,即我们要更改程序逻辑的地方,过程本文暂且不表。
获取进程句柄   ->          申请虚拟地址空间    ->      在注入点跳转到我们新申请的地址         ->            虚拟地址空间内写入我们想实现的逻辑      ->    跳回原位置

示意图:
(图2)
其中,A是 注入点
          B是虚拟地址空间首地址
          C是执行完注入代码需要返回的地址,至于为什么是这个地址,后面详说
          D是在虚拟地址空间的注入代码的后面,此处添加一个跳转指令,跳转回原处

4.具体实现(示例代码中删除了所有与操作不直接有关的代码,如返回值判断等)

4.1 找到注入点,过程本文暂且不表。


找到的注入点机器码和汇编码:
       (图3)

双击查看代码地址:
   (图4)
其中 0x01360000是进程首地址(每次启动可能不相同)
0x1332 是此处代码的偏移 (每次都相同)


4.2获取进程句柄


        //获取目标进程窗口句柄
        hWindow = FindWindow("#32770", "HelperTest")->GetSafeHwnd();
        //获取进程pid
        GetWindowThreadProcessId(hWindow, &pid);
        //获取进程句柄
        hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,pid);
        //获取进程首地址
        int rv = mem_GetProcessAddr(pid,baseAddr);

4.3申请虚拟地址空间


由于我们注入的代码量很少,64字节足够了
        LPVOID virAddr = NULL;
      virAddr = VirtualAllocEx(hProcess,NULL,64,MEM_COMMIT, PAGE_EXECUTE_READWRITE);


4.4 注入点跳转到virAddr


此处有一个小问题。根据图3原代码 ,注入点的第一条指令是4个字节 ,而跳转指令需要5个字节(参照图3中的call 指令),如果直接把此处代码改写成跳转指令,势必会影响下一条指令的运行。
所以我们的跳转指令需要占用 4+5 = 9个字节 ,然后再把后面的call指令 复制到 虚拟地址空间中 。

此处应先把原来注入点的两条指令保存下来
        list<DWORD> mlist;
        DWORD OldAddr = 0x1332;
        mlist.push_back(OldAddr);
        BYTE OldCode = {0};
        ULONG ReadddLen = sizeof(OldCode);
        int rv = mem_ReadBlock(hProcess,baseAddr,mlist,9,OldCode,&ReadddLen);       


关于跳转的偏移量,根据所查资料和实际调试,
有                              跳转指令下一条指令的地址 + 偏移量= 目标地址
细化一下                   跳转指令地址 + 跳转指令长度(5) + 偏移量 = 目标地址


又                           跳转指令地址= 注入点地址
且                           注入点地址=程序首地址 + 注入点代码的偏移                           

所以有                      程序首地址 + 注入点代码的偏移    + 跳转指令长度   + 偏移量= 目标地址
                              偏移量=目标地址-(程序首地址 + 注入点代码的偏移   + 跳转指令长度)
带入                         偏移量=virAddr   -( 程序首地址 + 0x1332 +5 )      ,其中程序首地址可由相关函数求得


所以跳转的机器码为    E9偏移量(4个字节)   ,把这5个字节写到注入点即可,后面4个字节用nop(0x90)补齐。


具体代码
        //把原来的指令改成 跳转指令 跳转地址为virAddr
        DWORD jmpOffset = (DWORD)virAddr - (baseAddr +0x1332 +5);
        BYTE ByteOffset;
        IntTo4Bytes(jmpOffset,ByteOffset);
        BYTE JmpToNewCode = {0};
        JmpToNewCode = 0xE9;                        //jmp指令
        memcpy(JmpToNewCode+1,ByteOffset,4);
        memset(JmpToNewCode+5,0x90,4);        //用nop补全

        ULONG WrittenLen = sizeof(JmpToNewCode);
        rv = mem_WriteBlock(hProcess,baseAddr,mlist,9,JmpToNewCode,&WrittenLen);

4.5将自己的新逻辑写到 virAddr里


为了简便我们的逻辑就简单点。
类比图3我们写两条指令   
                add dword ptr,05
                add dword ptr,04

那么如何将 汇编代码改成机器码呢
第一种,类比图3中的机器码,直接改数字,但是此种方法不具有普遍性。
第二种,用其他工具把汇编转化成机器码,把机器码复制过来就好了
第三种,也是我用的方法
写一个函数
void InjFunc(){
        _asm{
                add dword ptr,05
                add dword ptr,04
        }
}

那么 InjFunc 往后的第4个字节就是 _asm里的汇编码(注意一定要是release编译,debug会产生许多额外代码)
前三个字节是push      ebp
和                  mov         ebp,esp
我们并不需要
        //虚拟地址空间内写入新的机器码
        BYTE NewCode;
        memcpy(NewCode,(void*)((char*)InjFunc+3),8);

4.6 解决4.4遗留的问题,复原call指令


按照4.4的方法计算好偏移量,把E8 偏移量追加到NewCode 中
        memcpy(NewCode+8,0xE8,1);               //call指令
        memcpy(NewCode+9,bnewOffset,4);      //偏移量

4.7 跳转回图2中的C地址




A至C共9个字节,前5个字节是我们在4.4中写入的跳转指令 ,后 4个字节全是nop 所以跳转到 任意nop地址或者nop+ 的地址都可以
跳转方法参照4.4


5、相关功能代码
//读取内存数据
int mem_ReadBlock(IN HANDLE hProcess,IN DWORD baseAddr,IN list<DWORD> &offSetList,IN DWORD readLen, OUT BYTE *OutBuffer,IN OUT ULONG* BufferLen){
        if(*BufferLen <readLen){
                return HELPER_ERROR_BUFFER_TOO_SMALL;
        }
        if(offSetList.size()> 1){

                DWORD newBaseAddr,rv,ReadddLen;
                DWORD CurBaseAddr = baseAddr + offSetList.front();
                offSetList.pop_front();
                rv = ReadProcessMemory(hProcess,(LPVOID)CurBaseAddr,(LPVOID)&newBaseAddr,4,&ReadddLen);
                if(rv == FALSE){
                        return GetLastError();
                }

                rv = mem_ReadBlock(hProcess,newBaseAddr,offSetList,readLen,(BYTE*)OutBuffer,BufferLen);
                return rv;

        }
        else if(offSetList.size() == 1){
                DWORD CurBaseAddr = baseAddr + offSetList.front();
                int rv = ReadProcessMemory(hProcess,(LPVOID)CurBaseAddr,OutBuffer,readLen,BufferLen);
                if(rv == FALSE){
                        return GetLastError();
                }
        }
        else{
                returnHELPER_ERROR_OFFSET_INVALID;
        }
        return HELPER_ERROR_SUCCESS;
}

//读取内存数据
int mem_WriteBlock(IN HANDLE hProcess,IN DWORD baseAddr,IN list<DWORD>& offSetList,IN DWORD WriteLen, IN BYTE *InBuffer,OUT ULONG* WrittenLen){

        if(offSetList.size()> 1){

                DWORD newBaseAddr,rv,ReadddLen;
                DWORD CurBaseAddr = baseAddr + offSetList.front();
                offSetList.pop_front();
                rv = ReadProcessMemory(hProcess,(LPVOID)CurBaseAddr,(LPVOID)&newBaseAddr,4,&ReadddLen);
                if(rv == FALSE){
                        return GetLastError();
                }

                rv = mem_WriteBlock(hProcess,newBaseAddr,offSetList,WriteLen,(BYTE*)InBuffer,WrittenLen);
                return rv;

        }
        else if(offSetList.size() == 1){
                DWORD CurBaseAddr = baseAddr + offSetList.front();
                int rv = WriteProcessMemory(hProcess,(LPVOID)CurBaseAddr,InBuffer,WriteLen,WrittenLen);
                if(rv == FALSE){
                        return GetLastError();
                }
        }
        else{
                returnHELPER_ERROR_OFFSET_INVALID;
        }
        return HELPER_ERROR_SUCCESS;
}




//获取目标进程首地址
intmem_GetProcessAddr(DWORD dwPID, DWORD& baseAddr)
{
        HANDLE hModuleSnap = INVALID_HANDLE_VALUE;
        MODULEENTRY32 me32;

        // 在目标进程中获取所有进程的snapshot
        hModuleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID);
        if (hModuleSnap == INVALID_HANDLE_VALUE)
        {       
                return HELPER_ERROR_NOTFIND_PROADDR;
        }

        // 设置MODULEENTRY32数据结构大小字段
        me32.dwSize = sizeof(MODULEENTRY32);

        //检索第一个模块的信息,不成功则返回
        if (!Module32First(hModuleSnap, &me32))
        {
                CloseHandle(hModuleSnap);    // 清除句柄对象
                return HELPER_ERROR_NOTFIND_PROADDR;
        }

        // 从me32中得到基址
        baseAddr = (DWORD)me32.modBaseAddr;

        // 别忘了最后清除模块句柄对象
        CloseHandle(hModuleSnap);
        returnHELPER_ERROR_SUCCESS;
}

其中读写内存的函数用ReadProcessMemory 和WriteProcessMemory 就好,封装成这样,主要是为了通过多级指针来查找内存。
mem_GetProcessAddr是 网上搜的某大神写的源码

6、结语


通过做这中类型的东西,感觉自己对汇编和内存的理解更深了一步,继续努力吧。
第一次发帖,思路不是很清晰,有问题的地方希望大佬们多加指正。
另外我还有个疑问,还有没有更优雅的方法 把汇编码转化成机器码 写到内存里呢?

ggxxuser 发表于 2019-11-26 18:02

这里面每一个字我都认识,但是放在一起就不认识了,关于内存和指针的也太难了。

苏紫方璇 发表于 2019-11-26 17:25

汇编到机器码还可以用汇编引擎

tony666 发表于 2020-2-21 10:37

iTruth 发表于 2020-2-19 10:20
楼主,我按照你的步骤去做了,代码确实也修改成功了但为什么改完程序一会就闪退了呢?
这时我测试程序的源码
...

不是 JMP 00760000吗,你OD看看在哪个地方崩的

sniper9527 发表于 2019-11-26 19:16

感谢分享

我的盘由我做主 发表于 2019-11-26 19:16

ggxxuser 发表于 2019-11-26 18:02
这里面每一个字我都认识,但是放在一起就不认识了,关于内存和指针的也太难了。

其实咱们都一样,我们太难了!!!!

tony666 发表于 2019-11-26 20:41

苏紫方璇 发表于 2019-11-26 17:25
汇编到机器码还可以用汇编引擎

大佬能说详细一点吗? 查了一下,结果似乎都是反汇编,是先编译再用工具反汇编?

tony666 发表于 2019-11-26 20:42

ggxxuser 发表于 2019-11-26 18:02
这里面每一个字我都认识,但是放在一起就不认识了,关于内存和指针的也太难了。

加油!谁都可以的:lol

tony666 发表于 2019-11-26 20:43

我的盘由我做主 发表于 2019-11-26 19:16
其实咱们都一样,我们太难了!!!!

加油,你也很厉害:lol

苏紫方璇 发表于 2019-11-26 20:52

tony666 发表于 2019-11-26 20:41
大佬能说详细一点吗? 查了一下,结果似乎都是反汇编,是先编译再用工具反汇编?

可以查一下x64dbg用的那两个,asmjit和xedparse

tony666 发表于 2019-11-26 20:53

苏紫方璇 发表于 2019-11-26 20:52
可以查一下x64dbg用的那两个,asmjit和xedparse

多谢指点,我去学习学习:lol
页: [1] 2 3
查看完整版本: 代码注入初探 ,c++实现CE 的代码注入功能