之前一期我们学习了 IAT 的基本结构,相信大家对 C++ 有了一个基本的认识,这一期放点干货,我把 ring3 层恶意代码常用的编程技术给大家整理了一下,所有代码都经过我亲手调试并打上了非常详细的注释供大家学习,如下图:

我会在其中挑出几个,采用反汇编的方式,给大家展示恶意代码的执行流程以及原理,由于 ring3 层的技术过于古老,希望大家秉着学习和巩固的心态来看待该文章。
一共九种技术,十套源代码,分两期向大家介绍编程相关思路,希望大家与我一起学习,共同进步。
源码下载地址(稍后会把源码上传到 github 上):
https://pan.baidu.com/s/1KpKdh4EWCGT828ONeB1HzA
APC 注入
APC 即 Asynchronous procedure call,异步程序调用。在一个进程中,当一个执行到 SleepEx() 或者 WaitForSingleObjectEx() 时,系统就会产生一个软中断,当线程再次被唤醒时,此线程会首先执行 APC 队列中的被注册的函数,利用 QueueUserAPC() 这个 API,并以此去执行我们的 DLL 加载代码,进而完成 DLL 注入的目的。
测试环境:在 64 位 win7 环境下 VS2013 release 编译
32 位程序,32 位目标进程,32 位 dll,成功
64 位程序,64 位目标进程,64 位 dll,成功
APC 原理分析
将编译后的 exe 文件拖入 IDA,转到 main 函数

发现 main 函数一共 call 了 9 次,其中四次 call 值得我们关注.
位于 00401348 处的 call sub_4012A0
位于 0040137B 处的 call sub_4013F0
位于 004013A0 处的 call sub_4014C0
位于 004013D4 处的 call sub_4015A0
首先分析 00401348 处的 call sub_4012A0,转到该函数的领空

发现函数调用了 GetCurrentProcess,OpenProcessToken,LookupPrivilegeValueA 以及 AdjustTokenPrivileges 函数,通过这些函数我们可以推测该函数主要用于提权,通过 OpenProcessToken 获取访问令牌,通过 LookupPrivilegeValueA 获取本地唯一标识符,通过 AdjustTokenPrivileges 来调整访问令牌。
然后我们退出该函数领空接着分析主函数,我们在 0040134F 发现 main 函数申请了 8 字节的堆空间,接着在 0040135A 处判断有没有分配成功,成功则转向 loc_401374

xorps 是 SSE2 的异或指令,对应的是 xmm0~7 浮点寄存器,这里应该是编译器对计算速度进行了优化。这里科普一点小知识:
FPU: 8 个 80 位浮点寄存器(数据),16 位状态寄存器,16 位控制寄存器,16 为标识寄存器。使用 FPU 指令对这些寄存器进行操作,这些寄存器构成一个循环栈,st7 栈底,st0 栈顶,当一个值被压入时,被存入 st0,原来 st0 中的值被存入 st7。
SSE: 8 个 128 位寄存器(从 xmm0 到 xmm7),MXSCR 寄存器,EFLAGS 寄存器,专有指令(复杂浮点运算)
对 xmm0 进行清零,并对 [esi] 进行初始化。接着 call sub_4013F0。转入该函数

函数逻辑很简单,创建快照,枚举进程找到 notepad.exe 的 PID 并进行有效性的判断并返回。
紧接着转到 0040139C 处的 loc_40139C,在 004013A0 处调用 sub_4014C0 函数,转到该函数,由于该 exe 是 release 版的,优化选项是最快,所以在函数调用的时候,我们看不到 push 操作,这个因为在最快的模式下函数的调用约定是 __fastcall 参数是以寄存器进行传递的,所以该函数有两个参数一个是 ecx 代码线程的 PID,一个是 edx 之前我们分配堆空间的首地址。知道了这些就容易判断了。

该函数的目的是创建快照枚举线程,并把所有线程的地址插入到之前分配的堆空间中,由此我们可以怀疑之前分配的堆空间可能是一个链表,存储着线程 ID。接着回到主函数

紧接着调用 OpenProcess,并在 004013D4 处 call sub_4015A0,转到 sub_4015A0(注意 call 之前寄存器的赋值)
ecx 代表进程句柄,edx 代表之前分配的堆空间,也就是线程 ID 链表

