Linux——入口代码模糊测试指南
小职 2020-10-13 来源 :https://blogs.oracle.com/linux/fuzzing-the-linux-k 阅读 1069 评论 0

摘要:本篇介绍调试寄存器以及进入内核的不同方法,希望对Linux的相关学习有所帮助。

本篇介绍调试寄存器以及进入内核的不同方法,希望对Linux的相关学习有所帮助。

Linux——入口代码模糊测试指南

堆栈段(%ss)

 

寄存器%ss应该是我们在进入内核的指令之前设置的最后一个寄存器,这样我们就可以确保看到任何延迟陷阱或异常的影响。我们可以使用与上面%ds相同的代码;我们不使用popw %ss的原因是,我们可能已经将%rsp设置为指向一个“奇怪”的位置,所以此时堆栈可能无法使用。

 

32位兼容模式(%cs)

 

有趣的是:你实际上可以在执行过程中把你的64位进程改变成32位进程,甚至不需要告诉内核。CPU包含了一种机制,在ring 3模式下是允许的:远跳转指令。

 

特别是,我们要使用的指令是“绝对间接远跳转指令,地址由m16:32给出”。由于要弄清楚具体的语法和字节可能有点麻烦,所以下面将借助于一个完整的汇编例子进行解释。

 

  .global main

main:

    ljmpl *target

  

1:

    .code32

    movl $1, %eax # __NR_exit == 1 from asm/unistd_32.h

    movl $2, %ebx # status == 0

    sysenter

    ret

  

    .data

target:

    .long 1b # address (32 bits)

    .word 0x23 # segment selector (16 bits)

这里,ljmpl指令使用target标签处的内存,该标签是一个32位指令指针,后跟一个16位段选择器(这里指向用户空间的32位代码段0x23)。这里的目标地址1b不是十六进制值,它实际上是对标签1的引用;b代表“向后”。这个标签处的代码是32位的,这就是为什么我们使用sysenter,而不是以前使用的syscall。调用约定也不同,实际上,我们需要使用32位ABI中的系统调用号(SYS_exit在64位系统上是60,但这里是1)。另一个有趣的事情是,如果你尝试在strace下运行这段代码,将会看到如下所示的结果:

 

[...]

