前言
安全研究人员与恶意攻击者之间的博弈从未停止,安全研究人员通过动态二进制插桩技术实现对恶意代码的行为分析,而从攻击者角度,为增强恶意代码的抗分析能力,反插桩技术应运而生。
恶意代码仍然是当今主要的网络威胁之一,而通过对恶意代码的分析能够提取其行为信息,挖掘其潜在的威胁情报。目前的分析技术总体上分为静态分析、动态分析以及混合分析,这其中,动态二进制插桩(Dynamic Binary Instrumentation,DBI)技术具有着广泛的应用,主要是指在二进制程序动态执行时注入插桩代码,从而实现程序分析。利用DBI技术,编写分析插件,即可实现对恶意代码的网络通信、文件读写、进程创建、注册表操作等信息的跟踪,实现深度分析。
网络的另一端,攻击者当然不会坐以待毙。与反调试、反虚拟机技术类似,所谓反插桩技术,即攻击者会在恶意代码中加入特殊代码,检测当前是否处于插桩分析状态:如果检测到当前处于插桩状态,则认为恶意代码当前的执行环境是处于安全人员的监控分析下,立即结束;否则,则认为是正常的受害主机,继续执行后续恶意操作。
if(amIUnderInstrumentAnalysis())
{
goDie();
}
else
{
beMalicious();
}
各DBI平台都会强调其自身分析优势和特性,这其中就包括“透明性”,即代码在插桩前后的执行状态是不受影响的,仿佛是感觉不到插桩平台的存在。但其实,对于狡猾的恶意代码而言,插桩平台并没有实现真正的透明,其执行过程中总会由于其自身的技术特性在内存等方面留下蛛丝马迹,而这点滴的蛛丝马迹就是反插桩技术所检测的依据。
目前的DBI平台包括Pin、DynamoRIO、Valgrind等,本文主要针对Pin和DynamoRIO进行剖析。
从整体上,将反插桩技术从检测机理的角度划分为四大方面,并分别阐述。
- 基于代码缓存:在DBI平台下,真正执行的代码位于代码缓存中,可针对此特性进行检测。
- 基于环境特征:动态二进制插桩平台的运行环境不可避免的会留下多种“指纹”,内存中残留的字符串信息、进程信息等,均可作为检测依据。
- 基于JIT编译:JIT编译是DBI技术的重要技术特征,可针对此特性检测。
- 其他反插桩技术:利用运行时开销、不支持指令等实现插桩平台的检测。
基于代码缓存的反插桩技术
EIP 检测
由于代码缓存技术的使用,原始代码只是驻留在内存中作为参考副本,不会执行。真正执行的是代码缓存中的代码。因此,可以通过检测指令指针EIP是否符合既定预期,来判断当前是否处于插桩分析状态。
EIP检测方法需要利用一些特殊的指令来保存当前执行的上下文状态,这其中就包括我们所关注的EIP寄存器,一旦获取到实际执行过程中的真实EIP数值,就可以将其与程序原本所预期的数值进行比较。如果两者数值不同,则证明处于插桩分析状态。
具有这一类功能的指令包括:
int 0x2E (sysenter)
fsave
fxsave
下面以int 0x2e
中断指令为例展开分析:
int 0x2e
是由用户模式切换至系统模式的传统方式,并且支持当前所有的x86 CPUs,当前已被sysenter
取代。调用int 0x2e
指令,CPU会通过IDT表找到0x2e所应对的nt!KiSystemService
函数,并将执行权交给nt!KiSystemService
,由于该函数位于内核空间,由此完成用户模式至内核模式的转换。而在这种转换执行前,需要利用堆栈保存当前现场,即上下文状态,其中就包含EIP寄存器。
int 0x2e
除了在进入内核前会保留上下文状态外,更重要的是,在执行完该指令后,edx
寄存器内会存储其返回的eip
,即int 0x2e
的下一条指令地址。
下面我们通过调试来对int 0x2e
指令进行详细分析。调试采用windbg host+target内核调试,示例代码如下:
UINT32 RealEDX;
__asm {
int 3
mov eax, 19h
int 2eh
mov RealEDX,edx
};
printf("edx:0x%x",1);
编译并执行该程序,触发windbg中断,单步执行到int 0x2e
指令处
Break instruction exception - code 80000003 (first chance)
001b:012cd4a8 cc int 3
2: kd> u . L5
001b:012cd4a8 cc int 3
001b:012cd4a9 b819000000 mov eax,19h
001b:012cd4ae cd2e int 2Eh
001b:012cd4b0 8955f4 mov dword ptr [ebp-0Ch],edx
001b:012cd4b3 8b45f4 mov eax,dword ptr [ebp-0Ch]
2: kd> p
001b:012cd4a9 b819000000 mov eax,19h
2: kd> p
001b:012cd4ae cd2e int 2Eh
查看IDT表内0x2e中断号对应的处理函数为nt!KiSystemService
,在其入口处下断点
2: kd> !idt 2e
Dumping IDT:
2e: 83e7bfee nt!KiSystemService
2: kd> bp 83e7bfee
然后,查看在进入nt!KiSystemService
前的堆栈和寄存器状态:
2: kd> r
eax=00000019 ebx=7ffdb000 ecx=00000000 edx=00426b00 esi=00000000 edi=0023fd88
eip=012cd4ae esp=0023fcac ebp=0023fd88 iopl=0 nv up ei ng nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000286
001b:012cd4ae cd2e int 2Eh
2: kd> dd esp
0023fcac 00000000 00000000 7ffdb000 cccccccc
0023fcbc cccccccc cccccccc cccccccc cccccccc
运行,在nt!KiSystemService
入口处触发中断, 查看堆栈状态:
2: kd> g
Breakpoint 0 hit
nt!KiSystemService:
83e7bfee 6a00 push 0
2: kd> dd esp
9ac4bc9c 012cd4b0 0000001b 00000286 0023fcac
9ac4bcac 00000023 00000000 00000000 00000000
9ac4bcbc 00000000 0000027f 00000000 00000000
9ac4bccc 00000000 00000000 00000000 00001f80
9ac4bcdc 00000000 00000000 00000000 00000000
9ac4bcec 00000000 00000000 00000000 00000000
9ac4bcfc 00000000 00000000 00000000 00000000
9ac4bd0c 00000000 00000000 00000000 00000000
可以看到此时栈顶的5个数值刚好是寄存器按如下指令入栈的结果:
Push SS
Push esp
Push eflags
Push cs
Push eip
执行完int 0x2e
指令,查看寄存器状态:
2: kd> bp 012cd4b0
2: kd> bl
0 e 83e7bfee 0001 (0001) nt!KiSystemService
1 e 012cd4b0 0001 (0001)
2: kd> bc 0
2: kd> g
Breakpoint 1 hit
001b:012cd4b0 8955f4 mov dword ptr [ebp-0Ch],edx
2: kd> r
eax=c000000d ebx=7ffdb000 ecx=0023fcac edx=012cd4b0 esi=00000000 edi=0023fd88
eip=012cd4b0 esp=0023fcac ebp=0023fd88 iopl=0 nv up ei ng nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=0030 gs=0000 efl=00000286
001b:012cd4b0 8955f4 mov dword ptr [ebp-0Ch],edx ss:0023:0023fd7c=cccccccc
2: kd> u .
001b:012cd4b0 8955f4 mov dword ptr [ebp-0Ch],edx
上述结果显示,int 0x2e
返回时,edx
寄存器保存有当前的eip
数值,即0x012cd4b0
,也就是即将执行的mov
指令地址。
而在插桩状态下,由于代码缓存的引入,int 0x2e
执行完成后,其edx
则会指向代码缓存的内存区,而不会指向原程序的内存地址区域。
由此可见,可利用int 0x2e
指令,检测指令返回时的edx
,实现对插桩平台的检测。
自修改代码
自修改代码(Self-modifying code,SMC)是指在程序动态运行期间,修改自身的指令。主要应用场景为各类病毒用来躲避杀软的查杀,达到保护自身的目的。
如果程序位于动态二进制插桩的执行状态,由于其执行的是位于代码缓存中的指令,也就意味着其执行的指令是并未经过修改的指令,从而导致程序的执行与其预期存在差异。利用这种执行的差异性,即可实现对插桩平台的检测。
如下图所示:
- 在插桩执行状态下,原始指令被拷贝至代码缓存
- 执行代码缓存内的指令ins1,由于其自修改代码的特性,会修改原始指令的wrong_ins3为ins3,而代码缓存内的指令并未发生改变
- 随着程序的执行,最终执行了位于代码缓存内的wrong_ins3,触发了crash。由于插桩平台的代码缓存特性,造成了执行的差异性。
因此,利用这种自修改代码的特性,结合具体指令类型,可有多种不同变换的反插桩方法,但其道理相通。
基于环境特征的反插桩技术
PE特征
如图所示,观察到pin和dynamorio在插桩时,分别会将pinvm.dll
和dynamorio.dll
注入进程内。
以pinvm.dll
为例,其包含的特殊字符串和汇编代码片段等均可作为检测的依据。
pinvm.dll特殊字符串:
pinvm.dll特殊汇编代码片段:
pinvm.dll导出函数:PinWinMain等
pinvm.dll的section name:.charmve
除了pinvm.dll之外,pin在分析时,还会将用户根据需求自行编写开发的pintools注入到进程中,其导出函数和section names 等特征也可作为插桩环境的检测依据:
父进程
由于目标进程是由动态二进制插桩平台启动进行分析的,因此目标进程是DBI平台的子进程。换言之,目标进程可以通过检测父进程来判断插桩平台是否存在。例如,Pin和DynamoRIO在分析时,目标程序的父进程信息如下:
基于JIT编译的反插桩技术
何为JIT编译?
JIT,just-in-time,翻译为即时编译,是一种动态编译技术。通常,编译技术包括静态编译和动态编译。静态编译的程序在执行前已被全部翻译为机器码,然后再载入运行;动态编译则更加强调“运行时”的概念,边运行边翻译。
在JIT编译下,二进制程序的原始汇编代码加载到内存中,作为插桩变换的参考底本,其本身永远不会被执行。真正执行则是经过JIT编译、位于code cache中插桩变换后的指令。
因此,JIT编译自身的某些特征特性可作为检测的依据。
内存页权限 WX
JIT编译所需的前提条件是能够将编译后的指令代码写入(W)内存中然后执行指令(X),因此,插桩程序的所具备RWX属性的内存页数量要高于未经插桩的程序。如下图所示,分别展示了pin插桩前后,calc.exe的内存页状态:
未经pin插桩的calc.exe:
经过pin插桩的calc.exe:
因此,可以通过扫描整个进程地址空间来计算标记为RWX属性的内存页数量,如果数量明显高于正常程序的结果,则可判定存在插桩分析。
DLL HOOK
DBI平台需要使用HOOK函数,从而在未经插桩的trace尾部拦截程序的执行。具体而言,DBI需要在某些函数的入口处插入jmp跳转指令(Hook)。如下图所示,在pin插桩前后,ntdll.KiUserApcDispatcher
函数的入口变化如下:
未经pin插桩:
经过pin插桩后:
除此之外,具有类似情况的函数还包括:KiUserCallbackDispatcher、KiUserExceptionDispatcher、LdrInitializeThunk
等。
Memory Allocation
正如前文所提到的,JIT编译需要使用RWX的内存页来进行指令的插桩变换和缓存。正因如此,DBI平台需要通过申请内存来满足其需求。而其所有的内存申请操作都是通过底层的ZwAllocateVirtualMemory
函数调用所实现的。基于此行为特征,可以通过统计ZwAllocateVirtualMemory
的调用次数,与无插桩时的执行状态相对比,实现对DBI的检测。
其中参数特征为:
ZwAllocateVirtualMemory
=> AllocationType = MEM_COMMIT | MEM_RESERVE
=> Protect = PAGE_EXECUTE_READWRITE
其他反插桩技术
运行时开销
毫无疑问,插桩技术在提供灵活的分析手段的同时,也会增加程序运行时的开销,主要表现在程序执行的时间上。
尽管各插桩平台努力提高插桩分析的性能,但这种运行时的开销仍然可以作为反插桩检测的依据。
利用NtQueryPerformanceCounter、GetTickCount、timeGetTime
等API函数或者KUSER_SHARED_DATA
数据结构来获取Windows Time,识别插桩环境。
示例如下:
Start = GetTickCount();
...//执行耗时的操作
Stop = GetTickCount();
TimeUsed = Stop-Start;
if(TimeUsed > Threshold)
{
FindDBIandGoDie()//检测到DBI
}
也可通过rdtsc指令来获取CPU Time。
不支持指令
在pin平台的pinvm.dll中存在以下提示信息,说明pin插桩平台并不支持FAR RET
可以利用该指令触发的异常,来实现插桩平台检测。
总结
本文对多种反插桩技术进行梳理与分析,旨在为安全研究人员提供一些思路参考。
实际情况下,对不同插桩平台的检测手段多种多样,本文的不足与疏漏之处,敬请批评指正,共同学习提高。