通过该函数调用的一些 API 如:GetModuleHandleA,GetProcAddress,VirtualAllocEx(在进程中分配地址空间),WriteProcessMemory(在刚分配的空间中写入数据),OpenThread(打开线程句柄),QueueUserAPC(APC 对象加入到指定线程的 APC 队列中),
我们可以基本推测:函数获取 LoadLibraryA 的地址,并为 LoadLibraryA 的参数分配内存空间并写入数据,接着将 LoadLibraryA的地址与参数作为 QueueUserAPC 函数的参数添加到每一个线程 APC 队列中,从而实现注入。
DLL 注入
老生常谈的技术了,就简单的介绍一下,不做逆向分析了
所谓 DLL 注入就是将一个 DLL 放进某个进程的地址空间里,让它成为那个进程的一部分。要实现 DLL 注入,首先需要打开目标进程。
测试环境:在 64 位 win7 环境下
32 位程序,32 位目标进程,成功
64 位程序,64 位目标进程,成功
代码注入
跟 DLL 注入一样也是很古老的技术了,不做逆向分析了
代码注入是一种向目标进程注入代码,并使之独立运行的技术,一般调用 CreateRemoteThread() 创建远程线程的方式完成
与 DLL 注入类似都是先 VirtualAllocEx 分配空间,WriteProcessMemory 写入数据,最后调用 CreateRemoteThread() 创建远程线程。
但相对于 DLL 注入来说,代码注入有以下优点:
1、占用内存小
2、不容易被发现。毕竟 DLL 注入,能用工具把注入的 DLL 找出来
测试环境:在 64 位 win7 环境下
32 位程序,32 位目标进程,成功
64 位程序,64 位目标进程,成功
进程替换
通过将一个可执行文件(恶意代码文件)重写到一个运行进程的内存空间,从而实现恶意代码的移植
编程思路:
1、创建一个挂起状态 (SUSPEND) 的进程。
2、读取主线程的上下文 (CONTEXT), 并读取新创建进程的基址。
3、使用 VirtualAllocEx 和 WriteProcessMemory 写入恶意代码并覆盖新建进程的内存空间,实现进程替换。
4、设置主线程的上下文,启动主线程。
测试环境:
在 64 位 win7 环境下:
32 位程序,32 位目标进程,成功
32 位程序,64 位目标进程,成功
在 32 位 win xp 环境下:
32 位程序,32 位目标进程,成功
一般我们把恶意代码存到PE文件的资源节,当程序运行时从资源节释放恶意代码,并把原来的内存空间覆盖
进程替换原理分析
先把 exe 载入到 PEView,查看资源节

我们发现资源节隐藏着一个 PE 文件,我们用 winHex 将 PE 提取出来,并载入 IDA,看看隐藏的恶意代码做了什么。

emmmm,很简单,调用两次 MessageBoxW 函数,然后退出……
好了,接下来分析主 PE 文件了,将 exe 载入到 IDA 中,进行静态分析,定位到 main 函数,函数很长,我们一个一个分析

首先分析 0040113C 处的 call sub_401000,转到该函数

emmmm,IDA 貌似没有完全分析出来,根据我们上面分析 APC 注入的经验,我们可以推断这是一个提权函数。
紧接着在 00401149,0040116A,0040117C,0040118D 对数据段进行访问,转到数据段,发现一大堆数字按下 a 键,如下图

组合起来就是 C:\Windows\System32\notepad.exe,很有可能恶意代码会创建并替换这个进程。接着向下分析
在 004011A8 处 call sub_4010A0,我们转到函数

发现函数调用了与资源有关的 API,该函数很有可能释放藏在资源节中的恶意数据,返回值是恶意代码的基址。回到主函数

发现函数对恶意代码的PE文件的有效性进行校验,接着创建一个挂起的进程(因为 dwCreationFlags 等于 4),然后获取挂起进程上下文。此时 ebx 指向 PEB,eax 指向 OEP。

调用 ReadProcessMemory,获取 PEB+8 处的地址,也是该 PE 文件的加载基址。

