文章目录隐藏
各位好,我是KAAAsS。2022年的新年解谜红包也顺利的结束了~和往年一样,我也写了官方题解来解释解释今年的解谜红包。题目依旧在这里:https://redpacket.kaaass.net/。
今年的解谜红包继续了去年的形式,采用了分关卡的形式。这种形式不仅能给解谜提供更明确的指引,而且给题目的准备也提供了极大的便利。另外值得一提的是,今年是新年解谜红包的第五个年头!因此在谜题设置上,不仅吸取了去年的经验给出了更多指引(希望区别于CTF),还在谜题中保留了一些延续性。从结果上而言,第一关最终的访问人数到达了173。虽然数字上并没有去年多(去年同期为283),但是在答题率、红包领取率上都更符合预期了(时隔2年,终于再次有人解出)。因此我感觉今年是比去年做的更好了。
题目链接:https://redpacket.kaaass.net/problem/c9ad8ab4-3f0a-4297-8e0d-5f11bef341c5
本题目题面上是对去年#2题的延续。不过由于今年将它放在第一题,因此我也用心的装饰了下本题的门面。可以看到,今年的页面比起去年是有一个明显的提升的。截图无法展现动态效果,强烈建议打开网页深度体验动画。
初露端倪
颇能驾驭一种统一的设计语言
点击“开红包”后,将会显示一串红包码,但是文字部分被遮挡。
这是由于这一段文本的父元素设置了text-overflow: clip; overflow: hidden;
,因此,直接复制或使用调试工具就能获得元素完整的内容。
e901175a62f2-4b4f-b98f-ebeb323dfe85且请在以第一个字符为一计算的第八个字符的右边加一个半角横线…… 不同设备可能得到不同的结果
然而按照此指引并不能得到正确的答案。此处的提示有两个,一是不同的设备得到的结果并不一定相同,二是来自“活动规则”。
活动规则: 1. 用户点击“开红包”按钮即可获得红包码 2. 由于用户的显示设备宽度有限,因此页面的显示内容可能会经过一系列优化、简洁化处理 3. 平台保证且仅保证发出红包码,因设备问题导致红包码无法领取的,平台不承担任何责任
这说明宽度的限制并不止一处。通过对点击按钮时发出的请求进行观察,发现请求(stage1?w=66
)包含了参数w
。而且由于返回的结果末尾是省略号,显然后端也对返回长度进行的限制。查看页面关于请求相关的源代码,果然可以看到相关逻辑。该参数实际上是通过设备屏幕宽度(window.screen.width
)计算得到的。
let deviceWidth = window.screen.width;
let scaleRate = Math.log(Math.max(deviceWidth - 500, 1.0)) / Math.log(1100);
let w = Math.ceil(scaleRate * 36 + 30);
fetch(`./api/stage1?w=${w}`)
因此,尝试使用更大的w
发出请求,发现w
其实就是返回内容的长度。在120左右就能得到足够解题用的信息了:
e901175a62f2-4b4f-b98f-ebeb323dfe85且请在以第一个字符为一计算的第八个字符的右边加一个半角横线然后所有字母向右旋转(字母挖空之后内容移动)两格比如a-bcd就是c-dab然后就行了我超凑满一百字嗯啊啊啊……
根据题目说明,实现一个向右旋转函数就可以得到结果了。
def rotate_right_alpha(s, n):
"右旋的一种参考实现"
alphas = list(filter(str.isalpha, s))
alphas = alphas[-n:] + alphas[:-n]
p = -1
return ''.join(alphas[p:=p+1] if ch.isalpha() else ch for ch in s)
8000000px
的设备。因此对于不存在设备问题的各位,本题可以直接跳步。“你 KAAAsS 哥什么时候骗过你”。当然,如果你碰巧手头没有专业设备,也可以使用调试工具模拟-
将UUID分区后各自旋转w
也存在最大值。在解题过程中,共有4人成功发现题目链接:https://redpacket.kaaass.net/problem/f901175e-62e2-4a4f-b98f-bfeb323ebd85
本关NETA自近期对Github上Linux内核仓库的一次行为艺术,原始commit骗到了不少人。实际上,这是利用Github的机制,不同fork其实是原始仓库的分支。因此用commit hash也可以从原仓库获得这个commit(比如git fetch origin [hash]
)。不过题目提到了红包其实在下一个提交里,因此本题要求找到原Fork的仓库。
一个很直观的想法就是进行搜索。虽然Github搜索默认不展示Fork,但提供了fork:true
的选项。可惜,这并不适用于本题的情况。因为参考Github的文档,只有Star数大于父仓库的Fork才会开启代码搜索功能。那这个父仓库是最高的还是直接父仓库呢?很遗憾,经我实验,确实是最高的父仓库。也就是说,Fork的Star数要大于torvalds/linux。这对出题者来说颇具挑战,因此搜索肯定是吹了。
另一种办法就是一一穷举Fork了。因为Github的网页端只列出了100个Forks(我也确保了答案没有出现),所以需要通过Github的API来获取。那怎么判断呢?可以通过Github Commit页面的提示“This commit does not belong to any branch on this repository”所属的接口/{name}/linux/branch_commits/{hash}来判断。这也是大多数人解出此题的方法。
但如果只是这样,面对更早的Fork要怎么办呢?Linux内核有四万多个Fork,一个个爬取实在是没有效率。而且如果真的只有这种解法,那解谜红包也太没意思了。实际上,我们还能通过一些残留的信息,来缩小Fork的时间范围。
不如我们就以前些天的原Commit为例。由于Commit是携带了父Commit的信息的,因此发生提交前的历史是可追溯的,由此可以根据它与主分支共享历史的最后时间,推测出Fork的最早时间(不过,要确保Linus不会Hack自己的仓库)。由于它的最近一次提交就是Merge tag ‘for-linus’…,因此Fork不会早于2022-01-25T06:02:46Z
。
有了这个信息后,我们转而查看下一次Linux本人对仓库的Push,来确定Fork发生的最晚时间。因为如果Fork晚于Linus的下一次Push,那master分支被更新,Fork就会包含之后的Commit了。而由于Push包含两个时间点之间的Commit,因此master分支的下一个Commit并不是真正的最晚时间。这也可以通过Github的API来确定,是2022-01-25T16:31:34Z
。
有了这两个时间,且Github的Fork API可以按创建时间排序,我们就可以快速的找到两个时间之间创建的Fork,只有14个,范围已经大大缩小了。不过,我们还能进一步缩小范围。由于这个Commit本身的时间是2022-01-25T16:31:19Z
,因此Push的时间应该晚于它。再加上这个条件我们就可以发现,只剩下一个在2022-01-25T16:31:32Z
进行Push的仓库了(手速挺快)。而这就是这次行为艺术的“幕后黑手”:nytpu/linux。
但话说回来,其实Linux总共也只有四万多个Fork,全遍历一遍也用不了太久,好像也用不着这种方法。其实这个方法的真正应用也确实不是这样。在解谜过程中,其实有不少朋友问这个仓库是否可见。由于Github不允许Fork为私有,因此唯一的可能就是仓库被删除了。那删除了的Fork真的就没办法追溯了吗?当然有啦,答案就是通过API获得仓库的Push事件。即使是删除了仓库,但Fork事件也依旧不会被删除。而由于仓库事件众多,因此先定时间范围然后二分查找得到区间内的PushEvent就很有必要了。那有同学可能要问了,即使是能找到用户,但仓库都删了怎么看下一个Commit?答案是,通过用户事件API就可以得到用户Push的Commit Hash了,而同样这个Commit可以在父仓库中查看。这其实也是本题最完善的形态,但是这个难度如果放在#2也就太不讲理了。
不过,这个方法也并非万无一失。首先是最早时间,可以通过提前Fork再更新来把仓库创建时间错开。然后最晚时间也不一定,可以把近几次的Commit删掉,直到不是这次Push就可以了。同样Push时间的推测也不一定,因为Commit时间可以乱改。所以还是给Git配置个GPG来的保险啦。
sora-cyann
与穹妹头像不仅是出于我的个人爱好,其实还是因为红包平台规则页面的Banner就是一张alt
为Sora cyann
的穹妹,而且是同一张图。不过其实本题一开始打算的是用另一个账号,测试Fork搜索也用的是它。不过后来想想,还是保留了这个提示,让移动端(我试过FastHub)也能完成这个题目。感兴趣的话,可以按照文章中的提示找出这个账户题目链接:https://redpacket.kaaass.net/problem/1a138448-888b-4ce8-8ebf-0460fe577e67
本题的题意还是比较明显的,题目就指出了AES。在代码中寻找AES,发现出现在加密部分:
def encrypt_image(self, image_idx):
# 读入图片
path = f'{image_path}/{image_idx}.bmp'
with open(path, 'rb') as f:
data = f.read()
# 随机生成 Key
key = bytes(rand.choices(list(range(256)), k=16))
# 加密
cipher = AES.new(key, AES.MODE_ECB)
enc = cipher.encrypt(padding(data))
# 压缩后 Base64 编码
return base64.b64encode(zlib.compress(enc)).decode('utf-8')
程序的大概功能就是加密一张图片返回,然后生成若干个备选项以供选择。程序逻辑非常简单,也没有什么问题,随机数也是用os.urandom
初始化的。因此,重点还是AES的运用,这里选择了ECB模式。在Google上搜索“ecb image”:
而且题目的图片也正好全是BMP,所以解题方法就很简单了——直接把随便一个图的BMP头(前54字节)替换掉加密结果就可以了。肉眼对比一下,其实两个图的关系还是比较明显的(毕竟图是挑过的)。
原图
加密后恢复的图,叠加原图
最终完成所有关卡并留名的Dalao们如下,TQL!
nc
,也因此放到了#3。总体而言,今年的解谜红包我个人是很满意的(虽然也出了锅,不过倒也无伤大雅)。但是,后来有朋友和我交流说本次解谜红包太简单了、有点水。我开始其实是不以为然的,因为毕竟这是红包嘛,也不是打CTF比赛。但深思之后,我确实意识到有必要弄清楚解谜红包的意义。于是经过沉思后,我希望给出我自己的答案(因为这毕竟是我搞的嘛,233)。
解谜红包是怎么来的呢?我在第一年红包的题解中其实有致谢,最早是因为看到@SuperFashi佬的博客。后来朋友给我发了@CancerGary佬的红包,我做完之后发现确实很有意思。于是次日(大年初一)回家的路上用备忘录写下了那年的红包。当时参考了@SuperFashi佬的红包,设置了Stage形式的5关(甚至连解法也搬来了)。实现也不太难,全用PHP,第二天就发出来了。因为当时和群友交流的过程中产生了不少有趣的事情,甚至还追加了隐藏关(所有隐藏关都是发后追加的),而且大多都是私聊交流的,所以我就在题解中追加了很多“有趣的事”,希望没参加的朋友看题解也能得到乐趣。那年正好是高三寒假,初五赶完题解后就差不多收拾收拾润了。
第一年的红包包含了很多豆知识,也就是我说的“实用”。再加上趣闻、隐藏,所以大概我觉得光看题解也不会显得太乏味。于是,第二年的红包也是按着同样的思路设计的,但是塞爆。结果就是那年Stage1的通过都很少,搞得紧急在Stage2脸上追加了红包,续红包也是从那年来的。不过那年还是有dalao收了,所以我可能还没有意识到其中的问题。
第三年发生了很多事,其中最有意思的应该属我终于弄懂第一年群友说的“大过年还让我做CTF”是什么意思了。因为红包,我阴差阳错地离开ACM,成为了一名MISC手。也许对我而言,CTF比赛就是一个个红包(而且真能开出不少钱)。于是那年的题不仅精心巨大塞爆,思路还参考了做过的题,结果也就理所当然的寄了——红包无人解出、Stage3只有一个请求。这给了我挺大的打击。终于在那时,我意识到了“红包的意义在于被收”。有人回老家、有人工作一年难得休息,每个人都有自己的事情要做。搞一个打不开的宝箱不会让任何人开心,也决不显得自己聪明。
这一切都落地于去年红包发生的大变化:增加了若干红包、明确标出了关卡以便解题的导向、去掉了隐藏。虽然还是没人领到最后,但至少比去年好了不少。不过那年的题我也不尽满意,大多题目指引不太明显,甚至故弄玄虚。后来也有朋友跟我提过这事。于是到了今年,所有的题目都尝试增加了引导或标志。为了让题解也有趣,有些题(#2)甚至是先写题解再出的。
一路下来,解谜红包似乎一直都和“分享”离不开关系——始于感觉有趣的分享、题目希望分享一些“技术”、题解希望分享趣事,但乐趣却不总是在场。我很喜欢@SeraphJACK去年的红包,但反思今年的题目,好像确实少了点柳暗花明的乐趣(解法都比较“直”)。我是否在解谜红包上托付了太多呢?
一不留神竟然已经写了这么多,劳烦各位看了那么多无聊的话。给大家拜个晚年,祝各位新年快乐!明年见!至于明年的红包会怎样,会不会继续这个形式?会不会有隐藏?我也不知道。但如果可以的话,我希望它至少有趣。