恶意代码对抗技术入门

  • 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;
}

猜你喜欢

转载自blog.csdn.net/kelxLZ/article/details/112909915
今日推荐