write(1, "\366\242[\204\374\177\0\0\0\0\0\0\0\0\0\0\376\242[\204\374\177\0\0\t\243[\204\374\177\0\0"..., 140722529079224

+++ exited with 0 +++

strace显然认为我们仍然是一个64位进程,并认为我们调用了write(),而实际上我们是在调用exit()(最后一行就证明了这一点,它清楚地告诉我们进程退出了)。

 

由于ljmp的内存操作数和目标地址都是32位的,我们需要确保它们都位于高32位都为0的地址中,最好的方法是使用mmap()和MAP_32BIT标志来分配内存。

 

struct ljmp_target {

    uint32_t rip;

    uint16_t cs;

} __attribute__((packed));

  

struct data {

    struct ljmp_target ljmp;

};

  

static struct data *data;

  

int main(...)

{

    ...

  

    void *addr = mmap(NULL, PAGE_SIZE,

        PROT_READ | PROT_WRITE,

        MAP_PRIVATE | MAP_ANONYMOUS | MAP_32BIT,

        -1, 0);

    if (addr == MAP_FAILED)

        error(EXIT_FAILURE, errno, "mmap()");

  

    data = (struct data *) addr;

  

    ...

}

  

void emit_code()

{

    ...

  

    // ljmp *target

    *out++ = 0xff;

    *out++ = 0x2c;

    *out++ = 0x25;

    for (unsigned int i = 0; i < 4; ++i)

        *out++ = ((uint64_t) &data->ljmp) >> (8 * i);

  

    // cs:rip (jump target; in our case, the next instruction)

    data->ljmp.cs = 0x23;

    data->ljmp.rip = (uint64_t) out;

  

    ...

}

这里有几件事需要注意:

 

这将改变CPU模式,这意味着后续指令必须在32位中有效(否则,您可能会得到一般保护故障或无效操作码异常)。

 

上面我们用来加载段寄存器的指令序列(例如movw ..., %ax; movw %ax, %ss)在32位和64位上有完全相同的编码,所以我们可以在切换到32位代码段后毫不费力地执行它——这对于确保我们在进入内核之前仍然可以加载%ss特别有用。

 

我们可以选择是否始终更改为段4(段选择器0x23),或者尝试更改为随机段选择器(例如使用get_random_segment_selector())。如果我们选择一个随机的,我们甚至可能不知道我们是仍然在32位还是64位模式下执行。

 

我们可能希望在从内核返回后尝试跳回我们的正常代码段(段6,段选择器0x33),如果我们没有退出、崩溃或被杀死的话。对于不同的段选择器,该过程完全相同。

 

调试寄存器(%dr0等)

 

x86上的调试寄存器用于设置代码断点和数据观察点。寄存器%dr0到%dr3用于设置实际的断点/观察点地址,寄存器%dr7用于控制这四个地址的使用方式(是断点还是观察点等)。

 

设置调试寄存器比我们目前看到的要棘手一些,因为你不能直接在用户空间加载它们。就像修改LDT一样,内核要确保我们不会在内核地址上设置断点或观察点,但更重要的是,CPU本身不允许ring 3直接修改这些寄存器。我所知道的设置调试寄存器的唯一方法就是使用ptrace()。

 

ptrace()是一个非常难用的API。有很多隐含的状态需要跟踪器手动跟踪,还有很多围绕信号处理的边缘情况。幸运的是,在这种情况下,我们只需要附加到子进程,设置调试寄存器,然后脱离即可;即使在我们停止跟踪之后,调试寄存器的变化也会持续存在。

 

#include

#include

  

#include

#include

  

int main(...)

{

    pid_t child = fork();

    if (child == -1)

        error(EXIT_FAILURE, errno, "fork()");

  

    if (child == 0) {

        // make us a tracee of the parent

        if (ptrace(PTRACE_TRACEME, 0, 0, 0) == -1)

            error(EXIT_FAILURE, errno, "ptrace(PTRACE_TRACEME)");

  

        // give the parent control

        raise(SIGTRAP);

  

        ...

  

        exit(EXIT_SUCCESS);

    }

  

    // parent; wait for child to stop

    while (1) {

        int status;

        if (waitpid(child, &status, 0) == -1) {

            if (errno == EINTR)

                continue;

  

            error(EXIT_FAILURE, errno, "waitpid()");

        }

  

        if (WIFEXITED(status))

            exit(WEXITSTATUS(status));

        if (WIFSIGNALED(status))

            exit(EXIT_FAILURE);

  

        if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP)

            break;

  

        continue;

    }

  

    // set debug registers and stop tracing

    if (ptrace(PTRACE_POKEUSER, child, offsetof(struct user, u_debugreg[0]), ...) == -1)

        error(EXIT_FAILURE, errno, "ptrace(PTRACE_POKEUSER)");

    if (ptrace(PTRACE_POKEUSER, child, offsetof(struct user, u_debugreg[7]), ...) == -1)

        error(EXIT_FAILURE, errno, "ptrace(PTRACE_POKEUSER)");

    if (ptrace(PTRACE_DETACH, child, 0, 0) == -1)

        error(EXIT_FAILURE, errno, "ptrace(PTRACE_DETACH)");

  

    ...

}

即使在这个小例子中,等待子程序停止也是有点麻烦的。waitpid()总是有可能在子程序到达raise(SIGTRAP)之前返回,例如,如果它被某个外部进程杀死了。我们对这些情况的处理方式也是简单的退出。

 

由于设置调试寄存器需要跟踪,处理信号被进行多次上下文切换(这些都很慢),我建议对每个子进程只做一次,然后让子进程连续多次尝试进入内核。

 

设置任何一个调试寄存器都可能失败,所以在实际的fuzzer中,我们可能希望忽略所有错误,每次将%dr7设置为一个断点,例如:

 

