0CTF/TCTF 线上赛刚刚结束,分享一下其中两道题的解法
Element
这题流程十分简单,先check格式,一共44位,flag{12位十六进制数a
-12位十六进制数b
-12位十六进制数c
}
之后字符串转整数,以及给出了a = 0x391BC2164F0A
接下来有一段不太常见的汇编指令punpckldq
subpd
pshufd
addpd
,查找资料发现应该是整数转浮点。
然后就是一堆数学算式的check。题目意思是:
- 已知三角形最短边a,内切圆半径r,外接圆半径R,求剩余两边b和c。
就难度来说只是高中数学的难度,列出两条方程求解。至于求解的方法,可以用z3,fsolve,Mathematical等等,甚至可以用一些高级计算器手动输方程求解。
至于精度其实没必要考虑的太多,因为上述工具解出来的结果基本正确,跟正确值的差距在1-2左右。可以自己写个脚本,把题目中的计算过程抄一遍看最后R和r的误差,在b和c左右两边浮动一两位,找到误差最低的那个值就是了。
Sanitize
这是道算法题,但是我也没搞清它的算法23333,因为我是爆破出的结果。
这道题比较特殊,不像常规的加密解密题有个常量对比,这题没有一个常量可以比较,因此不能直接爆破,而且长度很长,所以得用一些骚方法爆。这里分享一下我的爆破解题的思路。
程序流程
- 从同目录下的flag文件读取flag字符串。
- 输入一个字符串s,长度大于3小于32。
- 输入一个整数n,接下来的n行,每行接受一个整数输入,存入数组num。所有的num对flag长度取模,且不能重复。
- 接下来会新建一张类似图的数据结构,依次向图里面添加结点。结点的结构:
00000000 node struc ; (sizeof=0x20, mappedto_6)
00000000 ch db ? ; 该节点存放的字符
00000001 db ? ; undefined 没用
00000002 db ? ; undefined 没用
00000003 db ? ; undefined 没用
00000004 d dd ? ; 我也不清楚有啥用,大概跟变形有关
00000008 p0 dq ? ; 指向一个结点
00000010 p1 dq ? ; 指向一个结点
00000018 p2 dq ? ; 指向一个结点
00000020 node ends
添加结点分为两步:
(1).在一个恰当的位置插入新的结点,一般是根结点或根节点的下一个结点。
(2).根据每个节点数据的大小,对这张图进行修改与平衡
这里其实比较像红黑树或二叉平衡树,总之这张图一直都是在变化的。
重点:添加结点/修改形状的过程中必然有许多if else分支,在6030B8
记录进入每个分支的次数(关键,以此来计算/爆破flag),以及某些函数的调用次数(这里其实用处不大,一般都跟字符串s的长度或输入整数的数目n有关,对flag的内容没啥关系)
添加结点的顺序:先将输入的字符串按顺序依次添加,再按照num数组的内容,找到对应的flag字符添加。比如输入了 3 2 0 1,则依次添加'a','f','l'这三个字符
main函数结束后,进入函数sub_401980
,将6030B8
开始的每个字节输出。(这里的指针qword_603230
确实是指向6030B8
的,在程序最开头有定义)
解法
理清整个流程,思考解题的方法:
分析清题目的算法,根据每个分支的不同计算出flag。
然而我个人水平有限,在分析算法过程中耗费太多时间,也没有分析出个所以然,只能摸清个大概。
常规方法走不通,想想其他方法。
思考:每次加入结点的顺序都相同,只有最后一个(或其中某一个)加入的结点不同,返回给我们的表应该是不同的。我们可以选一段字符串输入,然后再依次加入结点'f','l',以及一个不同的字符结点,看看输出的内容有何变化。
先打本地找找规律。
from __future__ import print_function
from pwn import *
from time import *
context.log_level = 'error'
seed = int(time())
def rand():
global seed
seed = seed * 0x343FD + 0x269EC3
return (seed >> 16) & 0x7FFF
flag = 'flag{'
payload = ''
for j in range(5):
payload += chr((rand()%(0x7f-0x20))+0x20)#随机生成一段字符串,测试下来发现长度为5时变化比较大
for i in range(0x20,0x7f):
f = open('flag','w+')
f.write(flag+chr(i)+'a'*20) #第六位为测试的字符,每轮写入不同的字符
f.close()
payload += '\n'
payload+=str(3)+'\n'+str(0)+'\n'+str(1)+'\n'+str(5)+'\n'
p = process('./sanitize')
p.send(payload)
s = p.recv()
p.close()
#对接受到的数据稍微处理一下输出。大部分都是根字符串s的长度/整数数量n有关,或是不变的1或0,比较有用的就38,39,42
t=[]
tt = []
for j in range(len(s)/8):
t.append(s[j*8:j*8+8])
# print(t)
for j in range(len(t)):
temp = 0
for k in range(4):
temp+=int(t[j][2*k:2*k+2],16)*(256**k)
tt.append(temp)
print('%4d'%tt[38],end = '')
print('%4d'%tt[39],end = '')
print('%4d'%tt[42],end = ' ')
print(chr(i))
部分输出如下:
xd}zg #payload字符串内容
...前面都一样...
2 5 5 c
2 5 5 d←分界点1
1 6 6 e
1 6 6 f←分界点2
0 7 7 g
0 7 7 h
0 7 7 i
0 7 7 j
0 7 7 k←分界点3
1 6 6 l
1 6 6 m
1 6 6 n
1 6 6 o
1 6 6 p
1 6 6 q
1 6 6 r
...后面都一样...
上面这个例子可以发现,在这种payload(随机生成的)的情况下当第六位字符小于f或者g到k或者大于l,会有三种不同的输出,这样我们就有了三个区间,来区分三个区间内的字符了。
由于构造的payload不一样,分界点也不同,因此理论上可以构造出(0x7f-0x20)种不同的payload组合,就能鉴定每一个字符了。
由于不懂原理,因此只能粗暴的使用随机数构造字符串,尝试找到不同的payload能够区分每一个字符。
于是有了下面一段代码:
from pwn import *
from time import *
context.log_level = 'error'
seed = int(time())
def rand():
global seed
seed = seed * 0x343FD + 0x269EC3
return (seed >> 16) & 0x7FFF
flag = 'flag{'
check0 = []
compare = []
g = open('payload','w')
h = open('check','w')
for i in range(0x20,0x7e):
while(True):
f = open('flag','w+')
f.write(flag+chr(i)+'a'*20)
f.close()
payload = ''
for j in range(5):
payload += chr((rand()%(0x7f-0x20))+0x20)
payload += '\n'
payload+=str(3)+'\n'+str(5)+'\n'+str(1)+'\n'+str(0)+'\n'
p = process('./sanitize')
p.send(payload)
s = p.recv()
p.close()
f = open('flag','w+')
f.write(flag+chr(i+1)+'a'*20)
f.close()
p = process('./sanitize')
p.send(payload)
r = p.recv()
p.close()
if s!=r:#第五位为i和i+1时,返回的结果不同
g.write(payload)
h.write(s)
check0.append(s)
print(chr(i))
break
g.close()
h.close()
这样我们得到了一组payload,他能区分从0x20到0x7f的每一位,使这一位和下一位返回的数据不同,返回的数据保存在check内。
这样理论上我们就能够区分每一个不同的字符了。尝试写爆破脚本:
from __future__ import print_function
from pwn import *
from time import *
context.log_level = 'error'
flag = 'flag{this_is_a_testing_flag_TESTING_0123456P)+_)!@#":}'
f = open('flag','w+')
f.write(flag)
f.close()
payloads = []
check = []
g = open('payload','r')
h = open('check','r')
for i in range(0x7f-0x20):
s = g.read(14)
payloads.append(s)
s = h.read(657)
check.append(s)
for i in range(5,0x7f):
for j in range(0x20,0x7f):
payload = payloads[j-0x20][0:8]+str(i)+payloads[j-0x20][9:]
p = process('./sanitize')
p.send(payload)
s = p.recv()
p.close()
payload = payloads[j-0x20+1][0:8]+str(i)+payloads[j-0x20+1][9:]
p = process('./sanitize')
p.send(payload)
r = p.recv()
p.close()
if s!= check[j-0x20] and r == check[j-0x20+1]:#这一个字符的payload返回数据相同,下一位不同,它有可能是此字符。
print(chr(j+1),end = '')
break
输出:
"!!""!""""""""!"!"!""!"""""""""&&&&&&&")+")!"#"&"
仔细分析:
我们有了对应的payload和check,假设第五位是't',它肯定能通过t的那一个check,但也通过了双引号"的check,才会有上面这种情况。也就是说我们拿到了一个必要不充分的条件。
另一方面想,对于不同的字符,能通过的check应当是不同的。
测试一下:
from __future__ import print_function
from pwn import *
from time import *
context.log_level = 'error'
payloads = []
check = []
g = open('payload','r')
h = open('check','r')
for i in range(0x7f-0x20):
s = g.read(14)
payloads.append(s)
s = h.read(657)
check.append(s)
g.close()
h.close()
check1 = []
for i in range(0x7f-0x20):
check1.append('')
print(check1)
print('flag{')
for ii in range(0x20,0x7f):
f = open('flag','w+')
f.write('flag{'+chr(ii))
f.close()
print(chr(ii),end=' ')
t = open('check2','a')
for i in range(0x20,0x7f-2):
payload = payloads[i-0x20][0:8]+str(5)+payloads[i-0x20][9:]
p = process('./sanitize')
p.send(payload)
s = p.recv()
p.close()
payload = payloads[i-0x20+1][0:8]+str(5)+payloads[i-0x20+1][9:]
p = process('./sanitize')
p.send(payload)
r = p.recv()
p.close()
if s!= check[i-0x20] and r == check[i-0x20+1]:
t.write(chr(i+1))
print(chr(i+1),end = '')
t.write('\n')
t.close()
print('')
部分输出
p "%(,/259>ADHQp
q "%(,/259>ADHQq
r "%(,/2579>ADHQr
s "%(,/2579>ADHQs
t "%(,/2579>ADHQt
u "%(,/2579>ADHQu
v "%(,/2579>ADHQv
w "%(,/2579>ADHQw
可以看到,对于字符't',它确实通过了t的check,但同时也通过了对其他一些字符的check。
同时可以发现,左边的字符和右边的字符串是一一对应的(或许会有重复,但一般就两到三个重复)。
有了一一对应的关系,就能爆破出flag了
from __future__ import print_function
from pwn import *
from time import *
context.log_level = 'error'
flag = 'flag{this_is_a_testing_flag_TESTING_0123456P)+_)!@#":}'
f = open('flag','w+')
f.write(flag)
f.close()
payloads = []
check = []
check2 = []
result = []
g = open('payload','r')
h = open('check','r')
t = open('check2','r')
for i in range(0x7f-0x20):
s = g.read(14)
payloads.append(s)
s = h.read(657)
check.append(s)
s = t.readline()
check2.append(s[0:-1])
result.append('')
flag = 'flag{'
for i in range(5,0x7f):
for j in range(0x20,0x7f-2):
payload = payloads[j-0x20][0:8]+str(i)+payloads[j-0x20][9:]
# p = process('./sanitize')
# p = remote("111.186.63.16",20193)
p.send(payload)
s = p.recv()
p.close()
payload = payloads[j-0x20+1][0:8]+str(i)+payloads[j-0x20+1][9:]
p = process('./sanitize')
# p = remote("111.186.63.16",20193)
p.send(payload)
r = p.recv()
p.close()
if s!= check[j-0x20] and r == check[j-0x20+1]:
print(chr(j+1),end = '')
result[i-5]+=(chr(j+1))
print('')
flag+=chr(check2.index(result[i-5])+0x20)
print(flag)
部分输出:
flag{this_is_a_testing_flag_TESTING_0123456P)+)!
flag{this_is_a_testing_flag_TESTING_0123456P)+)!@
flag{this_is_a_testing_flag_TESTING_0123456P)+)!@#
flag{this_is_a_testing_flag_TESTING_0123456P)+)!@#"
flag{this_is_a_testing_flag_TESTING_0123456P)+)!@#":
flag{this_is_a_testing_flag_TESTING_0123456P)+_)!@#":}
本地能打远程也能打,不过速度比本地慢,五分钟内应该能爆完= =
总感觉我的方法有些取巧,这题还有很多地方没搞懂,期待官方writeup以及各位大佬的讲解。