PWN堆house-of-lore
- 这个堆利用就需要伪造堆块了,
house of lore这个利用方式只对glibc2.23-glibc2.31之间的这个版本 glibc2.31以上的版本house of lore的利用方式基本失效。
前置知识
-
这里也统一认知,之前介绍了在堆块的物理地址中
高地址为前,低地址为后,接下来对堆块的逻辑地址的前后进行一个认知的统一。 -
通常在释放的堆块如果被
bins管理,他们都会将fd指针和bk指针给利用起来,之前一直没搞明白fd指针指向前面还是后面,bk指针指向前面还是后面。 -
fd指针的全称为forward pointer,bk指针的全称为backward pointer。在fastbin和tcachebin中我们使用了fd指针,并且这个时候fd指针都指向的是远离bins堆块的方向。这边就以tcache_bins的图片为例子。

- 所以这次我们对逻辑地址做一个认知统一。
- 远离
bins的堆块是更前面的堆块,所以fd指针指向的是远离bins的堆块 - 靠近
bins的堆块是更后面的堆块,所以bk指针指向的是靠近bins的堆块
- 远离

smallbin机制
-
这里为了能更好的理解
house of lore接下来就要了解一下smallbin这个bins的运行机制,这样更有利于我们学习house of lore -
之前我们释放的堆块基本上都是被放入
fastbin、tachacbin、unsortedbin,这三个bins中,很少有见到释放的堆块被放入smallbin中。所以我们先研究一下怎样才能使得被释放的堆块放入smallbin中。 -
smallbin是一个双向循环链表的结构,并且smallbin是一个长为64的一个数组。在相关宏定义中这个#define NSMALLBINS 64可以知道是64个堆块的数组。 -
从
malloc.c中的这个结构体定义中struct malloc_state还可以得知mchunkptr bins[NBINS * 2 - 2];,这个堆块长度为254长度,其中包含了smallbin和largebin。- 注意:这里会存在一个误区,这边的bins既然是254个元素是否与之前unlink 图片所说的largebins和smallbins的个数不一样
- 注意:由于smallbins和largebins是一个双向链表结构,而bins数组中的每一个元素相当与一个指针,为了使得bins能够有两个指针,所以对应smallbins[0]它会将bins[0]作为fd指针,bins[1]作为bk指针,利用这种方式构造一个双向头结点
-
从
62-63行代码的宏定义in_smallbin_range可以得知,chunk_size<MIN_LARGE_SIZE即在64位系统中小于0x400大小的chunk都会被放入smallbin的堆块里面,在32位系统中小于0x200大小的chunk都会被放入smallbin的堆块里面(在相关代码中的宏定义有做具体介绍) -
从
66-68行代码的宏定义smallbin_index可以得知,每个相邻smallbin存储的空闲堆块size位在64位中相差0x10字节,在32位中相差0x8字节
1 | // 堆块结构 |
smallbin这个堆块结构也是一个先进先出的堆块结构,(堆块被释放后放入smallbin的过程与fastbin放入机制相同,都是使用头插法),只不过smallbin是双向链表。接下来给出一个smallbin大致的结构图,这样更有利于我们了解(以64位系统为例子),注意smallbin和largebin中是这样的- 而
smallbin这个堆块的取出与fastbin堆块取出机制不同,fastbin取出的是最靠近bins的堆块。而smallbin是先取出的是最远离bins的堆块,即如果要使用smallbin的堆块时先取出的是chunk0

