首页
社区
课程
招聘
[原创]通过GDK8观察堆
2021-10-11 18:40 5707

[原创]通过GDK8观察堆

2021-10-11 18:40
5707

 实验环境

主机:      Windows 10;64位;专业版

                调试工具为Nano Code;与GDK套件协同工作的集成调试工具,兼容WinDBG。

目标机:  GDK8  Ubuntu 18.04;64位  ArmV8架构

                Nano Debugger;用于支持符号化的Linux应用程序调试

        与GDK7不同的是,GDK8在与主机建立连接通过的不再是USB3.0电缆(DCI技术),而是通过远程连接的方式,将目标机与主机连接起来。同时GDK8也是一台可以直接使用的主机,大幅度的减少了准备调试环境所需的时间,省去各种因为软件和硬件不兼容所带来的烦恼。

       GDK8调试套件包含了不少东西,如遥控器、电源、HDMI电缆、GDK8、还有一根蓝色电缆用于刷固件、研究底层逻辑。

观察过程

       堆是什么?--》在程序运行过程中,堆可以提供动态分配的内存,允许程序申请大小未知的内存。堆其实就是程序虚拟地址空间的一块连续的线性区域,它由低地址向高地址方向增长。我们一般称管理堆的那部分程序为堆管理器;在知道堆是什么后,我们在GDK8中去观察堆及其相关的数据结构。

        首先我们需要在GDK8中下载libc符号[sudo apt install libc6-dbg],下载的符号一般会放在/usr/lib/debug/lib目录内;可以通过find命令查找libc符号所在位置[sudo find /usr/lib/debug -name “libc-x.xx.so”];找到libc符号之后,就可以将libc符号复制到主机C盘的temp目录[C:\temp]当中(目前仅支持这1种路径),并且还需要将libc-x.xx.so更名为类似于libc.so.6的形式;如下图所示。

       随后将GDK8和Nano Code建立连接,并加载libc符号及其他所需符号;如下图所示。

       接着获取main_arena全局变量(属于malloc_state结构体)的地址;main_arena中的next指针指向的是下一个arena的地址,最后1个arena中的next指针指向的是main_arena;通过dt查看数据类型的时候,直接显示数值大小的是全局变量,显示数值对应数据类型的是结构体;如下图所示。

       当获取main_arena的地址之后,根据next指针找出所有的arena(每个线程都有它专属的arena;主线程的arena称为main_arena,子线程的arena称为non_main_arena),并将它们显示出来;arena对应的是malloc_state结构体;malloc_state结构体中的flags字段记录分配区的一些状态,如比特0的位置记录了分配区是否有fast bin chunk,比特1 记录了分配区是否能返回连续的虚拟地址空间,比特3记录了分配区的内存是否损坏。

       在main_arena中,当程序第一次动态分配内存的时候,堆会被分成两块,一块是分配区,另一块是Top Chunk,Top Chunk的起始地址亦是top指针所指向的位置;在之后动态分配内存的过程中,只要Top Chunk的位置还够用,分配区就会从Top Chunk中划分空间供自己使用,但是假如Top Chunk中的空间不够用了,那么就会通过brk机制扩展空间。

       在main_arena中的top指针记录了分配区的结束位置的地址,top指针的上方为top chunk;而分配区的开始位置的地址,如下图所示。

        在GDK8中手工验证一下,这个地址是否正确;如下图所示,可以看到没有问题。

       在non_main_arena中的情况则会复杂一些;首先non_main_arena中如果分配内存数量不够大的话,那么分配区的开始位置的地址和结束位置的地址同main_arena中的情况是一样的;但是如果分配内存数量足够的大,那么在non_main_arena中的分配区会划分成1个又1个的子堆;不管non_main_arena是否划分了子堆,在主子堆中,分配区的开始位置永远是[bins指针+mchunk_size(去标志位;bins指针对应malloc_chunk中的mchunk_size)],而其他子堆分配区的起始位置的地址为[heap info的地址+0x20]。

       假如想要获取全部的子堆,就要遍历一下链表,不过这个链表并不是循环的,而且需要依靠perv指针找上一个),直到找到第1个为止;因此我们需要获取链表中的最后1个子堆,具体获取方式如下所示(以64位系统为例)。

    HEAP_MAX_SIZE = 0x4000000;

    heap_address = top & ~(HEAP_MAX_SIZE - 1);

