原创干货 | 【恶意代码分析技巧】17-对抗检测分析

admin 2021年5月6日19:12:45评论64 views字数 21079阅读70分15秒阅读模式

为了对抗安全工具和安全人员的检测分析,许多恶意代码都包含了对抗检测分析的功能。一方面,恶意代码使用各种技术检测当前的运行环境,判断自己是否正在被检测分析;另一方面,恶意代码还会使用各种混淆技术增加恶意代码分析的难度。

1.反调试

反调试主要是为了对抗调试分析,当进程检测到自己处于调试状态时,就不会执行恶意代码,从而让分析人员/工具误以为当前进程不包含恶意代码。

1.1.检测进程

进程运行过程中,程序可以通过一些方式获得自身的运行状态,判断自己是否处于调试状态。

1.1.1.PEB

PEB(Process EnvironmentBlock,进程环境块),记录了当前进程信息的结构体,利用PEB可以直接判断进程是否处于调试状态。

获取PEB结构地址的方式是通过TEB(ThreadEnvironmentBlock,线程环境块),FS寄存器指向当前活动线程的TEB结构,PEB和TEB的结构与系统有关,32位XP下PEB和TEB的结构可以参考下图(超链接:http://www.secist.com/archives/3488.html):

原创干货 | 【恶意代码分析技巧】17-对抗检测分析

由上可知,FS、FS:[0x18]均指向TEB,FS:[0x30]指向PEB。

在PEB中,有三种方式判断当前进程是否处于调试状态:

①+0x02 BeingDebugged

PEB中该成员记录进程是否处于调试状态,当进程处于调试状态时,该值为1;

②+0x18 ProcessHeap

ProcessHeap指向进程第一个堆的位置,而堆的结构中Flags(+0x0C)和ForceFlags(+0x10)标志会受到调试器的影响。

在正常情况下Flags的值为2,ForceFlags的值为0。当调试器存在时,Flags会受到影响变为:

原创干货 | 【恶意代码分析技巧】17-对抗检测分析

ForceFlags会受到影响变为:

原创干货 | 【恶意代码分析技巧】17-对抗检测分析

需要注意的是Flag和ForceFlags在堆结构中的的位置也受到系统版本的影响,上述是在XP,32位环境下的演示,更多系统的介绍可以参考此文(超链接:https://wiki.x10sec.org/reverse/anti-debug/heap-flags/#heap-flags)。

③+x068 NtGlobalFlag

在32位机器上,NtGlobalFlag字段位于PEB(进程环境块)0x68的偏移处,64位机器则是在偏移0xBC位置。该字段的默认值为0.当调试器正在运行时,该字段会被设置为0x70:

原创干货 | 【恶意代码分析技巧】17-对抗检测分析

1.1.2.IsDebuggerPresent
函数原型

BOOL WINAPI IsDebuggerPresent(void);

该API用于判断进程是否由用户模式的调试器调试。

该API内部就是检查PEB的BeingDebugged成员。

1.1.3.NtQueryInformationProcess

__kernel_entry NTSTATUS NtQueryInformationProcess(

IN HANDLE ProcessHandle, //进程句柄

IN PROCESSINFOCLASS ProcessInformationClass, //要获取的进程信息的类型

OUT PVOID ProcessInformation, //输出的进程信息

IN ULONG ProcessInformationLength,

OUT PULONG ReturnLength

);

这个函数是Ntdll.dll中一个API,它能获取给定进程的信息。第二个参数是一个枚举类型,其中与反调试有关的成员有ProcessDebugPort(0x7)、ProcessDebugObjectHandle(0x1E)和ProcessDebugFlags(0x1F)。当API第二个参数为这几个值时,可以通过输出的进程信息判断进程是否处于调试状态。

①ProcessDebugPort(0x7)

进程处于调试状态时,系统会为它分配一个调试端口,NtQueryInformationProcess()可以利用ProcessDebugPort获取调试端口的值,调试状态该值为0xFFFFFFFF,正常运行时该值为0。

②ProcessDebugObjectHandle(0x1E)

调试的时候会生成调试对象(DebugObject),进程处于调试状态的时候,调试对象句柄值就存在,处于非调试状态的时候,调试对象句柄值为NULL。

③ProcessDebugFlags(0x1F)

检测DebugFlags(调试标志)的值,若为0,则处于调试状态,若为1,则处于非调试状态。

1.1.4.checkRemoteDebuggerPresent

BOOL CheckRemoteDebuggerPresent(
HANDLE hProcess,
PBOOL pbDebuggerPresent //如果hProcess所代表的进程被调试,此变量返回TRUE,否则返回FALSE);

这个API内部就是通过ProcessDebugPort判断进程是否处于调试状态。

1.1.5.STARTUPINFO

获取StartupInfo结构体信息可以使用GetStartupInfo函数:

VOID GetStartupInfo(
LPSTARTUPINFO lpStartupInfo // address of STARTUPINFO structure

);

程序双击启动时,是explorer.exe调用CreateProcess()函数创建进程启动的,explorer会把CreateProcess倒数第2个参数STARTUPINFO结构体中的值设置为0,但调试器启动程序的时候不会,所以我们可以通过判断该STARTUPINFO结构体中的某些值是否与双击启动时STARTUPINFO结构体的值是否相同来判断是否被调试。

1.1.6.SedebugPrivilege

一般程序正常启动时是不具备调试权限(SedebugPrivilege)的,除非自己有提权的需要主动开启,但是调试器启动程序的时候,由于调试器本身会开启调试权限,所以被调试进程会继承调试权限,因此我们可以通过检查进程是否具有调试权限来进行反调试。

可以通过CSSS.EXE进程间接地测试SeDebugPrivilege权限。可以通过判断能否使用OpenProcess打开该CSSS.EXE进程来检查当前进程是否具有调试权限,因为只有拥有管理员权限+调试权限的进程才能打开csrss.exe的句柄。

1.1.7.0xCC探测

在调试过程中,我们一般会设置许多断点,软件断点对应的汇编指令是0xCC,若能检测到0xCC代码的存在,则可以判断程序处于调试状态。

但是,0xCC既可能是指令,也可能是数据,所以在进行0xCC探测时一定要选择正确的位置!

工程师们在进行软件调试的时候,往往会在一些重要的API上下断点,这些断点都设置在API代码的第一个字节,所以只需要检测API代码的第一个字节是否为0xCC,即可大致判断出进程是否处于调试状态。

还有一种方式,就是计算核心的代码区域代码的校验和,如果发现不一致,即可判断进程处于调试状态。

1.2.检测运行环境

1.2.1.注册表检测

查看JIT调试器是否有调试器字符串:

HKLMSOFTWAREMicrosoftWindowsNTCurrentVersionAeDebug(32位系统)

HKLMSOFTWAREWow6432NodeMicrosoftWindowsNTCurrentVersionAeDebug(64位系统)

1.2.2.窗口检测

①FindWindow

HWND FindWindowA(

LPCSTR lpClassName,

LPCSTR lpWindowName

);

我们可以直接调用Findwindow查找已知调试器的窗口(类)名,例如Ollydbg进程的窗口名为”ollydbg”,可以直接查找该窗口:

hWnd = CWnd::FindWindow(_T("ollydbg"), NULL);

if (hWnd != NULL)

MessageBox(”发现OD!”);

除了OD,还有一些其他的调试器/分析工具:

原创干货 | 【恶意代码分析技巧】17-对抗检测分析
②EnumWindow

BOOL EnumWindows(

WNDENUMPROC lpEnumFunc,

LPARAM lParam

);
EnumWindow函数枚举所有屏幕上的顶层窗口,并将窗口句柄传送给应用程序定义的回调函数,在回调函数中可以用GetWindowText得到窗口标题。

BOOL CALLBACK EnumWndProc(HWND hwnd, LPARAM lParam)

{

char cur_window[1024];

GetWindowTextA(hwnd, cur_window, 1023);

if (strstr(cur_window, "WinDbg") != NULL|| strstr(cur_window, "OllyDBG") != NULL)

{

((BOOL)lParam) = TRUE;

}

return TRUE;

}

BOOL CheckDebug()

{

BOOL ret = FALSE;

EnumWindows(EnumWndProc, (LPARAM)&ret);

return ret;

}

③GetForeGroundWindow

HWND GetForegroundWindow( );

GetForeGroundWindow返回用户当前工作的窗口(前台窗口)。当程序被调试时,调用这个函数将获得调试器的窗口句柄:

BOOL CheckDebug()

{

char fore_window[1024];

GetWindowTextA(GetForegroundWindow(), fore_window, 1023);

if (strstr(fore_window, "WinDbg") != NULL|| strstr(fore_window, "OllyDBG") != NULL )

{return TRUE;}

else

{return FALSE;}

}

1.2.3.进程扫描

遍历当前系统里的进程,查找系统中是否有有调试器相关的进程,如果有,则说明当前可能处于调试环境。

进程遍历的方式很多,我们主要介绍一下几种:
①使用powershell;
②使用WMI查询;
③CreateToolhelp32Snapshot进程快照;
④EnumProcesses()枚举进程;
⑤ZwQuerySystemInformation;
⑥WTSOpenServer()、WTSEnumerateProcess();

①使用powershell;

使用powershell的get-ciminstance类,查询win32_process信息就能获取所有进程信息:

原创干货 | 【恶意代码分析技巧】17-对抗检测分析

②使用WMI查询;

使用WQL语句查询:

strComputer = "."

Set objWMIService = GetObject("winmgmts:" & strComputer & "rootCIMV2")

Set colItems = objWMIService.ExecQuery("SELECT * FROM Win32_Process" ,,48)

For Each objItem in colItems

Wscript.Echo "-------------------------------------------"

Wscript.Echo "CommandLine: " & objItem.CommandLine

Wscript.Echo "Name: " & objItem.Name

Next

③CreateToolhelp32Snapshot进程快照;

使用进程快照是最常使用的进程遍历技术。这个技术还需要结合Process32First/Process32Next使用。

//CreateToolhelp32Snapshot可以通过获取进程信息为指定的进程、进程使用的堆[HEAP]、模块[MODULE]、线程建立一个快照。

HANDLE WINAPI CreateToolhelp32Snapshot(

DWORD dwFlags, //用来指定“快照”中需要返回的对象,可以是TH32CS_SNAPPROCESS等

DWORD th32ProcessID //一个进程ID号,用来指定要获取哪一个进程的快照,当获取系统进程列表或获取 当前进程快照时可以设为0

);//返回快照的句柄HANDLE hSnapshot

//当我们利用函数CreateToolhelp32Snapshot()获得当前运行进程的快照后,我们可以利用process32First函数来获得第一个进程的句柄。

BOOL WINAPI Process32First(

HANDLE hSnapshot,//_in

LPPROCESSENTRY32 lppe//_out

);

//当我们利用函数CreateToolhelp32Snapshot()获得当前运行进程的快照后,我们可以利用Process32Next函数来获得下一个进程的句柄。

BOOLWINAPIProcess32Next(

HANDLE hSnapshot,//_in

LPPROCESSENTRY32 lppe//_out

);

我们获取到的进程信息都存储在LPPROCESSENTRY32的结构体中:

typedef struct tagPROCESSENTRY32 {

DWORD dwSize; // 结构大小;

DWORD cntUsage; // 此进程的引用计数;

DWORD th32ProcessID; // 进程ID;

DWORD th32DefaultHeapID; // 进程默认堆ID;

DWORD th32ModuleID; // 进程模块ID;

DWORD cntThreads; // 此进程开启的线程计数;

DWORD th32ParentProcessID;// 父进程ID;

LONG pcPriClassBase; // 线程优先权;

DWORD dwFlags; // 保留;

WCHAR szExeFile[MAX_PATH]; // 进程全名;

} PROCESSENTRY32;

利用该结构体,我们就能获取进程名、进程ID、父进程ID等信息。

进程遍历的核心代码:

HANDLE hProcessSanp = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

BOOL bMore = Process32First(hProcessSanp, &pe32);

while (bMore)

{

printf("Process Name: %sttProcess ID: %dn", pe32.szExeFile, pe32.th32ProcessID);

bMore = Process32Next(hProcessSanp, &pe32);

}

CloseHandle(hProcessSanp);

④EnumProcesses枚举进程;

调用EnumProcesses,可以获得所有进程的PID列表,还需要进一步调用EnumProcessModules和GetModuleBaseName来获取进程名

BOOL EnumProcesses( //枚举进程,获取模块句柄

DWORD *lpidProcess,//获取PID列表

DWORD cb,

LPDWORD lpcbNeeded

);

BOOL EnumProcessModules( //枚举进程模块

HANDLE hProcess,

HMODULE *lphModule,

DWORD cb,

LPDWORD lpcbNeeded

);

DWORD GetModuleBaseNameA( //获取模块名——进程名

HANDLE hProcess,

HMODULE hModule,

LPSTR lpBaseName,

DWORD nSize

);

核心代码

if (!EnumProcesses(aProcesses, sizeof(aProcesses), &cbNeeded)) //枚举进程

return;

cProcesses = cbNeeded / sizeof(DWORD); //计算进程个数

for (i = 0; i < cProcesses; i++)

if (aProcesses[i] != 0){

HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, aProcesses[i]); //获得进程句柄

if (NULL != hProcess){

HMODULE hMod;

DWORD cbNeeded;

if (EnumProcessModules(hProcess, &hMod, sizeof(hMod), &cbNeeded)) //枚举进程模块信息

{

GetModuleBaseName(hProcess, hMod, szProcessName, sizeof(szProcessName) / sizeof(TCHAR)); //取得主模块全名,每个进程第一模块则为进程主模块

}

}

_tprintf(TEXT("%s (PID: %u)n"), szProcessName, aProcesses[i]); //输出进程名及PID

}

}

⑤ZwQuerySystemInformation

NTSTATUS ZwQuerySystemInformation(

IN SYSTEM_INFORMATION_CLASS SystemInformationClass, //处理进程信息,只需要处理类别为5的即可

Inout PVOID SystemInformation,

In ULONG SystemInformationLength,

Out_opt PULONG ReturnLength

}

ZwQuerySystemInformation是一个NativeAPI,调用这个API能查询各种系统信息。ZwQuerySystemInformation的第一个参数SYSTEM_INFORMATION_CLASS是一个枚举类型,详细的枚举列表可以查看这里(超链接:https://www.cnblogs.com/ymzh1/p/9408195.html),当第一个参数为SystemProcessInformation(0x5)时,函数执行完毕后第二个参数为输出进程信息的缓冲区,为一个指向SYSTEM_PROCESS_INFORMATION结构体指针,第三个参数为返回长度,第四参数为已返回的长度。

typedef struct SYSTEM_PROCESS_INFORMATION

{

ULONG NextEntryDelta; //链表下一个结构和上一个结构的偏移

ULONG ThreadCount;

ULONG Reserved[6];

LARGE_INTEGER CreateTime;

LARGE_INTEGER UserTime;

LARGE_INTEGER KernelTime;

UNICODE_STRING ProcessName; //进程名字

KPRIORITY BasePriority;

ULONG ProcessId; //进程的pid号

ULONG InheritedFromProcessId;

ULONG HandleCount;

ULONG Reserved2[2];

VM_COUNTERS VmCounters;

IO_COUNTERS IoCounters; //windows 2000 only

struct _SYSTEM_THREADS Threads[1];

}SYSTEM_PROCESSES, *PSYSTEM_PROCESSES;

在SYSTEM_PROCESS_INFORMATION结构中就记录着进程信息,结构的NextEntryOffset成员指向下一项目,遍历此链表即可枚举进程,指向0则表示已达链表尾部。

进程遍历核心代码:

//获取大小

status = ZwQuerySystemInformation(SystemProcessesAndThreadsInformation, NULL, 0, &dwRetSize);

//申请内存

pBuftmp = ExAllocatePool(NonPagedPool, dwRetSize); //dwRetSize 需要的大小

if (pBuftmp != NULL)

{

//再次执行,将枚举结果放到指定的内存区域

status = ZwQuerySystemInformation(SystemProcessesAndThreadsInformation, pBuftmp, dwRetSize, NULL);

if (NT_SUCCESS(status))

{

pSysProcInfo = (PSYSTEM_PROCESS_INFORMATION)pBuftmp;

//循环遍历

while (TRUE)

{

pEproc = NULL;

if (PsLookupProcessByProcessId((HANDLE)pSysProcInfo->ProcessId, &pEproc) == STATUS_SUCCESS)

KdPrint(("Eprocess: 0x%08Xn", pEproc));

//ptagProc->NextEntryOffset==0 即遍历到了链表尾部

if (pSysProcInfo->NextEntryOffset == 0)

break;

//下一个结构

pSysProcInfo = (PSYSTEM_PROCESS_INFORMATION)((ULONG)pSysProcInfo + pSysProcInfo->NextEntryOffset);

}

}

}

⑥WTSOpenServer()、WTSEnumerateProcess()

利用WTSOpenServer打开终端服务器(本地主机),然后调用WTSEnumerateProcess遍历进程。

HANDLE WTSOpenServerA(

LPSTR pServerName // 终端服务器名称,本地为空

);

BOOL WTSEnumerateProcessesA(

IN HANDLE hServer,// 终端服务器的句柄

IN DWORD Reserved,

IN DWORD Version,//指定枚举请求的版本,必须为1

OUT PWTS_PROCESS_INFOA *ppProcessInfo,//输出参数,指向PWTS_PROCESS_INFO结构的指针, WTS_PROCESS_INFO 结构里存有进程的信息,包括name和ID

OUT DWORD *pCount//输出参数,返回枚举到的个数,即PWTS_PROCESS_INFO的数量。

);

核心代码:

WCHAR * szServerName = NULL;

HANDLE WtsServerHandle = WTSOpenServer(szServerName);

// 然后开始遍历终端服务器上的所有进程,这里我们是指本机的所有进程.

PWTS_PROCESS_INFO pWtspi;

DWORD dwCount;

WTSEnumerateProcesses(WtsServerHandle, 0, 1, &pWtspi, &dwCount)

for (DWORD i = 0; i < dwCount; i++){

printf("ProcessID: %d (%ls)n", pWtspi[i].ProcessId,

pWtspi[i].pProcessName);}

1.2.4.父进程检测

通常进程的父进程是explorer.exe,否则可能程序被调试,一些恶意程序使用这一特性进行反调试。

有两种方式可以比较父进程是否是explorer.exe。

①使用进程快照:

在进程遍历中我们使用Process32First/Process32Next得到了PROCESSENTRY32结构体,PROCESSENTRY32.th32ParentProcessID成员记录了父进程的PID,如果父进程的PID不是explorer.exe、cmd.exe、service.exe的PID,那么当前进程很有可能被调试。也可以通过PID调用OpenProcess、GetProcessImageFileName获取父进程的文件名再比较。

②NtQueryInformationProcess

在前文我们介绍过NtQueryInformationProcess了,不在赘述。这里我们调用NtQueryInformationProcess的时候设置第四个参数的值是PROCESS_BASIC_INFORMATION,我们就能得到_PROCESS_BASIC_INFORMATION信息:

typedef struct _PROCESS_BASIC_INFORMATION {

PVOID Reserved1;

PPEB PebBaseAddress;

PVOID Reserved2[2];

ULONG_PTR UniqueProcessId;

PVOID Reserved3;

} PROCESS_BASIC_INFORMATION;

typedef PROCESS_BASIC_INFORMATION *PPROCESS_BASIC_INFORMATION;

该结构体中的"Reserved3"字段就是父进程的PID,有了PID接下来的操作就和上面一样了。

1.2.5.内核对象扫描

使用NtQueryObject查询内核对象:

NTSTATUS NtQueryObject(

In_opt HANDLE Handle,

In OBJECT_INFORMATION_CLASS objectInformationClass,

Out_opt PVOID ObjectInformation,

In ULONG ObjectInformationLength,

Out_opt PULONG ReturnLength

);

当一个调试活动开始,一个"debugobject"被创建。我们可以查询这个调试对象列表,以实现调试器的检测。

将参数ObjectInformationClass设为ObjeceAllTypesInformation(3),调用NtQueryObject后,包含相关信息的结构体指针就返回给第三个参数ObjectInformation,然后从中检测是否存在调试对象。

1.2.6.调试模式检测

当进行内核调试时,虚拟机处于被调试状态,可以调用NtQuerySystemInformation查看系统是否处于调试状态。

NtQuerySystemInformation第一个参数SYSTEM_INFORMATION_CLASS=0x23时,会将调试信息输出到第2个参数指向的结构中,该结构是2个1字节的结构,当处于调试时这2字节都会被写入1。

1.3.异常处理

1.3.1.发生异常时系统的处理顺序

进行异常处理的模块主要有三个:SEH、SetUnhandleExceptionFilter注册的回调函数和系统默认的异常处理UEF。当线程初始化,会默认安装一个SEH,每个线程都拥有自己的SEH链表。此外进程中也有一个全局的异常处理,当线程无法处理时,进程SEH发挥作用,进程的异常处理回调函数需要通过API函数SetUnhandleExceptionFilter来注册。这种处理可能影响所有线程。除了进程线程异常处理,操作系统会为所有程序安装一个异常处理(系统默认的异常处理UEF),这个最终的处理一般是错误对话框。

发生异常时系统的处理顺序如下:

①操作系统首先判断异常是否应发送给目标程序的异常处理例程,如果决定应该发送,并且目标程序正在被调试,则系统挂起程序并向调试器发送EXCEPTION_DEBUG_EVENT消息,由调试器处理该异常;

②如果程序没有被调试或者调试器未能处理异常,操作系统就会继续查找程序是否安装了线程相关的异常处理例程——SEH,如果安装,系统就把异常发送给程序的SEH处理例程;

③SEH是链表结构,如果当前SEH无法处理这个异常,将会传递给下一个SEH处理;

④如果这些SEH例程均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂起程序通知调试器;

⑤如果程序未处于被调试状态或者调试器依然没有能够处理,并且你调用SetUnhandledExceptionFilter安装了最后异常处理例程的话,系统转向对它的调用。

⑥如果你没有安装最后异常处理例程或者它没有处理这个异常,系统会调用默认的系统处理程序,通常显示一个对话框,你可以选择关闭或者最后将其附加到调试器上的调试按钮。如果没有调试器能被附加于其上或者调试器也处理不了,系统就调用ExitProcess终结程序。

⑦不过在终结之前,系统仍然对发生异常的线程异常处理句柄来一次展开,这是线程异常处理例程最后清理的机会。

1.3.2.SEH反调试

SEH(Structured ExceptionHandling),结构化异常处理,是windows系统提供的处理程序错误或异常的功能,在程序源代码中使用__try,__except,__finally关键字来具体实现。

SEH以链的形式存在,第一个异常处理器若未处理相关异常,它就会被传递到下一个异常处理器。由 _EXCEPTION_REGISTRATION_RECORD结构体组成链表:

typedef struct _EXCEPTION_REGISTRATION_RECORD

{

PEXCEPTION_REGISTRATION_RECORD Next; //指向下一个结构体的指针ffffffff表示最后一个

PEXCEPTION_DISPOSITION Handler; //异常处理函数

} EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;

异常处理函数的定义:

EXCEPTION_DISPOSITION _except_handler(

EXCEPTION_RECORD *ExceptionRecord, //指向EXCEPTION_RECORD结构体

void * EstablisherFrame, //指向_EXCEPTION_REGISTRATION_RECORD结构体

struct _CONTEXT *ContextRecord,//指向_CONTEXT结构体,用于备份CPU

void * DispatcherContext //可忽略);

这是一个回调函数,由系统调用给出参数。

其中EXCEPTION_RECORD结构体:

typedef struct _EXCEPTION_RECORD {

DWORD ExceptionCode; //异常类型

DWORD ExceptionFlags;

struct _EXCEPTION_RECORD *ExceptionRecord;

PVOID ExceptionAddress; //异常发生地址

DWORD NumberParameters;

ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];

} EXCEPTION_RECORD, *PEXCEPTION_RECORD;

ExceptionCode是异常类型,常见的几种异常:

define EXCEPTION_ACCESS_VIOLATION 0xC0000005//试图访问不存在或不具访问权限的内存区域;

define EXCEPTION_BREAKPOINT 80000003// 就是INT 3断点,0xCC;

define EXCEPTION_ILLEGAL_INSTRUCTION 0xC000001D//CPU无法解析的指令;

define EXCEPTION_INT_DIVIDE_BY_ZERO 0xC0000094// 0被除;

define EXCEPTION_SINGLE_STEP 0x80000004//单步执行的意思,通过这个异常暂停,将标志寄存器TF设为1,也可以单步运行

异常处理函数的返回值是_EXCEPTION_DISPOSITION枚举类型:

typedef enum _EXCEPTION_DISPOSITION

{

ExceptionContinueExecution = 0,//继续执行发生异常代码

ExceptionContinueSearch = 1, //运行下一个异常处理器SEH

ExceptionNestedException = 2, //在OS内部使用

ExceptionCollidedUnwind = 3 //在OS内部使用

} EXCEPTION_DISPOSITION;

正常运行的程序在发送异常时,最先起作用的是SEH机制。在SEH机制的作用下,OS会接收异常,然后调用进程中注册的SEH处理,但是如果进程处于被调试状态时,调试器就会接收异常并处理。调试器处理异常的例程和程序本身调用的SEH例程是不同的,攻击者可以利用这些不同来检测程序是否处于调试状态。

1.3.3.SetUnhandledExceptionFilter反调试

SetUnhandledExceptionFilter是用于注册异常回调函数的,当SEH不能出来异常,SetUnhandledExceptionFilter注册的函数就会来处理异常。

SetUnhandledExceptionFilter函数原型:

LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(

LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter // 异常处理回调函数指针。NULL值指定UnhandledExceptionFilter中的默认处理 。

);//该 SetUnhandledExceptionFilter函数返回的功能建立的前一个异常过滤器的地址。一个NULL的返回值意味着当前没有顶层异常处理程序。

其注册的异常处理回调函数原型:

LONG ***(

_EXCEPTION_POINTERS *ExceptionInfo //异常的描述以及异常时的处理器上下文

);

它的返回值是以下三者之一:

define EXCEPTION_EXECUTE_HANDLER 1 //表示我已经处理了异常, 可以优雅地结束了

define EXCEPTION_CONTINUE_SEARCH 0 //表示我不处理, 其他人来吧, 于是windows调用默认的处理程序显示一个错误框, 并结束

define EXCEPTION_CONTINUE_EXECUTION - 1// 表示错误已经被修复, 请从异常发生处继续执行

本质上,使用SetUnhandledExceptionFilter注册异常处理函数和使用SEH进行反调试的方式是相同的。

1.3.4.TF陷阱标志——异常

陷阱标志是EFLAGS寄存器的第九个比特位,又叫TF标志位。

当TF=1时,CPU进入单步执行模式,在单步执行中,CPU没执行1条指令后就会触发1个EXCEPTION_SINGLE_STEP异常,并将TF置0。正常运行的程序在遇到TF=1时,将会触发异常,进入程序自身SEH;而处于调试状态的程序,调试器将会处理这个异常,几乎所有的调试器都会处理这个异常,导致无法进程程序自身的SEH,从而改变程序执行流程:

原创干货 | 【恶意代码分析技巧】17-对抗检测分析

1.3.5.INT 3反调试

INT 3就是我们平时说的软件断点,INT3反调试属于SEH反调试,不过是我们选择了一个特殊的异常INT3异常。

1.3.6.INT 2D反调试

INT 2D是内核模式中用来触发断点异常的指令,也可以在用户模式下触发异常。

正常程序在运行的过程中,遇到INT2D,将会触发断点异常,进入SEH;处于调试状态的程序将由调试器处理这个异常,大多数的调试器只是将eip+1,然后就放行了,这就将改变程序的流程。恶意代码变可以通过这一点检测调试器。

如下图所示,一下调试器在执行完INT2D指令后,下一条指令的第一个字节将被忽略,后一个字节将会被识别为新的指令继续执行:

原创干货 | 【恶意代码分析技巧】17-对抗检测分析

1.3.7.GetLastError反调试

一些API在调用在出现错误后,可以调用GetLastError()来获取错误原因。恶意代码可以主动触发错误,调试器往往会先处理异常,并将异常结果交给程序,这个时候调用GetLastError()得到的错误原因和非调试状态运行得到的错误原因不同。恶意代码可以根据这点检测调试器。

①OutputDebugString

OutputDebugString这个API的作用是在调试器中显示一个字符串。使用SetLastError函数,将当前的错误码设置为一个任意值。如果进程没有被调试器附加,调用OutputDebugString函数会失败,错误码会重新设置,因此GetLastError获取的错误码应该不是我们设置的任意值。但如果进程被调试器附加,调用OutputDebugString函数会成功,这时GetLastError获取的错误码应该没改变。

②DeleteFiber

对于DeleteFiber函数,如果给它传递一个无效的参数的话会抛出ERROR_INVALID_PARAMETER异常。如果进程正在被调试的话,异常会被调试器捕获。所以,同样可以通过验证LastError值来检测调试器的存在。如代码所示,0x57就是指ERROR_INVALID_PARAMETER。

③其他

同样还可以使用CloseHandle、CloseWindow产生异常,使得错误码改变。

1.4.时钟检测

被调试的程序的运行速度往往低于正常状态下程序的运行速度,这是因为当进程被调试时,调试器事件处理代码、步过指令、中断等将占用大量CPU时间。一些恶意代码会计算相邻指令之间的时间差,如果时间差较大,则说明程序处于调试状态。

1.4.1.API

常用于计算CPU时间的API有QueryPerformanceCounter和GetTickCount:

BOOL QueryPerformanceCounter(

LARGE_INTEGER *lpPerformanceCount //参数指向计数器的值

);

DWORD GetTickCount(void);

QueryPerformanceCounter用于得到高精度计时器的值;GetTickCount函数返回最近系统重启时间与当前时间的相差毫秒数,返回值是DWORD,由于时钟计数器的大小原因,计数器每49.7天(0xFFFFFFFF毫秒)就被重置一次。

这两个API任选一个,记录执行一段操作前后的时间戳,比较两个时间戳,如果存在滞后,则可以认为存在调试器。也可以记录触发一个异常前后的时间戳,如果不是调试进程,可以很快完成异常处理。

还有一些其他可以获取时间的API也可以用于反调试,可以参考:https://docs.microsoft.com/zh-cn/windows/win32/sysinfo/time-functions。

1.4.2.CUP计数器——RDTSC

RDTSC(ReadTime StampCount)是一条汇编指令,其操作码是0x0F31,它返回至系统重新启动以来的时钟数,并且将其作为一个64位的值存入EDX:EAX中,EDX是高位,EAX是低位。

与前面反调试的技术类似,恶意代码运行两次rdtsc指令,比较两次读取之间的差值进行反调试。

BOOL CheckDebug(){

DWORD time1, time2;

__asm{

rdtsc

mov time1, eax

rdtsc

mov time2, eax

}

if (time2 - time1 < 0xff){

return FALSE;

}

else{

return TRUE;

}

}

1.5.TLS回调函数

TLS(Thread LocalStorage)回调函数会先于EP代码执行,所以反调试技术中经常使用它。TLS回调函数内部使用IsDebuggerPresent()等函数判断调试与否,如果发现处于调试状态直接停止运行。

回调函数记录在PE结构数据目录表中第9索引——IMAGE_DIRECTORY_ENTRY_TLS,结构如下:

typedef struct _IMAGE_TLS_DIRECTORY32 {

DWORD StartAddressOfRawData;//内存起始地址,用于初始化新线程的TLS

DWORD EndAddressOfRawData;//内存终止地址

WORD AddressOfIndex;//运行库使用该索引来定位线程局部数据

WORD AddressOfCallBack;//PIIMAGE_TLS_CALLBACK函数指针数组的地址

WORD SizeOfZeroFill;//用0填充TLS变量区域的大小

WORD Characteristics;//保留,0

}

AddressOfCallBacks就是线程建立退出时的回调函数,在线程建立退出时,列表中每一个函数都会调用。TLS_CALLBACK回调函数的结构:

void NTAPI TLS_CALLBACK(PVOID DllHandle, DWORD Reason, PVOID Reserved)

{

switch (Reason)

{

case DLL_PROCESS_ATTACH://1,一个新进程启动时

//反调试代码

break;

case DLL_THREAD_ATTACH://2,一个新线程启动时

break;

case DLL_THREAD_DETACH://3,终止进程

break;

case DLL_PROCESS_DETACH://4,终止一个线程

break;

}

}

2.反虚拟机与反反虚拟机

许多恶意代码会使用反虚拟机技术逃避分析。恶意代码会检测自己是否运行在虚拟机中,并根据检测结果执行不同的行为,从而绕过利用虚拟机沙箱进行的行为分析。

主流的虚拟机软件有VMware、VBOX、QEMU等,这些软件在创建的虚拟机都有一些特有的文件、进程、服务等,许多恶意代码通过通过检测这些特有的东西检测虚拟机。

2.1.检测运行环境

2.1.1.检测注册表

恶意代码可以通过检测如下注册表项检测虚拟机:

原创干货 | 【恶意代码分析技巧】17-对抗检测分析
通过检测如下注册表键检测虚拟机:

原创干货 | 【恶意代码分析技巧】17-对抗检测分析
2.1.2.检测文件
通过检测如下文件文件夹检测虚拟机:

原创干货 | 【恶意代码分析技巧】17-对抗检测分析
2.1.3.特殊进程
通过检测如下进程检测虚拟机:

原创干货 | 【恶意代码分析技巧】17-对抗检测分析

2.1.4.服务

通过检测如下服务检测虚拟机:

原创干货 | 【恶意代码分析技巧】17-对抗检测分析

可以使用EnumServicesStatusExW枚举系统服务,核心代码如下:

LPENUM_SERVICE_STATUS st;

st = (LPENUM_SERVICE_STATUS)LocalAlloc(LPTR, 64 * 1024);

DWORD ret = 0;

DWORD size = 0;

//打开服务管理器

HANDLE sc = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);

//枚举服务

EnumServicesStatus(sc, SERVICE_WIN32, SERVICE_STATE_ALL, (LPENUM_SERVICE_STATUS)st, 1024 * 64, &size, &ret, NULL);

//打印服务名,描述信息

for (int i = 0; i<ret; i++) {

printf("%-20s%-50s", st[i].lpServiceName, st[i].lpDisplayName);

}

2.1.5.检测窗口

VBox创建的虚拟机具有名为”VBoxTrayToolWndClass”的窗口类,可以使用FindWindows查找这个窗口。

2.1.6.检测命名管道

一些虚拟机/沙箱环境会有一些特殊的命名管道,可以根据这个检测,使用CreateFile可以检测是否存在某个命名管道:

原创干货 | 【恶意代码分析技巧】17-对抗检测分析

2.2.网卡信息

2.2.1.MAC地址

通常,MAC地址的前三个字节标识一个提供商。例如,00:05:69:xx:xx:xx的MAC地址,我们都可以认为与VMware有关,我们可以以此来检测虚拟机。除了00:05:69,我们还要关注的MAC地址有:

原创干货 | 【恶意代码分析技巧】17-对抗检测分析

2.2.2.网卡名称

VMWare创建的虚拟机包含"VMWare"字符,VirtualBox创建的虚拟机包含”VirtualBox”字符。

2.2.3.获取网卡信息

有两种方式获取网卡信息。

①使用ipconfig/all命令

原创干货 | 【恶意代码分析技巧】17-对抗检测分析

②使用GetAdaptersInfo函数

GetAdaptersInfo是IPHelper API,用于获取网卡信息:

DWOED GetAdaptersInfo(

PIP_ADAPTER_INFO pAdapterInfo;//存储网卡信息的缓冲区指针

PULONG pOutBufLen;//缓冲区大小

);

//IP_ADAPTER_INFO结构体:

typedef struct _IP_ADAPTER_INFO {

struct _IP_ADAPTER_INFO* Next; //指向链表中下一个PIP_ADAPTER_INFO信息

DWORD ComboIndex; //预留值

char AdapterName[MAX_ADAPTER_NAME_LENGTH + 4];//用ANSI字符表达的适配器名称

char Description[MAX_ADAPTER_DESCRIPTION_LENGTH + 4]; //用ANSI字符表达的适配器描述

UINT AddressLength;//适配器硬件地址以字节计算的长度

BYTE Address[MAX_ADAPTER_ADDRESS_LENGTH]; //硬件地址以BYTE数组表示

DWORD Index; //适配器索引

UINT Type; //适配器类型

UINT DhcpEnabled;//适配器是否开DHCP

PIP_ADDR_STRING CurrentIpAddress;//预留值

IP_ADDR_STRING IpAddressList;//该适配器的IPV4地址列表

IP_ADDR_STRING GatewayList;//该适配器的IPV4网关地址列表

IP_ADDR_STRING DhcpServer;//该适配器的DHCP服务器的IPV4列表

BOOL HaveWins; //是否有WINS服务器

IP_ADDR_STRING PrimaryWinsServer;//主WINS服务器

IP_ADDR_STRING SecondaryWinsServer;//第二WINS服务器

time_t LeaseObtained;//服务器规定IP地址使用期限,此为地址租用开始时间

time_t LeaseExpires; //地址租约到期时间

} IP_ADAPTER_INFO, *PIP_ADAPTER_INFO

直接调用GetAdaptersInfo并解析_IP_ADAPTER_INFO结构便能获得网卡名称、网卡描述、MAC地址等信息,可以根据这些信息判断当前是否处于虚拟机环境。

2.3.漏洞指令

idt_trick:在真实的机器上,IDT在内存中比在即虚拟机内存更低;

ldt_trick在真实的机器上,LDT在内存中比在即虚拟机内存更低;

gdt_trick在真实的机器上,GDT在内存中比在即虚拟机内存更低;

str_trick存储任务寄存器(STR):虚拟机中的STR返回的值不同于从原生系统获得的值。

2.4.其他

还有一些检测虚拟机沙箱的技术,这里简单介绍一下:

2.4.1.检测时间加速

一些恶意样本会使用Sleep()挂起进程,一些沙箱为了提高分析速度,会HOOKSleep()这个API,让Sleep()的时间缩短。一些恶意程序根据这一特点检测沙箱环境。判断是否产生时间加速的方式如下:

GetTickCount()- GetTickCount()==Sleep(*)

2.4.2.检测鼠标移动

正常使用的系统,用户经常操作会导致鼠标位置变化,而沙箱中鼠标位置一般不会变化,可以使用GetCursorPos获取两次鼠标的坐标,不变化认为在沙箱中。

2.4.3.计算内存/硬盘容量

容量特别小的可以认为处于虚拟机/沙箱环境。

2.4.4.访问存在的网络

许多沙箱是离线的,不能访问外部网络,于是一些恶意程序会访问百度、谷歌等网站,判断网络环境,只有这些网络可达才会执行恶意行为。

2.4.5.访问不存在的网络

针对“访问存在的网站”这一反虚拟机技术,一些离线沙箱想到了解决办法,为了模拟真实的网络环境,这些沙箱对任何的网络访问都返回可达,让恶意程序误以为在真实的网络环境中。于是恶意程序又有了新的检测沙箱环境的方式——访问不存在的网络,如果不存在的网络能够成功访问,说明这个访问是虚假的,WannaCry就是利用一个不存在的网址www.iuqerfsodp9ifjaposdfjhgosurijfaewrwergwea.com来检测虚拟机环境。

3.对抗分析

3.1.加壳

这个没什么好说的,最最常见的混淆技术。简单的壳一句命令就能脱掉,复杂的壳分析个三四天也难有成效。加壳技术着实不是几句话就能说完的,网上关于加壳脱壳的技术文章数不胜数,在这里笔者就不多说了。

3.2.使用隐写术

隐写术在CTF中很常见,在恶意代码中也经常使用。

最简单的是将恶意代码字节直接缀在图片的尾部,在执行的时候从图片中取出恶意代码字节并执行。

复杂一点会使用到LSB算法、Outguess算法等隐写算法隐藏恶意代码数据。使用这些隐写算法的隐藏效果非常好,但缺点是,利用多个字节来隐藏一个bit,效率很低。

在实际分析中,对抗隐写术的利器就是下好断点!

关于恶意代码使用隐写术的更多细节,感兴趣的同学可以去看看《将恶意代码隐藏在图像中:揭秘恶意软件使用的隐写术》。

3.3.ADS备用数据流

ADS(Alternate Data Streams,备用数据流/NTFS交换数据流)是NTFS磁盘格式的一个特性,在NTFS文件系统下,每个文件都可以存在多个数据流。该特性允许将附加数据添加到一个已存在的NTFS文件中,,而在资源管理器中却只能看到宿主文件,找不到寄宿文件,只有在用户访问流时,才能看到。ADS数据流使用normalFile.txt:Streams:$DATA来命令,这允许一个程序去读写一个流,恶意代码喜欢使用它来隐藏数据。

在NTFS分区创建ADS数据流文件有两种形式:一是指定宿主文件;二是创建单独的ADS文件。常用的创建命令有两个:echo和type。

1、创建指定宿主文件的ADS数据流文件

原创干货 | 【恶意代码分析技巧】17-对抗检测分析

原创干货 | 【恶意代码分析技巧】17-对抗检测分析

2、创建单独的ADS数据流文件

原创干货 | 【恶意代码分析技巧】17-对抗检测分析

可以使用dir/r 查看ADS数据流文件情况,ADS数据流在文件末端会有:$DATA:

原创干货 | 【恶意代码分析技巧】17-对抗检测分析

在前面的demo中,我们添加的是txt的数据流,实际上我们完全可以将exe文件添加进ADS数据流。然后利用start命令执行exe文件:

原创干货 | 【恶意代码分析技巧】17-对抗检测分析

ADS数据流文件有三种删除方式。一是直接删除宿主文件,二是将宿主文件移到FAT32等非NTFS分区中;三是利用工具软件,如IceSword删除。

流文件不能直接通过网络传输,可以先试用winrar打包,winrar支持打包ADS数据流:

原创干货 | 【恶意代码分析技巧】17-对抗检测分析

APT32曾经使用备用数据流执行VBS脚本:

原创干货 | 【恶意代码分析技巧】17-对抗检测分析

参考资料

https://www.freebuf.com/column/181083.html

https://max.book118.com/html/2017/0119/85399686.shtm

https://www.4hou.com/web/15211.html

https://github.com/LordNoteworthy/al-khaser

https://blog.csdn.net/m0_37809075/article/details/82585542

https://juejin.im/post/5aa118c5f265da237e094c0e#heading-19

https://attack.mitre.org/techniques/T1096/

https://www.52pojie.cn/thread-867401-1-1.html

https://www.4hou.com/technology/19065.html

《恶意代码分析实战》

相关推荐: Potato家族本地提权分析

0x00 前言 在实际渗透中,我们用到最多的就是Potato家族的提权。本文着重研究Potato家族的提权原理以及本地提权细节 0x01 原理讲解 1.利用Potato提权的是前提是拥有SeImpersonatePrivilege或SeAssignPrimar…

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年5月6日19:12:45
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   原创干货 | 【恶意代码分析技巧】17-对抗检测分析http://cn-sec.com/archives/246240.html