블로그 이전했습니다. https://jeongzero.oopy.io/
리눅스 커널 디버깅하기
본문 바로가기
컴퓨터 관련 과목/운영체제 & 커널

리눅스 커널 디버깅하기

728x90

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 만들기


이제 파일시스템을 만들어야한다. 파일시스템에 관련해서는 아래의 내용을 참조하면 된다.

kernel of linux - File system of Linux(1)
여태까지 Linux FIie System을 공부하기 위해 먼저 Unix의 File System에 대해서 학습을 했다. 유닉스가 기반이 되야지 후에 설명될 Linux가 더 쉽게 이해가 된다. 이제 Linux의 파일 시스템에 대해서 학습을 해보자. 잠깐 유닉스 파일시스템 복습을 해보자. 파일 시스템은 크게 4가지로 구성된다.
https://wogh8732.tistory.com/298?category=807175

빌드한 커널을 이용하여 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. 참고


728x90