- Author:ZERO-A-ONE
- Date:2021-01-20
来自视频《漏洞银行|浅谈恶意代码对抗技术》,主要从两个方面进行入手:
- 对抗反汇编
- 反调试
一、对抗反汇编(静态)
1.1 基本概念
- 汇编语言:CPU执行的是机器码,为了方便记忆,在汇编语言中用助记符代替机器指令的操作码
- 反汇编:把目标代码转为汇编代码的过程
- 对抗反汇编技术:就是在程序中使用一些特殊构造的代码或数据,让反汇编工具产生不正确的汇编代码
1.2 反汇编算法
1.2.1 线性反汇编算法
- 线性扫描是一种基础的反汇编算法,对文件二进制数据从开始到最后进行依次遍历并翻译成汇编语言。算法简单最容易实现,也容易造成分析错误
- 利用libdisasm反汇编库可以快速实现反汇编
- http://bastard.sourceforge.net/libdisasm.html
示例代码:
char buffer[BUF_SIZE];
int position = 0;
while(position < BUF_SIZE){
x86_insn_t insn; //初始化一个结构体
int size=x86_sisasm(buffer,BUF_SIZE,0,position,&insn); //填充insn结构体
if(size != 0){
char disassembly_line[1024];
x86_format_insn(&insn,disassembly_line,1024,intel_syntax); //接收反汇编的结果
printf("%s\n",disassembly_line);
position += size;
}else{
position++;
}
}
x86_cleanup();
1.2.2 挫败线性反汇编算法
在可执行文件中除了指令码,还有包含程序运行时所需的数据,如果在代码中插入了无用的数据字节,可能造成反汇编器解析错误
如上图的左边所示,很容易发现这个代码中的
call near ptr 15FF2A71h
存在很大的问题,因为这个在内存中这是一个很大的地址,不可能跳转到这个位置,就可以认为存在反汇编错误。如右图所示才是正确的反汇编结果
1.2.3 面向代码流的反汇编算法
- 面向代码流的反汇编是一种更先进的反汇编算法,反汇编器不会盲目地反汇编整个文件,它会检查每一条指令,建立一个需要反汇编的地址列表。这种算法被广泛地应用于商业反汇编器,例如IDA
1.2.4 两种反汇编算法对比
当线性反汇编器遇到jmp指令,不管代码逻辑如何,都会一昧地按照字节序列去反汇编,如图,恶意代码作者可以利用此特性来隐藏一些敏感字符串
面向代码流地反汇编器在遇到jmp指令时,会计算跳转到的位置,从而将不会执行的代码视为数据
1.3 对抗反汇编算法
1.3.1 面向代码流算法缺陷
恶意代码实现对抗反汇编技术的主要方法是利用反汇编器选择算法和假设算法的漏洞:
- 条件分支使得面向代码流的反汇编器从true或false两个分支处选择一个优先进行反汇编,大多数面向代码流的反汇编器会优先处理false分支
- 大多数反汇编器会首先反汇编紧随call调用的字节,其次是call调用的位置字节
1.3.2 相同目标的跳转指令
有如下的反汇编代码指令:
74 03 jz short near ptr loc_4011C4+1
75 01 jnz short near ptr loc_4011C4+1
loc_4011C4: ;CODE XREF:sub_4011C0
; sub_4011C0+2j
E8 58 C3 90 90 call near ptr 90D0D521h
jz和jnz两个跳转均指向同一个地址,相当于无条件跳转jmp指令,但是反汇编器看到jnz后,依然会默认优先反编译false分支,尽管事实上这个分支永远不会执行
从E8开始即为loc_4011C4,即原来无论如何jz和jnz都应该跳过E8而从58开始执行,然而因为反编译器的优先反编译false分支,错把E8视作指令码的一部分,反编译成了call指令,正确的反汇编代码应该是:
74 03 jz short near ptr loc_4011C5
75 01 jnz short near ptr loc_4011C5
;------------------------------------------------
E8 db 0E8h
;------------------------------------------------
loc_4011C5: ;CODE XREF:sub_4011C0
; sub_4011C0+2j
58 pop eax
C3 retn
1.3.3 固定条件的跳转指令
跳转指令总是固定的,导致跳转指令总是跳转到true分支,由于反汇编器会优先处理false分支,但是false分支代码会与true分支代码冲突
33 C0 xor eax,eax
74 01 jz short near ptr loc_4011C4+1
loc_4011C4: ;CODE XREF: 004011C2j
;DATA XREF: .rdata:004020AC0
E9 58 C3 68 94 jmp near ptr 94A8D521h
正确的反汇编代码应该为:
33 C0 xor eax,eax
74 01 jz short near ptr loc_4011C4+1
;------------------------------------------------
E9 db 0E9h
;------------------------------------------------
loc_4011C5: ;CODE XREF: 004011C2j
;DATA XREF: .rdata:004020AC0
58 pop eax
C3 retn
1.3.4 反-反-反汇编
之前介绍的两种方法都是因为有流氓字节的存在导致反汇编器产生不正确的反汇编代码,从而将数据字节翻译成指令字节,因此可以手动进行数据、指令转换,在IDA中,按键C可将数据转换成指令,按键D可将指令转换成数据
在IDA中标红的地方,说明存在反汇编错误
有一种利用方式会导致流氓字节不能忽略,所有字节都是指令的一部分,且有的字节在执行时会同时属于两个或多个指令,目前业内没有一款反汇编器能够将单个字节表示为两条指令的组成部分
1.3.5 无效的反汇编–入门案例
- 在4个字节序列中,第一条指令是一条2字节的jmp指令,jmp指令的跳转目标是他的第二个字节,这样第二个字节FF既属于jmp-1指令,又属于inc eax指令
- 反汇编器如果将FF作为jmp指令的一部分,就不能作为inc eax指令开头字节进行显示
- 这4字节序列本质上是复杂的NOP序列,几乎可以插入在程序的任何位置,从而破坏反汇编链。遇到这样的字节序列时,可将上述4字节转换成数据进行忽略
相当于是:
JMP -1
INC EAX
DEX EAX
1.3.6 无效的反汇编–进阶案例
- 此序列等价于xor eax eax,但混淆过的反汇编代码会无法识别retn返回指令,会导致函数无法正常结束,造成大量代码错误反汇编
- 解决办法就是仔细分析机器码,正确分析真正进行的修改数据操作
相当于是:
MOV ax,05EBh
XOR eax,eax
JZ -6
JMP 5
Real Code
在IDA中这段代码会被反汇编成为:
66 B8 EB 05 mov ax,5EBh
31 C0 xor eax,eax
74 FA jz short near ptr sub_4011C0+2
loc_4011C8:
E8 58 C3 90 90 call near ptr 98A80525h
正确修改后应该为:
66 byte_4011C0 db 66h
88 db 0B8h
EB db 0E8h
05 db 5
;------------------------------------------------
31 C0 xor eax,eax
;------------------------------------------------
74 db 74h
FA db 0FAh
E8 db 0E8h
;------------------------------------------------
58 pop eax
C3 retn
二、反调试(动态)
2.1 反调试技术
- 恶意代码利用反调试技术可识别当前是否被调试,识别后通常会改变执行路径或修改自身让程序崩溃,从而增加调试时间和复杂度
2.2 使用Windows API
-
IsDebuggerPresent:查询进程环境块(PEB)中的IsDebugger标志,没有被调试返回0,被调试返回非0(关于PEB细节后面会介绍)
-
CheckRemoteDebuggerPresent:与IsDebuggerPresent类似,区别在于它还可以检测其他进程是否被调试
-
NtQueryInformationProcess:用于提取一个给定进程的信息,第一个参数是进程句柄,第二个参数是信息类型,第二个参数如果设置为ProcessDebugPort(值为0x7),返回进程是否被调试
-
FindWindowA:检测窗口名
-
OutputDebugString:在调试器中显示一个字符串
-
使用SetLastError函数设置任意一个错误码,如果进程没有被调试,调用OutputDebugString会失败,系统会重置错误码
-
DWORD errorValue = 12345; SetLastError(errorValue); OutputDebugString("Test for Debugger"); if(GetLastError() == errorValue){ ExitProcess(); } else{ ... }
-
2.3 注册表检测
- 以下是调试器在注册表中常用的位置:
HKLM\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\AeDebug
,默认情况下,它被设置为Dr.Wastson
,如果被修改为OllyDbg,恶意代码就可能确定它正在被调试
2.4 进程环境块(PEB)
- Windows为每个正在运行的进程维护者PEB结构,结构中包含这个进程相关的所有用户态参数,其中就包含着进程的调试状态
- 具体参考:https://bbs.pediy.com/thread-52398.html
PEB结构体如下所示:
typedef struct _PEB {
// Size: 0x1D8
000h UCHAR InheritedAddressSpace;
001h UCHAR ReadImageFileExecOptions;
002h UCHAR BeingDebugged; //Debug运行标志
003h UCHAR SpareBool;
004h HANDLE Mutant;
008h HINSTANCE ImageBaseAddress; //程序加载的基地址
00Ch struct _PEB_LDR_DATA *Ldr //Ptr32 _PEB_LDR_DATA
010h struct _RTL_USER_PROCESS_PARAMETERS *ProcessParameters;
014h ULONG SubSystemData;
018h HANDLE ProcessHeap;
01Ch KSPIN_LOCK FastPebLock;
020h ULONG FastPebLockRoutine;
024h ULONG FastPebUnlockRoutine;
028h ULONG EnvironmentUpdateCount;
02Ch ULONG KernelCallbackTable;
030h LARGE_INTEGER SystemReserved;
038h struct _PEB_FREE_BLOCK *FreeList
03Ch ULONG TlsExpansionCounter;
040h ULONG TlsBitmap;
044h LARGE_INTEGER TlsBitmapBits;
04Ch ULONG ReadOnlySharedMemoryBase;
050h ULONG ReadOnlySharedMemoryHeap;
054h ULONG ReadOnlyStaticServerData;
058h ULONG AnsiCodePageData;
05Ch ULONG OemCodePageData;
060h ULONG UnicodeCaseTableData;
064h ULONG NumberOfProcessors;
068h LARGE_INTEGER NtGlobalFlag; // Address of a local copy
070h LARGE_INTEGER CriticalSectionTimeout;
078h ULONG HeapSegmentReserve;
07Ch ULONG HeapSegmentCommit;
080h ULONG HeapDeCommitTotalFreeThreshold;
084h ULONG HeapDeCommitFreeBlockThreshold;
088h ULONG NumberOfHeaps;
08Ch ULONG MaximumNumberOfHeaps;
090h ULONG ProcessHeaps;
094h ULONG GdiSharedHandleTable;
098h ULONG ProcessStarterHelper;
09Ch ULONG GdiDCAttributeList;
0A0h KSPIN_LOCK LoaderLock;
0A4h ULONG OSMajorVersion;
0A8h ULONG OSMinorVersion;
0ACh USHORT OSBuildNumber;
0AEh USHORT OSCSDVersion;
0B0h ULONG OSPlatformId;
0B4h ULONG ImageSubsystem;
0B8h ULONG ImageSubsystemMajorVersion;
0BCh ULONG ImageSubsystemMinorVersion;
0C0h ULONG ImageProcessAffinityMask;
0C4h ULONG GdiHandleBuffer[0x22];
14Ch ULONG PostProcessInitRoutine;
150h ULONG TlsExpansionBitmap;
154h UCHAR TlsExpansionBitmapBits[0x80];
1D4h ULONG SessionId;
} PEB, *PPEB;
其中与反调试技术密切相关的成员如下所示:
002h UCHAR BeingDebugged;
00Ch struct _PEB_LDR_DATA *Ldr;
018h HANDLE ProcessHeap;
068h LARGE_INTEGER NtGlobalFlag;
接下来分别讲解以上4个PEB成员
2.4.1 检测BeingDebugged标志
- 程序运行时,fs:[30h]指向PEB基址,程序可检查BeginDebugger标志确认自己是否被调试
- 当进程处于调试状态时,BeingDebugged的值会被设置为1,进程在非调试状态下运行时,其值被设置为0。所以我们可以通过判断这个成员的值来决定我们程序的运行流程
测试代码如下:
int main()
{
charresult=0;
__asm
{
moveax,fs:[0x30];//获取PEB的地址。
moval,BYTE PTR [eax+2];
movresult,al;//得到BeingDebugged成员的值。
}
if(result==1)
printf("isdebugging\n");
else
printf("notdebugging\n");
system("pause");//为了观察方便,添加的。
return 0;
}
对于汇编代码来说即为:
mov eax,dword ptr fs:[30h]
mov ebx,byte ptr [eax+2]
test ebx,ebx
jz NoDebuggerDetected
或者:
push dword ptr fs:[30h]
pop edx
cmp byte ptr [edx+2],1
je DebuggerDetected
2.4.2 检测ProcessHeap标志
- ProcessHeap是PEB中未被微软公开的属性,位于PEB结构体0x18处,ProcessHeap中包含ForceFlags,该属性值可判断当前进程是否被调试,在XP中偏移为0x10,Win7下为0x44
- ProcessHeap是指向HEAP结构体的指针,在HEAP结构体中的两个成员Flags和Force Flags,它们的偏移分别为0xC和0x10,进程运行正常时,Heap.Flags的成员值为0x2,HEAP.ForceFlags成员的值为0x0。进程处于调试状态时,这些值会发生变化
测试代码如下:
int main()
{
intresult=0;
__asm
{
moveax,fs:[0x30]; //PEB地址
moveax,[eax+0x18];//ProcessHeap成员
moveax,[eax+0x10];//ForceFlags成员
movresult,eax;
}
if(result!=0)
printf("isdebugging\n");
else
printf("notdebugging\n");
system("pause");
return 0;
}
汇编代码也可以是:
mov eax,large fs:30h
mov eax,dword ptr [eax+18h]
cmp dword ptr ds:[eax+10h],0
jne DebuggerDetected
2.4.3 检测NtGlobalFlag标志位
- 由于调试器中启动进程与正常模式下启动进程有区别,所以它们创建内存堆的方式也不同,NTGlobalFlag也是未公开的,在PEB偏移0x68,当值为0x70时,代表程序从调试器启动
测试代码如下:
int main()
{
intresult=0;
__asm
{
moveax,fs:[0x30]; //PEB地址
moveax,[eax+0x68];//NtGlobalFlag成员
movresult,eax;
}
if(result==0x70)
printf("isdebugging\n");
else
printf("notdebugging\n");
system("pause");
return 0;
}
汇编代码如下:
mov eax,large fs:30h
cmp dword ptr ds:[eax+68h],70h
jz DebuggerDetected
2.4.4 检测Ldr标志位
调试进程时,其堆内存就会出现一些特殊的标识,表示它正处于被调试状态。这些标识中最醒目的是在未使用的堆内存区域中填充着OxFEEEFEEE。我们利用这一特征即可判断进程是否处于被调试状态。PEB.Ldr成员指向一个_PEB_LDR_DATA结构体,而这个结构体就是在堆内存区域中创建的,所以我们可以扫描该区域来判断进程是否处于调试状态下
检测代码如下:
int main()
{
LPBYTEpLdr;
DWORDpLdrSig[4]={
0xEEFEEEFE,0xEEFEEEFE,0xEEFEEEFE,0xEEFEEEFE};
__asm
{
moveax,fs:[0x30]; //PEB地址
moveax,[eax+0xC];//Ldr
movpLdr,eax;
}
__try
{
while(1){
if(!memcmp(pLdr,pLdrSig,sizeof(pLdrSig)){
printf("is debuggig\n");
break;}
else{
pLdr++;}
}
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
printf("notdebugging\n");
}
system("pause");
return 0;
}