Linux Kernel exploit :: Sendpage :: ~preview~

By puing | August 6, 2018

Linux Kernel Exploit Sendpage Preview



안녕하세요~ 지난 편까지는 CTF 문제로 커널을 공부했던 puing입니다!
하다보니 CTF 문제보다는 실제 커널에서 있었던 취약점을 공부해 보고 싶은 마음이 생겨서,
앞으로는 리눅스 커널에 존재했던 취약점들을 공부하면서 글을 남겨보려고 합니다.

제일 처음 다뤄볼 커널 취약점은 sendpage 취약점입니다.

지금으로부터 약 10년 전 쯤 알려진 취약점인데요.
하하 너무 오래전 취약점을 꺼내들었나…싶지만!
커널 초심자 입장에서 더티카우 먼저 하려니 너무 빡쎄기도 하고 해서ㅋㅋ
예전의 쉬운 취약점들부터 차근차근 공부 하고 나름대로 정리 해 보려고 합니다.
언젠가는 최신 커널 취약점도 금방금방 분석 할 수 있겠죠? :)

커널 취약점 공부가 처음이신 분들에게도 많은 도움이 되었으면 좋겠습니다.
시작 해 볼까요?




1. What is Sendpage??

linux sendpage, linux sendpage 취약점, 이런식으로 검색하면 나오는 일명 sendpage 의 취약점은 sock_sendpage 라는 함수에서 일어나는 취약점입니다.


sock_sendpage 함수의 선언와 정의는 아래와 같습니다.


sock_sendpage


static ssize_t sock_sendpage(struct file *file, struct page *page,int offset, size_t size, loff_t *ppos, int more);
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);
}


바로 이 함수에서 취약점이 존재합니다!! ㅎㅎ
(검색해보셨다면 바로 아시겠지만!) sendpage 취약점은 sock_sendpage 함수 안에서 null pointer dereference 가 일어나 결국엔 root 권한으로 상승할 수 있게 되는 취약점입니다.


그렇다면 null pointer dereference 가 뭘까요?




2. What is Null Pointer Dereference??


쉽게 말해 널(Null)을 가리키고 있는 포인터를 (읽거나, 쓰거나) 참조하는 행위입니다.
즉, 0을 가리키고있는 포인터에 임의의 값을 써준다거나, 읽기 시도를 하거나 호출을 하게되면 이게 바로 null pointer dereference 가 되는 것이죠.

실제로 sendpage 취약점은 널(Null)을 가리키고있는 함수 포인터를 호출하게되면서 일어난 취약점입니다.

예제 - null.c

#include <stdio.h>
#include <string.h>

int main()
{
    char *p;
    char *buf;

    p = 0;
    buf = "hello\n";

    strncpy(p,buf,10);

    printf("p : %s\n",p);

    return 0;
}


위의 간단한 코드를 보면, bufhello 문자열의 주소를 담고 있고 p 에는 0이 담겨있습니다.
그리고 strncpy 함수를 이용하여 buf 의 내용을 p 로 복사합니다.


RSI: 0x400654 --> 0x70000a6f6c6c6568 ('hello\n')
RDI: 0x0

...
...

0x400596 <main+32>:	mov    edx,0xa
0x40059b <main+37>:	mov    rsi,rcx
0x40059e <main+40>:	mov    rdi,rax
=> 0x4005a1 <main+43>:	call   0x400440 <strncpy@plt>


strncpy 함수 호출 직전에 break point 를 걸고 보면- buf 에는 0x400654 라는 주소가 들어가있고, 이 주소는 hello 문자열을 가리키고 있습니다. strncpy 함수를 호출하면 0x400654 에 있는 내용이 0 으로 복사되겠죠? 하지만 segmentation fault 가 일어납니다.



그 이유는 당연하죠?ㅎㅎ 주소 0은 유효한 주소가 아니기때문에 그냥 접근할 수 없기 때문입니다.





3. sock_sendpage function & vulnerability


다시 sock_sendpage 함수를 보면, null check 를 하지않고 바로 호출하는 것을 볼 수 있습니다.


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


그런데 0 을 참조하는게 무슨 문제가 될까요?


sendpage 에 널(NULL)이 들어가있다고 가정해봅시다.


구조체를 살펴보면, sendpage 는 함수포인터입니다.


struct socket

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


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



socket 구조체를 보면, 구조체 포인터 ops 가 있고, proto_ops 구조체에는 sendpage 라는 함수 포인터가 있습니다.
이 포인터는 함수 주소를 담고 있겠죠?

그런데 이 변수에 널(NULL) 이 들어가있는 상태에서 sendpage 를 호출하면 어떻게 될까요?


주소 0에 있는 instruction 들을 실행하게 됩니다.


