为了对抗安全工具和安全人员的检测分析,许多恶意代码都包含了对抗检测分析的功能。一方面,恶意代码使用各种技术检测当前的运行环境,判断自己是否正在被检测分析;另一方面,恶意代码还会使用各种混淆技术增加恶意代码分析的难度。
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):
由上可知,FS、FS:[0x18]均指向TEB,FS:[0x30]指向PEB。
在PEB中,有三种方式判断当前进程是否处于调试状态:
①+0x02 BeingDebugged
PEB中该成员记录进程是否处于调试状态,当进程处于调试状态时,该值为1;
②+0x18 ProcessHeap
ProcessHeap指向进程第一个堆的位置,而堆的结构中Flags(+0x0C)和ForceFlags(+0x10)标志会受到调试器的影响。
在正常情况下Flags的值为2,ForceFlags的值为0。当调试器存在时,Flags会受到影响变为:
ForceFlags会受到影响变为:
需要注意的是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:
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,还有一些其他的调试器/分析工具:
②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信息就能获取所有进程信息:
②使用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,从而改变程序执行流程:
1.3.5.INT 3反调试
INT 3就是我们平时说的软件断点,INT3反调试属于SEH反调试,不过是我们选择了一个特殊的异常INT3异常。
1.3.6.INT 2D反调试
INT 2D是内核模式中用来触发断点异常的指令,也可以在用户模式下触发异常。
正常程序在运行的过程中,遇到INT2D,将会触发断点异常,进入SEH;处于调试状态的程序将由调试器处理这个异常,大多数的调试器只是将eip+1,然后就放行了,这就将改变程序的流程。恶意代码变可以通过这一点检测调试器。
如下图所示,一下调试器在执行完INT2D指令后,下一条指令的第一个字节将被忽略,后一个字节将会被识别为新的指令继续执行:
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.检测注册表
恶意代码可以通过检测如下注册表项检测虚拟机:
通过检测如下注册表键检测虚拟机:
2.1.2.检测文件
通过检测如下文件文件夹检测虚拟机:
2.1.3.特殊进程
通过检测如下进程检测虚拟机:
2.1.4.服务
通过检测如下服务检测虚拟机:
可以使用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可以检测是否存在某个命名管道:
2.2.网卡信息
2.2.1.MAC地址
通常,MAC地址的前三个字节标识一个提供商。例如,00:05:69:xx:xx:xx的MAC地址,我们都可以认为与VMware有关,我们可以以此来检测虚拟机。除了00:05:69,我们还要关注的MAC地址有:
2.2.2.网卡名称
VMWare创建的虚拟机包含"VMWare"字符,VirtualBox创建的虚拟机包含”VirtualBox”字符。
2.2.3.获取网卡信息
有两种方式获取网卡信息。
①使用ipconfig/all命令
②使用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数据流文件
2、创建单独的ADS数据流文件
可以使用dir/r 查看ADS数据流文件情况,ADS数据流在文件末端会有:$DATA:
在前面的demo中,我们添加的是txt的数据流,实际上我们完全可以将exe文件添加进ADS数据流。然后利用start命令执行exe文件:
ADS数据流文件有三种删除方式。一是直接删除宿主文件,二是将宿主文件移到FAT32等非NTFS分区中;三是利用工具软件,如IceSword删除。
流文件不能直接通过网络传输,可以先试用winrar打包,winrar支持打包ADS数据流:
APT32曾经使用备用数据流执行VBS脚本:
参考资料
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
《恶意代码分析实战》
0x00 前言 在实际渗透中,我们用到最多的就是Potato家族的提权。本文着重研究Potato家族的提权原理以及本地提权细节 0x01 原理讲解 1.利用Potato提权的是前提是拥有SeImpersonatePrivilege或SeAssignPrimar…
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论