首页/文章/技术

堆前置知识

堆前置基础知识回顾

学习不是为了征服世界,而是为了不辜负每一次好奇心的出发。

程序运行时的内存区域

一个 Linux x86-64 , 进程地址可以这么理解

空间高地址
stack
mmap area
heap
.bss
.data
.rodata
.text低地址

.text

保存程序代码,也就是 CPU 执行的机器指令

c
void hello(){    puts("hello");}

编译后这个函数对应的机器码就在.text

.rodata

.rodata - read only data,用来保存只读数据,例如上个函数的 hello就放在 .rodata

.data

保存已经初始化的全局或静态变量

c
int global_number = 123;

global_number就放在 .data

.bss

保存未初始化,或初始化为 0 的全局变量

c
int global_counter;char global_buffer[128];

这些变量在程序启动时会被初始化为 0

heap

heap 用于动态内存分配

c
malloccallocreallocfree
c
char *p = malloc(32)

这里返回一块堆内存的地址

stack

主要保存:

  1. 函数局部变量
  2. 参数
  3. 返回地址
  4. 寄存器
  5. 函数调用现场

heap 与 stack 的区别

栈上的对象跟随函数调用自动创建和销毁
例如:

c
void func(){    char buf[4];}

进入 func()buf存在,离开后自动销毁。
而堆由程序员手动申请和释放

c
char *p = malloc(32);free(p);

指针

地址和值

c
int x = 123;

我们假设 x0x7fffffffe000
即:

地址
0x7fffffffe000123
我们取地址
c
int *p = &x;
text
此时 p 保存 x 的地址 p = 0x7fffffffe000

&,*

c
int x = 123;int *p = &x;

含义如下:

text
x    x 的值&x   x 的地址p    p 保存的地址*p   p 指向地址处的值

即:

text
p = &x*p = x

指针类型

c
char *p1;int *p2;long *p3;

区别

text
char * 解引用读取 1 字节int * 读取四字节long * 读取 8 字节
c
int arr[4] = {1,2,3,4};int *p = arr;

那么p+1通常表示地址加 4
我们也可以强制转换指针类型

c
char *q = (char *)arr;

此时 q+1 表示地址加 1

指针变量占据内存

c
#include <stdio.h>int main() {    printf("%zu\n",sizeof(void *));    printf("%zu\n",sizeof(char *));    printf("%zu\n",sizeof(int *));}

alt text

指针与堆内存

c
char *p = malloc(32);

这个代码有两部分

text
p ---> 指针变量malloc(32) 返回值 ---> 堆块地址

如果 p 为局部变量 ,那么就是栈上的 p 指向的内容在堆

空指针

空指针表示目前不指向有效对象。

c
int *p = NULL;

也就是说我们需要检查指针是否为空

c
if(p != NULL){    printf("%d\n",*p);}

如果我们直接解引用空指针,就会崩溃

悬空指针

指的是已经不再指向有效对象,但仍然保存旧地址的指针。
最常见来源

c
char *p = malloc(32);free(p);

虽然我们 freep,但是 p 的值没有自动改变,指向的是已经释放了的堆块。
更安全的写法为

c
free(p);p = NULL;

小端序

低位字节放在低地址,高位字节放在高地址。
例如:

text
0x12345678排列为78 56 34 12
c
#include <stdio.h>#include <stdint.h>int main() { uint64_t x = 0x1122334455667788; unsigned char *p = (unsigned char *)&x; for (int i = 0; i < 8; i++) { printf("%02x ", p[i]); } puts(""); return 0;}

alt text

结构体内存布局

C 语言中的 struct 会按照字段声明顺序放入内存,但编译器可能插入填充字节

基本结构体

c
struct User {    int id;    long score;};

由于通常要求 8 字节对齐,所以内存空间实际为:

变量字节
id4bytes
padding4bytes
score8bytes

所以 sizeof 的大小为 16 而非 12

offsetof观察偏移

c
#include <stdio.h>#include <stddef.h>struct A { char c; long x; int y;};int main() { printf("sizeof(struct A) = %zu\n", sizeof(struct A)); printf("offset c = %zu\n", offsetof(struct A, c)); printf("offset x = %zu\n", offsetof(struct A, x)); printf("offset y = %zu\n", offsetof(struct A, y)); return 0;}