// stddef.h offsetof() doesn't always allow non-const array indices,

// so precompute them here.

const unsigned int debugreg_offsets[] = {

    offsetof(struct user, u_debugreg[0]),

    offsetof(struct user, u_debugreg[1]),

    offsetof(struct user, u_debugreg[2]),

    offsetof(struct user, u_debugreg[3]),

};

  

for (unsigned int i = 0; i < 4; ++i) {

    // try random addresses until we succeed

    while (true) {

        unsigned long addr = get_random_address();

        if (ptrace(PTRACE_POKEUSER, child, debugreg_offsets[i], addr) != -1)

            break;

    }

  

    // Condition:

    // 0 - execution

    // 1 - write

    // 2 - (unused)

    // 3 - read or write

    unsigned int condition = std::uniform_int_distribution

    if (condition == 2)

        condition = 3;

  

    // Size

    // 0 - 1 byte

    // 1 - 2 bytes

    // 2 - 8 bytes

    // 3 - 4 bytes

    unsigned int size = std::uniform_int_distribution

  

    unsigned long dr7 = ptrace(PTRACE_PEEKUSER, child, offsetof(struct user, u_debugreg[7]), 0);

    dr7 &= ~((1 | (3 << 16) | (3 << 18)) << i);

    dr7 |= (1 | (condition << 16) | (size << 18)) << i;

    ptrace(PTRACE_POKEUSER, child, offsetof(struct user, u_debugreg[7]), dr7);

}

进入内核

 

在本系列的第一篇文章中,我们已经看到了如何进行系统调用的代码;在这里,我们使用相同的基本方法,但也考虑到所有其他进入内核的方式。正如我前面提到的,syscall指令不是进入64位内核的唯一方法,甚至不是进行系统调用的唯一方法。对于系统调用,我们有以下选项:

 

int $0x80

sysenter

syscall

实际上,查看硬件生成的异常表也很有用。其中许多异常的处理方式与系统调用和常规中断略有不同;例如,当您试图加载一个带有无效段选择器的段寄存器时,CPU会将一个错误代码压入(内核)堆栈上。

 

我们可以触发许多异常,但不是所有的异常。例如,通过简单地执行除零来生成除零异常是非常简单的,但是我们不能轻松地按需生成NMI。(也就是说,我们可以做一些事情来使NMI更有可能发生,尽管是以一种不可控制的方式:如果我们在VM中测试内核,我们可以从主机注入NMI,或者我们可以启用内核NMI watchdog功能。)

 

enum entry_type {

    // system calls + software interrupts

    ENTRY_SYSCALL,

    ENTRY_SYSENTER,

    ENTRY_INT,

    ENTRY_INT_80,

    ENTRY_INT3,

  

    // exceptions

    ENTRY_DE, // Divide error

    ENTRY_OF, // Overflow

    ENTRY_BR, // Bound range exceeded

    ENTRY_UD, // Undefined opcode

    ENTRY_SS, // Stack segment fault

    ENTRY_GP, // General protection fault

    ENTRY_PF, // Page fault

    ENTRY_MF, // x87 floating-point exception

    ENTRY_AC, // Alignment check

  

    NR_ENTRY_TYPES,

};

  

enum entry_type type = (enum entry_type) std::uniform_int_distribution

  

// Some entry types require a setup/preamble; do that here

switch (type) {

case ENTRY_DE:

    // xor %eax, %eax

    *out++ = 0x31;

    *out++ = 0xc0;

    break;

case ENTRY_MF:

    // pxor %xmm0, %xmm0

    *out++ = 0x66;

    *out++ = 0x0f;

    *out++ = 0xef;

    *out++ = 0xc0;

    break;

case ENTRY_BR:

    // xor %eax, %eax

    *out++ = 0x31;

    *out++ = 0xc0;

    break;

case ENTRY_SS:

    {

        uint16_t sel = get_random_segment_selector();

  

        // movw $imm, %bx

        *out++ = 0x66;

        *out++ = 0xbb;

        *out++ = sel;

        *out++ = sel >> 8;

    }

    break;

default:

    // do nothing

    break;

}

  

