Android 堆溢出攻击—[0]原理

来源:互联网 发布:诺基亚n97的软件 编辑:程序博客网 时间:2024/05/22 05:18

栈,是一种“先进后出”的数据表,正是由于这个特点,系统使用栈辅助函数调用及返回,函数中使用的局部变量在栈中分配,并在函数返回时会被销毁,也就是说对于函外部的程序来说函数内部的局部变量是不可见的,显然这样的变量不能完全满足程序的需求,比如,函数执行后需要把数据的改变传递到程序的其他部分,这样的需求很常见。虽然可以通过全局变量的方式的解决,但全局变量只能在编写程序时预先定义好,并在编译期间为全局变量分配好内存空间,所以全局变量不能满足程序在运行期间动态生成并保存数据的要求,很好的满足了上面两点需求。

堆和栈一样是进程虚拟空间中的一块内存区域,但堆要比栈区大的多,基本上占据了进程虚拟内存空间的绝大部分。在程序看来,可以通过malloc函数在堆上任意申请堆空间,通过返回一个指针指向申请的堆内存,在程序的各个部分都可以操作堆上的数据,并且只有程序通过free函数自主放弃对申请的内存空间的使用权后,该内存才会被销毁。

这里写图片描述

那么我们知道栈的使用遵循了先进后出规则,那么malloc是怎么在堆上申请内存的呢?大部分系统中对堆的管理由系统运行时库实现,系统运行时库中会包含一个堆分配器专门负责对堆的管理,在Android 5.0之前bionic(android上的运行时库)中的堆分配器是dlmalloc,之后则使用jemalloc作为默认堆分配器。每种堆分配虽然对外提供的接口相同,但内部实现往往不同,能力也不尽相同,比如,Android之所以在5.0中使用jemalloc替换dlmalloc就是因为看中jemalloc强大的多核/多线程分配能力 。

堆溢出攻击

堆的使用虽然和栈完全不同,但其内存中保存的数据并没有什么区别,比如,整型数值、数组、字符串等,所以也避免不了缓冲区溢出的问题,并且溢出原理和栈是一样,只是攻击者不能通过堆溢出来覆盖函数返回地址来劫持PC,堆溢出需要其他方法来劫持PC。

unlink利用方法

unlink利用方法只存在于通过boundary tag实现基本结构的堆分配器,比如dlmalloc,而jemalloc在实现上未采用boundary tag方式天生就对该利用方法免疫。

boundary tag即边界标记法,指在堆中分配内存时会在头部或尾部添加额外的元数据,这些元数据包含了该内存块的信息及和其他堆内存的关联,用于快速查找可用的堆内存和堆释放的堆内存进行合并。

首先,先来回顾一下 dlmalloc的边界表示法的实现—最基本结构chunk,

struct malloc_chunk{    size_t               prev_foot;//size of previous chunk (if free).    size_t               head;     //size and inuse bits.    struct malloc_chunk* fd;       //double links -- used only if free    struct malloc_chunk* bk;}typedef struct malloc_chunk  mchunk;typedef struct malloc_chunk* mchunkptr;

该结构体由四个field组成.
prev_foot,记录了上一个邻接chunk的最后4个字节.
head, 记录当前chunk的size以及C和P位,C = 0 表示当前的内存块是空闲的,反之表示已分配;P = 0 表示前一个内存块是空闲的,反之表示已分配。
fd, bk指针,只对空闲chunk起作用,用于链接相同大小的空闲chunk,非空闲chunk的没有fd,bk指针,从fd指针位置开始就是用户数据。

典型空闲chunk结构图,
这里写图片描述

dlmalloc为了更高效的分配堆块采用分箱(bins)机制把内部划定一些chunk集合,每个集合中记录的都是固定大小或区间的free chunk,当分配时可以直接从分箱中找到最贴近用户要求的那一个free chunk,以small bins为例:dlmalloc对small bins的规定是这样的,以每8字节为一个分割划分bins。也就是按照8, 16, 24, 32, … , 256这样的排列,一共32个分箱.每个分箱中存放相同大小的free chunk,由一个双向环形链表管理。为了编程上的便捷,每个链表都附加了一个头节点(可以避免边界条件下繁冗的判断),

这里写图片描述

分箱中的双向链表正是由chunk结构中的前向指针fd和后向指针bk实现free chunk的相互链接。

当有小块堆内存申请时,按照申请大小找到合适的分箱,并从该分箱中把最前面的free chunk从双向链表中“卸下”,并把后面的free chunk重新链接好;当有释放堆块请求时,按照其大小找到合适的分箱,将释放的堆块“链入”到双向链表的最前面;当释放时,如果堆块的前后存在free chunk,需要进行合并,把需要合并的堆块从其双向链表中“卸下”,合并后“链入”到合适的分箱双向链表中。

“卸下”、“链入”都是对双向链表指针的操作,dlmalloc中“卸下”操作宏(以small为例),

#define unlink_chunk(M, P, S)\  if (is_small(S)) unlink_small_chunk(M, P, S)\  else { tchunkptr TP = (tchunkptr)(P); unlink_large_chunk(M, TP); }/* Unlink a chunk from a smallbin  */#define unlink_small_chunk(M, P, S) {\  mchunkptr F = P->fd;\  mchunkptr B = P->bk;\  bindex_t I = small_index(S);\  assert(P != B);\  assert(P != F);\  assert(chunksize(P) == small_index2size(I));\  if (RTCHECK(F == smallbin_at(M,I) || (ok_address(M, F) && F->bk == P))) { \    if (B == F) {\      clear_smallmap(M, I);\    }\    else if (RTCHECK(B == smallbin_at(M,I) ||\                     (ok_address(M, B) && B->fd == P))) {\      F->bk = B;\      B->fd = F;\    }\    else {\      CORRUPTION_ERROR_ACTION(M);\    }\  }\  else {\    CORRUPTION_ERROR_ACTION(M);\  }\}

这里,可以简单的抽象为,

int unlink_small_chunk(mchunkptr chunk){    chunk -> bk -> fd = chunk -> fd;    chunk -> fd -> bk = chunk -> bk;    return 0;}

下图展示了这个过程,

这里写图片描述

重点来了,当堆溢出发生后,溢出的数据可以淹没下一个堆块的chunk,

  1. 当后一个堆块是一个free chunk,直接执行下一步,如果后一个堆块是已分配,修改其chunk结构中的C位(C = 0)伪造成free chunk,执行下一步。

  2. 修改后一个chunk的结构,让其前向指针fd指向“邪恶代码”,后向指针bk指向任意地址。

这样,当溢出的堆块被free时会触发合并操作,dlmalloc会把后一个chunk进行“卸下”操作,

chunk -> bk -> fd = chunk -> fd;

此时,伪造的fd指针指向邪恶代码将会被写入伪造的bk指针所指向地址。为了能够劫持PC指针,伪造的bk可以指向got表中重要函数的地址,当程序调用该函数时就会指向“邪恶代码”。

chunk -> bk -> fd,指向了伪造的bk指针偏移8字节位置,所以需要注意指向got表中的重要函数时的偏移量。

下图展示了这个过程,

这里写图片描述

防御

上面描述了早期unlink攻击的简单原理,随着堆分配器的安全性改进,添加了对unlink的校验功能,

  assert(P != B);\  assert(P != F);\  assert(chunksize(P) == small_index2size(I));\

检测“卸下”的chunk的前向指针指向的内存块,保证其后向指针指向当前块,检查前向指针指向的内存块,保证其前向指针指向当前块,这样已经可以有效的缓解早期的unlink指针。

0 0