alt text

常见对齐规则

变量字节对齐
char1bytes,对齐 1
short2bytes,对齐 2
int4bytes,对齐 4
long8bytes,对齐 8
指针8bytes,对齐 8

结构体中的数组

c
struct Item {    char name[16];    int price;};

布局:

name[16]
price

结构体中的指针

c
struct Item{    char name[16];    char *p;};

布局为

text
name[16]p ptr
c
struct Item item;item.p = malloc(32);

关系为

text
name[16]p ptr -> heap addr                     heap:                    p content

结构体数组

c
struct Item items[3];

此时结构体在内存中连续排列

指针数组

c
struct Item *items[3];

数组中保存的是指针

text
items[0] ptritems[1] ptritems[2] ptr

每个指针都可以指向堆上的一个结构体

函数指针

语法

c
void (*fp)();

含义

text
fp 是一个指针,指向一个函数,这个函数无参,返回值是 void
c
#include <stdio.h>void hello() { puts("hello");}void goodbye() { puts("goodbye");}int main() { void (*fp)(); fp = hello; fp(); fp = goodbye; fp(); return 0;}

alt text

带参数的函数指针

c
#include <stdio.h>void print_number(int x) { printf("%d\n", x);}int main() { void (*fp)(int); fp = print_number; fp(123); return 0;}

alt text
void (*fp)(int)表示

text
fp 指向一个函数,这个函数接收一个  int 参数,返回 void

普通指针和函数指针的区别

普通指针用来读写数据,函数指针用来决定执行什么函数

EFL 基础

file

alt text

text
ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1ae7b9855afea02882e7fc25082946f082c8ec03, for GNU/Linux 3.2.0, not stripped

nm

bash
lmx@lmx:~/pwn/ZeroG/format$ nm -n format_station                 w __gmon_start__                 U __libc_start_main@@GLIBC_2.2.5                 U printf@@GLIBC_2.2.5                 U puts@@GLIBC_2.2.5                 U read@@GLIBC_2.2.5                 U setbuf@@GLIBC_2.2.5                 U __stack_chk_fail@@GLIBC_2.40000000000401000 T _init00000000004010d0 T _start0000000000401100 T _dl_relocate_static_pie0000000000401110 t deregister_tm_clones0000000000401140 t register_tm_clones0000000000401180 t __do_global_dtors_aux00000000004011b0 t frame_dummy00000000004011b6 T ret_gadget00000000004011e9 T pop_rdi_ret0000000000401215 t read_canary0000000000401257 T init_io00000000004012c5 T vuln00000000004013bd T main0000000000401400 T __libc_csu_init0000000000401470 T __libc_csu_fini0000000000401478 T _fini0000000000402000 R _IO_stdin_used0000000000402070 r __GNU_EH_FRAME_HDR0000000000402274 r __FRAME_END__0000000000403db0 d __frame_dummy_init_array_entry0000000000403db0 d __init_array_start0000000000403db8 d __do_global_dtors_aux_fini_array_entry0000000000403db8 d __init_array_end0000000000403dc0 d _DYNAMIC0000000000403fb0 d _GLOBAL_OFFSET_TABLE_0000000000404000 D __data_start0000000000404000 W data_start0000000000404008 D __dso_handle0000000000404010 B __bss_start0000000000404010 D _edata0000000000404010 D __TMC_END__0000000000404020 B stdout@@GLIBC_2.2.50000000000404030 B stdin@@GLIBC_2.2.50000000000404040 B stderr@@GLIBC_2.2.50000000000404048 b completed.80610000000000404050 B _end

常见符号类型

text
T 代码段符号B .bss 段符号D .data 段符号R 只读数据符号U 未定义符号,通常来自动态库

readelf

