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
취약점을 세 편에 걸쳐서 알아봤는데요!
커널 공부하시는 분들께 많이 도움이 되었을까 모르겠습니다.
부디 도움이 되었길 바라면서…!!
조만간 다른 취약점으로 찾아뵙겠습니다!
그 때 까지 모두 즐거운(?) 커널탐구생활 보내세요!