-
被释放的堆块被放入
smallbin的条件:- 首先被释放的堆块要符合
smallbin堆块大小的范围: - 接下来分析一下代码得出如下结论:当我们释放一个堆块,这个堆块被放入
unsortedbin后,我们之后再申请一个堆块,这个堆块的申请如果既没有使用unsortedbin中的堆块,也没有使用smallbin中的堆块,这样先前被放入unsortedbin中的堆块就会被放入smallbin - 当
smallbin中的堆块被切割后,切割后的堆块会先放入unsortedbin中(因为size位改变,现在的chunk中的size并不在当前smallbin范围中),之后我们再使用malloc申请一个堆块,并且申请的堆块没有使得unsortedbin中的堆块被切割,那么在unsortedbin中的堆块就会被放入smallbin中的堆块
- 首先被释放的堆块要符合
-
接下来我们给出一个示例程序:
1 |
|
- 接下来我们使用gdb动态调试查看一下堆块的运行机制。我们先申请一个
p1堆块和p2堆块。

- 然后将
p1堆块释放,该堆块就会被放入unsortedbin中

- 然后我们再申请一个
p3堆块,这样p1堆块就会被放入smallbin[idx1]中管理

- 然后我们再申请一个
p4堆块,这样原来的p1堆块就会被切割,并放入unsortebin中

- 之后申请一个
p5的堆块,这样p1堆块就会又被放入smallbin[idx2]中管理,其中idx2 < idx1

