Linux Kernel exploit :: Sendpage :: ~analysis 下~

By puing | August 27, 2018

Linux Kernel Exploit Sendpage Analysis 下


안녕하세요~ 줄곧 CTF 문제로 커널을 공부했던 puing입니다!
저번편에서는 sendpage의 exploit code 내용을 살펴봤었죠!

혹시 저번 편을 안 보고 오신 분들이라면 Sendpage 프리뷰 1편과 분석기 2편을 참고 해주세요!

Linux Kernel Exploit :: Sendpage :: ~Preview~
Linux Kernel exploit :: Sendpage :: ~analysis 上~




sendpage 취약점은, null pointer dereference 로 인해 sock_sendpage 함수에서 null 을 가리키고 있는 포인터 변수를 호출하면서 주소 0x0 번지를 호출하게 되면서 발생하는 취약점이죠.

exploit code에서 mmap 함수로 초기화된 영역(주소 0x0 번지)을 반환받아서 이 곳에 change_cred 함수로 jump 하는 opcode 를 넣어준 후, change_cred 함수에서 권한 관련 변수들을 0으로 채워넣어 권한 상승을 하는 것을 살펴보았습니다. 그리고 socket(), mkstemp(), unlink(), ftruncate(), sendfile() 함수들을 차례대로 호출한 후, execl 함수로 /bin/sh 을 실행시켜 root 권한의 쉘을 딸 수 있게 되는 것 까지 알아보았고요!


저번 편에서 주소 0x0 번지로 점프한 후 실행되는 exploit 내용들을 살펴봤다면, 이번 편에서는 어쩌다 null pointer 를 호출하게됐는지? 를 살펴보겠습니다.

socket 함수에서는 인자로 들어간 PF_BLUETOOTH 옵션을 기억해주면 되고, sendfile 함수는 내부적으로 sendpage 함수를 호출하게하는 함수라는 것만 기억해두시면 됩니다.

시작 해 볼까요?




어쩌다가 Null Pointer를 호출하게 되었나

sendpage preview 에서 봤듯이!


취약점이 존재하는 sock_sendpage 함수에서 sendpage 함수를 호출하게되는데, socket 구조체에 있는 (proto_ops 구조체를 가리키는) ops 를 통해 sendpage 함수를 호출하게 됩니다.


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 = 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);
}


(복습하는 차원에서^^) socket 구조체와 proto_ops 구조체를 보면, socket 구조체에 proto_ops 구조체를 가리키는 ops 포인터 변수가 있습니다.


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;
};


이 ops 는 proto_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, size_t total_len);
    int     (*recvmsg)   (struct kiocb *iocb, struct socket *sock,
                      struct msghdr *m, size_t 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);
};


sendpage 함수포인터가 보이네요 :)


즉, socket 구조체에서 proto_ops 구조체를 통해 sendpage 를 호출하게되는거죠!


먼저, socket 함수 원형은 아래와 같습니다.


socket(int domain, int type, int protocol)


제가 지금껏 socket programming 을 할 일이 있었을 땐, 보통 socket(AF_INET,SOCK_STREAM,0) 이 형태를 자주 사용했었습니다. 해당 옵션을 찾아보면 알겠지만, IPv4,TCP/IP 프로토콜을 사용한다는 의미입니다.


위와 같은 형태로 socket 을 생성하면 inet_create 라는 함수가 불립니다.


inet_create(...)
{
    ...
    ...

        err = -EPROTONOSUPPORT;
        if (!protocol)
                goto out_rcu_unlock;

        sock->ops = answer->ops;
        answer_prot = answer->prot;
        answer_no_check = answer->no_check;
        answer_flags = answer->flags;
        rcu_read_unlock();
    
    ...
    ...
    
}


inet_create 함수에서 sock->ops 에 값을 넣어주는 부분이 있죠? 여기에 어떤 값을 넣어주는지 디버깅을 통해 알아보겠습니다!!




디버깅 준비하기

여기서 디버깅을 하기위해 몇가지 설정해줘야했었는데요,

먼저! VM 설정파일(XXX.vmx)을 열어서 아래 문장들을 추가합니다.


debugStub.listen.guest32 = "TRUE"
debugStub.hideBreakpoints = "FALSE"
debugStub.listen.guest32.remote = "TRUE"
monitor.debugOnStartGuest32 = "TRUE"


제가 사용하고 있는 cent os 환경은 32bit 라서 위와 같이 설정해주시면 되지만, 64bit 환경은 조금 다르답니다!


설정후, 해당 VM 을 실행시키면 아래와같이 검은 화면인체로 멈춰있게됩니다.

그리고 다른 VM 에서 gdb 를 실행시키고 아래 명령어를 입력하면 끝입니다!

