Linux Kernel Exploit Development with VMware - Lab2::Module Debugging
목차
The Kernel Module
TestVM의 mod_sample
디렉토리에 sample.c
파일이 존재한다.
sample.c
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
/*
* Test kernel module
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/device.h>
#define DEVICE_NAME "sample"
static int dev_counter = 0;
static int device_open(struct inode *, struct file *);
static ssize_t device_read(struct file *, char *, size_t , loff_t *);
static int device_close(struct inode *, struct file *);
static struct class *class;
static int major_no;
static struct file_operations fops = {
.open = device_open,
.release = device_close,
.read = device_read
};
static int __init load(void) {
printk(KERN_INFO "Sample module loaded\n");
major_no = register_chrdev(0, DEVICE_NAME, &fops);
class = class_create(THIS_MODULE, DEVICE_NAME);
device_create(class, NULL, MKDEV(major_no, 0), NULL, DEVICE_NAME);
return 0;
}
static void __exit unload(void) {
device_destroy(class, MKDEV(major_no, 0));
class_unregister(class);
class_destroy(class);
unregister_chrdev(major_no, DEVICE_NAME);
printk(KERN_INFO "Sample module unloaded\n");
}
static int device_open(struct inode *inode, struct file *file) {
if (dev_counter)
return -EBUSY;
dev_counter++;
return 0;
}
static int device_close(struct inode *inode, struct file *f) {
dev_counter--;
return 0;
}
static ssize_t device_read(struct file *file, char *buf, size_t size, loff_t *off) {
char tmp[5] = "test";
printk(KERN_INFO "%s\n", tmp);
return 0;
}
module_init(load);
module_exit(unload);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("vnik");
MODULE_DESCRIPTION("Test kernel module.");
MODULE_VERSION("0.1");
위 코드에서 module_init(load);
을 통해 module을 등록하고, load
함수에서는 device_create()
호출을 통해 디바이스를 등록한다.
해당 디바이스를 열거나, 닫거나, 읽을 때 fops
구조체 안의 필드에 명시된 포인터의 함수가 호출되게 된다.
간단한 명령어를 통해 확인이 가능하다. 먼저 module을 make
한 후 insmod
명령을 통해 등록한다.
Makefile
1
2
3
4
5
6
7
8
obj-m += sample.o
ccflags-y += -g
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
Compile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
test@ubuntu:~/exercises/mod_sample$ make
make -C /lib/modules/3.5.0-23-generic/build M=/home/test/exercises/mod_sample modules
make[1]: Entering directory `/usr/src/linux-headers-3.5.0-23-generic'
CC [M] /home/test/exercises/mod_sample/sample.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/test/exercises/mod_sample/sample.mod.o
LD [M] /home/test/exercises/mod_sample/sample.ko
make[1]: Leaving directory `/usr/src/linux-headers-3.5.0-23-generic'
test@ubuntu:~/exercises/mod_sample$ ls
Makefile Module.symvers sample.ko sample.mod.o
modules.order sample.c sample.mod.c sample.o
test@ubuntu:~/exercises/mod_sample$ sudo insmod ./sample.ko
test@ubuntu:~/exercises/mod_sample$ lsmod | grep sample
sample 12695 0
test@ubuntu:~/exercises/mod_sample$ ls /dev/sample
/dev/sample
test@ubuntu:~/exercises/mod_sample$
이어 /dev/sample
디바이스를 cat
명령어로 읽은 후 dmesg
명령으로 커널 로그를 확인해본다.
1
2
3
4
test@ubuntu:~/exercises/mod_sample$ sudo cat /dev/sample
test@ubuntu:~/exercises/mod_sample$ dmesg | tail -n 1
[ 5217.787281] test
test@ubuntu:~/exercises/mod_sample$
Debugger Attach to Kernel Module
lab1과 비슷하게 먼저 breakpoint를 걸어보자. sample.ko
내부의 함수에 breakpoint를 걸어야 하기 때문에 module의 segmentation 주소를 적어둔다.
TestVM
1
2
3
4
5
test@ubuntu:~/exercises/mod_sample$ sudo cat /sys/module/sample/sections/{.text,.data,.bss}
0xffffffffa018f000
0xffffffffa0191000
0xffffffffa0191338
test@ubuntu:~/exercises/mod_sample$
이후 위 주소를 가지고 MasterVM
에서 sample.ko
모듈의 symbol을 추가한다.
MasterVM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
root@master:~# gdb -q kernels/vmlinux-3.5.0-23-generic
Reading symbols from kernels/vmlinux-3.5.0-23-generic...done.
(gdb) target remote 192.168.94.1:8864
Remote debugging using 192.168.94.1:8864
native_safe_halt ()
at /build/buildd/linux-lts-quantal-3.5.0/arch/x86/include/asm/irqflags.h:50
warning: Source file is more recent than executable.
50 }
(gdb) add-symbol-file ./modules/sample.ko 0xffffffffa018f000 -s .data 0xffffffffa0191000 -s .bss 0xffffffffa0191338
add symbol table from file "./modules/sample.ko" at
.text_addr = 0xffffffffa018f000
.data_addr = 0xffffffffa0191000
.bss_addr = 0xffffffffa0191338
(y or n) y
Reading symbols from ./modules/sample.ko...done.
(gdb)
이제 sample.ko
모듈 내의 함수 device_read()
에 breakpoint를 걸어보자.
1
2
3
4
(gdb) b * device_read
Breakpoint 1 at 0xffffffffa018f050: file /home/vnik/exercises/mod_sample/sample.c, line 62.
(gdb) c
Continuing.
TestVM
1
test@ubuntu:~/exercises$ sudo cat /dev/sample
MasterVM
1
2
3
4
5
6
7
8
(gdb) c
Continuing.
Breakpoint 4, device_read (file=0xffff880078d3c200,
buf=0xce3000 <error: Cannot access memory at address 0xce3000>, size=32768,
off=0xffff88007b477f48) at /home/vnik/exercises/mod_sample/sample.c:62
62 /home/vnik/exercises/mod_sample/sample.c: No such file or directory.
(gdb)
BreakPoint에서 정상적으로 interrupt된다.
Debug Kernel Module
이제 device_read()
함수에서 출력하는 문자열 "test"
를 "thisisaverylongstring"
이라는 문자열로 변경해보자.
우선 device_read()
함수의 printk()
호출 부분에 BreakPoint를 건다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(gdb) disassemble device_read
Dump of assembler code for function device_read:
0xffffffffa018f050 <+0>: push %rbp
0xffffffffa018f051 <+1>: mov %rsp,%rbp
0xffffffffa018f054 <+4>: sub $0x10,%rsp
0xffffffffa018f058 <+8>: nopl 0x0(%rax,%rax,1)
0xffffffffa018f05d <+13>: mov $0xffffffffa0190024,%rdi
0xffffffffa018f064 <+20>: mov %gs:0x28,%rax
0xffffffffa018f06d <+29>: mov %rax,-0x8(%rbp)
0xffffffffa018f071 <+33>: xor %eax,%eax
0xffffffffa018f073 <+35>: lea -0xd(%rbp),%rsi
0xffffffffa018f077 <+39>: movl $0x74736574,-0xd(%rbp)
0xffffffffa018f07e <+46>: movb $0x0,-0x9(%rbp)
0xffffffffa018f082 <+50>: callq 0xffffffff81685975 <printk>
0xffffffffa018f087 <+55>: xor %eax,%eax
0xffffffffa018f089 <+57>: mov -0x8(%rbp),%rdx
0xffffffffa018f08d <+61>: xor %gs:0x28,%rdx
0xffffffffa018f096 <+70>: jne 0xffffffffa018f09a <device_read+74>
0xffffffffa018f098 <+72>: leaveq
0xffffffffa018f099 <+73>: retq
0xffffffffa018f09a <+74>: nopw 0x0(%rax,%rax,1)
0xffffffffa018f0a0 <+80>: callq 0xffffffff81052a90 <__stack_chk_fail>
End of assembler dump.
MasterVM
1
2
3
4
(gdb) b * device_read + 50
Breakpoint 5 at 0xffffffffa018f082: file /home/vnik/exercises/mod_sample/sample.c, line 64.
(gdb) c
Continuing.
TestVM
1
test@ubuntu:~/exercises$ sudo cat /dev/sample
MasterVM
1
2
3
4
5
6
7
(gdb) c
Continuing.
Breakpoint 5, device_read (file=<optimized out>, buf=<optimized out>, size=32768,
off=0xffff8800776dff48) at /home/vnik/exercises/mod_sample/sample.c:64
64 /home/vnik/exercises/mod_sample/sample.c: No such file or directory.
(gdb)
printk()
함수의 두번째 인자는 x64 호출 규약
에 의해 rsi
레지스터에 존재하므로 gdb에서 vmalloc()
함수를 통해 받은 임의의 주소에 "thisisaverylongstring"
문자열을 쓴 후, rsi
레지스터를 해당 주소값으로 변경해준다.
MasterVM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(gdb) c
Continuing.
Breakpoint 1, device_read (file=<optimized out>, buf=<optimized out>, size=32768,
off=0xffff880078f73f48) at /home/vnik/exercises/mod_sample/sample.c:64
64 in /home/vnik/exercises/mod_sample/sample.c
(gdb) p vmalloc(32)
$4 = (void *) 0xffffc90000372000
(gdb) set {char[25]}$4="thisisaverylongstring\n"
(gdb) x/s $4
0xffffc90000372000: "thisisaverylongstring\n"
(gdb) set $rsi=$4
(gdb) x/s $rsi
0xffffc90000372000: "thisisaverylongstring\n"
(gdb) c
Continuing.
TestVM
1
2
3
test@ubuntu:~$ dmesg | tail -n 1
[ 3127.299859] thisisaverylongstring
test@ubuntu:~$
성공적으로 "thisisaverylongstring"
문자열이 출력되었다.
다음 Lab으로 넘어가자.