- 此外还可以进行这样的动态调试去查看
smallbins是的双向链表机制
1 |
|
相关代码
- 接下来给出
glibc2.23中的smallbin的相关源码。 - 下面是
malloc中关于smallbin的代码
1 | if (in_smallbin_range (nb)) |
- 下面是
_int_free中的smallbin相关操作
_int_free中的源码
1 | /* |
相关宏定义
1 | // min_large_size存储在largebin的最小值,也就是smallbin存储堆块size的最大范围 |
- 在
glibc2.27中添加了一个检查机制
1 | if (__glibc_unlikely (bck->fd != victim)) |
实验
源码
1 | /* |
- 还是老样子,将源码翻译一遍。
1 | /* |
- 接下来进行一下动态调试,我们先申请一个大小为
0x100的堆块,这个堆块实际的size位大小为0x110


- 并且我们在栈上分别申请了
0x18大小和0x20大小的内存空间


- 接下来我们就会利用栈上的内存空间去伪造一个
fake_chunk,在伪造这个fake_chunk的时候我们需要我们前面申请的victim堆地址(victim的prev_size的地址即堆块的起始地址),将fake_chunk的fd指针指向victim的起始地址。这样是为了绕过small bin corrupted的检查的检查。


- 之后我们修改
stack_buffer_1[3]=stack_buffer_2,stack_buffer_2[2]=stack_buffer_1,这样做的目的是为了绕过最后一次malloc中small bin corruped的检查。这样伪造后,伪造的堆块就会构成一个双向链表。



- 之后我们申请一个
1000大小的堆块,这样就使得我们之后将victim释放后不会与topchunk合并,并且victim这个堆块就会被放入unsortedbin中管理 - 并且由于
unsortedbin也是一个双向链表,所以victim->fd和victim->bk指向的是unsortedbin这个堆块地址


- 之后我们再申请一个更大的堆块,使得申请的堆块不是由
unsortedbin、smallbins中的堆块切割下来的。在使用malloc过程中就会使得原来在unsortedbin链表中的victim堆块,被放入smallbins中

- 之后我们再通过
UAF漏洞修改victim的bk指针,这时我们smallbin堆块的链表结构就如下图


- 这个时候我们再申请一个与之前申请
victim大小的堆块,就会将smallbin中的堆块给申请回来,这样就会使得fake_chunk被链到smallbin中 - 这时就会触发
smallbin->bk = victim->bk=stack_buffer1_addr


- 这时我们再申请一个
0x100大小的堆块,由于smallbin中的堆块是先进先出,主要取出的是bins->bk所指堆块,所以我们就将stack_buffer1处的这个堆块给申请过来了。


- 这时我们就申请到栈上的数据了,并且我们可以将
jackpot的地址写入到栈上了,这时我们就覆盖这个函数的地址为返回地址


利用方式
- 现在就给这个
house of lore的利用方式做一个总结。首先house of lore这个堆块的利用方式是针对的是smallbin。 - 漏洞利用的条件:
- 需要
UAF漏洞,这样才能对smallbin中堆块的bk指针进行修改(如果没有UAF漏洞可以尝试使用off-by-null漏洞尝试使用堆风水,去构造UAF漏洞) - 还需要知道堆地址,这样我们在伪造
fake_chunk的时候才能向fake_chunk->fd指针这个位置写入chunk_adddr从而绕过检查 - 可能还需要栈地址或者是其他段地址,这样我们在申请任意堆块内存的时候就可以绕过检查
- 需要
- 接下来归纳一下我们需要伪造的堆块的数据有哪些:
- 对于放入
smallbin中的堆块victim,我们需要修改其bk指针,修改bk指针为fake_chunk1_addr - 对于我们要申请的目标地址
fake_chunk1,我们要伪造其fd指针,使得其fd指针指向victim,使得bk指针指向fake_chunk2 - 对于我们要借助绕过申请目标地址的辅助
chunk即fake_chunk2,我们要修改其fd指针为fake_chunk1
- 对于放入

house-of-lore_level1
- 接下尝试写一题,题目来源:
ctfhub-house-of-lore
level_1分析1
- 拿到附件后我们就先
check一下附件,看看附件开启了什么保护机制。发现没有开PIE

- 然后我们再逆向该程序,使用
IDA pro,先来查看一下main函数的运行逻辑。main函数的大致逻辑如下:- 先对输入输出进行初始化
- 然后再初始化一个
name_message - 之后就会进入一个
menu的循环之中,这个时候程序会让用户read_int即输入一个选项,这里我们就来归纳一下对应选项的菜单,这里的menu函数就不进去查看了。- 选项1:
add操作 - 选项2:
edit操作 - 选项3:
dele操作 - 选项4:
change name操作 - 选项5:
change message操作,注意在程序只能调用一次change message,调用完之后再调用就不会调用change message函数了 - 选项6:
exit - 选项其他:
Invalid choice
- 选项1:

- 以上就是main函数的执行流程,然后我们现在先来查看一下
init_message这个函数的功能- 首先会让用户输入
name,name也是一个全局变量 - 之后会申请一个堆块保存给全局变量
message,申请堆块后会让用户向这个堆块中输入内容
- 首先会让用户输入

- 接下来我们继续安装
增删改查的顺序查看每个函数的功能- 先查看的是
add函数,这里出现了一个名为page_list的全局变量,这个全局变量是一个指针数组,存储的是地址,数组里面有7个元素 - 如果
page_list满了就会输出Full并且结束该函数 - 如果没满,就会提示用户输入
size,这个size就是之后我们要申请的堆块的大小 - 申请完之后,
malloc返回的指针就赋值给对应page_list的地方 - 之后又出现了一个名为
size_list的全局变量,这个全局变量是一个int类型的数组,这个数组存储的是我们所申请的size的值
- 先查看的是

- 之后来查看
dele函数,该函数的程序运行逻辑如下:- 用户首先要选择需要释放的堆块,之后程序会检查这个索引是否合理
- 之后释放这个堆块,然后同时将相应的
page_list和size_list置零。

- 然后查看
edit函数,函数逻辑如下:- 首先要求用户输入要
edit堆块的index - 然后就使用
read向堆块中输入内容,指定输入长度为size_list
- 首先要求用户输入要

- 然后查看
change_name()函数,函数的主要逻辑如下,就是重新向name写入东西

- 再查看
change_message()这个函数,函数的具体逻辑如下:- 首先泄露出这个
message的堆地址,然后会释放message所指向的堆块 - 然后再让用户输入之后要申请的堆块大小
- 申请堆块,
malloc的返回值将赋值给buf - 之后就是向
buf写入内容,长度不能超过我们所申请的。 - 之后还会修改
message所指向的堆块注意这里就存在着UAF漏洞 - 修改
message所指向堆块后就会更新message
- 首先泄露出这个

- 逆向该程序的逻辑后,我们找到了漏洞利用的主要地方,这个地方就是在
change_message()这个函数中,我们可以利用这个函数进行UAF漏洞 - 我们现在也对全局变量进行一个归纳和汇总:
name:存储着用户输入的数据,相当于字符数组,长度为0x40大小。message:是一个指针,指向malloc返回的堆块地址page_list:是一个指针数组,指向malloc返回的堆块地址,一共有7个元素size_list:是一个int类型的数组,存储着page_list对应索引申请的堆块大小
level_1分析2
- 接下来我们边写脚本,边进行动态调试。根据程序运行逻辑我们编写了如下代码进行交互。
1 | from pwn import * |
- 根据分析1,我们可以修改
释放后的message指向的堆块,接下来我们来查看释放后的堆块会被放在什么bins中。我们会发现这个堆块会被放入smallbins中,这时我们可以read(0, message, 0x10uLL)从而修改这个堆块的fd、bk指针

- 所以这题的考点就是
house-of-lore,现在我们就要对堆块进行伪造,从而可以使用malloc申请到任意地址。这里由于我们bss段地址是固定的,并且我们的name变量是可以写的,所以我们就使用name这个数据块伪造堆块,使其绕过检查 - 这时我们通过
house-of-lore的利用,就可以将这个name的地址申请过来,并且由于之前的实验中的堆块并没有伪造size位,只需要伪造fd、bk即可,所以我们接下来就对其进行伪造。 - 首先我们已经将堆块的地址泄露出来了,泄露出来的同时我们要修改处于
smallbins中堆块的bk指针。我们先来修改一下 - 这时我们修改的堆块就会出现一个问题,就是我们只要修改
bk指针,我们会顺手把fd指针也被修改了。在写这题的时候我有注意到这一点,但是后面发现,在house of lore中修改smallbins中的fd指针貌似不会对这种堆利用方式有什么影响

-
查看
glibc源码时发现,在malloc中只进行了bck->fd != victim检查,在free中才对fwd->bk != bck进行检查,而house-of-lore利用中我们后续并没有再释放堆块,所以并不会调用free,所以并不需要关系fd指针。 -
现在我们就开始进行
house-of-lore的堆块伪造,由于之前我们确定name这个.bss段,但是我们只能对.bss这个段写入0x20的数据,所以我们要在利用name-0x10这个字段,伪造fake_chunk1的prevsize和size字段。

- 这时我们就通过
change_message修改放在smallbin中的chunk。


1 | payload1 = b'a' |
- 现在我们在修改
bk指针的同时就已经把堆地址给泄露出来了。现在我们就可以使用change_name伪造堆块,由于输入字节数的原因,我们就可以进行如下操作,将fake_chunk2与fake_chunk1的bk指针共用一个内存空间。
1 | payload = p64(heap_addr-0x10)+p64(0x6020A0+0x8) |


- 这时我们再进行两次申请,就可以将
name申请回来,并且可以使用edit编辑,编辑到page_list这个数组
1 | add(0xb0) |
- 这时我们就还差libc地址没有泄露,在做这题的时候一直卡在泄露这块,看了
wp才发现,可以这么泄露:- 我们可以修改我们申请回来的
name这个空间溢出到page_list[0],将这个地址修改为free_got的地址 - 然后再修改
page_list[1],将其地址修改为puts_got表 - 再一次通过
edit修改这时我们修改的时free_got表存储的值,将其改为puts_plt表,这时我们调用 - 最后我们再
free(page_list[1]),这时传递的是puts_got表的地址,该地址存储着puts的地址 - 我们
free(page_list[1])实际上是puts(puts_got),这时我们就泄露了libc的地址
- 我们可以修改我们申请回来的
1 | free_got = 0x602018 |
- 最后我们再劫持
atoi_got,将其劫持为system的地址,之后我们在read_int的时候直接输入/bin/sh\x00,这样就可以直接getshell
1 | dele(1) |
level_1_exp
- exp如下:
1 | from pwn import * |
house-of-lore_level2