接着在宿主进程中为恶意代码分配内存空间,起始地址是恶意代码加载映像基址 OptionalHeader.ImageBase,大小是 OptionalHeader.SizeOfImage,

调用 WriteProcessMemory,使恶意代码的 PE 头替换宿主的 PE 头。替换 PE 头之后就要替换节区了,如下图

第一个 WriteProcessMemory 通过一个循环替换节区,第二个 WriteProcessMemory 将 ebx+8 的值改为恶意代码的加载基址 OptionalHeader.ImageBase。

最后一个 WriteProcessMemory 修改 eax 的值也就是 OEP 的值,将其替换为恶意代码的 OEP(OptionalHeader.AddressOfEntryPoint),最后调用 SetThreadContext 设置线程上下文,ResumeThread 运行进程。
5 字节 InLineHook 与 7 字节 InLineHook
简单的介绍一下 Hook:
Hook 是 Windows 中提供的一种系统机制在对特定的系统事件进行 hook 后,一旦发生已 hook 事件,对该事件进行 hook 的程序就会收到系统的通知,这时程序就能在第一时间对该事件做出响应。
Hook 有很多种,一般在 ring0 层下大显神威,如 SSDT Hook,idt Hook,IRP Hook 等等。在 ring3 层我们只需要了解其原理即可。
InLineHook 与普通 Hook 的区别:
InLineHook 是直接在以前的函数替里面修改指令,用一个跳转或者其他指令来达到挂钩的目的。 而普通的 hook 只是修改函数的调用地址,而不是在原来的函数体里面做修改。一般来说 普通的 hook 比较稳定使用。 inline hook 更加高级一点,一般也跟难以被发现
测试环境:
在 64 位 win7 环境下
32 位程序,成功
64 位程序,失败
InLineHook 通过跳转指令来覆盖函数的首部,一般分为两类
5 字节 InLineHook
jmp address
address 计算公式为:
address = 目标地址 - 原地址 - 5
*(DWORD *)(m_bNewBytes + 1) = (DWORD)pfnHookFunc - (DWORD)m_pfnOrig - 5;
7 字节 InLineHook
mov eax,address
jmp eax
address 通过函数地址进行赋值,记得注意字节序
DWORD dwData = (DWORD)pfnHookFunc; // 函数地址
byteData[0] = (dwData & 0xFF000000) >> 24;
byteData[1] = (dwData & 0x00FF0000) >> 16;
byteData[2] = (dwData & 0x0000FF00) >> 8;
byteData[3] = (dwData & 0x000000FF);
bJmpCode[0] = '\xb8';
bJmpCode[1] = byteData[3];
bJmpCode[2] = byteData[2];
bJmpCode[3] = byteData[1];
bJmpCode[4] = byteData[0];
bJmpCode[5] = '\xFF';
bJmpCode[6] = '\xE0';
最后调用 WriteProcessMemory 将 shellcode 写入即可。
HookDllInject(实现反弹 shell)
这也是 DLL 注入的一种,之前是通过 CreateRemoteThread 进行注入,这次我们通过 SetWindowsHookEx(全局钩子)实现 DLL 注入。
HHOOK WINAPI SetWindowsHookEx(
__in int idHook//钩子类型
__in HOOKPROC lpfn //回调函数地址
__in HINSTANCE hMod//实例句柄
__in DWORD dwThreadId) //线程ID
测试环境:
在 64 位 win7 环境下
64 位程序,64 位 DLL,64 位目标进程,成功反弹 shell
HookDllInject(实现反弹 shell)原理分析
经过 vs2013 编译生成了两个 PE 文件,HookDllInject.exe 与 inject2.dll,我们先分析 inject2.dll,载入 LoadPe 查看导出表。

发现一个导出函数,我们可以用 windows 自带的 rundll32.exe 调用该 DLL 的 inject 函数同时打开 wireshark 进行抓包,我们可以获取更加详细的信息,这里我就不做了,直接拖入 IDA 进行静态分析,记得用 64 位的 IDA,因为我们生成的 exe 和 DLL 全是 64 位的。直接定位到 inject 函数的领空。

我们发现了 WSAStartup,sock,WSAGetLastError 等函数,大致可以推断这是一个 socket 连接,我们主要寻找连接的 IP 地址和端口号,方便我们在本机上模拟服务环境。

