💾 Archived View for aphrack.org › issues › phrack63 › 7.gmi captured on 2021-12-04 at 18:04:22. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2021-12-03)
-=-=-=-=-=-=-
==Phrack Inc.== Volume 0x0b, Issue 0x3f, Phile #0x07 of 0x14 |=-------=[ Playing Games With Kernel Memory ... FreeBSD Style ]=--------=| |=-----------------------------------------------------------------------=| |=-----------------=[ Joseph Kong <jkong01@gmail.com> ]=-----------------=| |=--------------------------=[ July 8, 2005 ]=---------------------------=| --[ Contents 1.0 - Introduction 2.0 - Finding System Calls 3.0 - Understanding Call Statements And Bytecode Injection 4.0 - Allocating Kernel Memory 5.0 - Putting It All Together 6.0 - Concluding Remarks 7.0 - References --[ 1.0 - Introduction The kernel memory interface or kvm interface was first introduced in SunOS. Although it has been around for quite some time, many people still consider it to be rather obscure. This article documents the basic usage of the Kernel Data Access Library (libkvm), and will explore some ways to use libkvm (/dev/kmem) in order to alter the behavior of a running FreeBSD system. FreeBSD kernel hacking skills of a moderate level (i.e. you know how to use ddb), as well as a decent understanding of C and x86 Assembly (AT&T Syntax) are required in order to understand the contents of this article. This article was written from the perspective of a FreeBSD 5.4 Stable System. Note: Although the techniques described in this article have been explored in other articles (see References), they are always from a Linux or Windows perspective. I personally only know of one other text that touches on the information contained herein. That text entitled "Fun and Games with FreeBSD Kernel Modules" by Stephanie Wehner explained some of the things one can do with libkvm. Considering the fact that one can do much more, and that documentation regarding libkvm is scarce (man pages and source code aside), I decided to write this article. --[ 2.0 - Finding System Calls Note: This section is extremely basic, if you have a good grasp of the libkvm functions read the next paragraph and skip to the next section. Stephanie Wehner wrote a program called checkcall, which would check if sysent[CALL] had been tampered with, and if so would change it back to the original function. In order to help with the debugging during the latter sections of this article, we are going to make use of checkcall's find system call functionality. Following is a stripped down version of checkcall, with just the find system call function. It is also a good example to learn the basics of libkvm from. A line by line explanation of the libkvm functions appears after the source code listing. find_syscall.c: /* * Takes two arguments: the name of a syscall and corresponding number, * and reports the location in memory where the syscall is located. * * If you enter the name of a syscall with an incorrect syscall number, * the output will be fubar. Too lazy to implement a check * * Based off of Stephanie Wehner's checkcall.c,v 1.1.1.1 * * find_syscall.c,v 1.0 2005/05/20 */ #include <stdio.h> #include <fcntl.h> #include <kvm.h> #include <nlist.h> #include <limits.h> #include <sys/types.h> #include <sys/sysent.h> #include <sys/syscall.h> int main(int argc, char *argv[]) { char errbuf[_POSIX2_LINE_MAX]; kvm_t *kd; u_int32_t addr; int callnum; struct sysent call; struct nlist nl[] = { { NULL }, { NULL }, { NULL }, }; /* Check for the correct number of arguments */ if(argc != 3) { printf("Usage:\n%s <name of system call> <syscall number>" " \n\n", argv[0]); printf("See /usr/src/sys/sys/syscall.h for syscall numbers" " \n"); exit(0); } /* Find the syscall */ nl[0].n_name = "sysent"; nl[1].n_name = argv[1]; callnum = atoi(argv[2]); printf("Finding syscall %d: %s\n\n", callnum, argv[1]); /* Initialize kernel virtual memory access */ kd = kvm_openfiles(NULL, NULL, NULL, O_RDWR, errbuf); if(kd == NULL) { fprintf(stderr, "ERROR: %s\n", errbuf); exit(-1); } /* Find the addresses */ if(kvm_nlist(kd, nl) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } if(!nl[0].n_value) { fprintf(stderr, "ERROR: %s not found (fubar?)\n" , nl[0].n_name); exit(-1); } else { printf("%s is 0x%x at 0x%x\n", nl[0].n_name, nl[0].n_type , nl[0].n_value); } if(!nl[1].n_value) { fprintf(stderr, "ERROR: %s not found\n", nl[1].n_name); exit(-1); } /* Calculate the address */ addr = nl[0].n_value + callnum * sizeof(struct sysent); /* Print out location */ if(kvm_read(kd, addr, &call, sizeof(struct sysent)) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } else { printf("sysent[%d] is at 0x%x and will execute function" " located at 0x%x\n", callnum, addr, call.sy_call); } if(kvm_close(kd) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } exit(0); } There are five functions from libkvm that are included in the above program; they are: kvm_openfiles kvm_nlist kvm_geterr kvm_read kvm_close kvm_openfiles: Basically kvm_openfiles initializes kernel virtual memory access, and returns a descriptor to be used in subsequent kvm library calls. In find_syscall the syntax was as follows: kd = kvm_openfiles(NULL, NULL, NULL, O_RDWR, errbuf); kd is used to store the returned descriptor, if after the call kd equals NULL then an error has occurred. The first three arguments correspond to const char *execfile, const char *corefile, and const char *swapfiles respectively. However for our purposes they are unnecessary, hence NULL. The fourth argument indicates that we want read/write access. The fifth argument indicates which buffer to place any error messages, more on that later. kvm_nlist: The man page states that kvm_nlist retrieves the symbol table entries indicated by the name list argument (struct nlist). The members of struct nlist that interest us are as follows: char *n_name; /* symbol name (in memory) */ unsigned long n_value; /* address of the symbol */ Prior to calling kvm_nlist in find_syscall a struct nlist array was setup as follows: struct nlist nl[] = { { NULL }, { NULL }, { NULL }, }; nl[0].n_name = "sysent"; nl[1].n_name = argv[1]; The syntax for calling kvm_nlist is as follows: kvm_nlist(kd, nl) What this did was fill out the n_value member of each element in the array nl with the starting address in memory corresponding to the value in n_name. In other words we now know the location in memory of sysent and the user supplied syscall (argv[1]). nl was initialized with three elements because kvm_nlist expects as its second argument a NULL terminated array of nlist structures. kvm_geterr: As stated in the man page this function returns a string describing the most recent error condition. If you look through the above source code listing you will see kvm_geterr gets called after every libkvm function, except kvm_openfiles. kvm_openfiles uses its own unique form of error reporting, because kvm_geterr requires a descriptor as an argument, which would not exist if kvm_openfiles has not been called yet. An example usage of kvm_geterr follows: fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); kvm_read: This function is used to read kernel virtual memory. In find_syscall the syntax was as follows: kvm_read(kd, addr, &call, sizeof(struct sysent)) The first argument is the descriptor. The second is the address to begin reading from. The third argument is the user-space location to store the data read. The fourth argument is the number of bytes to read. kvm_close: This function breaks the connection between the pointer and the kernel virtual memory established with kvm_openfiles. In find_syscall this function was called as follows: kvm_close(kd) The following is an algorithmic explanation of find_syscall.c: 1. Check to make sure the user has supplied a syscall name and number. (No error checking, just checks for two arguments) 2. Setup the array of nlist structures appropriately. 3. Initialize kernel virtual memory access. (kvm_openfiles) 4. Find the address of sysent and the user supplied syscall. (kvm_nlist) 5. Calculate the location of the syscall in sysent. 6. Copy the syscall's sysent structure from kernel-space to user-space. (kvm_read) 7. Print out the location of the syscall in the sysent structure and the location of the executed function. 8. Close the descriptor (kvm_close) In order to verify that the output of find_syscall is accurate, one can make use of ddb as follows: Note: The output below was modified in order to meet the 75 character per line requirement. [---------------------------------------------------------] ghost@slavetwo:~#ls find_syscall.c ghost@slavetwo:~#gcc -o find_syscall find_syscall.c -lkvm ghost@slavetwo:~#ls find_syscall find_syscall.c ghost@slavetwo:~#sudo ./find_syscall Password: Usage: ./find_syscall <name of system call> <syscall number> See /usr/src/sys/sys/syscall.h for syscall numbers ghost@slavetwo:~#sudo ./find_syscall mkdir 136 Finding syscall 136: mkdir sysent is 0x4 at 0xc06dc840 sysent[136] is at 0xc06dcc80 and will execute function located at 0xc0541900 ghost@slavetwo:~#KDB: enter: manual escape to debugger [thread pid 12 tid 100004 ] Stopped at kdb_enter+0x32: leave db> examine/i 0xc0541900 mkdir: pushl %ebp db> mkdir+0x1: movl %esp,%ebp db> c ghost@slavetwo:~# [---------------------------------------------------------] --[ 3.0 - Understanding Call Statements And Bytecode Injection In x86 Assembly a Call statement is a control transfer instruction, used to call a procedure. There are two types of Call statements Near and Far, for the purposes of this article one only needs to understand a Near Call. The following code illustrates the details of a Near Call statement (in Intel Syntax): 0200 BB1295 MOV BX,9512 0203 E8FA00 CALL 0300 0206 B82F14 MOV AX,142F In the above code snippet, when the IP (Instruction Pointer) gets to 0203 it will jump to 0300. The hexadecimal representation for CALL is E8, however FA00 is not 0300. 0x300 - 0x206 = 0xFA. In a near call the IP address of the instruction after the Call is saved on the stack, so the called procedure knows where to return to. This explains why the operand for Call in this example is 0xFA00 and not 0x300. This is an important point and will come into play later. One of the more entertaining things one can do with the libkvm functions is patch kernel virtual memory. As always we start with a very simple example ... Hello World! The following is a kld which adds a syscall that functions as a Hello World! program. hello.c: /* * Prints "FreeBSD Rox!" 10 times * */ #include <sys/types.h> #include <sys/param.h> #include <sys/proc.h> #include <sys/module.h> #include <sys/sysent.h> #include <sys/kernel.h> #include <sys/systm.h> /* * The function for implementing the syscall. */ static int hello (struct thread *td, void *arg) { printf ("FreeBSD Rox!\n"); printf ("FreeBSD Rox!\n"); printf ("FreeBSD Rox!\n"); printf ("FreeBSD Rox!\n"); printf ("FreeBSD Rox!\n"); printf ("FreeBSD Rox!\n"); printf ("FreeBSD Rox!\n"); printf ("FreeBSD Rox!\n"); printf ("FreeBSD Rox!\n"); printf ("FreeBSD Rox!\n"); return 0; } /* * The `sysent' for the new syscall */ static struct sysent hello_sysent = { 0, /* sy_narg */ hello /* sy_call */ }; /* * The offset in sysent where the syscall is allocated. */ static int offset = 210; /* * The function called at load/unload. */ static int load (struct module *module, int cmd, void *arg) { int error = 0; switch (cmd) { case MOD_LOAD : printf ("syscall loaded at %d\n", offset); break; case MOD_UNLOAD : printf ("syscall unloaded from %d\n", offset); break; default : error = EOPNOTSUPP; break; } return error; } SYSCALL_MODULE(hello, &offset, &hello_sysent, load, NULL); The following is the user-space program for the above kld: interface.c: #include <stdio.h> #include <sys/syscall.h> #include <sys/types.h> #include <sys/module.h> int main(int argc, char **argv) { return syscall(210); } If we compile the above kld using a standard Makefile, load it, and then run the user-space program, we get some very annoying output. In order to make this syscall less annoying we can use the following program. As before an explanation of any new functions and concepts appears after the source code listing. test_call.c: /* * Test understanding of call statement: * Operand for call statement is the difference between the called function * and the address of the instruction following the call statement. * * Tested on syscall hello. Normally prints out "FreeBSD Rox!" 10 times, * after patching only prints it out once. * * test_call.c,v 2.1 2005/06/15 */ #include <stdio.h> #include <fcntl.h> #include <kvm.h> #include <nlist.h> #include <limits.h> #include <sys/types.h> /* * Offset of string to be printed * Starting at the beginning of the syscall hello */ #define OFFSET_1 0xed /* * Offset of instruction following call statement */ #define OFFSET_2 0x12 /* * Replacement code */ unsigned char code[] = "\x55" /* push %ebp */ "\x89\xe5" /* mov %esp,%ebp */ "\x83\xec\x04" /* sub $0x4,%esp */ "\xc7\x04\x24\x00\x00\x00\x00" /* movl $0,(%esp) */ "\xe8\x00\x00\x00\x00" /* call printf */ "\xc9" /* leave */ "\x31\xc0" /* xor %eax,%eax */ "\xc3" /* ret */ "\x8d\xb4\x26\x00\x00\x00\x00" /* lea 0x0(%esi),%esi */ "\x8d\xbc\x27\x00\x00\x00\x00"; /* lea 0x0(%edi),%edi */ int main(int argc, char *argv[]) { char errbuf[_POSIX2_LINE_MAX]; kvm_t *kd; u_int32_t offset_1; u_int32_t offset_2; struct nlist nl[] = { { NULL }, { NULL }, { NULL }, }; /* Initialize kernel virtual memory access */ kd = kvm_openfiles(NULL, NULL, NULL, O_RDWR, errbuf); if(kd == NULL) { fprintf(stderr, "ERROR: %s\n", errbuf); exit(-1); } /* Find the address of hello and printf */ nl[0].n_name = "hello"; nl[1].n_name = "printf"; if(kvm_nlist(kd, nl) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } if(!nl[0].n_value) { fprintf(stderr, "ERROR: Symbol %s not found\n" , nl[0].n_name); exit(-1); } if(!nl[1].n_value) { fprintf(stderr, "ERROR: Symbol %s not found\n" , nl[1].n_name); exit(-1); } /* Calculate the correct offsets */ offset_1 = nl[0].n_value + OFFSET_1; offset_2 = nl[0].n_value + OFFSET_2; /* Set the code to contain the correct addresses */ *(unsigned long *)&code[9] = offset_1; *(unsigned long *)&code[14] = nl[1].n_value - offset_2; /* Patch hello */ if(kvm_write(kd, nl[0].n_value, code, sizeof(code)) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } printf("Luke, I am your father!\n"); /* Close kd */ if(kvm_close(kd) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } exit(0); } The only libkvm function that is included in the above program that hasn't been discussed before is kvm_write. kvm_write: This function is used to write to kernel virtual memory. In test_call the syntax was as follows: kvm_write(kd, nl[0].n_value, code, sizeof(code)) The first argument is the descriptor. The second is the address to begin writing to. The third argument is the user-space location to read from. The fourth argument is the number of bytes to read. The replacement code (bytecode) in test_call was generated with help of objdump. [---------------------------------------------------------] ghost@slavetwo:~#objdump -DR hello.ko | less hello.ko: file format elf32-i386-freebsd Disassembly of section .hash: 00000094 <.hash>: 94: 11 00 adc %eax,(%eax) 96: 00 00 add %al,(%eax) OUTPUT SNIPPED Disassembly of section .text: 00000500 <hello>: 500: 55 push %ebp 501: 89 e5 mov %esp,%ebp 503: 83 ec 04 sub $0x4,%esp 506: c7 04 24 ed 05 00 00 movl $0x5ed,(%esp) 509: R_386_RELATIVE *ABS* 50d: e8 fc ff ff ff call 50e <hello+0xe> 50e: R_386_PC32 printf 512: c7 04 24 ed 05 00 00 movl $0x5ed,(%esp) 515: R_386_RELATIVE *ABS* 519: e8 fc ff ff ff call 51a <hello+0x1a> 51a: R_386_PC32 printf 51e: c7 04 24 ed 05 00 00 movl $0x5ed,(%esp) 521: R_386_RELATIVE *ABS* 525: e8 fc ff ff ff call 526 <hello+0x26> 526: R_386_PC32 printf OUTPUT SNIPPED 57e: c9 leave 57f: 31 c0 xor %eax,%eax 581: c3 ret 582: 8d b4 26 00 00 00 00 lea 0x0(%esi),%esi 589: 8d bc 27 00 00 00 00 lea 0x0(%edi),%edi [---------------------------------------------------------] Note: Your output may vary depending on your compiler version and flags. Comparing the output of the text section with the bytecode in test_call one can see that they are essentially the same, minus setting up nine more calls to printf. An important item to take note of is when objdump reports something as being relative. In this case two items are; movl $0x5ed,(%esp) (sets up the string to be printed) and call printf. Which brings us to ... In test_call there are two #define statements, they are: #define OFFSET_1 0xed #define OFFSET_2 0x12 The first represents the address of the string to be printed relative to the beginning of syscall hello (the number is derived from the output of objdump). While the second represents the offset of the instruction following the call to printf in the bytecode. Later on in test_call there are these four statements: /* Calculate the correct offsets */ offset_1 = nl[0].n_value + OFFSET_1; offset_2 = nl[0].n_value + OFFSET_2; /* Set the code to contain the correct addresses */ *(unsigned long *)&code[9] = offset_1; *(unsigned long *)&code[14] = nl[1].n_value - offset_2; From the comments it should be obvious what these four statements do. code[9] is the section in bytecode where the address of the string to be printed is stored. code[14] is the operand for the call statement; address of printf - address of the next statement. The following is the output before and after running test_call: [---------------------------------------------------------] ghost@slavetwo:~#ls Makefile hello.c interface.c test_call.c ghost@slavetwo:~#make Warning: Object directory not changed from original /usr/home/ghost @ -> /usr/src/sys machine -> /usr/src/sys/i386/include OUTPUT SNIPPED J% objcopy % hello.kld ld -Bshareable -d -warn-common -o hello.ko hello.kld objcopy --strip-debug hello.ko ghost@slavetwo:~#sudo kldload ./hello.ko Password: syscall loaded at 210 ghost@slavetwo:~#gcc -o interface interface.c ghost@slavetwo:~#./interface FreeBSD Rox! FreeBSD Rox! FreeBSD Rox! FreeBSD Rox! FreeBSD Rox! FreeBSD Rox! FreeBSD Rox! FreeBSD Rox! FreeBSD Rox! FreeBSD Rox! ghost@slavetwo:~#gcc -o test_call test_call.c -lkvm ghost@slavetwo:~#sudo ./test_call Luke, I am your father! ghost@slavetwo:~#./interface FreeBSD Rox! ghost@slavetwo:~# [---------------------------------------------------------] --[ 4.0 - Allocating Kernel Memory Being able to just patch kernel memory has its limitations since you don't have much room to play with. Being able to allocate kernel memory alleviates this problem. The following is a kld which does just that. kmalloc.c: /* * Module to allow a non-privileged user to allocate kernel memory * * kmalloc.c,v 2.0 2005/06/01 * Date Modified 2005/06/14 */ #include <sys/types.h> #include <sys/param.h> #include <sys/proc.h> #include <sys/module.h> #include <sys/sysent.h> #include <sys/kernel.h> #include <sys/systm.h> #include <sys/malloc.h> /* * Arguments for kmalloc */ struct kma_struct { unsigned long size; unsigned long *addr; }; struct kmalloc_args { struct kma_struct *kma; }; /* * The function for implementing kmalloc. */ static int kmalloc (struct thread *td, struct kmalloc_args *uap) { int error = 1; struct kma_struct kts; if(uap->kma) { MALLOC(kts.addr, unsigned long*, uap->kma->size , M_TEMP, M_NOWAIT); error = copyout(&kts, uap->kma, sizeof(kts)); } return (error); } /* * The `sysent' for kmalloc */ static struct sysent kmalloc_sysent = { 1, /* sy_narg */ kmalloc /* sy_call */ }; /* * The offset in sysent where the syscall is allocated. */ static int offset = 210; /* * The function called at load/unload. */ static int load (struct module *module, int cmd, void *arg) { int error = 0; switch (cmd) { case MOD_LOAD : uprintf ("syscall loaded at %d\n", offset); break; case MOD_UNLOAD : uprintf ("syscall unloaded from %d\n", offset); break; default : error = EOPNOTSUPP; break; } return error; } SYSCALL_MODULE(kmalloc, &offset, &kmalloc_sysent, load, NULL); The following is the user-space program for the above kld: interface.c: /* * User Program To Interact With kmalloc module */ #include <stdio.h> #include <sys/syscall.h> #include <sys/types.h> #include <sys/module.h> struct kma_struct { unsigned long size; unsigned long *addr; }; int main(int argc, char **argv) { struct kma_struct kma; if(argc != 2) { printf("Usage:\n%s <size>\n", argv[0]); exit(0); } kma.size = (unsigned long)atoi(argv[1]); return syscall(210, &kma); } Using the techniques/functions described in the previous two sections and the following algorithm coined by Silvio Cesare one can allocate kernel memory without the use of a kld. Silvio Cesare's kmalloc from user-space algorithm: 1. Get the address of some syscall 2. Write a function which will allocate kernel memory 3. Save sizeof(our_function) bytes of some syscall 4. Overwrite some syscall with our_function 5. Call newly overwritten syscall 6. Restore syscall test_kmalloc.c: /* * Allocate kernel memory from user-space * * Algorithm to allocate kernel memory is as follows: * * 1. Get address of mkdir * 2. Overwrite mkdir with function that calls man 9 malloc() * 3. Call mkdir through int $0x80 * This will cause the kernel to run the new "mkdir" syscall, which will * call man 9 malloc() and pass out the address of the newly allocated * kernel memory * 4. Restore mkdir syscall * * test_kmalloc.c,v 2.0 2005/06/24 */ #include <stdio.h> #include <fcntl.h> #include <kvm.h> #include <nlist.h> #include <limits.h> #include <sys/types.h> #include <sys/syscall.h> #include <sys/module.h> /* * Offset of instruction following call statements * Starting at the beginning of the function kmalloc */ #define OFFSET_1 0x3a #define OFFSET_2 0x56 /* * kmalloc function code */ 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 <kmalloc+0x20> */ "\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 <kmalloc+0x36> */ "\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 <kmalloc+0x52> */ "\x83\xc4\x14" /* add $0x14,%esp */ "\x89\xc2" /* mov %eax,%edx */ "\x5b" /* pop %ebx */ "\xc9" /* leave */ "\x89\xd0" /* mov %edx,%eax */ "\xc3"; /* ret */ /* * struct used to store kernel address */ 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 <size>\n", argv[0]); exit(0); } /* Initialize kernel virtual memory access */ kd = kvm_openfiles(NULL, NULL, NULL, O_RDWR, errbuf); if(kd == NULL) { fprintf(stderr, "ERROR: %s\n", errbuf); exit(-1); } /* Find the address of mkdir, M_TEMP, malloc, and copyout */ 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); } } /* Calculate the correct offsets */ offset_1 = nl[0].n_value + OFFSET_1; offset_2 = nl[0].n_value + OFFSET_2; /* Set the code to contain the correct addresses */ *(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; /* Save mkdir syscall */ if(kvm_read(kd, nl[0].n_value, origcode, sizeof(code)) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } /* Patch mkdir */ if(kvm_write(kd, nl[0].n_value, code, sizeof(code)) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } /* Allocate kernel memory */ kma.size = (unsigned long)atoi(argv[1]); syscall(136, &kma); printf("Address of kernel memory: 0x%x\n", kma.addr); /* Restore mkdir */ if(kvm_write(kd, nl[0].n_value, origcode, sizeof(code)) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } /* Close kd */ if(kvm_close(kd) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } exit(0); } Using ddb one can verify the results of the above program as follows: [---------------------------------------------------------] ghost@slavetwo:~#ls test_kmalloc.c ghost@slavetwo:~#gcc -o test_kmalloc test_kmalloc.c -lkvm ghost@slavetwo:~#sudo ./test_kmalloc Usage: ./test_kmalloc <size> ghost@slavetwo:~#sudo ./test_kmalloc 10 Address of kernel memory: 0xc2580870 ghost@slavetwo:~#KDB: enter: manual escape to debugger [thread pid 12 tid 100004 ] Stopped at kdb_enter+0x32: leave db> examine/x 0xc2580870 0xc2580870: 70707070 db> 0xc2580874: 70707070 db> 0xc2580878: dead7070 db> c ghost@slavetwo:~# [---------------------------------------------------------] --[ 5.0 - Putting It All Together Knowing how to patch and allocate kernel memory gives one a lot of freedom. This last section will demonstrate how to apply a call hook using the techniques described in the previous sections. Typically call hooks on FreeBSD are done by changing the sysent and having it point to another function, we will not be doing this. Instead we will be using the following algorithm (with a few minor twists, shown later): 1. Copy syscall we want to hook 2. Allocate kernel memory (use technique described in previous section) 3. Place new routine in newly allocated address space 4. Overwrite first 7 bytes of syscall with an instruction to jump to new routine 5. Execute new routine, plus the first x bytes of syscall (this step will become clearer later) 6. Jump back to syscall + offset Where offset is equal to x Stealing an idea from pragmatic of THC we will hook mkdir to print out a debug message. The following is the kld used in conjunction with objdump in order to extract the bytecode required for the call hook. hacked_mkdir.c: /* * mkdir call hook * * Prints a simple debugging message */ #include <sys/types.h> #include <sys/param.h> #include <sys/proc.h> #include <sys/module.h> #include <sys/sysent.h> #include <sys/kernel.h> #include <sys/systm.h> #include <sys/linker.h> #include <sys/sysproto.h> #include <sys/syscall.h> /* The hacked system call */ static int hacked_mkdir (struct proc *p, struct mkdir_args *uap) { uprintf ("MKDIR SYSCALL : %s\n", uap->path); return 0; } /* The sysent for the hacked system call */ static struct sysent hacked_mkdir_sysent = { 1, /* sy_narg */ hacked_mkdir /* sy_call */ }; /* The offset in sysent where the syscall is allocated */ static int offset = NO_SYSCALL; /* The function called at load/unload */ static int load (struct module *module, int cmd, void *arg) { int error = 0; switch (cmd) { case MOD_LOAD : uprintf ("syscall loaded at %d\n", offset); break; case MOD_UNLOAD : uprintf ("syscall unloaded from %d\n", offset); break; default : error = EINVAL; break; } return error; } SYSCALL_MODULE(hacked_mkdir, &offset, &hacked_mkdir_sysent, load, NULL); The following is an example program which hooks mkdir to print out a simple debug message. As always an explanation of any new concepts appears after the source code listing. test_hook.c: /* * Intercept mkdir system call, printing out a debug message before * executing mkdir. * * Algorithm is as follows: * 1. Copy mkdir syscall upto but not including \xe8. * 2. Allocate kernel memory. * 3. Place new routine in newly allocated address space. * 4. Overwrite first 7 bytes of mkdir syscall with an instruction to jump * to new routine. * 5. Execute new routine, plus the first x bytes of mkdir syscall. * Where x is equal to the number of bytes copied from step 1. * 6. Jump back to mkdir syscall + offset. * Where offset is equal to the location of \xe8. * * test_hook.c,v 3.0 2005/07/02 */ #include <stdio.h> #include <fcntl.h> #include <kvm.h> #include <nlist.h> #include <limits.h> #include <sys/types.h> #include <sys/syscall.h> #include <sys/module.h> /* * Offset of instruction following call statements * Starting at the beginning of the function kmalloc */ #define KM_OFFSET_1 0x3a #define KM_OFFSET_2 0x56 /* * kmalloc function code */ unsigned char km_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 <kmalloc+0x20> */ "\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 <kmalloc+0x36> */ "\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 <kmalloc+0x52> */ "\x83\xc4\x14" /* add $0x14,%esp */ "\x89\xc2" /* mov %eax,%edx */ "\x5b" /* pop %ebx */ "\xc9" /* leave */ "\x89\xd0" /* mov %edx,%eax */ "\xc3"; /* ret */ /* * Offset of instruction following call statements * Starting at the beginning of the function hacked_mkdir */ #define HA_OFFSET_1 0x2f /* * hacked_mkdir function code */ unsigned char ha_code[] = "\x4d" /* M */ "\x4b" /* K */ "\x44" /* D */ "\x49" /* I */ "\x52" /* R */ "\x20" /* sp */ "\x53" /* S */ "\x59" /* Y */ "\x53" /* S */ "\x43" /* C */ "\x41" /* A */ "\x4c" /* L */ "\x4c" /* L */ "\x20" /* sp */ "\x3a" /* : */ "\x20" /* sp */ "\x25" /* % */ "\x73" /* s */ "\x0a" /* nl */ "\x00" /* null */ "\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) */ "\x89\x44\x24\x04" /* mov %eax,0x4(%esp) */ "\xe8\xfc\xff\xff\xff" /* call 17 <hacked_mkdir+0x17>*/ "\x31\xc0" /* xor %eax,%eax */ "\x83\xc4\x08" /* add $0x8,%esp */ "\x5d"; /* pop %ebp */ /* * jump code */ unsigned char jp_code[] = "\xb8\x00\x00\x00\x00" /* movl $0,%eax */ "\xff\xe0"; /* jmp *%eax */ /* * struct used to store kernel address */ 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 km_offset_1; u_int32_t km_offset_2; u_int32_t ha_offset_1; struct nlist nl[] = { { NULL },{ NULL },{ NULL },{ NULL },{ NULL },{ NULL},{ NULL }, }; unsigned long diff; int position; unsigned char orig_code[sizeof(km_code)]; struct kma_struct kma; /* Initialize kernel virtual memory access */ kd = kvm_openfiles(NULL, NULL, NULL, O_RDWR, errbuf); if(kd == NULL) { fprintf(stderr, "ERROR: %s\n", errbuf); exit(-1); } /* Find the address of mkdir, M_TEMP, malloc, copyout, uprintf, and kern_rmdir */ nl[0].n_name = "mkdir"; nl[1].n_name = "M_TEMP"; nl[2].n_name = "malloc"; nl[3].n_name = "copyout"; nl[4].n_name = "uprintf"; nl[5].n_name = "kern_rmdir"; if(kvm_nlist(kd, nl) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } for(i = 0; i <= 5; i++) { if(!nl[i].n_value) { fprintf(stderr, "ERROR: Symbol %s not found\n" , nl[i].n_name); exit(-1); } } /* Determine size of mkdir syscall */ diff = nl[5].n_value - nl[0].n_value; unsigned char mk_code[diff]; /* Save a copy of mkdir syscall */ if(kvm_read(kd, nl[0].n_value, mk_code, diff) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } /* Determine position of 0xe8 */ for(i = 0; i < (int)diff; i++) { if(mk_code[i] == 0xe8) { position = i; } } /* Calculate the correct offsets for kmalloc */ km_offset_1 = nl[0].n_value + KM_OFFSET_1; km_offset_2 = nl[0].n_value + KM_OFFSET_2; /* Set the km_code to contain the correct addresses */ *(unsigned long *)&km_code[44] = nl[1].n_value; *(unsigned long *)&km_code[54] = nl[2].n_value - km_offset_1; *(unsigned long *)&km_code[82] = nl[3].n_value - km_offset_2; /* Save mkdir syscall */ if(kvm_read(kd, nl[0].n_value, orig_code, sizeof(km_code)) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } /* Replace mkdir with kmalloc */ if(kvm_write(kd, nl[0].n_value, km_code, sizeof(km_code)) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } /* Allocate kernel memory */ kma.size = (unsigned long)sizeof(ha_code) + (unsigned long)position + (unsigned long)sizeof(jp_code); syscall(136, &kma); /* Restore mkdir */ if(kvm_write(kd, nl[0].n_value, orig_code, sizeof(km_code)) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } /* Calculate the correct offsets for hacked_mkdir */ ha_offset_1 = (unsigned long)kma.addr + HA_OFFSET_1; /* Set the ha_code to contain the correct addresses */ *(unsigned long *)&ha_code[34] = (unsigned long)kma.addr; *(unsigned long *)&ha_code[43] = nl[4].n_value - ha_offset_1; /* Place hacked_mkdir routine into kernel memory */ if(kvm_write(kd, (unsigned long)kma.addr, ha_code, sizeof(ha_code)) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } /* Place mk_code into kernel memory */ if(kvm_write(kd, (unsigned long)kma.addr + (unsigned long)sizeof(ha_code) - 1, mk_code, position) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } /* Set the jp_code to contain the correct address */ *(unsigned long *)&jp_code[1] = nl[0].n_value + (unsigned long)position; /* Place jump code into kernel memory */ if(kvm_write(kd, (unsigned long)kma.addr + (unsigned long)sizeof(ha_code) - 1 + (unsigned long)position , jp_code, sizeof(jp_code)) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } /* Set the jp_code to contain the correct address */ *(unsigned long *)&jp_code[1] = (unsigned long)kma.addr + 0x14; if(kvm_write(kd, nl[0].n_value, jp_code, sizeof(jp_code)) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } printf("I love the PowerGlove. It's so bad!\n"); /* Close kd */ if(kvm_close(kd) < 0) { fprintf(stderr, "ERROR: %s\n", kvm_geterr(kd)); exit(-1); } exit(0); } The comments state that the algorithm for this program is as follows: 1. Copy mkdir syscall upto but not including \xe8. 2. Allocate kernel memory. 3. Place new routine in newly allocated address space. 4. Overwrite first 7 bytes of mkdir syscall with an instruction to jump to new routine. 5. Execute new routine, plus the first x bytes of mkdir syscall. Where x is equal to the number of bytes copied from step 1. 6. Jump back to mkdir syscall + offset. Where offset is equal to the location of \xe8. The reason behind copying mkdir upto but not including \xe8 is because on different builds of FreeBSD the disassembly of the mkdir syscall is different. Therefore one cannot determine a static location to jump back to. However, on all builds of FreeBSD mkdir makes a call to kern_mkdir, thus we choose to jump back to that point. The following illustrates this. [---------------------------------------------------------] ghost@slavezero:~#nm /boot/kernel/kernel | grep mkdir c047c560 T devfs_vmkdir c0620e40 t handle_written_mkdir c0556ca0 T kern_mkdir c0557030 T mkdir c071d57c B mkdirlisthd c048a3e0 t msdosfs_mkdir c05e2ed0 t nfs4_mkdir c05d8710 t nfs_mkdir c05f9140 T nfsrv_mkdir c06b4856 r nfsv3err_mkdir c063a670 t ufs_mkdir c0702f40 D vop_mkdir_desc c0702f64 d vop_mkdir_vp_offsets ghost@slavezero:~#nm /boot/kernel/kernel | grep kern_rmdir c0557060 T kern_rmdir ghost@slavezero:~#objdump -d --start-address=0xc0557030 --stop-address=0xc0557060 /boot/kernel/kernel | less /boot/kernel/kernel: file format elf32-i386-freebsd Disassembly of section .text: c0557030 <mkdir>: c0557030: 55 push %ebp c0557031: 31 c9 xor %ecx,%ecx c0557033: 89 e5 mov %esp,%ebp c0557035: 83 ec 10 sub $0x10,%esp c0557038: 8b 55 0c mov 0xc(%ebp),%edx c055703b: 8b 42 04 mov 0x4(%edx),%eax c055703e: 89 4c 24 08 mov %ecx,0x8(%esp) c0557042: 89 44 24 0c mov %eax,0xc(%esp) c0557046: 8b 02 mov (%edx),%eax c0557048: 89 44 24 04 mov %eax,0x4(%esp) c055704c: 8b 45 08 mov 0x8(%ebp),%eax c055704f: 89 04 24 mov %eax,(%esp) c0557052: e8 49 fc ff ff call c0556ca0 <kern_mkdir> c0557057: c9 leave c0557058: c3 ret c0557059: 8d b4 26 00 00 00 00 lea 0x0(%esi),%esi ghost@slavezero:~# [---------------------------------------------------------] [---------------------------------------------------------] ghost@slavetwo:~#nm /boot/kernel/kernel | grep mkdir c046f680 T devfs_vmkdir c0608fd0 t handle_written_mkdir c05415d0 T kern_mkdir c0541900 T mkdir c074a9bc B mkdirlisthd c047d270 t msdosfs_mkdir c05c7160 t nfs4_mkdir c05bcfd0 t nfs_mkdir c05db750 T nfsrv_mkdir c06a2676 r nfsv3err_mkdir c06216a0 t ufs_mkdir c06fef40 D vop_mkdir_desc c06fef64 d vop_mkdir_vp_offsets ghost@slavetwo:~#nm /boot/kernel/kernel | grep kern_rmdir c0541930 T kern_rmdir ghost@slavetwo:~#objdump -dR --start-address=0xc0541900 --stop-address=0xc0541930 /boot/kernel/kernel | less /boot/kernel/kernel: file format elf32-i386-freebsd Disassembly of section .text: c0541900 <mkdir>: c0541900: 55 push %ebp c0541901: 89 e5 mov %esp,%ebp c0541903: 83 ec 10 sub $0x10,%esp c0541906: 8b 55 0c mov 0xc(%ebp),%edx c0541909: 8b 42 04 mov 0x4(%edx),%eax c054190c: c7 44 24 08 00 00 00 movl $0x0,0x8(%esp) c0541913: 00 c0541914: 89 44 24 0c mov %eax,0xc(%esp) c0541918: 8b 02 mov (%edx),%eax c054191a: 89 44 24 04 mov %eax,0x4(%esp) c054191e: 8b 45 08 mov 0x8(%ebp),%eax c0541921: 89 04 24 mov %eax,(%esp) c0541924: e8 a7 fc ff ff call c05415d0 <kern_mkdir> c0541929: c9 leave c054192a: c3 ret c054192b: 90 nop c054192c: 8d 74 26 00 lea 0x0(%esi),%esi ghost@slavetwo:~# [---------------------------------------------------------] The above output was generated from two different FreeBSD 5.4 builds. As one can clearly see the dissassembly dump of mkdir is different for each one. In test_hook the address of kern_rmdir is sought after, this is because in memory kern_rmdir comes right after mkdir, thus its address is the end boundary for mkdir. The bytecode for the call hook is as follows: unsigned char ha_code[] = "\x4d" /* M */ "\x4b" /* K */ "\x44" /* D */ "\x49" /* I */ "\x52" /* R */ "\x20" /* sp */ "\x53" /* S */ "\x59" /* Y */ "\x53" /* S */ "\x43" /* C */ "\x41" /* A */ "\x4c" /* L */ "\x4c" /* L */ "\x20" /* sp */ "\x3a" /* : */ "\x20" /* sp */ "\x25" /* % */ "\x73" /* s */ "\x0a" /* nl */ "\x00" /* null */ "\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) */ "\x89\x44\x24\x04" /* mov %eax,0x4(%esp) */ "\xe8\xfc\xff\xff\xff" /* call 17 <hacked_mkdir+0x17>*/ "\x31\xc0" /* xor %eax,%eax */ "\x83\xc4\x08" /* add $0x8,%esp */ "\x5d"; /* pop %ebp */ The first 20 bytes is for the string to be printed, because of this when we jump to this function we have to start at an offset of 0x14, as illustrated from this line of code: *(unsigned long *)&jp_code[1] = (unsigned long)kma.addr + 0x14; The last three statements in the hacked_mkdir bytecode zeros out the eax register, cleans up the stack, and restores the ebp register. This is done so that when mkdir actually executes its as if nothing has already occurred. One thing to remember about character arrays in C is that they are all null terminated. For example if we declare the following variable, unsigned char example[] = "\x41"; sizeof(example) will return 2. This is the reason why in test_hook we subtract 1 from sizeof(ha_code), otherwise we would be writing to the wrong spot. The following is the output before and after running test_hook: [---------------------------------------------------------] ghost@slavetwo:~#ls test_hook.c ghost@slavetwo:~#gcc -o test_hook test_hook.c -lkvm ghost@slavetwo:~#mkdir before ghost@slavetwo:~#ls -F before/ test_hook* test_hook.c ghost@slavetwo:~#sudo ./test_hook Password: I love the PowerGlove. It's so bad! ghost@slavetwo:~#mkdir after MKDIR SYSCALL : after ghost@slavetwo:~#ls -F after/ before/ test_hook* test_hook.c ghost@slavetwo:~# [---------------------------------------------------------] One could also use find_syscall and ddb to verify the results of test_hook --[ 6.0 - Concluding Remarks Being able to patch and allocate kernel memory gives one a lot of power over a system. All the examples in this article are trivial as it was my intention to show the how not the what. Other authors have better ideas than me anyways on what to do (see References). I would like to take this space to apologize if any of my explanations are unclear, hopefully reading over the source code and looking at the output makes up for it. Finally, I would like to thank Silvio Cesare, pragmatic, and Stephanie Wehner, for the inspiration/ideas. --[ 7.0 - References [ Internet ] [1] Silvio Cesare, "Runtime Kernel Kmem Patching" http://reactor-core.org/runtime-kernel-patching.html [2] devik & sd, "Linux on-th-fly kernel patching without LKM" http://www.phrack.org/show.php?p=58&a=7 [3] pragmatic, "Attacking FreeBSD with Kernel Modules" http://www.thc.org/papers/bsdkern.html [4] Andrew Reiter, "Dynamic Kernel Linker (KLD) Facility Programming Tutorial" http://ezine.daemonnews.org/200010/blueprints.html [5] Stephanie Wehner, "Fun and Games with FreeBSD Kernel Modules" http://www.r4k.net/mod/fbsdfun.html [ Books ] [6] Muhammad Ali Mazidi & Janice Gillispie Mazidi, "The 80x86 IBM PC And Compatible Computers: Assembly Language, Design, And Interfacing" (Prentice Hall) |=[ EOF ]=---------------------------------------------------------------=|