1. 환경
- Host : Ubuntu18.04
- Guest : linux kernel 4.7 → qemu 이용
$ sudo apt-get install qemu qemu-system $ git clone https://kernel.googlesource.com/pub/scm/linux/kernel/git/torvalds/linux $ git checkout v4.7
해당 커널은 가져오면 다음과 같이 linux 폴더안에 여러 파일들을 확인할수 있다
╭─wogh8732@ubuntu ~/Desktop/kernel_study/for_qemu/linux ‹523d939ef98f*›
╰─$ ls
arch Documentation ipc Makefile net security vmlinux
block drivers Kbuild Makefile.orig pie.patch sound vmlinux.o
certs firmware Kconfig mm README System.map
COPYING fs kernel modules.builtin REPORTING-BUGS tools
CREDITS include lib modules.order samples usr
crypto init MAINTAINERS Module.symvers scripts virt
╭─wogh8732@ubuntu ~/Desktop/kernel_study/for_qemu/linux ‹523d939ef98f*›
이제 시작해보자.
2. 리눅스 커널 빌드
우선 참조한 블로그를 토대로 진행을 할예정인데, kgdb를 이용하기 위해서 미리 설정을 해줘야한다.
필요한 패키지들을 설치하자
$ sudo apt-get install build-essential libncurses5 libncurses5-dev bin86 kernel-package libssl-dev bison flex libelf-dev
출처: https://harryp.tistory.com/839 [Park's Life]
2.1 커널 이미지 빌드
이제 위에서 말한 kgdb를 위한 설정을 해야한다. (linux 디렉토리에 들어가서)
- make defconfig
각 arch 마다 기본 config 가 존재하는데, 이것이 바로 defconfig 파일이다.
arch/x86/configs/* --> x86 기반 arch 의 defconfig 파일들 모음.
arch/arm/configs/* --> arm 기반 arch 의 defconfig 파일들 모음.
- make meuconfig
kernel hacking → Compile-time checks and compiler options → 맨위에꺼 체크
kernel hacking → KGDB: kernel debugger 체크
위 옵션들을 체크하고 저장하고 나오면 체크한 설정들을 기반으로 .config 파일이 생성된다.
- make → 커널 빌드
makefile을 보면 .config 파일을 참조하여 커널을 빌드하게끔 되어있다. 여기서 나는 gcc 관련 에러가 나와서 기존 gcc 버전을 7에서 6으로 낮추니까 됐다. 빌드에 성공하면 커널이미지 파일이 arch/x86/boot 하위에 bzimage 이름으로 생긴다.
╭─wogh8732@ubuntu ~/Desktop/kernel_study/for_qemu/linux/arch/x86/boot ‹523d939ef98f*› ╰─$ file bzImage bzImage: Linux kernel x86 boot executable bzImage, version 4.7.0+ (wogh8732@ubuntu) #3 SMP Mon Nov 23 22:38:21 PST 2020, RO-rootFS, swap_dev 0x6, Normal VGA
2.2 rootfs 만들기
이제 파일시스템을 만들어야한다. 파일시스템에 관련해서는 아래의 내용을 참조하면 된다.
빌드한 커널을 이용하여 qemu 동작시 루트파일시스템을 추가해야한다. 따라서 busybox를 이용해서 rootfs을 만들자. 우선 busybox를 다운받자
$ wget https://busybox.net/downloads/busybox-4.7.tar.bz2
$ tar -xvf busybox-1.31.0.tar.bz2
$ cd busybox-1.31.0
╭─wogh8732@ubuntu ~/Desktop/kernel_study/for_qemu/for_busybox/busybox-1.31.0
╰─$ ls
applets debianutils LICENSE procps
applets_sh docs loginutils qemu_multiarch_testing
arch e2fsprogs mailutils README
archival editors Makefile runit
AUTHORS examples Makefile.custom scripts
busybox filter_log Makefile.flags selinux
busybox.links findutils Makefile.help shell
busybox_unstripped include make_single_applets.sh size_single_applets.sh
busybox_unstripped.map init miscutils sysklogd
busybox_unstripped.out _install modutils testsuite
Config.in INSTALL networking TODO
configs klibc-utils NOFORK_NOEXEC.lst TODO_unicode
console-tools libbb NOFORK_NOEXEC.sh util-linux
coreutils libpwdgrp printutils
busybox를 잘 다운받았다면, 해당 폴더에 들어가 빌드를 하자
- make defconfig
- make menuconfig
setting → —Build Options 에서 Build static binary 체크
qemu로 리눅스 커널을 올릴때, 디폴트 라이브러리들이 없으므로, 모두 static으로 라이브러리를 빌드되게 하는 옵션이다. 따라서 qemu에서 실행시킬 바이너리들을 빌드할땐 gcc - static 옵션을 무조건 붙여야한다.
menuconfig 화면을 나오면, .config 파일에
CONFIG_STATIC=y
가 설정되어 있는것을 볼수있다.
- make busybos → 빌드
- mkdir _install
- make CONFIG_PREFIX = _install install
╭─wogh8732@ubuntu ~/Desktop/kernel_study/for_qemu/for_busybox/busybox-1.31.0 ╰─$ cd _install ╭─wogh8732@ubuntu ~/Desktop/kernel_study/for_qemu/for_busybox/busybox-1.31.0/_install ╰─$ ls bin linuxrc sbin usr
이렇게 나오면 정상적인 빌드가 완료된것이다. 이제 위 4개의 디렉토리를 묶어서 rootfs를 만들자아아아아 그전에 여기서 공부한 커널 모듈을 디버깅하기 위해 요 안에 아래의 코드를 빌드해서 넣어보자.
chardev.c
#include <linux/init.h> #include <linux/module.h> #include <linux/types.h> #include <linux/kernel.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/sched.h> #include <linux/device.h> #include <linux/slab.h> #include <asm/current.h> #include <linux/uaccess.h> MODULE_LICENSE("Dual BSD/GPL"); #define DRIVER_NAME "chardev" #define BUFFER_SIZE 256 static const unsigned int MINOR_BASE = 0; static const unsigned int MINOR_NUM = 2; static unsigned int chardev_major; static struct cdev chardev_cdev; static struct class *chardev_class = NULL; static int chardev_open(struct inode *, struct file *); static int chardev_release(struct inode *, struct file *); static ssize_t chardev_read(struct file *, char *, size_t, loff_t *); static ssize_t chardev_write(struct file *, const char *, size_t, loff_t *); struct file_operations chardev_fops = { .open = chardev_open, .release = chardev_release, .read = chardev_read, .write = chardev_write, }; struct data { unsigned char buffer[BUFFER_SIZE]; }; static int chardev_init(void) { int alloc_ret = 0; int cdev_err = 0; int minor; dev_t dev; printk("The chardev_init() function has been called."); alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DRIVER_NAME); if (alloc_ret != 0) { printk(KERN_ERR "alloc_chrdev_region = %d\n", alloc_ret); return -1; } //Get the major number value in dev. chardev_major = MAJOR(dev); dev = MKDEV(chardev_major, MINOR_BASE); //initialize a cdev structure cdev_init(&chardev_cdev, &chardev_fops); chardev_cdev.owner = THIS_MODULE; //add a char device to the system cdev_err = cdev_add(&chardev_cdev, dev, MINOR_NUM); if (cdev_err != 0) { printk(KERN_ERR "cdev_add = %d\n", alloc_ret); unregister_chrdev_region(dev, MINOR_NUM); return -1; } chardev_class = class_create(THIS_MODULE, "chardev"); if (IS_ERR(chardev_class)) { printk(KERN_ERR "class_create\n"); cdev_del(&chardev_cdev); unregister_chrdev_region(dev, MINOR_NUM); return -1; } for (minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) { device_create(chardev_class, NULL, MKDEV(chardev_major, minor), NULL, "chardev%d", minor); } return 0; } static void chardev_exit(void) { int minor; dev_t dev = MKDEV(chardev_major, MINOR_BASE); printk("The chardev_exit() function has been called."); for (minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) { device_destroy(chardev_class, MKDEV(chardev_major, minor)); } class_destroy(chardev_class); cdev_del(&chardev_cdev); unregister_chrdev_region(dev, MINOR_NUM); } static int chardev_open(struct inode *inode, struct file *file) { char *str = "helloworld"; int ret; struct data *p = kmalloc(sizeof(struct data), GFP_KERNEL); printk("The chardev_open() function has been called."); if (p == NULL) { printk(KERN_ERR "kmalloc - Null"); return -ENOMEM; } ret = strlcpy(p->buffer, str, sizeof(p->buffer)); if(ret > strlen(str)){ printk(KERN_ERR "strlcpy - too long (%d)",ret); } file->private_data = p; return 0; } static int chardev_release(struct inode *inode, struct file *file) { printk("The chardev_release() function has been called."); if (file->private_data) { kfree(file->private_data); file->private_data = NULL; } return 0; } static ssize_t chardev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { struct data *p = filp->private_data; printk("The chardev_write() function has been called."); printk("Before calling the copy_from_user() function : %p, %s",p->buffer,p->buffer); if (copy_from_user(p->buffer, buf, count) != 0) { return -EFAULT; } printk("After calling the copy_from_user() function : %p, %s",p->buffer,p->buffer); return count; } static ssize_t chardev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { struct data *p = filp->private_data; printk("The chardev_read() function has been called."); if(count > BUFFER_SIZE){ count = BUFFER_SIZE; } if (copy_to_user(buf, p->buffer, count) != 0) { return -EFAULT; } return count; } module_init(chardev_init); module_exit(chardev_exit);
makefile
1 obj-m += test_device.o 2 3 all: 4 make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules 5 clean: 6 make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
find . | cpio -H newc -o | gzip > rootfs.img.gz
cpio는 파일 시스템 압축을 위한 명령어라고 보면 된다. 위 명령어를 치면 rootfs.img.gz가 생성되고, 이게 바로 rootfs이다.
3. qemu 실행
위에서 만든 커널 이미지, rootfs 를 가지고 qemu 위에서 돌려보자
$ nano boot.sh // 경로는 알아서 잘 주길 console=ttyS0 oops=panic panic=1 quiet kaslr --------------- #!/bin/bash qemu-system-x86_64 \ -m 256M -kernel ./arch/x86/boot/bzImage \ -initrd ../for_busybox/busybox-1.31.0/_install/rootfs.img.gz \ -append "root=/dev/ram rdinit=/bin/sh kgdboc=ttyS0,115200 kgdbwait" \ -serial pty
- -m : 할당할 램 사이즈
- -kernel : 만든 커널 이미지
- -initrd : 초기 ram disk를 rootfs로 만들걸로 로드해서 부팅
- -append : rootfs 위치, 초기 부팅후 실행 스크립트(여기선 쉘),
- kgdbwait → 부팅후 gdb가 붙길 기다리게함
- (추가)
- -nographic : qemu 그래픽안뜸
- -s : remote로 붙어서 디버깅하기 위함. 포트는 1234임(디폴트인듯)
- -smp 4 : cpu 4개 할당
부팅 스크립트를 만들고 실행을 시키면 아래와 같이 qemu가 동작한다.
왼쪽이 qemu인데, 로그 메시지를 잘보면
waiting for connection from remote gdb...
라는 로그를 볼수 있다. 즉, 커널 부팅후 아까 설정한 쉘을 띄우기 전에 gdb가 붙기를 기다린다. 이제 Host에서 붙어보자. host에서는 boot.sh의 serial pty 설정으로 인해 시리얼로 붙을수 있게 된다. 정확한 시리얼은 host에서 로그를 보면
qemu-system-x86_64: -serial pty: char device redirected to /dev/pts/1 (label serial0)
즉 /dev/pts1/1로 붙으면 된다.
4. 디버깅
linux 폴더로 이동하면 vmlinux가 있다. 아까 커널을 빌드했을때 menuconfig 에서 Compile the kernel with debug info 옵션을 선택했기 때문에 디버깅 심볼들을 포함해서 커널 이미지가 생성된다. 즉 커널을 빌드할때 vmlinux 바이너리에서 Instruction set을 뽑아내어 bzImage(커널 이미지)가 만들어지는 것이다.
따라서 Host에서 guest 커널에 붙으려면
- gdb vmlinux
- target remote /dev/pts/1
guest에 붙었다. 테스트로 sys_sync함수에 bp를 걸어보자
- b sys_sync
- c
c를 하면 qemu가 다시 가동되면서 쉘이 실행된다. 이제 터미널에서 time을 입력하면, syscall인 sys_sync 함수에서 bp가 걸릴것이다. 즉 system call 디버깅 가능. 보통 user 단에서 어셈으로 보면 syscall 했을때 trap 걸리면서 커널영역으로 넘어간뒤, 커널에서 처리하는데 바로 밑에가 커널에서 처리하는 시스템콜 로직임.(sys_sync) 자세한건 여기서 확인하면 된다.
이렇게 커널디버깅을 하면된다. 앞으로 커널 익스를 공부할때 커널 모듈을 디버깅하기 위해 정리를 했다. 실제 CTF문제의 환경세팅이랑은 많이 다른거같은데, 앞으로 새로운 방법들은 추가적으로 정리할 예정이다. 커널 디버깅 방법도 많이 때문임.
5. 참고
'컴퓨터 관련 과목 > 운영체제 & 커널' 카테고리의 다른 글
[LInux Kernel] 메모리 관리 : 가상 메모리 (0) | 2021.01.24 |
---|---|
Linux kernel protection (0) | 2020.12.06 |
prepare_kernel_cred(), commit_creds() 함수란? (0) | 2020.11.22 |
ioctl 이란? (0) | 2020.11.18 |
linux Character Device Drivers 만들기 (4) | 2020.11.16 |
Uploaded by Notion2Tistory v1.1.0