TCTF/0CTF两道逆向题详解
apeng CTF 8424浏览 · 2019-03-30 01:35

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,因为我是爆破出的结果。

这道题比较特殊,不像常规的加密解密题有个常量对比,这题没有一个常量可以比较,因此不能直接爆破,而且长度很长,所以得用一些骚方法爆。这里分享一下我的爆破解题的思路。

程序流程

  1. 从同目录下的flag文件读取flag字符串。
  2. 输入一个字符串s,长度大于3小于32。
  3. 输入一个整数n,接下来的n行,每行接受一个整数输入,存入数组num。所有的num对flag长度取模,且不能重复。
  4. 接下来会新建一张类似图的数据结构,依次向图里面添加结点。结点的结构:
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以及各位大佬的讲解。

1 条评论
某人
表情
可输入 255