bash
lmx@lmx:~/pwn/ZeroG/format$ readelf -S format_stationThere are 30 section headers, starting at offset 0x3ae0:节头:  [号] 名称              类型             地址              偏移量       大小              全体大小          旗标   链接   信息   对齐  [ 0]                   NULL             0000000000000000  00000000       0000000000000000  0000000000000000           0     0     0  [ 1] .interp           PROGBITS         0000000000400318  00000318       000000000000001c  0000000000000000   A       0     0     1  [ 2] .note.gnu.pr[...] NOTE             0000000000400338  00000338       0000000000000020  0000000000000000   A       0     0     8  [ 3] .note.gnu.bu[...] NOTE             0000000000400358  00000358       0000000000000024  0000000000000000   A       0     0     4  [ 4] .note.ABI-tag     NOTE             000000000040037c  0000037c       0000000000000020  0000000000000000   A       0     0     4  [ 5] .gnu.hash         GNU_HASH         00000000004003a0  000003a0       0000000000000034  0000000000000000   A       6     0     8  [ 6] .dynsym           DYNSYM           00000000004003d8  000003d8       0000000000000108  0000000000000018   A       7     1     8  [ 7] .dynstr           STRTAB           00000000004004e0  000004e0       000000000000007f  0000000000000000   A       0     0     1  [ 8] .gnu.version      VERSYM           0000000000400560  00000560       0000000000000016  0000000000000002   A       6     0     2  [ 9] .gnu.version_r    VERNEED          0000000000400578  00000578       0000000000000030  0000000000000000   A       7     1     8  [10] .rela.dyn         RELA             00000000004005a8  000005a8       0000000000000078  0000000000000018   A       6     0     8  [11] .rela.plt         RELA             0000000000400620  00000620       0000000000000078  0000000000000018  AI       6    23     8  [12] .init             PROGBITS         0000000000401000  00001000       000000000000001b  0000000000000000  AX       0     0     4  [13] .plt              PROGBITS         0000000000401020  00001020       0000000000000060  0000000000000010  AX       0     0     16  [14] .plt.sec          PROGBITS         0000000000401080  00001080       0000000000000050  0000000000000010  AX       0     0     16  [15] .text             PROGBITS         00000000004010d0  000010d0       00000000000003a5  0000000000000000  AX       0     0     16  [16] .fini             PROGBITS         0000000000401478  00001478       000000000000000d  0000000000000000  AX       0     0     4  [17] .rodata           PROGBITS         0000000000402000  00002000       0000000000000070  0000000000000000   A       0     0     4  [18] .eh_frame_hdr     PROGBITS         0000000000402070  00002070       000000000000006c  0000000000000000   A       0     0     4  [19] .eh_frame         PROGBITS         00000000004020e0  000020e0       0000000000000198  0000000000000000   A       0     0     8  [20] .init_array       INIT_ARRAY       0000000000403db0  00002db0       0000000000000008  0000000000000008  WA       0     0     8  [21] .fini_array       FINI_ARRAY       0000000000403db8  00002db8       0000000000000008  0000000000000008  WA       0     0     8  [22] .dynamic          DYNAMIC          0000000000403dc0  00002dc0       00000000000001f0  0000000000000010  WA       7     0     8  [23] .got              PROGBITS         0000000000403fb0  00002fb0       0000000000000050  0000000000000008  WA       0     0     8  [24] .data             PROGBITS         0000000000404000  00003000       0000000000000010  0000000000000000  WA       0     0     8  [25] .bss              NOBITS           0000000000404020  00003010       0000000000000030  0000000000000000  WA       0     0     32  [26] .comment          PROGBITS         0000000000000000  00003010       000000000000002b  0000000000000001  MS       0     0     1  [27] .symtab           SYMTAB           0000000000000000  00003040       00000000000006f0  0000000000000018          28    45     8  [28] .strtab           STRTAB           0000000000000000  00003730       0000000000000293  0000000000000000           0     0     1  [29] .shstrtab         STRTAB           0000000000000000  000039c3       0000000000000116  0000000000000000           0     0     1Key to Flags:  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),  L (link order), O (extra OS processing required), G (group), T (TLS),  C (compressed), x (unknown), o (OS specific), E (exclude),  D (mbind), l (large), p (processor specific)
bash
lmx@lmx:~/pwn/ZeroG/format$ readelf -r format_station重定位节 '.rela.dyn' at offset 0x5a8 contains 5 entries:  偏移量          信息           类型           符号值        符号名称 + 加数000000403ff0  000500000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0000000403ff8  000600000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0000000404020  000700000005 R_X86_64_COPY     0000000000404020 stdout@GLIBC_2.2.5 + 0000000404030  000900000005 R_X86_64_COPY     0000000000404030 stdin@GLIBC_2.2.5 + 0000000404040  000a00000005 R_X86_64_COPY     0000000000404040 stderr@GLIBC_2.2.5 + 0重定位节 '.rela.plt' at offset 0x620 contains 5 entries:  偏移量          信息           类型           符号值        符号名称 + 加数000000403fc8  000800000007 R_X86_64_JUMP_SLO 0000000000401080 puts@GLIBC_2.2.5 + 0000000403fd0  000100000007 R_X86_64_JUMP_SLO 0000000000000000 __stack_chk_fail@GLIBC_2.4 + 0000000403fd8  000200000007 R_X86_64_JUMP_SLO 0000000000000000 setbuf@GLIBC_2.2.5 + 0000000403fe0  000300000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0000000403fe8  000400000007 R_X86_64_JUMP_SLO 0000000000000000 read@GLIBC_2.2.5 + 0

