堆内存管理

在堆溢出章节我们的glibc版本为2.23,这是ubuntu 16.04的默认版本,也是pwn题最常用的。

堆是用于存放除了栈里的东西之外所有其他东西的内存区域,有动态内存分配器负责维护。分配器将堆视为一组不同大小的块(block)的集合来维护,每个块就是一个连续的虚拟内存器片(chunk)。当使用 malloc()free() 时就是在操作堆中的内存。对于堆来说,释放工作由程序员控制,容易产生内存泄露。

堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

如果每次申请内存时都直接使用系统调用,会严重影响程序的性能。通常情况下,运行库先向操作系统“批发”一块较大的堆空间,然后“零售”给程序使用。当全部“售完”之后或者剩余空间不能满足程序的需求时,再根据情况向操作系统“进货”。

需要注意的是,在内存分配与使用的过程中,Linux 有这样的一个基本内存管理思想,只有当真正访问一个地址的时候,系统才会建立虚拟页面与物理页面的映射关系。 所以虽然操作系统已经给程序分配了很大的一块内存,但是这块内存其实只是虚拟内存。只有当用户使用到相应的内存时,系统才会真正分配物理页面给用户使用。

malloc

首先看源码说明

/*
  malloc(size_t n)
  Returns a pointer to a newly allocated chunk of at least n bytes, or null
  if no space is available. Additionally, on failure, errno is
  set to ENOMEM on ANSI C systems.
  If n is zero, malloc returns a minumum-sized chunk. (The minimum
  size is 16 bytes on most 32bit systems, and 24 or 32 bytes on 64bit
  systems.)  On most systems, size_t is an unsigned type, so calls
  with negative arguments are interpreted as requests for huge amounts
  of space, which will often fail. The maximum supported value of n
  differs across systems, but is in all cases less than the maximum
  representable value of a size_t.
*/

总结一下就是malloc返回对应大小字节的指针,当n==0时,返回当前系统允许的最小内存块。当n为负数时,系统并不会认为是负数,所以就会申请很大的内存块。但通常来说系统并没有这么多的空闲内存,所以绝大多数情况会失败。

free

看一下源码

/*
      free(void* p)
      Releases the chunk of memory pointed to by p, that had been previously
      allocated using malloc or a related routine such as realloc.
      It has no effect if p is null. It can have arbitrary (i.e., bad!)
      effects if p has already been freed.
      Unless disabled (using mallopt), freeing very large spaces will
      when possible, automatically trigger operations that give
      back unused memory to the system, thus reducing program footprint.
    */

free会释放由指针p所指向的内存块,同样该函数也会有一些异常处理。当p指针为空时,函数不执行任何操作,当已经被释放过一次后,再次释放就会有一些玄学的现象,这就是double free。除了被禁用 (mallopt) 的情况下,当释放很大的内存空间时,程序会将这些内存空间还给系统,以便于减小程序所使用的内存空间。

进程堆管理

Linux 提供了两种堆空间分配的方式,一个是 brk() 系统调用,另一个是 mmap() 系统调用。可以使用 man brkman mmap 查看。

brk() 的声明如下:

#include <unistd.h>

int brk(void *addr);

void *sbrk(intptr_t increment);

参数 *addr 是进程数据段的结束地址,brk() 通过改变该地址来改变数据段的大小,当结束地址向高地址移动,进程内存空间增大,当结束地址向低地址移动,进程内存空间减小。brk()调用成功时返回 0,失败时返回 -1。 sbrk()brk() 类似,但是参数 increment 表示增量,即增加或减少的空间大小,调用成功时返回增加后减小前数据段的结束地址,失败时返回 -1。

在上图中我们看到 brk 指示堆结束地址,start_brk 指示堆开始地址。BSS segment 和 heap 之间有一段 Random brk offset,这是由于 ASLR 的作用,如果关闭了 ASLR,则 Random brk offset 为 0,堆结束地址和数据段开始地址重合。

例子:源码

#include <stdio.h>
#include <unistd.h>
void main() {
        void *curr_brk, *tmp_brk, *pre_brk;

        printf("当前进程 PID:%d\n", getpid());

        tmp_brk = curr_brk = sbrk(0);
        printf("初始化后的结束地址:%p\n", curr_brk);
        getchar();

        brk(curr_brk+4096);
        curr_brk = sbrk(0);
        printf("brk 之后的结束地址:%p\n", curr_brk);
        getchar();

        pre_brk = sbrk(4096);
        curr_brk = sbrk(0);
        printf("sbrk 返回值(即之前的结束地址):%p\n", pre_brk);
        printf("sbrk 之后的结束地址:%p\n", curr_brk);
        getchar();

        brk(tmp_brk);
        curr_brk = sbrk(0);
        printf("恢复到初始化时的结束地址:%p\n", curr_brk);
        getchar();
}