gdb$ set architecture i386
gdb$ target remote 192.168.1.106:8832


continue 하게되면, 검은 화면이었던 VM 은 정상적으로 부팅이되고 gdb 를 실행시킨 VM 에서 디버깅을 할 수 있게됩니다.




디버깅 해 봅시다!

그럼 다시 본론으로 돌아와서!
inet_create 함수에서 sock->ops 에 어떤 값을 써주는지 디버깅을 통해서 봅시다.



  • /proc/kallsyms 는 커널 심볼이 들어있는 파일입니다.
    • 저같은 경우, 커널 심볼 위치(주소)를 보거나, 특정 주소에 있는 심볼이 무엇인지 알아보고자 할 때 자주 씁니다 ^^



sock->ops 에 값을 넣어주는 부분을 찾아서 어떤 값을 넣어주는지 보면..




proto_ops 구조체에 있는 함수 포인터들에 각각의 함수들을 넣어줍니다. sendpage 함수 포인터에는 tcp_sendpage 함수 주소가 들어가네요.


이대로 sendfile 함수가 호출되어 sock_sendpage 함수가 호출되면 어떻게 될까요?

tcp_sendpage 함수가 호출됩니다. ^^



그런데 sendpage 취약점은 null pointer dereference 로 null 을 가리키고있는 포인터 변수 sendpage 를 그대로 호출하게되면서 생기는 취약점이라고 했었죠? 하지만 포인터 변수 sendpage 에는 tcp_sendpage 함수 주소가 들어가있고, tcp_sendpage 함수가 호출되면서 주소 0x0 으로 가지 않습니다 ㅠㅠ


실제로 우리가 저번 편에서 사용하였던 exploit code 에서 socket 함수를 사용할 때, socket(AF_INET,SOCK_STREAM,0) 으로 사용하게되면 제대로 익스되지가 않습니다 ㅎㅎ



사실, socket 함수를 사용할 때, 어떤 옵션을 주느냐에 따라 불리는 함수, 그리고 proto_ops 에 초기화되는 값들이 조금씩 다릅니다.


AF_INET,SOCK_STREAM 옵션을 주고 socket 함수를 사용했을 땐 inet_create 함수가 호출되고 proto_ops 구조체에는 inet_release,inet_bind,inet_stream.. 함수들 주소로 채워졌었죠? sendpage 포인터 변수에는 tcp_sendpage 함수 주소가 들어갔었고요.

원래의 exploit code 내용대로 PF_BLUETOOTH,SOCK_DGRAM 옵션을 주소 socket 함수를 사용해봅시다.


socket(PF_BLUETOOTH,SOCK_DGRAM,0)


위와 같이 사용했을 경우! inet_create 함수는 호출되지않고, 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 에 값을 넣어주는 곳이 있습니다. 여기에 어떤 값이 들어가는지 봅시다.




proto_ops 구조체의 맨 마지막 sendpage 변수에 들어간 함수 주소가 없네요? sendpage 변수에는 0이 들어가 있습니다. 특정 옵션을 줬을 때, 제대로 초기화를 하지 않고, 또 함수 호출전에 null check 를 하는 루틴이 없었기때문에 sock_sendpage 함수에서 그대로 0x0 을 호출하게됩니다 ㅠㅠ



0x0 을 호출하게되면, 그 이후는 저번편에서 봤던 exploit code 내용대로 흘러가게됩니다.(특정 구조체 위치를 알아내서 권한 관련 변수들을 0으로 바꿔서 권한 상승을하고 쉘을 획득했었죠 ^^)


이렇게 함수 포인터를 제대로 초기화하지 않고 호출하여 취약점이 생기게됐네요 ㅠㅠ


이후 버전 커널 소스를 보면, l2cap_sock_create 함수에서 sock->ops 에 값을 넣어줄때 여전히 sendpage 함수포인터엔 아무 값도 들어가있지 않아요 ㅠㅠ



하지만 sock_sendpage 함수에서 null check 하는 부분이 추가 되어 sendpage 취약점은 패치가 되었습니다 ㅎㅎ



요샌 주소 0x0 를 할당받기 어려워졌지만 혹시 모르죠.. 언젠가 sendpage 처럼 null 을 호출해주는 null pointer dereference 취약점이 또 발견될지…^^




이렇게 SendPage 취약점을 세 편에 걸쳐서 알아봤는데요!
커널 공부하시는 분들께 많이 도움이 되었을까 모르겠습니다.
부디 도움이 되었길 바라면서…!!

조만간 다른 취약점으로 찾아뵙겠습니다!

그 때 까지 모두 즐거운(?) 커널탐구생활 보내세요!

comments powered by Disqus