在 0000000180002149 与 0000000180002156 处发现了值为 127.0.0.1 的 IP 地址,值为 443 的端口号,同时我们发现该函数在 connect 服务端之后会调用 send 函数将 “Injected Shell” 字符串发送给服务端,我们可以通过该字符串判断是否连接成功。

接着会调用 CallNextHookEx 将钩子信息传递到当前钩子链中的下一个子程。说明该 DLL 很可能是通过全局钩子进行注入的,接下来我们分析 HookDllInject.exe 文件

主函数很简单,首先接受用户输入的进程 PID 号,printf 输出一段字符串,将用户输入的值赋值给 ecx 作为参数,然后在 00000001400012B7 处 call sub_1400010D0(在优化模式下,函数调用约定是 fastcall,通过寄存器传递参数),进入 sub_1400010D0 函数

先进行局部变量的初始化,并把该函数唯一的参数赋给 edi,紧接着调用 OpenProcess, K32EnumProcessModules, K32GetModuleBaseNameW 三个函数枚举进程,找到目标进程的名称

接下来会在 000000014000117E 处 call sub_140001000,参数是用户输入的 PID 号(edi),转到 sub_140001000

该函数主要功能是创建快照,枚举线程,当线程的 th32OwnerProcessID(此线程所属进程的进程 ID)与 PID 相等时,打开线程句柄并获取线程 PID。接着回到原来的函数

获取线程 ID 之后,载入我们之前分析过的 inject2.dll,并得到导出函数 injec t的地址,紧接着调用 SetWindowsHookExW,
第一个参数的值是 2 转换一下就是 WH_KEYBOARD 键盘消息
第二个参数是 inject 的函数地址
第三个参数是 inject2.dll 的句柄
第四个参数是刚获取的线程 ID
当目标进程发出 WH_KEYBOARD 消息时就会调用 inject 函数,从而实现一个反弹 shell。
我们可以使用 nc 在本机模拟服务器
nc -l -p 443
打开 notepad.exe 作为目标进程,运行 HookDllInject.exe,输入 notepad.exe 的 PID(通过 tasklist 查看),接着在记事本上顺便输入一个数字,你会看到 nc 已经接收到了字符串信息,如图

IATHook(实现简单的进程保护)
之前的我们已经学习了 IAT 的相关知识,所以基础的东西我就不再补充了。
编程思路分三步进行
1、获取要 Hook 函数的地址
2、找到该函数所保存的 IAT 中的地址
3、把 IAT 中的地址修改为 Hook 函数的地址
测试环境:
32 位 winxp 环境下
32 位 Dll 注入到 32 位 taskmgr.exe 进程,成功
IATHook(实现简单的进程保护)原理分析
使用 vc++6.0 对源码进行编译,生成一个 DLL 文件,载入 IDA,进入 DLLMain

程序首先对 ul_reason_for_call 的值进行判断,如果值为 DLL_PROCESS_ATTACH (当 DLL 被加载时调用)将 TerminateProcess的地址赋给 eax,接着调用 call sub_10001020,我们转到这个函数

一开始对 PE 文件进行操作,获得的 PE 头,获取导入表的地址,我们主要关心它是怎么修改 IAT 列表中函数的地址的。往下拉

这里会先对函数地址进行判断,判断是不是我们要 Hook 的函数地址,也就是判断函数地址是不是 TerminateProcess 的地址,如果是,就调用 VirtualProtect 和 WriteProcessMemory 函数,VirtualProtect 在之前我讲 ROP 技术绕过 DEP 的时候介绍过,这里调用该函数的目的是使修改内存属性,使该区域的内存可读可写,以便于我们进行修改。接着调用 WriteProcessMemory 将新函数的地址覆盖原有地址,而新函数的地址正好是该函数的一个参数,在 call sub_10001020 之前已经入栈,我们回到 DLLMain,进入新函数的领空

新函数很简单,只是单单的弹出一个对话框。当目标进程调用 TerminateProcess 时仅仅弹出对话框然后退出,核心代码分析完毕。
我们将该 DLL 注入到任务管理器的进程中(taskmgr.exe),我们试着结束进程,进程没有结束,弹出一个对话框,如图

