Skip to content

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
1
2
3
4
5
6
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
1
2
3
4
5
6
7
8
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 >= sizeoffset < 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
1
2
3
4
5
6
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 != initWrite: status == deadFree: status == dead

C
1
2
3
4
5
6
7
8
9
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
1
2
3
4
5
6
7
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的长度。

内存错误之所以这么多,某种意义上是因为我们对这类型的漏洞了解得很多。