开启两个终端,一个用于执行程序,另一个用于观察内存地址。首先我们看关闭了 ASLR 的情况。第一步初始化:

# echo 0 > /proc/sys/kernel/randomize_va_space
$ ./a.out
当前进程 PID27759
初始化后的结束地址:0x56579000
# cat /proc/27759/maps
...
56557000-56558000 rw-p 00001000 08:01 28587506                           /home/a.out
56558000-56579000 rw-p 00000000 00:00 0                                  [heap]
...

数据段结束地址和堆开始地址同为 0x56558000,堆结束地址为 0x56579000

第二步使用 brk() 增加堆空间:

$ ./a.out
当前进程 PID27759
初始化后的结束地址:0x56579000

brk 之后的结束地址:0x5657a000
# cat /proc/27759/maps
...
56557000-56558000 rw-p 00001000 08:01 28587506                           /home/a.out
56558000-5657a000 rw-p 00000000 00:00 0                                  [heap]
...

堆开始地址不变,结束地址增加为 0x5657a000

第三步使用 sbrk() 增加堆空间:

$ ./a.out
当前进程 PID27759
初始化后的结束地址:0x56579000

brk 之后的结束地址:0x5657a000

sbrk 返回值(即之前的结束地址):0x5657a000
sbrk 之后的结束地址:0x5657b000
# cat /proc/27759/maps
...
56557000-56558000 rw-p 00001000 08:01 28587506                           /home/a.out
56558000-5657b000 rw-p 00000000 00:00 0                                  [heap]
...

第四步减小堆空间:

$ ./a.out
当前进程 PID27759
初始化后的结束地址:0x56579000

brk 之后的结束地址:0x5657a000

sbrk 返回值(即之前的结束地址):0x5657a000
sbrk 之后的结束地址:0x5657b000

恢复到初始化时的结束地址:0x56579000
# cat /proc/27759/maps
...
56557000-56558000 rw-p 00001000 08:01 28587506                           /home/a.out
56558000-56579000 rw-p 00000000 00:00 0                                  [heap]
...

再来看一下开启了 ASLR 的情况:

# echo 2 > /proc/sys/kernel/randomize_va_space
$ ./a.out
当前进程 PID28025
初始化后的结束地址:0x578ad000
# cat /proc/28025/maps
...
5663f000-56640000 rw-p 00001000 08:01 28587506                           /home/a.out
5788c000-578ad000 rw-p 00000000 00:00 0                                  [heap]
...

可以看到这时数据段的结束地址 0x56640000 不等于堆的开始地址 0x5788c000

mmap() 的声明如下:

#include <sys/mman.h>

void *mmap(void *addr, size_t len, int prot, int flags,
    int fildes, off_t off);

mmap() 函数用于创建新的虚拟内存区域,并将对象映射到这些区域中,当它不将地址空间映射到某个文件时,我们称这块空间为匿名(Anonymous)空间,匿名空间可以用来作为堆空间。mmap() 函数要求内核创建一个从地址 addr 开始的新虚拟内存区域,并将文件描述符 fildes 指定的对象的一个连续的片(chunk)映射到这个新区域。连续的对象片大小为 len 字节,从距文件开始处偏移量为 off 字节的地方开始。prot 描述虚拟内存区域的访问权限位,flags 描述被映射对象类型的位组成。

munmap() 则用于删除虚拟内存区域:

#include <sys/mman.h>

int munmap(void *addr, size_t len);

例子:源码

#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
void main() {
    void *curr_brk;

    printf("当前进程 PID:%d\n", getpid());
    printf("初始化后\n");
    getchar();

    char *addr;
    addr = mmap(NULL, (size_t)4096, PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
    printf("mmap 完成\n");
    getchar();

    munmap(addr, (size_t)4096);
    printf("munmap 完成\n");
    getchar();
}

第一步初始化:

$ ./a.out
当前进程 PID28652
初始化后
# cat /proc/28652/maps
...
f76b2000-f76b5000 rw-p 00000000 00:00 0
f76ef000-f76f1000 rw-p 00000000 00:00 0
...

第二步 mmap:

]$ ./a.out
当前进程 PID28652
初始化后
mmap 完成
# cat /proc/28652/maps
...
f76b2000-f76b5000 rw-p 00000000 00:00 0
f76ee000-f76f1000 rw-p 00000000 00:00 0
...

第三步 munmap:

$ ./a.out
当前进程 PID28652
初始化后
mmap 完成
munmap 完成
# cat /proc/28652/maps
...
f76b2000-f76b5000 rw-p 00000000 00:00 0
f76ef000-f76f1000 rw-p 00000000 00:00 0
...

