Memory Errors
内存错误非常普遍,可能会导致严重的后果
Background
以冯诺依曼架构为基础的计算机系统中,代码和数据都储存在相同的内存空间,并可以通过统一的方式来寻址。
只有一个内存,充分利用这个内存是编译器、编程语言、runtime(运行时)的责任。
传统划分内存的方式是分为四类:Text, Stack, Heap, Global。
在Linux x86-64系统中,高地址存放Environment,然后是Stack,地址从高到低增长;低地址存放Text,Data,BSS(Block Started by Symbol),Heap,地址从低到高增长。
C |
---|
| #include <stdlib.h>
// this is in the data section
const char *HELLO = "hello";
// this is in the BSS section
long counter;
void main() {
// this is in the stack memory
int val;
// the msg pointer is in the stack memory
// the msg content is in the heap memory
char *msg = malloc(120);
// msg content is explicitly freed here
free(msg);
// the val and msg pointer is implicitly freed here
}
// the global memory is only destroyed on program exit
|
Exploit
Stack Layout(Linux x86-64 convention)
C |
---|
| long foo(long a, long b, long c, long d, long e, long f, long g, long h) {
long xx = a * b * c;
long yy = d + e + f;
long zz = g - h;
return xx + yy + zz;
}
|
这个函数的栈布局如下:
address |
content |
High address |
... |
rbp + 24 |
h |
rbp + 16 |
g |
rbp + 8 |
return address |
rbp |
saved rbp |
rbp - 8 |
xx |
rbp - 16 |
yy |
rbp - 24 |
zz |
Low address |
... |
参数a到f通过寄存器传递,参数g和h通过栈传递。
在C和C++中,heap(堆)是用于手动分配程序运行中内存的区域。
C |
---|
| typedef struct Response {
int status;
char message[40];
} response_t;
void process(){
response_t *res = malloc(sizeof(response_t));// allocate memory on the heap
struct Response res2;// allocate memory on the stack
}
|
Definition
当一个程序没有内存错误的时候,它是内存安全的。
对于一个对象,读取和写入这个对象使用的参数分别是:object_id, offset, length,写入还需要一个value。
而对于这个对象在内存中的存储,使用的参数分别是:object_id, size [int], alive [bool]。
内存违规出现在offset + length >= size
和offset < 0
的时候。
Example: spatial safety violation
C |
---|
| int foo(int x) {
int arr[16]={0};
return arr[x];//x is not checked
}
long foo(){
int a = 0;
return *(long*)(&a);//a is not long
}
int foo(int *p){
// if p == NULL, then dereference p will cause a segmentation fault
return *p + 42;
}
|
空指针解引用有时被认为是一种未定义行为,一般来说,空指针解引用会导致段错误。
在正常情况下,对象的object_id != 0
,所以空指针解引用发生在object_id == 0
的时候。
Example: temporal safety violation
C |
---|
| int foo(){
int *p = malloc(sizeof(int));
*p = 42;
free(p);
return *p;//p is freed
}
|
!alive
的情况下,对对象的读写操作会导致内存错误。
Object结构:(object_id, size [int], status [alloc|init|dead])(已分配但未初始化 init、完全可用 alloc、或已释放 dead)。
时间安全性违规可以在以下情况下发生:Read: status != init
、Write: status == dead
和Free: status == dead
。
C |
---|
| int foo(){
int p;
return p;//这个p是在栈上分配的,可能为残留值
}
int foo() {
int *p = malloc(sizeof(int));
return *p;//这个p是在堆上分配的,可能为任意值
}
|
Example: memory leak
内存泄漏出现在当存在一个object_id
出现了status != dead
的时候。
C |
---|
| int foo() {
int *p = malloc(sizeof(int));
int *q = malloc(sizeof(int));
*p = 42;
free(q);
return *p;//p is not freed
}
|
Case Study: Heartbleed
Heartbleed是一个OpenSSL的漏洞,它允许攻击者读取服务器内存中的数据。
C |
---|
| int dtls1_process_heartbeat(SSL *s) {
unsigned char *p = &s->s3->rrec.data[0], *pl;
unsigned short hbtype;
unsigned int payload;
unsigned int padding = 16; /* Use minimum padding */
/* Read type and payload length first */
hbtype = *p++;
n2s(p, payload);
pl = p;
if (s->msg_callback) s->msg_callback(0, s->version, TLS1_RT_HEARTBEAT, &s->s3->rrec.data[0], s->s3->rrec.length, s, s->msg_callback_arg);
if (hbtype == TLS1_HB_REQUEST) {
unsigned char *buffer, *bp;
int r;
/* Allocate memory for the response, size is 1 bytes
* message type, plus 2 bytes payload length, plus
* payload, plus padding
*/
buffer = OPENSSL_malloc(1 + 2 + payload + padding);
bp = buffer;
/* Enter response type, length and copy payload */
*bp++ = TLS1_HB_RESPONSE;
s2n(payload, bp);
memcpy(bp, pl, payload);
bp += payload;
/* Random padding */
RAND_pseudo_bytes(bp, padding);
r = dtls1_write_bytes(s, TLS1_RT_HEARTBEAT, buffer, 3 + payload + padding);
if (r >= 0 && s->msg_callback) s->msg_callback(1, s->version, TLS1_RT_HEARTBEAT, buffer, 3 + payload + padding, s, s->msg_callback_arg);
OPENSSL_free(buffer);
}
return 0;
}
|
Heartbleed漏洞的原因是在memcpy(bp, pl, payload);
这一行,payload
的值是由客户端传入的,如果payload
的值大于实际的pl
的长度,就会导致内存泄漏。在前面的输入检查中,没有检查payload
的值是否大于pl
的长度。
内存错误之所以这么多,某种意义上是因为我们对这类型的漏洞了解得很多。