2020-XNUCA Writeup(MISC)
pg: 题目还行,感觉做的时候挺有意思的,就是自己做题时太憨憨导致全部错失一血呜呜呜~~~
torch model
题目要求环境: python==3.7.7 、torch == 1.5.0
给了一个神经网络学习的模型训练python脚本(ipynb
格式),直接vscode打开可以自动装相关环境一键查看,vscode还是香的。
查看ipynb可以知道该模型的作用是识别题目给出的图片为flag
,flag
长度和sha256
已知。模型数据被保存为model_state_dict.pt
,并且中间有一段数据用随机数据进行了覆盖修改,查看题目给出的diff.png
以及torch.save
的源码可知:修改了torch
中保存模型数据信息的数据段,而且将模型数据字典中的键值名乱序保存进文件。pt文件中的数据用pickle
打包。
至此题目信息查看完毕,题目解法为修复pt文件数据,用torch读取模型识别flag。
这里我们用两个同长度的字符串代替原flag,重新训练两个模型,并将其保存为两个正常的pt文件。然后对比两个pt文件保存模型数据信息数据段的十六进制,发现除了serialized_storage_keys
的key
的信息,其余结构基本一致。所以修复只需要将其中的key
的键值名替换为题目文件中的key
的键值名。对比图如下:
提取题目模型文件和我们刚保存的模型文件中的serialized_storage_keys
数据,并解包替换重新打包写回文件,脚本如下:
1 | with open("./model_state_dict.pt",'rb') as model1: |
接下来是踩坑时刻。用此脚本复现该题目的话会发现修复之后的pt文件依旧不能被加载。
注意看脚本可以发现,pt文件数据虽然全是用pickle打包的,但是对于模型的obj
数据却没有选择解包替换,而是直接对被打包的数据进行了“带包”替换。这里如果有仔细分析torch.save
的源码的话,就会知道,官方对于obj的数据自定义了一套pickler检查规则,这里是因为结构信息几乎完全一致,所以就没有去读官方定义的那套规则。不过这样的话就需要注意修改obj
集合数据中打包的小结构体的一个关键数据:
图中选中部分是为一个被打包的小结构体,每一个小结构体都是obj
集合中的一个对象。结构体的magic header
是单个字节的十六进制\x71
,其后紧跟着的一个字节是这个结构体在obj
集合中的顺序序号,上图选中的即为\x2e
第46个结构体。这里注意下下图红色标记字节,从该字节开始的四个字节为pickle
打包数据中一个集合内相关对象的名字的长度,小端序。这里是\x0D\x00\x00\x00
,即长度为13,但是13其实是我生成新模型数据文件时新模型内的key
的名字长度,如果去观察偏移\x4ef
后的题目模型文件的key
数据,会发现这些9452xxxx
的键值名称长度其实都为14,这里因为数量较少,所以我手动修复了。此时pt文件中的模型关键信息已经修复完毕,torch已经可以解析加载pt文件。
(pg:这个长度不一样就离谱,第一天下午看题的时候一个下午都没发现自己把14数成13,结果别的师傅都把题刷烂了自己还纳闷题目文件为啥比自己生成的文件大了20字节,一直以为环境没配好反复配了几个小时环境,惨惨emmm)
但是,还记得题目图片中的random嘛,这个东西还没用呢,还有坑。此时直接加载模型会发现如下报错:
这里就是乱序储存导致torch
在load
模型训练数据时,serialized_storage_keys
中的每个键值对应的那一部分模型数据长度与现在实际load
解析得到的长度不一致导致的,这里它直接给出了应该是什么长度的键值。
RuntimeError: storage has wrong size: expected 120 got 10
120即为第一部分的长度,但此时第一个键值对应长度为十,这里可以直接去torch.load
的源码那里,用一个print
输出它解析到的obj
信息,从而得到每个数字键值名对应的对象的数据长度,然后将pt文件中\x4ef
偏移后的keys
集合数据重新排序复写,这里给出脚本,因为就十条,所以没搞自动化嘤嘤嘤QAQ
1 | with open("./model_state_dict_flag.pt",'rb') as f: |
至此,pt文件彻底修复,直接load然后加载图片即可获取flag
catchthecat
考察数据结构算法。(ps:千万别乱找网上的轮子!自己造的真香!)
两个脚本解决,第一个遍历迷宫获取迷宫地图,第二个求迷宫两点最短路径(此处顺便还要看点运气emmm)
首先题目源码给出,根据源码可知地图除了墙还有炸弹,遇见炸弹直接game over。所以写探索迷宫的算法时要考虑到重连问题。这里我用pwntools + 深度优先算法遍历迷宫数据。
1 | # -*- coding:utf-8 -*- |
遍历出地图之后,利用题目源码写抓猫算法。(ps:就是本地模拟服务器程序运行状态,random的seed是由连接题目程序时的时间决定的,seed已知,本地模拟服务器状态,深度优先求解PERSON
与CAT
的最短路径,这里因为嫌麻烦不想再该刚刚遍历地图的脚本了就选择了去网上找个现成的轮子,结果拿回来各种调试修bug,一个下午又被浪费了emmm 最后恼羞成怒又写了个轮子哭唧唧)
另外,出题人的服务器就离谱!!!此处疯狂diss出题人,第一天晚上做题的时候刚开始做就发现题目挂了,被迫睡大觉,第二天题目好了,但是服务器的时区延时是认真的么,不给时区信息的同时,服务器本地时间还比北京时间快了整整两秒!!!
其实本来网上的轮子也没啥,它的问题就那么一丢,本体调试几遍就能用了,但是跑flag的时候和服务器的输出一直对不上,结果一直在调试脚本找本地的bug,结果最后才知道本地已经完美了,数据对不上是服务器的seed
比本地延时了2s!!!淦!ping服务器,响应延迟只有不到20ms,当时因为想着出题人没给提示应该是默认北京时区互联网时间,所以做多也就试了把本地模拟服务器的时间种子值加1,依旧不对。最后突然试了下加2对了。不说了,我是fw TAT
1 | #!/usr/bin/python -u |