写在前面
大四党最后一次在校内组打Hacker Game(然而一共也就参加了两次),如图所示,今年对crypto太不友好了,就只有一道RSA,其他都不算是传统的crypto,刚开始还在打柏鹭杯,后面没啥时间了索性就都没看了,这次就只更一些个人觉得有趣的题吧。意外的是我web小白,居然ak了web,今年web确实出得很照顾新手。
FLAG 助力大红包
经典拼夕夕doge,一共需要256位好友点击助力链接才能拿到flag,但是助力判定不同的区域是前8位ip域,所以应该只能上脚本,首先用burp suite抓包确定基本的包的格式,发现content段有ip=xxx.xxx.xxx.xxx,我们可以构造payload满足上面条件即可,但是发现会返回前后端ip不匹配,因此可以考虑用http的X-forwarded-for段进行伪造。交互脚本如下:
1 | import requests |
图之上的信息
GraphQl
API,直接搜,找到两篇不错的参考:GraphQL安全指北,Hack In Paris 2019 CTF – “Meet Your Doctor”。直接url+graphql?query=payload
进行查询
payload下面的字段可以查询所有可用对象
1 | { |
得到所有对象及其类型:
1 | {"data": |
构造payload为下面字段查询指定对象的所有字段(查询数据库内所有query函数的具体信息和用法)
1 | query={ |
我们得到下面结果:
1 | {"data": |
我们得到一个query函数为user:Get a specific user information
,而我们需要获取的是admin的邮箱,再查用户(Guser
)的字段:
1 | {"data": |
有privateEmail
字段,那么就可以利用user
这个query进行爆破,一般admin是第一位用户,试了下发现id=1返回正确结果,payload如下:
http://202.38.93.111:15001/graphql?query={user(id:1){privateEmail}}
得到flag。
Easy RSA
唯一一道crypto题。简单了解RSA和基本数论就能解,主要用到Wilson 定理,$(p-1)!\equiv-1 \ mod \ p$。我们需要求 $y\ ! \ mod \ x$。而且y特别接近于x,由下面式子很容易求出表达式:。其他的加密过程逆向很显然。解题脚本如下:
1 | import gmpy2 |
马赛克
可能是写出来的解法最复杂的思路,但应该是比较直接的一个解法,不用深度优先遍历,纯用子集和(subset)来直接解每个马赛克格子可能的状况。思路如下:
- 根据马赛克每块内像素点的值我们可以逆推出原来这个区块可能的像素值为255的个数p_sum,注意p_sum有两个或者三个可能值。
- 马赛克size为(23,23),而二维码块size为(11 ,11),每个马赛克块会与9个二维码相交。因此一个马赛克可以分为9个区域,像素点数分布为[p1,p2, p3,…,p9],这些区域内二维码块像素值一定相同。
- 根据已经有的二维码块的值恢复上面部分p1到p9的部分区域。
- 在恢复部分后,求解subset问题:集合[p1,p2, p3,…,p9] 使得 。注意,为了防止错误恢复,我们每一轮都只在没有多解的情况下,尽可能地恢复可以确定的区域:这意味着p_sum只有一个满足条件并且subset解唯一。注意到p1到p9中可能有相等的块,比如p1=p2,我们认为(p2,2)和(p1,2)是同一解,但是如果区域内还有p3=p1,我们不能确定p1,p2,p3的值,但是我们可以恢复其他区域的值。因为集合比较小,我直接采用了爆破的方式求子集和问题。
最后写了个及其冗长的代码如下(没把大函数分开,recover函数巨长):
1 | import math |
实际上只用了两轮就恢复到可以扫出二维码了。但是不能完全恢复,可以考虑之后用深优遍历完全恢复。
Minecraft
其实吧,我觉得这题应该放逆向的,不算web。进入游戏界面F12,看js代码,发现一个可疑js代码flag.js
,进去之后发现这个代码极其难读,简直混淆得亲妈都不认识了。可以考虑进行反混淆一下,至少看起来不那么别扭,推荐jsnice进行反混淆,结果如下(加了些调试用的console)。
1 | const _0x22517d = _0x2c9e; |
至少识别出来了一些函数的名字,另外变量也好看多了。此时我们再动态调试一下(单步调试看看它进了哪些函数,再console.log()疯狂输出局部变量)。我们发现它获取一个字符串,经过(function(saveNotifs, data) )
这个函数进行了加密,然后与6fbde674819a59bfa12092565b4ca2a7a11dc670c678681daf4afb6704b82f0c
比较。进一步跟进,我们发现1356853149054377是prekey
,进入之后产生了 extra[]这个数组,这个数组才是后面真正需要用到的key。最关键的加密函数就在code里面,明文8个字节一组,分为左右两个部分进行了加密,并且恰好就是Feistel架构,这样解密流程与加密基本一致,逆向完成。
逆向的code如下:
1 | function re_code(p, t) { |
超 OI 的 Writeup 模拟器(前两问)
IDA看逆向出来的函数发现做了混淆,看到这么多逆向文件,第一个想到的是用angr
做符号执行,然后简单试了下发现angr
是能够执行到目标地址的,而且很快(不用去混淆),但是你会发现没有stdin的输入,发现用了getline
,而且是C语言linux框架下的getline(不是C++)。而angr处理库函数的本质是用simProcedure去hook库函数调用,而angr的simProcedure是没有getline
函数的,所以它无法处理getline
的库函数调用,自然无法在stdin得到输入。这里给出两种解决方案:
1. hook getline
自己写一个简单的getline的simProcedure,我尝试魔改gets写了个,但是好像不太行,可以得到stdin输入了,但是写入的地址不太对(有兴趣的可以改一下),你可以在本地angr源代码 angr/procedures/libc
目录下查看angr实现的所有simProceduere函数,理论上其他的所有C++ 和未实现的C库函数你可以参考它们的写方法进行定义然后hook:
1 | class mgetline(angr.SimProcedure): |
2 . 直接Load内存
我们发现主函数里面调用getline之前把lineptr初始化了0(即NULL),正常来说我们是不能往地址0处写入内容的,但是angr是符号执行,它可以为所欲为,我猜测它调用不了getline
就直接跳过了,然后任意给v6赋值,在lineptr原本指向的地址0处任意写字符串,所以它才可以执行到我们预设的correct code
处。
那么我们找到正确执行的路径后,直接取内存0处的16个字节就能得到验证码了。(甚至不用做约束)
1 | import angr |
最后前面两题的自动化脚本如下:
1 | import angr |
第三问由于做了混淆,并且出现了函数递归导致angr符号执行一直跑不出来,所以不能用上面方法解。(后面一直在尝试去ollvm类的混淆,结束后出题人说有原创混淆,好的,打扰了orz)。如果有机会能用 angr hook去掉递归类的混淆,后面的题目也应该能用上面脚本解的。