计算机病毒常用手段
一次性进群,长期免费索取教程,没有付费教程。
教程列表见微信公众号底部菜单
进微信群回复公众号:微信群;QQ群:460500587
微信公众号:计算机与网络安全
ID:Computer-network
本文介绍计算机病毒的常用手段及技术,从源代码方面透彻地分析计算机病毒种种神秘的功能。
一、键盘记录技术
木马病毒的盗号功能往往利用键盘记录技术进行,主流技术就是利用HOOK,也就是钩子(Windows消息处理机制的监视点),链式结构,进行账号密码的截获。截获方式分为全局消息钩子(截获系统中所有进程的按键消息),局部消息钩子(只能记录特定程序当前线程的按键消息)。
1、局部钩子
首先先来了解一下什么是局部消息钩子,局部消息钩子使用过程分为以下三个步骤:
(1)安装钩子。
SetWindowsHookEx是用来安装钩子的函数,其函数原型如下。
HHOOK WINAPI SetWindowsHookEx(
_In_ int idHook, //钩子类型
_In_ HOOKPROC lpfn, //回调函数地址
_In_ HINSTANCE hMod, //钩子指向的模板句柄
_In_ DWORD dwThreadId //安装钩子的线程ID
);
第1个参数idHook用于设置钩子函数截获的消息类型,主要使用有WH_CALLWNDPROC(使用SendMessage发送消息前安装钩子)、WH_CALLWNDPROCRET(使用SendMessage发送消息后安装钩子)、WH_GETMESSAGE(调用PeekMessage或者SendMessage后安装钩子)、WH_KEYBOARD(截获WM_KEYUP或WM_KEYDOWN时安装钩子)、WM_MOUSE(截获鼠标消息时安装钩子)。
第2个参数lpfn则会设置回调函数的地址,也就是接下来第2步设置的钩子回调函数的地址。
第3个参数hMod在局部消息钩子中设置为NULL。
第4个参数dwThreadId,指定需要安装钩子函数的线程ID。这里可以先使用CreateToolhelp32Snapshot函数快照得出所有线程ID的快照,随后使用Process32First以及Process32Next遍历线程得到指定程序(可以通过程序名,“××.exe”)的线程ID。也可以通过FindWindowEx找到程序的窗口句柄,然后通过GetWindo的线程ID。也可以通过FindWindowEx找到程序的窗口句柄,然后通过GetWindowThreadProcessId的返回值得到线程ID。目前很多程序,例如QQ、YY等聊天工具均用GUI绘图制得,无法得到其窗口句柄,因此第2种方法失效。而第一种方法,广泛流行的木马病毒并不是针对单一程序进行盗号,因此木马病毒通常情况下会使用全局钩子。
(2)设置钩子函数
LRESULT CALLBACK HOOKxxx //名称自定义
(
int nCode, //钩子目的代码
WPARAM wParam, //发送或接受消息的参数
LPARAM lParam //发送或接受消息的参数
)
{
... //对于各种消息的处理代码,如将接收到的消息通过CreateFile、WriteFile保存在某个位置等,如监测键盘大小写等。
return CallNextHookEx(....)
//钩子为链式结构,故需要一个回调函数,将信息从钩子间相互传递
}
接着详细查看一下CallNextHookEx回调函数的作用。
LRESULT WINAPI CallNextHookEx(
_In_opt_ HHOOK hhk, //由安装钩子函数返回得到的句柄
_In_ int nCode, //同HOOKxxx (因为保存了要传递给下一个钩子的全部信息,所以下面的参数均与自身钩子函数一致)
_In_ WPARAM wParam, //同HOOKxxx
_In_ LPARAM lParam //同HOOKxxx
);
(3)卸载钩子
钩子使用完毕后如果不卸载,则会占用大量的系统资源,使得系统运行缓慢也提高了被用户发现的可能性,因此需要卸载钩子。
BOOL WINAPI UnhookWindowsHookEx(
_In_ HHOOK hhk //由安装钩子函数返回得到的句柄
);
2、全局钩子
与局部钩子不同的是,全局钩子使用DLL文件加载函数。分析全局钩子之前,先来了解下什么是DLL。
DLL(Dynamic Link Library)即动态链接库,因为每个程序的进程都有自己的内存空间,要监控键盘的所有按键信息,记录键盘的程序就需要加载进入其他程序的内存空间,然后记录这个程序使用键盘的情况,而DLL文件是可以动态地加载进其他程序的内存空间,从而调用钩子函数进行键盘记录,故需要将以上3个步骤的函数放入DLL文件内。
先来看看DllMain函数参数的含义,参数一hModule即DLL自身的实例句柄,指向DLL文件被映射进进程空间的地址。
参数二ul_reason_for_call是函数调用的原因,可以设置为以下4个值之一。
DLL_PROCESS_ATTACH//被进程加载;
DLL_PROCESS_DETACH//被进程卸载;
DLL_THREAD_ATTACH//进程创建新进程时;
DLL_THREAD_DETACH//线程终止时。
参数三lpReserved,一般不用。
若编译方式为C++(默认均为.cpp),则需要接着进行外链声明,即:
extern "C" _declspec(dllexport)函数类型 函数名称(参数);
若编译方式为C语言,则不需要extern声明。_declspec(dllexport)为导出函数标志。
加载DLL的方式分为动态链接和静态链接。
隐式链接:
#include<"Dll库头文件.h"> //加载头文件
在主函数(WinMain)调用DLL文件的时候,需要前置声明加载该dll的LIB库(静态库,包含在DLL中创建的函数等)文件。
#pragma comment(lib,"DLL文件名.lib");
然后就可以正常使用DLL文件中的导出函数了。
隐式链接的特点为:使用方式简单,但被多次调用时,因内存无法释放使内存开销大,从而导致系统运行缓慢。
显式链接:
HINSTANCE hInst = LoadLibrary("DLL文件名.dll"); //映射内存,得到句柄
typedef 类型 (*xxx)(); //定义函数指针
××× 变量名 = (×××)GetProcAddress(hInst,"DLL导出函数名");
..... //键盘记录功能代码
FreeLibrary(hInst); //释放内存
显式链接比隐式链接复杂,但被多次调用时的内存开销小,这也是键盘记录程序作为首选的原因。下面接着分析上面函数:
HMODULE WINAPI LoadLibrary(
_In_ LPCTSTR lpFileName //加载DLL文件进入内存
);
该函数仅有一个参数lpFileName,若DLL文件和调用DLL的cpp原文件在同一目录下,则参数可以直接为“Dll名称.dll”;若不在同一个目录下,则需要写清楚要加载的DLL文件的路径。
FARPROC GetProcAddress( //函数返回指定导出的DLL函数的地址
HMODULE hModule, //加载进内存的DLL句柄
LPCWSTR lpProcName //导出函数名
);
因为该函数导出的是地址信息,所以可以使用相关指针进行接收。
最后一个函数如下。
BOOL FreeLibrary( //释放加载的DLL(类似C/C++语言中malloc/new与free/delete的对应关系)
HMODULE hLibModule //当前加载的DLL句柄
);
二、DLL注入
DLL注入又称远程线程注入。大多数木马病毒为了提高自身隐蔽性、目标程序的针对性、进程检测软件的躲避等能力,均采用这种方式。其中远程则是基于线程之间的,即通过在目标程序下创建线程来运行病毒功能。
先来了解DLL注入需要用到的函数,然后再通过其使用步骤来认识它。
HANDLE WINAPI OpenProcess( //得到目标进程句柄
DWORD dwDesiredAccess, //权限设置
BOOL bInheritHandle, //句柄继承
DWORD dwProcessId //目标PID(进程ID)
);
参数一dwDesiredAccess权限通常设置为PROCESS_ALL_ACCESS,即享有全部权限。参数二bInheritHamdle则设置为FALSE,即无句柄继承。参数三dwProcessId则为目标进程的PID。调用该函数得到句柄,这是假定为hPrcoess。
LPVOID VirtualAllocEx( //为目标进程申请一段内存空间
HANDLE hProcess, //目标进程句柄
LPVOID lpAddress, //申请内存的起始地址
DWORD dwSize, //内存大小,以字节为单位
DWORD flAllocationType, //内存类型
DWORD flProtect //内存权限
);
参数一wProcess为OpenProcess得到的进程句柄。参数二lpAddress一般设置为NULL,也就是由系统决定申请内存的起始地址。参数三dwsize分配的大小应为页内存的整数倍。参数四flAllocation Type一般选择为MEM_COMMIT,为特定的页面区域分配内存中或磁盘的页面文件中的物理存储空间。参数五flProtecf为PAGE_READWRITE,即可以读写该区域。
BOOL WriteProcessMemory( //将信息写入内存
HANDLE hProcess, //目标进程句柄
LPVOID lpBaseAddress, //目标进程内存空间的首地址
LPVOID lpBuffer, //写入数据的缓冲区,包含写入内存的首地址
DWORD nSize, //写入的字节数
LPDWORD lpNumberOfBytesWritten //实际写入的字节数
);
参数二lpBaseAddress即为VirtualAllocEx函数申请到的内存空间。为了下一步使用远程线程注入DLL,这步就应该将DLL文件的具体路径、文件名通过上述函数写入目标进程的内存空间,因此VirtualAllocEx的参数三dwSize以及本函数的参数四nSize则可以是包含DLL文件的具体路径的字符串长度。lpBuffer则设置为DLL文件的具体路径。
HANDLE CreateRemoteThreadEx(
HANDLE hProcess, //目标进程句柄
LPSECURITY_ATTRIBUTES lpThreadAttributes, //定义新线程的安全描述符
SIZE_T dwStackSize, //堆栈初始大小
LPTHREAD_START_ROUTINE lpStartAddress, //线程函数的起始地址
LPVOID lpParameter, //指针传参
DWORD dwCreationFlags, //线程运行状态
LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList, //新线程附加参数
LPDWORD lpThreadId //返回线程ID
);
参数二lpThreadAttributes、参数七lpAttributeList以及参数八lpThreadId通常设置为NULL,分别表示无安全描述符、无附加参数、无线程ID返回。参数三dwStackSize若设置为0,则将以默认大小运行程序。参数四lpStartAddress则设置为导出函数在目标程序内存中的地址,LoadLibrary函数位于Kernel32.dll中,几乎所有进程启动均会加载kernel32.dll模块,并且在任何进程中该函数地址相同,可以使用GetModuleHandle函数从Kernel32.dll中获得DLL句柄,然后放入GetProcAddress,进而获得类型为FARPROC的LoadLibrary函数加载的地址,随后转换其类型为LPTHREAD_START_ROUTINE即可。而用于传参的指针则是由VirtualAllocEx函数返回得到,即用于传递内存分配的信息。参数dwCreationFlags设置为0,表示线程被创建后立刻运行。
HMODULE GetModuleHandle( //得到DLL文件模块句柄
LPCTSTR lpModuleName //DLL模块名
);
DWORD WaitForSingleObject( //用于等待线程完成
HANDLE hHandle, //句柄对象
DWORD dwMilliseconds //等待线程时间间隔
);
参数一hHamdle设置为由CreateRemoteThreadEx返回的线程句柄,而参数二dwMiUiseconcls则设置为INFINITE,意为无限等待。
BOOL VirtualFreeEx( //释放使用VirtualAllocEx分配的内存
HANDLE hProcess, //使用OpenProcess返回的目标进程句柄
LPVOID lpAddress, //指向VirtualAllocEx分配的内存地址
DWORD dwSize, //释放的区域大小,这里必须设置为0,与下一个参数对应
DWORD dwFreeType //自由操作类型,设置为MEM_RELEASE,指向由
VirtualAllocEx分配的区域
);
DLL注入具体分为以下几个步骤。
(1)通过目标程序的PID(进程ID),使用OpenProcess函数得到进程句柄。
(2)使用VirtualAllocEx及得到的句柄,在目标进程下创建一段内存空间。
(3)使用WriteProcessMemory将DLL文件的路径信息写入内存。
(4)使用GetModuleHandle以及GetProcAddress得到LoadLibrary函数的地址(为了使用LoadLibrary加载恶意DLL文件,需要得到LoadLibrary函数在进程中的地址)。
(5)使用CreateRemoteThreadEx启动远程线程,加载恶意DLL文件。
(6)使用WaitForSingleObject等待线程运行完毕退出。
(7)使用VirtualFreeEx释放申请的内存。
(8)使用CloseHandle关闭句柄。
这样病毒代码就加载进了正常程序内,并且进程监视等软件无法检测到病毒运行的存在。当然,更加巧妙的隐藏自身的方式也可以将代码直接写入目标进程,那么下列步骤中的WriteProcessMemory就应将代码写入内存。随后利用步骤(4)得到代码内使用的API函数位于系统DLL文件在该进程内存空间的位置,那么此步骤应在目标进程空间内实现。为了便于得到写入代码的大小和地址,则往往将其封装入结构体,再使用sizeof(结构体)以及取地址符&(结构体)就可得到。
同时也可以通过“隐藏”DLL模块,进而隐藏DLL文件名称以及路径来躲避检测工具的扫描。这就需要知道DLL模块在内存中具体加载的位置,然后才能对其进行修改并隐藏。因为DLL被用于注入目标程序的内存空间,所以在PEB中就会保存该DLL模块的信息,那么所谓“隐藏”,也就是对PEB中DLL模块的信息进行修改。
首先来看一下32位系统下的TEB结构。在WinDbg中打开任意可执行程序,随后在0:000 >后输入!teb,显示结果如图1所示。
图1 TEB显示结果
从图1中可以得出TEB及PEB的地址(TEB:7ffdf000h;PEB:7ffdb000h)。随后输入dt _teb 7ffdf000来查看TEB结构信息,如图2所示。
图2 TEB结构
这里可以看出PEB结构位于TEB偏移0x30的地址上。而TEB结构的信息则存放于FS寄存器中,那么便可以通过汇编指令mov eax,fs:[0x30]来得到PEB的地址。
下面来查看PEB结构,输入dt _peb 7ffdb000得到PEB结构信息,如图3所示。
图3 PEB结构
其中有个重要的参数,相对PEB偏移地址为0x0c的Ldr结构,即struct _PEB_LDR_DATA *Ldr,其中Ldr为指向PEB_LDR_DATA结构的指针。
MSDN中PEB结构如下。
typedef struct _PEB
{
UCHAR InheritedAddressSpace;
UCHAR ReadImageFileExecOptions;
UCHAR BeingDebugged;
UCHAR SpareBool;
PVOID Mutant;
?
PVOID ImageBaseAddress;
PPEB_LDR_DATA Ldr;
}PEB,*PPEB;
现在再使用WinDbg查看结构。输入dt _PEB_LDR_DATA 0x00351ea0查看_PEB_LDR_DATA结构(见图4)。
图4 PEB_LDR_DATA结构
查阅MSDN。
typedef struct _PEB_LDR_DATA {
ULONG Length; //+0x00
BOOLEAN Initialized; //+0x04
PVOID SsHandle; //+0x08
LIST_ENTRY InLoadOrderModuleList; //+0x0c双向链表中包含进程中加载模块的节点
LIST_ENTRY InMemoryOrderModuleList; //+0x14
LIST_ENTRY InInitializationOrderModuleList; //+0x1c
} PEB_LDR_DATA, *PPEB_LDR_DATA;
这里的重点是参数三InMemoryOrderModuleList及参数四InLoadOrderModuleList,首先来看看它的类型_LIST_ENTRY。
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink; //指向下一个链表节点的Blink指针
struct _LIST_ENTRY *Blink; //指向上一个链表节点的Flink指针
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
该结构体包含两个指针,作为链接模块节点的工具,由此可以看出PEB中保存模板信息是以链表的形式构成的,并且参数中的每一项指向名为_LDR_DATA_TABLE_ENTRY的结构体:
typedef struct _LDR_DATA_TABLE_ENTRY {
PVOID Reserved1[2];
LIST_ENTRY InMemoryOrderLinks;
PVOID Reserved2[2];
PVOID DllBase; //DLL模块地址
PVOID EntryPoint;
PVOID Reserved3;
UNICODE_STRING FullDllName; //DLL路径名
BYTE Reserved4[8];
PVOID Reserved5[3];
union {
ULONG CheckSum;
PVOID Reserved6;
};
ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
很明显,该结构体存放了程序以及加载的DLL模块的地址DllBase和它们的路径信息,也就是UNICODE_STRING类型的参数FullDllName。
typedef struct _UNICODE_STRING {
USHORT Length; //长度
USHORT MaximumLength; //最大长度
PWSTR Buffer; //缓冲区
} UNICODE_STRING;
隐藏DLL的方式就是:
(1)将保存注入的DLL文件模块的信息链进行脱链操作。
(2)(可选)将信息链中保存的DLL路径及文件名清除。
具体实现方法:
(1)定位到程序Ldr的地址,以Ldr→InLoadOrderModuleList.Flink作为遍历模块的入口点。
(2)遍历模块找到注入的DLL模块。
(3)对该模块进行脱链、清除信息等操作。
步骤一,通过使用如下汇编代码。
当然也可以使用:
接着使用pPEB→Ldr→InLoadOrderModuleList.Flink得到模块链表遍历入口点。
步骤二,使用一个循环以pLdr→InLoadOrderModuleList.Flink作为链表头指针进行模块遍历,通过判断DLL模块地址,搜寻注入DLL文件的模块所在节点。
步骤三,脱链操作也就是将所在节点的上下相邻节点的Flink、Blink指针指向进行修改,指针的指向将跳过该注入DLL模块的节点,那么该节点就从链表中脱离出来,也就实现了隐藏DLL操作,而清除信息则是使用memset等函数对参数FullDllName进行清零处理。
首先使用CONTAINING_RECORD函数来获得LDR_DATA_TABLE_ENTRY结构的地址。
Flink指向的是LDR_DATA_TABLE_ENTRY结构中的InMemoryOrderLinks成员,因此需要得到一个指向LDR_DATA_TABLE_ENTRY结构的指针,即使用CONTAINING_RECORD函数。
在这里的用法则是PLDR_DATA_TABLE_ENTRY pLdrDataEntry = CONTAINING_RECORD(pLdr→InLoadOrderModuleList.Flink, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks),随后便可以使用pLdrDataEntry→FullDllName.buffer得到DLL文件信息,最后使用memset进行清零。
而在64位系统中,使用WinDbg查看同一程序的情况则不同于32位系统,如图5、图6所示。
图5 64位TEB结构
图6 32位TEB结构
PEB地址偏移并不是之前的0x30了,而是0x60,继续查看EB结构,如图7所示。
图7 64位PEB结构
Ldr的偏移地址同样也发生了改变,偏移地址改为0x18。
那么对应的FS寄存器中的值也需要进行改变。接下来,介绍另一种方式即使用API函数进行获取PEB地址的操作。
参数一指定为目标进程句柄,参数二ProcessInformationClass可取下列值。
这里,显然取ProcessBasicInformation,其中保存了PEB结构信息。
typedef struct _PROCESS_BASIC_INFORMATION {
PVOID Reserved1;
PPEB PebBaseAddress;
PVOID Reserved2[2];
ULONG_PTR UniqueProcessId;
PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;
该结构体的参数二即为PEB结构的基地址,也正是我们需要得到的信息。
函数中参数三ProcessInformation用来接收PEB信息的缓冲区(以PROCESS_BASIC_INFORMATION类型声明缓冲区接收信息),参数四ProcessInformationLength表示信息的大小(sizeof即可)。该函数并未被微软公开,因此需要使用GetProcAddress和LoadLibrary从Ntdll.dll中获取该函数地址。调用该函数以后,便得到了PEB结构的地址。
三、autorun.inf
U盘病毒在2007年大规模爆发,并且危害很大。2011年,随着微软发布名为KB967940的补丁后,U盘病毒在XP系统下主动传播功能近乎失效,如今在Windows 7/8甚至10普及的今天,U盘病毒更是失去了效果,但其原理却是值得一探究竟的。
病毒主要利用了autorun.inf的特性——随U盘打开而自动运行的功能。以记事本的形式打开该文件,则文件如图8所示。
图8 autorun.inf(1)
autorun.inf文件(见图9)格式一般如下:
[AutoRun]
open=打开的程序
该程序往往是隐藏在U盘中的病毒文件。下面的shell等同样也起到打开文件的作用。
图9 autorun.inf(2)
就本例来说,内置该文件的U盘被双击打开以后,会自动运行coink.exe病毒。该病毒则会将自身复制进系统目录,然后添加到自启项,实现木马病毒等功能。该病毒通过监控USB接口信息,一旦检测到有U盘插入,则复制自身进入U盘并创建改写U盘的autorun.inf文件,然后通过隐藏自身来实现感染U盘的目的,进而继续进行传播。接下来具体认识它的这些功能是如何实现的。
病毒一般会创建三个文件,在系统根目录下和U盘中创建自身,即可执行病毒(exe、com等文件);在U盘里创建或替换autorun.inf文件。
病毒检测U盘的手段并不神秘,不过是使用了GetDriveType函数而已。
UINT WINAPI GetDriveType(
LPCTSTR lpRootPathName //盘符名称
);
参数显然为盘符的名称了,例如C:。函数的返回值若为DRIVE_REMOVABLE,则表示是移动存储设备,如U盘等;若为DRIVE_FIXED,则为固定硬盘;若为DRIVE_CDROM,则为光驱。然后将这个函数放入循环,每隔一段时间检测全部磁盘就实现了检测U盘的功能。
紧接着就是隐藏自身文件的功能,代码如下所示。
BOOL SetFileAttributes(
LPCTSTR lpFileName, //文件详细路径
DWORD dwAttributes //文件属性
);
文件路径也就是autorun.inf以及病毒程序的具体路径,文件属性则设置为FILE_ATTRIBUTE_HIDDEN,那么文件就被隐藏了。
四、劫持
下面介绍病毒的各种劫持方式。
1、浏览器劫持
(1)hosts劫持
hosts(见图10)文件路径一般为C:\Windows\System32\drivers\etc,可以用记事本打开,其作用是加快域名访问速度,也就是当用户输入某网址并访问时,若hosts中存有该网址与对应IP则会通过该IP解析并访问该网站。因此,病毒也会通过修改对应网站的IP,将用户输入的网址劫持至指定IP的网站,这是hosts劫持。
图10 hosts文件
(2)BHO劫持
BHO(Browser Helper Object,浏览器辅助对象)是浏览器与程序员之间开放交互接口的业界标准。程序员通过这个接口可以编写代码控制浏览器的行为,别有用心的病毒编写者则会用此劫持浏览器,如篡改IE主页、弹广告等。
BHO在注册表中的位置是HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects\,如图11所示。其下的项名即为对应的BHO。
图11 BHO注册表
在HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\下,可以找到BHO对应的注册项。其中CLSID属于GUID(Globally Unique Identifier),也称作UUID(Universally Unique Identifier),它是全局唯一标识符,作为COM类的标识符。
其中InprocServer32下键值数据所表示的是BHO加载的DLL文件(见图12)。病毒则会将自身添加进BHO且加载恶意DLL来劫持浏览器。
图12 加载DLL
2、映像/镜像劫持
映像劫持(Image File Execution Options, IFEO)是早期病毒对付杀毒软件的一大手段,其实也只是使用完全权限,在注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\下新建一个Debugger项,项的名称为劫持的软件名称,如cmd.exe。随后将该项的键值改为一个不存在的文件,比如asdadasdasdasd.exe,如图13所示。
图13 IFEO
每当打开cmd.exe时,系统会先访问该注册表下的Debugger键值所指向的文件,即asdadasdasdasd.exe,而该文件不存在时cmd.exe自然就无法被打开,同理作用于杀毒软件,这就是病毒对抗杀毒软件的原理。
3、DLL劫持
操作系统在加载DLL文件时,先会搜索程序所在的目录,若没有则会搜索系统目录。DLL劫持正是利用系统的这个机制,在程序目录下创建一个与系统DLL名称、导出函数相同(为了维持程序正常运行),但是内含病毒代码的DLL文件。当应用程序加载该DLL时,病毒代码也就神不知鬼不觉地运行了。
五、反虚拟机技术
鉴于许多安全人员通过虚拟机搭建蜜罐系统作为分析病毒的强有力手段,病毒往往在自身代码中植入检测虚拟机的代码。
1、检测虚拟机的一般手段
检测虚拟机的一般手段主要是搜索相关VM虚拟机的字符串,如搜索虚拟机服务进程使用CreateToolhelp32Snapshot、Process32First、Process32Next三个函数遍历进程,来搜索虚拟机的相关进程。
CreateToolhelp32Snapshot函数将对用户选择的类型进行快照,本例就是快速读取进程列表。参数一dwFlags有如下选择。
TH32CS_INHERIT:快照句柄可以被继承。
TH32CS_SNAPALL:所有的进程、线程、由th32ProcessID指定的进程模块。
TH32CS_SNAPHEAPLIST:由th32ProcessID指定进程中的堆。
TH32CS_SNAPMODULE:由th32ProcessID指定进程模块。
TH32CS_SNAPMODULE32:由th32ProcessID指定64位程序进程中的32位进程模块。
TH32CS_SNAPPROCESS:系统全部进程信息。
TH32CS_SNAPTHREAD:系统全部线程信息。
本例选择TH32CS_SNAPPROCESS,而参数二则设置为0,表示无指定进程。
参数二指向了PROCESSENTRY32结构体。
检测虚拟机具体流程则为:
随后建立循环,在使用Process32First(hProcessSnap,&pe32),Process32Next(hProcessSn ap,&pe32)中间查找虚拟机进程,如使用strcmp(pe32.szExeFile,"VMwareUser.exe")语句。
2、通过内存来检测虚拟机
虚拟机为避免与物理主机冲突,它在内存映射方面与物理主机存在差异,因此可以通过检测内存的方式来检测虚拟机,如检测IDT(Interrupt Descriptor Table,中断描述符表)的地址,其中VMware虚拟机中IDT地址为0xFF××××××,而物理主机中IDT地址不会高于0xD0××××××,因此可以通过执行SIDT指令检测IDTR(Interrupt Descripter Table Register,中断描述符表寄存器),IDTR用于存放IDT地址,然后判断返回LowIDTbase的第一个字节是否高于0XD0,以此来检测虚拟机环境。
同理可以检测LDT(Local Descriptor Table,本地描述符表)与GDT(Global Descriptor Table,全局描述符表),当LDT位于0x0000时,为物理主机;当GDT位于0xFF××××××时,为虚拟主机。
3、其他方法
另外,还可以通过搜索“VMware”字符串来检测虚拟机的注册表项,进而检测虚拟机是否存在。此外也可以从物理硬件层面进行检测,如检测网卡的MAC地址、BIOS等。
六、反调试技术
病毒侦测虚拟机的同时,也存在被调试器分析的风险,因此病毒会在自身代码中实现反调试技术,常见的反调试技术有以下几种。
1、查询PEB
使用NtQueryInformationProcess函数,并且将其参数二设置为ProcessDebugPort(0x7),返回0表明程序没有被调试,调试状态下将返回调试的端口号。
以上三个函数均采用了检测PEB结构中的BeingDebugged参数信息,同样也可以通过汇编代码获取该信息。
最后检测dbg的值,若dbg的值为0,则表明非调试环境。同样在PEB结构中被改变的还有一些Heap(堆)标志,如ProcessHeap、NTGlobalFlag。
2、扫描进程
程序可以使用CreateToolhelp32Snapshot、Process32First、Process32Next三个函数持续对进程进行扫描,查找如“ida.exe”“OllyDbg.exe”之类的调试软件,也可以直接使用FindWindow函数进行窗口查找。
HWND FindWindow(
LPCTSTR lpClassName, //窗口的类
LPCTSTR lpWindowName //窗口的名称
);
参数一可以为NULL,参数二中只要包含调试程序名称的部分字符串即可,如FindWindow(NULL,"ollydbg ");即可。
3、时间间隔检测
这里介绍RDTSC指令。
RDTSC指令返回的是自开机以来的CPU的周期数,返回的是一个64位的值EDXL:EAX(高32位在EDX,低32位在EAX),那么使用方法是如下。
int time_first, time_last, time_sub
_asm{
rdtsc
mov time_first, eax
}
...... //部分程序代码
_asm{
rdtsc
mov time_last, eax
}
time_sub = time_last - time_first
通过比较time_sub的差值是否大于正常运行时间,从而判断程序是否位于调试环境,与之相似的函数有:
DWORD GetTickCount(void)
返回自计算机启动到调用该函数的时间差,以毫秒为单位。可以使用类似上文的方法,计算时间差,判断调试环境。
4、加壳
加壳是运用加密算法将程序压缩,修改程序入口点(Original Entry Point, OEP),加密程序源码,在内存中加载时进行解码,从而阻止调试软件对程序代码在用户层面的修改。
5、加花
加“花”是指加入花指令,花指令由一些汇编语言组成的干扰调试人员进行分析的代码,程序能够正常运行,但会造成反汇编出错。
微信公众号:计算机与网络安全
ID:Computer-network
【推荐书籍】