linux 에서 null pointer dereference 가 위험할 수 있는 이유는, 주소 0에 원하는 값을 써줄 수 있을때, 프로그램 흐름을 바꿀 수 있기 때문입니다.


우리가 주소 0에 원하는 값을 써줄 수 있다면, 원하는 값을 주소 0에 써주고, 이후에 (sendpage 에 NULL 이 들어가 있을 때) sock_sendpage 함수가 호출되면 우리가 쓴 값이 그대로 실행될 것입니다.


주소 0에 원하는 값을 쓰는건 간단합니다. mmap 함수를 사용하여 주소 0을 반환받을 수 있습니다. 그리고 반환된 영역에 원하는 값을 쓰기만 하면 됩니다.



mmap 함수는 특정 파일을 주소에 매핑시키는데 사용되는 함수입니다.

간단한 소스로 mmap 함수를 알아봅시다.


mmap.c

#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>

int main(int argc,char **argv)
{
    int flag = PROT_WRITE | PROT_READ;
    void *ret;
    int fd;

    fd = open(argv[1], O_RDWR);
    ret = mmap(0,40, flag, MAP_SHARED, fd, 0);

    printf("ret : %p\n", ret);
    printf("ret : %s\n", ret);

    return 0;
}


main 함수에서 인자를 받고, 파일로 열어서 mmap 함수의 인자로 주었습니다. 이 파일은 주소로 매핑되어 메모리 어딘가에 써지겠죠.
위에서 사용한 mmap 함수 인자들의 내용은- fd 에서 0을 시작으로 40만큼 주소에 매핑시키는데, 매핑 주소 영역은 READ, WRITE 권한을 가지고있고 MAP_SHARED 옵션을 주어 다른 프로세스와 공유한다는 뜻입니다.


➜  tmp cat tmp
hello world
➜  tmp ./test4 tmp
ret : 0x7f24266e9000
ret : hello world


mmap 을 호출하면 매핑된 주소를 반환합니다.
실행해보면 매핑된 주소와 그 내용을 볼 수 있습니다.

그런데, 아래와 같이 MAP_ANONYMOUS 옵션을 사용하면, mmap 함수는 파일 매핑을 하는게 아니라 널로 초기화된 영역을 반환합니다.


#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>

int main(int argc,char **argv)
{
    int flag = PROT_WRITE | PROT_READ;
    void *ret;
    int fd;

//    fd = open(argv[1], O_RDWR);
//    ret = mmap(0,40, flag, MAP_SHARED, fd, 0);

    ret = mmap(0, 0x1000, flag, MAP_PRIVATE|MAP_ANONYMOUS |MAP_FIXED, -1, 0);

    printf("ret : %p\n", ret);
    printf("ret : %d\n", ret);

    return 0;
}


➜  tmp ./test4
ret : (nil)
ret : 0


NULL 을 반환하는것을 확인할 수 있습니다!!
우리는 mmap 으로 초기화 된 주소 0을 받환받았습니다.
이제 주소 0에 원하는 값을 쓰기만 하면 끝입니다 ㅎㅎ ^^


  • 잠깐!
    • 제가 테스트한 환경은 cent os 4.8, kernel 2.6.9-xx 로 낮은 버전의 커널을 사용하는 환경이었습니다.
    • 최신 버전의 커널에서 위 코드를 돌리면 Operation not permitted 으로 mmap 함수에서 -1 을 리턴하게됩니다.
      (mmap_min_addr 설정을 바꾼다던가..하지 않는이상) 주소 0번지를 잘 안주려고 하네요ㅠㅠ


지금까지는 간단한 sendpage preview 였습니다 ㅎㅎ
sendpage 취약점 정밀(?) 분석은 다음 편부터 시작하겠습니다 ㅎㅎ




4. 번외 - 최신 버전 sock_sendpage


최신 버전 sock_sendpage 를 보면 null check 를 하는걸 볼 수 있습니다. ㅎㅎ


sock_sendpage

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) ? MSG_DONTWAIT : 0;
	/* more is a combination of MSG_MORE and MSG_SENDPAGE_NOTLAST */
	flags |= more;

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


최신 커널에서는 sock_sendpage 함수에서 kernel_sendpage 를 호출하고 kernel_sendpage 에서 포인터가 널인지 아닌지를 체크하네요 ㅎㅎ


kernel_sendpage

int kernel_sendpage(struct socket *sock, struct page *page, int offset,
		    size_t size, int flags)
{
	if (sock->ops->sendpage)
		return sock->ops->sendpage(sock, page, offset, size, flags);

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


그럼 다음편에서 본격적으로 sendpage 취약점을 분석하며 알아가보도록 하겠습니다!
다음 편으로 빠르게 찾아오도록 할게요!

comments powered by Disqus