【软件与系统安全】四、内存破坏漏洞
这是《【软件与系统安全】笔记与期末复习》系列中的一篇
2022-03-21 第四次课后半部分
mem-vul-part.pdf 内存破坏漏洞
与安全相关的语言问题(C 与 Java 比较)
回顾:
漏洞、攻击与危害
漏洞(vulnerability):可以被对缺陷具有利用能力的攻击者访问并利用的缺陷, 要素:
- 缺陷 (flaw)
- 攻击者能访问缺陷
- 攻击者有利用缺陷的能力
攻击(attack): 指攻击者尝试利用漏洞,例如 主动、被动、DoS…
危害(compromise):攻击成功则危害发生
- 软件错误(Software error):会导致软件无法满足预期的编程错误
- 软件漏洞(Software vulnerability):可能导致攻击的软件错误
C 字符串的用法与陷阱
在 C/C++ 程序中, 用 C 语言字符串编程时, 容易出现的错误
- 缓冲区溢出(Buffer overflows)
- Null 终止错误(null-termination errors)
- 差一错误(off-by-one errors)
差一错误 是在计数时由于边界条件判断失误导致结果多了一或少了一的错误
gets:无边界的字符串复制
strcpy 和 strcat
缓冲区溢出:栈溢出(stack smashing)
“x86 Linux 系统的线性地址空间分层”1
“System V AMD64 ABI 调用惯例”2
缓冲区溢出可被利用于修改:
- 栈上的: 返回指令指针, 函数指针, 局部变量. . .
- 堆数据结构
覆写返回指令指针
代码注入
将 ret 设置为被注入代码的起始地址, 被注入代码可以做任何事情,如下载和安装蠕虫
注入 Shell Code
shellcode: 注入的 payload 代码会执行起来一个 shell
在该 shell 中, 攻击者可以执行任何命令
该 shell 与当前进程具有相同的特权级
通常具有 root 权限的进程会被攻击
如何使用被注入的代码调用“execve”?
-
将函数“execve”的地址注入栈上
-
“execve”是一个 libc 中的函数, 动态链接到进程地址空间中
-
为了使得我们的可执行程序能够调用 libc 中的函数,可执行程序必须能找到被调用函数的地址
- 如何做到? 当前程序通过一个 stub(PLT, Procedure Linkage Table )调用“execve”, PLT 在链接时检索 GOT (Global Offset Table)中的地址集合(请回忆动态链接过程)
“劫持全局偏移量表(GOT)中的函数指针, 被动态链接函数所使用”3
- 如何做到? 当前程序通过一个 stub(PLT, Procedure Linkage Table )调用“execve”, PLT 在链接时检索 GOT (Global Offset Table)中的地址集合(请回忆动态链接过程)
整数溢出
略
堆溢出
堆溢出: 在堆上开辟的缓冲区中的缓冲区溢出漏洞
溢出发生在堆缓冲区
溢出堆上的元数据
Heap allocators (又称内存管理器)
- 哪些内存区域已被开辟,它们的大小
- 哪些内存区域可以被开辟
Heap allocators 维护了元数据 (前块大小, 本块大小, previous 指针, next 指针)
- 堆元数据与堆数据是内联关系
- 内存管理函数(如 malloc()和 free())内部,会修改元数据
堆溢出攻击会篡改元数据,并等待内存管理函数把被篡改的元数据写入目标地址
Heap Allocator
- 维护一个已开辟的块的双向链表,以及一个空闲块的双向链表
- malloc()和 free()会修改双向链表
移除一个块
攻击 Heap Allocator
-
通过溢出 chunk2, 攻击者控制 chunk2 的 bk 和 fd 指针
- 实际上就控制了“向哪儿写数据”(where)以及 “写入什么数据”(what), 可以随意修改堆, 因此又称 “write-what-where” 漏洞
-
假设攻击者想将 value 写入内存地址 addr
- 攻击者将 chunk2->fd 设为 value
- 攻击者将 chunk2->bk 设为 addr - offset,其中 offset 为 fd 字段在 chuck 结构体内的偏移量
free()
:
- chunk2->bk->fd = chunk2->fd
- chunk2->fd->bk = chunk2->bk
变为
- “(addr - offset)->fd = value”, 等同于
(*addr)=value
- “value->bk = addr - offset” (应确保value->bk有可写权限)
第一个写操作实现了攻击者的目标, 实现了任意的内存写操作
因 write-what-where 操作由free()完成, 故需从free()向上寻找潜在溢出
当溢出与代码注入/代码重用结合时,
- “
*(addr)=value
”的addr是预先选择的函数指针、返回指令指针、影响控制流的变量地址等, value是恶意的跳转目标 - fd字段在chunk结构体中的偏移量固定
- 因此, 决定被 free 的 chunk2 的元数据 (chunk2->fd, chunk2->bk) 应该被溢出修改的取值, 是可计算的
Use After Free
定义: 程序在堆上释放内存, 然后引用该内存, 就好像该内存位置仍然合法:攻击者可以控制使用已释放的指针进行的数据写入
又称作悬挂指针使用,也是一个 write-what-where 漏洞
大多数有效的 use-after-free 攻击利用另一类型的数据
struct A {
void (*fnptr)(char *arg);
char *buf;
};
struct B {
int B1;
int B2;
char info[32];
};
// 释放A, 开辟B, 实际做了什么?
x = (struct A *)malloc(sizeof(struct A));
free(x);
y = (struct B *)malloc(sizeof(struct B));
// 如何利用此漏洞?
y->B1 = 0xDEADBEEF;
x->fnptr(x->buf);
Use-after-free 是类型混淆的一个实例
禁止 Use After Free
如何以较简单的方式禁止use-after-free?
设置所有被释放的堆空间为NULL
这时在使用被释放的堆空间时产生一个 null-pointer dereference
目前, 操作系统对 null-pointer deference 有内建的防护机制
复杂度: 需要设置所有别名指针(aliased pointers)为NULL
Double Free
略
类型混淆
略
格式化字符串攻击
能够导致很灵活的恶意利用
- 代码注入: 被注入代码直接放入字符串
- 各种代码重用
int i; printf (“i = %d with address %08x\n", i, &i);
将格式化字符串的地址指针, i
和 &i
分别通过寄存器 rdi, rsi 和 rdx 传递, 并调用 printf
当程序运行至printf内部, 会在这些寄存器中查找参数;如果参数超过6个,还会在栈上查找参数
格式指示符字母的含义: printf - C++ Reference (cplusplus.com)
格式化字符串中的“%n
”
能够将到“%n”位置为止已经由printf打印出的字节数写到一个我们选定的变量中
int i;
printf ("foobar%n\n", (int *) &i);
printf ("i = %d\n", i);
// i 的值最终为6
对于“format-string.c”程序, 如果用户输入“foobar%n”会怎样?
- 从rsi获得一个地址, 且向该地址上的内存单元中写入整数6
- 如果输入的是“foobar%10u%n”呢? 可能会向该内存位置写入16
- 如何向一个任意地址写? 将该地址放在正确的位置 (寄存器或栈单元, 以作为printf的参数)
因此, 攻击者可以用任意内容更新任意内存。可否覆写一个函数指针并劫持控制流(且安装某些蠕虫代码)?
int main(int argc, char *argv[]) {
char buf[512];
fgets(buf, sizeof(buf), stdin); // no buffer overflow here
printf("The input is:");
printf(buf); // format string attacks here
return 0
}
攻击者可以
- 查看/修改内存的任意部分
- 执行任意代码, 只需要把代码也放入buf
格式化字符串攻击的修复
通过提供一个特殊的格式化字符串, 可以规避“%400s
”的限制:
“%497d\x3c\xd3\xff\xbf<nops><shellcode>
”。创建一个497字符长的字符串,加上错误字符串(“ERR Wrong command: ”),超过了outbuf的长度4字节。虽然“user”字符串只允许 400字节,可以通过滥用格式化字符串参数扩展其长度。因为第二个sprintf
不检查长度, 它可以用来突破outbuf的长度界限。此时我们写入了一个返回地址 (0xbfffd33c
), 并可以以之前栈溢出的利用方式进行攻击
防止格式化字符串漏洞
即限制攻击者控制格式化字符串的能力
- 如果有可能,硬编码字符串;且不用包含“
%*
”的格式化字符串 - 如果必须要用格式化字符串,至少不要用“
printf(arg)
” - 不要使用
%n
- 小心其他的引用:
%s
和sprintf
能够被用于构造栈内容披露攻击 - 编译器支持printf参数与格式化字符串的匹配检查
- 最大 3G ?