* 需要注意的是主子堆的heap info内的prev指针永远是空(主子堆一般排在链表的第一个),此链表并不是循环的。

        在GDK8中手工验证一下,这个地址是否正确;如下图所示,可以看到没有问题。

        从上面我们可以知道在main_arena及non_main_arena中分配区的范围及起始位置地址和结束位置的地址应该怎么样区获取,那么接下来,我们来谈一下chunk(块)的问题;无论是空闲chunk还是被分配的chunk,它们都是由malloc_chunk结构体进行描述,关于空闲的chunk并不会介绍太多,主要介绍一下被分配的chunk;在被分配的chunk中,假如上一个chunk尚未被释放,那么一般情况下prev size中不会记录任何东西(当然也有记录的时候,见下方),但是如果上一个chunk已经被释放了,那么perv size的大小就是上一个块的大小;而size中则会记录当前块的大小及当前块的属性,标志位的具体介绍见下方。

* 需要注意的是当前块的大小必是 2*SIZE_SZ 的整数倍;如果没有清除标志位,那么size的大小可能就不是2*SIZE_SZ的整数倍了。

* 比特0:记录当前块是否已经被分配;0表示不属于,1表示属于。

* 比特1:记录当前块是否是由mmap分配的;0表示不属于,1表示属于。

* 比特2:记录当前块是否不属于主线程;1表示不属于,0表示属于。

        在malloc_chunk中的fd及bk字段,假如当前块处于被分配的状态,那么从fd开始就是数据;但是假如当前块是空闲的,那么fd及bk会分别指向下/上1个的空闲块。

        fd_nextsize, bk_nextsize这两个指针指在块空闲的时候使用,指向下/上第1个大小与当前块不同的空闲块。

* 下一个块的起始位置 = 当前块的起始位置 + size(去标志位)

块未释放的情况下prev size的大小:

* 上一个chunk未越界,则prev size为0

* 上一个chunk已“越界”

       --》“越界“大小<=分配大小:prev size为上一个chunk的大小,包括越界字符。

       --》“越界”大小>=分配大小:prev size为未知大小,数值比较“异常”的大。

!heap扩展命令的简单介绍

!heap扩展命令的基本格式如下所示:

!heap [arena地址] [显示选项]

!heap      如果什么都不加,(不加地址,不加显示选项),命令会显示所有的arena,并列出概要信息;如下图所示。

        从图中我们可以看到arena对应的地址,块信息,标志位信息(flags),线程信息,内存大小信息。

!heap -地址 -d    d选项用于列出对应地址的详细信息;如下图所示。

        从图中我们可以看到,该arena是属于主线程的,并且可以看到他的地址,next指针指向的是下一个arena的地址;top指针指向top chunk的地址,我们仍可以看到top指针对应的malloc_chunk结构体内的详细信息;在往下是bins数组的信息;最后列出的是分配区的情况,从分配区中可以看到第一列是块对应的地址,第二列是块的属性,因为这是主线程,所以N全是0,假如是子线程的话N就是1,M为0代表不是内存映射,P代表这个块是被分配的,第三列prev size是上一个块的大小,由于上一个块未释放且未“越界”,因此看到的prev size就会是0,第四列size是当前块的大小。

!heap -地址 -h    h选项用于列出子堆的概要信息;如下图所示。

        从图中我们可以看到第二列是heap info的地址(很明显低位全是0);第二列size为该子堆分配区的大小。

!heap -地址 -s     s选项用于统计相同大小块的信息;如下图所示。

        从图中可以看到第二列是块的大小,第三列是拥有相同大小块的数量;第三列是总大小;第四列是该块的总数量占所有块数量的百分比。

参考资料

堆相关数据结构 - CTF Wiki

Heap Memory - heap-exploitation



[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞2
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回