本文作者:x-encounter
之前一期我们学习了 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:WindowsSystem32notepad.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 通过函数地址进行赋值,记得注意字节序
代码语言:javascript复制 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病毒,敬请关注……