可以看到第二行第一列地址从 f76ef000->f76ee000->f76ef000 变化。0xf76ee000-0xf76ef000=0x1000=4096

通常情况下,我们不会直接使用 brk()mmap() 来分配堆空间,C 标准库提供了一个叫做 malloc 的分配器,程序通过调用 malloc() 函数来从堆中分配块,声明如下:

#include <stdlib.h>

void *malloc(size_t size);
void free(void *ptr);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);

示例:

#include<stdio.h>
#include<malloc.h>
void foo(int n) {
    int *p;
    p = (int *)malloc(n * sizeof(int));

    for (int i=0; i<n; i++) {
        p[i] = i;
        printf("%d ", p[i]);
    }
    printf("\n");

    free(p);
}

void main() {
    int n;
    scanf("%d", &n);

    foo(n);
}

运行结果:

$ ./malloc
4
0 1 2 3
$ ./malloc
8
0 1 2 3 4 5 6 7
$ ./malloc
16
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

使用 gdb 查看反汇编代码:

gdb-peda$ disassemble foo
Dump of assembler code for function foo:
   0x0000066d <+0>:     push   ebp
   0x0000066e <+1>:     mov    ebp,esp
   0x00000670 <+3>:     push   ebx
   0x00000671 <+4>:     sub    esp,0x14
   0x00000674 <+7>:     call   0x570 <__x86.get_pc_thunk.bx>
   0x00000679 <+12>:    add    ebx,0x1987
   0x0000067f <+18>:    mov    eax,DWORD PTR [ebp+0x8]
   0x00000682 <+21>:    shl    eax,0x2
   0x00000685 <+24>:    sub    esp,0xc
   0x00000688 <+27>:    push   eax
   0x00000689 <+28>:    call   0x4e0 <malloc@plt>
   0x0000068e <+33>:    add    esp,0x10
   0x00000691 <+36>:    mov    DWORD PTR [ebp-0xc],eax
   0x00000694 <+39>:    mov    DWORD PTR [ebp-0x10],0x0
   0x0000069b <+46>:    jmp    0x6d9 <foo+108>
   0x0000069d <+48>:    mov    eax,DWORD PTR [ebp-0x10]
   0x000006a0 <+51>:    lea    edx,[eax*4+0x0]
   0x000006a7 <+58>:    mov    eax,DWORD PTR [ebp-0xc]
   0x000006aa <+61>:    add    edx,eax
   0x000006ac <+63>:    mov    eax,DWORD PTR [ebp-0x10]
   0x000006af <+66>:    mov    DWORD PTR [edx],eax
   0x000006b1 <+68>:    mov    eax,DWORD PTR [ebp-0x10]
   0x000006b4 <+71>:    lea    edx,[eax*4+0x0]
   0x000006bb <+78>:    mov    eax,DWORD PTR [ebp-0xc]
   0x000006be <+81>:    add    eax,edx
   0x000006c0 <+83>:    mov    eax,DWORD PTR [eax]
   0x000006c2 <+85>:    sub    esp,0x8
   0x000006c5 <+88>:    push   eax
   0x000006c6 <+89>:    lea    eax,[ebx-0x17e0]
   0x000006cc <+95>:    push   eax
   0x000006cd <+96>:    call   0x4b0 <printf@plt>
   0x000006d2 <+101>:   add    esp,0x10
   0x000006d5 <+104>:   add    DWORD PTR [ebp-0x10],0x1
   0x000006d9 <+108>:   mov    eax,DWORD PTR [ebp-0x10]
   0x000006dc <+111>:   cmp    eax,DWORD PTR [ebp+0x8]
   0x000006df <+114>:   jl     0x69d <foo+48>
   0x000006e1 <+116>:   sub    esp,0xc
   0x000006e4 <+119>:   push   0xa
   0x000006e6 <+121>:   call   0x500 <putchar@plt>
   0x000006eb <+126>:   add    esp,0x10
   0x000006ee <+129>:   sub    esp,0xc
   0x000006f1 <+132>:   push   DWORD PTR [ebp-0xc]
   0x000006f4 <+135>:   call   0x4c0 <free@plt>
   0x000006f9 <+140>:   add    esp,0x10
   0x000006fc <+143>:   nop
   0x000006fd <+144>:   mov    ebx,DWORD PTR [ebp-0x4]
   0x00000700 <+147>:   leave  
   0x00000701 <+148>:   ret
End of assembler dump.

参考

本文章首发在 网安wangan.com 网站上。

上一篇 下一篇
讨论数量: 0
只看当前版本


暂无话题~