malloc 和 free

堆内存由 allocator 管理,在 linux glib环境中,常见 allocator 是 ptmalloc

malloc

c
void *malloc(size_t size);

作用,申请至少 size 字节的堆内存
常见写法

c
char *p = malloc(32);if (p == NULL) {    exit(1);}

calloc

c
void *calloc(size_t nmemb,size_t size);

申请 nmemb * size 字节,并把内容初始化为 0.

c
int *arr = calloc(10,sizeof(int));

这两个函数的区别

text
malloc 只申请,不清零calloc 会申请并初始化为零

realloc

c
void *realloc(void *ptr,size_t size);

用于调整已有堆块大小
示例:

text
char *p = malloc(32);p = realloc(p,64);

realloc可能返回新地址 , 也可能原地扩展

free

c
void free(void *ptr)

作用是释放之前返回的内存。
但是释放后只不过把堆交还回管理器,指针并未清空。
常见安全释放函数

c
void safe_free(void **pp) { if (pp != NULL && *pp != NULL) { free(*pp); *pp = NULL; }}

堆块生命周期

  1. 未申请
  2. 已申请
c
char *p = malloc(32)
  1. 已释放

错误使用方式

  1. 释放后读
    c
    char *p = malloc(32);strcpy(p, "hello");free(p);puts(p);
  2. 释放后写
    c
    char *p = malloc(32);free(p);strcpy(p, "AAAA");
  3. 二次释放
c
char *p = malloc(32);free(p);free(p);

UAF 基础

UAF 即 use after free , 就是释放后继续使用

UAF read

c
#include <stdio.h>#include <stdlib.h>#include <string.h>int main() { char *p = malloc(32); strcpy(p, "hello"); free(p); puts(p); return 0;}

这是释放后继续读,为错误代码

UAF write

c
#include <stdlib.h>#include <string.h>int main() { char *p = malloc(32); free(p); strcpy(p, "AAAA"); return 0;}

UAF 和悬空指针的关系

悬空指针是状态,UAF 是行为

UAF 为什么容易变成漏洞

当堆内存释放后, allocator 可能把同一块内存重新分配给别的对象

c
char *a = malloc(32);free(a);char *b = malloc(32);

b 可能拿到和 a 一样的地址,如果程序仍保留 a 并继续使用

c
strcpy(a,"AAAA")

实际上破坏的是 b 指向的新对象

组合风险

堆 pwn 中,很多对象会放在堆上。对象内部可能包含普通数据,指针和函数指针

包含数据指针的结构体

c
struct User{    char name[32];    char *bio;};

如果能修改 bio 指针 ,那么后续代码

c
puts(user->bio);read(0,user->bio,64);

会泄露指定的数据或篡改指定地址的数据

包含函数指针的结构体

c
struct Handler {    void (*func)();    char name[32];};

如果 func 被修改,那么程序接下来的执行就会被攻击者决定

许可协议

本文采用 署名-非商业性使用-相同方式共享 4.0 国际 许可协议,转载请注明出处。

读者回信

正在翻开留言页...