强网杯2022 部分赛题复现
GameMaster
C#逆向,用dnspy打开.exe文件,找到入口点main
函数
1 | private static void Main(string[] args) |
注意到文件一开始读入的一个文件gamemessage
,然后把它存到一个叫Program.memory
中,接着往下浏览,发现没有什么关键代码,只是一些纸牌操作,考虑gamemessage
的大小为12KB,且不可运行,用010editor打开也是乱码,应该是通过某种加密得到的,这个加密应该是对Program.memory
来进行操作的。
往下翻阅main函数代码,进入verifyCode
函数
1 | private static void verifyCode(ArrayList arrayList, Game game) |
发现该函数调用了goldFunc
函数,继续跟进,发现一堆if
里面有俩疑似加密代码
发现是对Program.memory
进行一个异或34的操作。
这玩意有Mode,而且也是对Program.memory
进行的操作,应该是AES的ECB加密。考虑写脚本先解密gamemessage
1 | from Crypto.Cipher import AES |
输出文件gamedata
就是解密后的文件,我们再用010editor打开,发现两个熟悉的字符
考虑把前面的内容删除,这还是一个.net文件,接着扔进dnspy打开,找到T1这个类
1 | using System; |
发现这个加密函数没法直接逆,考虑到check1
中的keystream已经知道了,所以用z3直接解x, y, z
,然后再扔到ParseKey
函数中即可得到flag
1 | from z3 import * |
运行得到flag{Y0u_@re_G3meM3s7er!}
EasyRe
先用finger跑一下恢复符号表,可以看到调用了一个fork函数,这个fork函数应该就是sub_402150
中的那个ELF文件,名字叫re3
,我们可以dump出来或者直接上动调然后让程序自动生成
1 | __int64 __fastcall sub_4021BB(__int64 a1, __int64 a2) |
fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
(1)在父进程中,fork返回新创建子进程的进程ID;
(2)在子进程中,fork返回0;
(3)如果出现错误,fork返回一个负值。
在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
引用一位网友的话来解释fork函数返回的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的fork函数返回的值指向子进程的进程id, 因为子进程没有子进程,所以其fork函数返回的值为0.
那么main函数的逻辑就是:
fork函数在主进程返回的是子进程的ID,在子进程返回的是0
- 所以在主进程我们会进入 if 分支
- 在子进程会进入 else 分支
else分支有一个ptrace )函数,是用于子线程和主线程之间通的通信,其最重要的是第一个参数,我们按M可以找到其枚举PTRACE_TRACEME
。
主进程内容比较复杂,先看前面的几行代码
1 | wait(a1, 0LL, 0); |
发现首先是一个wait,等待子进程发送某信息才开始继续执行下面的内容,然后是sub_4017E5
中有一串字符值得注意
这个暂时还不知道有啥用,先放在一边,下面分析子进程。
对这个这个没分析的数据按两下C再按P就能找到start函数,main函数也是一堆未分析的数据
对着第一行按个C就能全分析出来了,再按P,得到C代码
1 | v14 = 1; |
第一个sub2090可以直接构造函数反编译
1 | int __fastcall sub_2490(const char *a1) |
发现是一个判断输入(长度为25*25)是否是'0'
或'1'
,并且把这个输入减去'0'
存到byte_55C0里面,即输入一串二进制数存到55C0中。
但是loc_21F9
却无法修复应该是一个SMC,但是由于这个dump出来的文件没法直接动调,应该是主进程中的某一步给这个子进程进行了调试然后又用了某些操作把这个SMC解出来的东西又给加密了,但是留意到后面*((char *)&savedregs + 25 * v13 + v11 - 0x28F)
和*((char *)&savedregs + 25 * v12 + v10 - 0x50F)
分别减去了0x28F和0x50F,这里是一个寄存器混淆,由于savedregs
的基址是0,我们减去0x28F和0x50F,分别可以算到之前的v8和v9两个地址上。
所以这里的loc_21F9
应该是把输入读入进去,出来v8和v9进入后面的check,那么这个函数应该就是关键的加密函数。
可以看到这里有一个int 3,异常断点,同时又发现这个0CAFEB055就是主进程中sub_401F2F
的异常值,该处的异常值是第一个if中的值,进入的第一个if语句中的函数。
由于不知道这几个函数在干嘛,我们考虑用动态调试,注意这里动态调试需要设置参数,具体多少随意。
我们一路调到MD5加密函数处,这个v24的值就是我们之前找到的那个字符串的值
后面这个for循环应该就是把加密后的MD5值存到v23数组里
而后面有一个异或运算,显示把v23转化成了一个长整型v27(比如v23为23271ceac9cf88e1
,那么v27就是0x23231CEAC9CF88E1
),然后和v29异或,然后通过ptrace复制回去
1 | v27 = sub_40B660(v23, 0LL, 16LL); |
v29我们可以看出是ptrace通信以后得到的一个返回值,这个返回值就是从re3子线程中提取的数据。
也就是说这个函数的目的就是从子进程中取出数据然后和之前我们存的那个字符串的MD5加密再转长整型得到的一个数进行异或。
因为有很多数据,一个一个手算有点麻烦,我们考虑到这个长整型存在RAX寄存器中,那么我们可以在图示的地方下断点后编辑断点,写脚本dump出每一次循环中RAX的值
得到所有异或值(加上之前的0x23231CEAC9CF88E1
,我们取出之后,再对re3中的数据上脚本进行异或解密即可。
1 | 0x23231CEAC9CF88E1,0x4d9403a34275494c, 0xf1ac2fea63c94ea9, 0xf32554baaf233dcc, 0xad5ea15de7bcf568, 0xabdfc454b2ec9fd0, 0xa5ee2b4680957b2b, 0xaf42f81128b7fb38, 0xca34bde4268cae3, 0x4ee274bc39f2d547, 0x53458e3ea10ab93b, 0x2e5fb32efac34cff, 0x99f8f6faa7a64aec, 0xef38004300eda44d, 0xee67c44e2bcd18fc, 0x9b1c209768ecb41e, 0xfae74344fcba3cdb, 0x62654e739151118d, 0xbfa53d12825ac60, 0x5fda7e9212d8d034, 0xe8e15b2ffd058214, 0x6258db99ec82ff1f, 0xc1f8d40001b68bf6, 0x6211d421f8ab1d50, 0xd25bc129ebbbd366, 0xaea9e2a30d3fcd24, 0x12e2013bc48da1de, 0x1db06bde7ca30286, 0x226499b91812859b, 0xb2b0d80d0f244ce4, 0xfba26ec5f66ad4a5, 0xef4975489b39baa5, 0x75da0adeb0d03511, 0xcbb9c9ef1c68088d, 0xb707f2ec82b077b8, 0x4989b97aadc513bb, 0x74c613b6d47fcde, 0x1d6396837a7ad9d8, 0x7f1a74782535fe54 |
同时我们发现之前的那串字符串前面的数据就是对应了re3中的addr
8723的16进制就是2213,写idapython脚本恢复数据
1 | Key = {8723: 2533025110152939745, 8739: 5590097037203163468, 8755: 17414346542877855401, 8771: 17520503086133755340, 8787: 12492599841064285544, 8803: 12384833368350302160, 8819: 11956541642520230699, 8835: 12628929057681570616, 8851: 910654967627959011, 8867: 5684234031469876551, 8883: 6000358478182005051, 8899: 3341586462889168127, 8915: 11094889238442167020, 8931: 17237527861538956365, 8947: 17178915143649401084, 8963: 11176844209899222046, 8979: 18079493192679046363, 8995: 7090159446630928781, 9011: 863094436381699168, 9027: 6906972144372600884, 9043: 16780793948225765908, 9059: 7086655467811962655, 9075: 13977154540038163446, 9091: 7066662532691991888, 9107: 15157921356638311270, 9123: 12585839823593393444, 9139: 1360651393631625694, 9155: 2139328426318955142, 9171: 2478274715212481947, 9187: 12876028885252459748, 9203: 18132176846268847269, 9219: 17242441603067001509, 9235: 8492111998925944081, 9251: 14679986489201789069, 9267: 13188777131396593592, 9283: 5298970373130621883, 9299: 525902164359904478, 9315: 2117701741234018776, 9331: 9158760851580517972} |
然后经过修复以后得到关键加密函数(修复的时候可能有花,应该是analyze的时候有的数据分析出锅的导致不能patch,附近的语句多尝试几次就可以修复了)
1 | void __fastcall sub_21F9(__int64 a1, __int64 a2, __int64 a3) |
然后你妈这是个数织游戏,属实是给我整不会了。
数织游戏的规则是:有一个n*m的方格,给两个文本,一个存行的信息,一个存列的信息,比如第一行的信息是5,4那么就表示这一行有两串长度为5,4的格子需要被上色,第一列的信息为2,3,1就表示这一列有三串长度为2,3,1的格子需要被上色,那么结合行列的信息,我们就可以得出这个格子到底有哪些格子被上色了。
你以为到这就完了?
这俩byte_50A0和byte_5320也不能直接用,也是在init和preinit中被修改了(乌鸡鲅鱼了),
进入修复函数以后进入这俩函数,里面的数据才是真实数据,我们把它们dump出来,然后按每 25个数据取出非0的数字就可以了(第一个数据是显示当前有多少非0的数,也不需要)
1 |
|
找一个在线解密网站 Nonogram (handsomeone.github.io) 解密
然后艰难读出flag{I LOVE PLAY ctf_QWB 2022}
😓