...

  

switch (type) {

    // system calls + software interrupts

  

case ENTRY_SYSCALL:

    // syscall

    *out++ = 0x0f;

    *out++ = 0x05;

    break;

case ENTRY_SYSENTER:

    // sysenter

    *out++ = 0x0f;

    *out++ = 0x34;

    break;

case ENTRY_INT:

    // int $x

    *out++ = 0xcd;

    *out++ = std::uniform_int_distribution

    break;

case ENTRY_INT_80:

    // int $0x80

    *out++ = 0xcd;

    *out++ = 0x80;

    break;

case ENTRY_INT3:

    // int3

    *out++ = 0xcc;

    break;

  

    // exceptions

  

case ENTRY_DE:

    // div %eax

    *out++ = 0xf7;

    *out++ = 0xf0;

    break;

case ENTRY_OF:

    // into (32-bit only!)

    *out++ = 0xce;

    break;

case ENTRY_BR:

    // bound %eax, data

    *out++ = 0x62;

    *out++ = 0x05;

    *out++ = 0x09;

    for (unsigned int i = 0; i < 4; ++i)

        *out++ = ((uint64_t) &data->bound) >> (8 * i);

    break;

case ENTRY_UD:

    // ud2

    *out++ = 0x0f;

    *out++ = 0x0b;

    break;

case ENTRY_SS:

    // Load %ss again, with a random segment selector (this is not

    // guaranteed to raise #SS, but most likely it will). The reason

    // we don't just rely on the load above to do it is that it could

    // be interesting to trigger #SS with a "weird" %ss too.

  

    // movw %bx, %ss

    *out++ = 0x8e;

    *out++ = 0xd3;

    break;

case ENTRY_GP:

    // wrmsr

    *out++ = 0x0f;

    *out++ = 0x30;

    break;

case ENTRY_PF:

    // testl %eax, (xxxxxxxx)

    *out++ = 0x85;

    *out++ = 0x04;

    *out++ = 0x25;

    for (int i = 0; i < 4; ++i)

        *out++ = ((uint64_t) page_not_present) >> (8 * i);

    break;

case ENTRY_MF:

    // divss %xmm0, %xmm0

    *out++ = 0xf3;

    *out++ = 0x0f;

    *out++ = 0x5e;

    *out++ = 0xc0;

    break;

case ENTRY_AC:

    // testl %eax, (page_not_writable + 1)

    *out++ = 0x85;

    *out++ = 0x04;

    *out++ = 0x25;

    for (int i = 0; i < 4; ++i)

        *out++ = ((uint64_t) page_not_writable + 1) >> (8 * i);

    break;

}

小结

 

我们现在几乎拥有了所有的东西,我们需要真正开始进行模糊测试了!不过还有几件事要做……

 

如果你运行目前的代码,很快就会遇到一些问题。首先,我们使用的许多指令可能会导致崩溃(而且是故意的),这使得fuzzer速度很慢。通过为一些常见的终止信号(SIGBUS、SIGSEGV等)安装信号处理程序,我们可以跳过故障指令,(希望)在同一个子进程内继续执行。

 

其次,我们进行的一些系统调用可能会产生意想不到的副作用。特别是,我们并不希望在I/O上进行阻塞,因为这将使fuzzer停止运行。一种解决方法是安装一个间隔定时器报警,以检测子进程何时挂起。另一种方法可以是过滤掉某些已知会阻塞的系统调用(如read()、select()、sleep()等)。其他“不幸”的系统调用可能是fork()、exit()和kill()。fuzzer删除文件或以其他方式扰乱系统的可能性较小,但我们仍需要使用某种形式的沙盒(如setuid(65534))。


本文由 @小职 发布于职坐标。未经许可,禁止转载。
喜欢 | 0 不喜欢 | 0
看完这篇文章有何感觉?已经有0人表态,0%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程