三线程保护程序
该程序有两套代码,winxp 那一套过于古老,我就不再做详细介绍,我在 xp 版的基础上进行更新写了一个 win7 版的,着重分析 win7 版
测试环境:
xp 版,在 32 位 win xp 下,vc++6.0 编译,成功
win7 版,在 64 位 win7 下,vs2013(x64) 编译,成功
这套代码呢,有一定的攻击性,运行之前要么先阅读一下源码了解程序做了什么,要么就做一个镜像好还原
对了,编译之后记得给 exe 改名,不然运行不了,你看一下源码就懂了……
三线程保护程序原理及分析
将编译后的 64 位 exe 载入 IDA,进入主函数

我们看到主函数逻辑还是很复杂的,限于篇幅,我们避轻就重,简单的分析一下这个 exe 干了什么
首先在 00000001400010E2 处调用提权函数,接着是一顿初始化字符串的操作,还调用了 GetSystemDirectoryA,然后调用 FindFirstFileA 仿佛在寻找什么

之后进行 CopyFile 的操作,看到这基本上能猜出来,这一段的代码是寻找 system32 目录下有没有这个文件,没有就复制过去。然后往后看,发现还有一次 FindFirstFileA 的调用,并且 CopyFile 的一个参数为 kernel.dll,可能恶意代码做了一个备份,将文件重命名为 kernel.dll 放在系统目录下,紧接着在 000000014000138C 处发现调用了 CreateFileA,打开了之前创建的备份文件 kernel.dll,获得文件句柄之后,紧接着调用 SystemTimeToFileTime,SetFileTime,SetFileAttributesA 修改备份文件的创建时间、修改时间并设置文件属性只读,隐藏。

接着向下看,在 0000000140001481 处 call sub_140001730,转到该函数

emmmm,接着避轻就重,在 000000014000179C 处 call sub_140001660,该函数是枚举进程的函数,获取目标进程的 PID,然后在 00000001400017C1 处调用 OpenProcess 函数,获取目标进程的句柄,紧接着调用 VirtualAllocEx 和 WriteProcessMemory,我们大致可以推测恶意代码会将某些东西注入到目标进程中,我们主要关注这些要注入东西是什么

在 00000001400018A6 处发现了要写入目标进程内存的内容,转到其领空,我们发现这是一个函数指针,所以之前的操作是代码注入

而且分析难度相当之大,全是隐式调用,call 的地址全是寄存器,所以我们要对这个函数的参数进行分析,退出该函数,很幸运在下面的代码中又有一次 VirtualAllocEx 和 WriteProcessMemory,应该就是写入参数的操作了,我们发现在第二次写内存之前有非常多的 GetProcAddress 操作,我们来看一看都获取了哪些函数的地址。经过耐心的分析,了解到要注入到远程线程的函数的功能:一直在宿主进程中一直打开我们的恶意进程,寻找恶意程序是否被删除,如果被删除再复制过去,复制完之后,运行恶意程序。最后调用 CreateRemoteThread 在宿主进程中运行这个死循环。

回到主函数接下来会创建一个线程,我们转到 lpStartAddress 的领空,看看这个新线程的回调函数做了什么,

回调函数都是一些常见的函数,主要是一个监听线程,目的是判断注册表的自启动项中有没有加入恶意代码的键值,如果没有就添加上,在 0000000140001D49 处会调用 GetExitCodeThread 函数,来检查远程线程的执行情况,如果不是 STILL_ACTIVE 状态,则创建远程线程,回到主函数

接下来会进入一个死循环,你的鼠标会一直按照设定好的形式进行浮动。
小结
如果大家看不太懂反汇编的话,可以下载源代码进行学习,我都打了详尽的注释。到这里提供的源代码算是分析完毕了,还剩下PE病毒,我打算用两篇的篇幅来实战一下PE病毒,敬请关注……
本文深入解析了九种恶意代码编程技术,包括APC注入、DLL注入、代码注入、进程替换等,并提供了十套源代码实例,涵盖从提权到执行流程的详细分析。
3363

被折叠的 条评论
为什么被折叠?



