MRCTF PWN、RE Writeup
RE
Real_CHECKIN
很标准的签到题,换表base64
复制出base64表,用python编写解密脚本。
|
|
Dynamic Debug
长度检测
动态调试发现程序实际上限制了输入的flag长度为0x20,所以我们先伪造一串长度为32的字符串来绕过这个检测,这里用python生成了字符串为**‘a’ * 0x20**
动态调试
重新运行程序,并输入正确长度的flag。
继续运行后执行到后续的函数位置,发现IDA 无法显示伪代码,猜测是相关代码被混淆了,导致IDA无法识别。
我们手动单步到关键代码处。
在跳转多次后,我们成功来到了一处看似很像关键代码处的位置。
伪代码修复
在这部分代码的头部按P键定义为函数,接着尝试查看伪代码,得到的伪代码没有变量识别,非常难看。
于是我们尝试修复堆栈信息,让IDA能够识别出来变量。
如图所示,我们可以尝试着在这部分之上使用Keypatch手动加入一个push rbp; mov rbp, rsp
让IDA能够识别出堆栈上的变量,紧接着再F5,就可以看到比较舒服的伪代码了。
加密算法分析
通过循环执行了32次,并且在循环内部对一个变量反复增加delta常数(0x9E3779B9),循环内部出现了TEA运算逻辑等特征,我们可以看出这部分内容实际上就是一个TEA加解密。在加密之后,把加密后的内容与储存在段上的数组内容进行比对,如果比对通过应该就可以通过验证,所以我们首先要提取出这部分的数组内容,这里通过LazyIDA插件导出DWORD数组数据。
最后手写一个TEA解密,把这个数组的内容解密出来就可以成功得到flag信息了。
解密程序
在数组结尾加一个0可以让%s输出的时候好看一些
|
|
EzGame
题目信息
个人来说最喜欢的一道题目,在比赛中也拿到了一血。
这是一个Unity制作的游戏(感觉是做过的比赛中遇到最能玩的一个游戏了)
GetFlag的条件是要求完全所有的任务。
而我们所有需要完成的任务有
我们查看所有的任务发现似乎是要把游戏打通,但是在比赛中这种做题方法对于我这种手残党来说一定是不可取的(划掉)。
解题思路
所以我们可以考虑找到内存中这几个变量的地址,然后修改内存中这几个变量的值,来达到Cheat的目的。一般来说,Unity的程序由于是C#开发的缘故,我们可以直接用Dnspy反编译程序来分析即可,但是Unity程序为了防止大量的外挂和盗版游戏,推出了一种il2cpp的操作,根据我的理解就是让C#的中间代码IL转换成C++来编译,最后生成汇编程序使得我们的反编译难度大大增加,不过幸亏在网上有大量这有关的资料和现成的程序,其中我认为非常有用的一个就是https://github.com/Perfare/Il2CppDumper/
我们利用这里的程序可以解析出一些函数的信息和静态变量地址偏移,这些信息对于我们后续分析有大量的帮助。
Il2CppDumper
这里打开Il2CppDumper.exe程序依次选择GameAssembly.dll文件和global-metadata.dat文件,发现很幸运的可以直接解析程序
接着我们可以在目录下找到几个文件,根据程序作者来说是用于IDA恢复符号信息用的,但是由于这道题加上了TMD壳,所以这对于我们不脱壳来分析是没啥用的。
我们用Dnspy打开DummyDll目录下的Assembly-CSharp.dll,来观察符号信息
可以发现这里存在我们需要修改的任务信息,但是由于这些信息都是放在一个class里面的变量,我们无法直接确定地址信息,所以我们需要先定位到这些变量的位置,这里我想到的是用CE来搜索这些变量中的某一个,然后利用这个变量来定位到其他变量和这个对象。
使用CE定位对象
这里我就来搜索tokenGet这个变量,这个变量储存的就是我们吃了几个这个钻石。
经过几次数量变动之后,我确定了一个地址
使用CE的结构分析功能,并手动输入在Dnspy中分析得到的结构信息。
Cheating
修改对应的值来完成任务
进入游戏查看进度
尝试GetFlag却被提示cheating
重新思考
这意味着一定在哪一步中存在着检测,于是我们尝试寻找是哪里出现了问题,在游戏的过程中发现在得到钻石的时候key的值会发现变化。
于是我猜测key的值应该是得到一个钻石会运算一次,这样运算105次最终得到最后的key信息才能解密flag,所以我们应该要分析得到程序运算key的方式,这样就可以还原出正确的key信息
调试游戏
由于程序使用了TMD壳进行反调试和防静态分析,所以我们需要对CE进行一些设置。
在这里设置使用VEH调试器
接着上面的部分,我们把key的地址加入到分析框处,并且右键“找出是什么改写了这个地址”(F6)
接着回到游戏来吃一个钻石,触发改写地址。
就可以找到对应的修改位置
发现上部分确实存在对key字节的一些移位操作,但是我对汇编代码的分析能力有限,所以这里想看伪代码,这里利用到了一些绕过壳的小技巧。
Dump程序 & IDA分析伪代码
使用火绒剑来对这块内存进行转储
再把转储的内容拖入IDA中进行分析,但是由于IDA分析的地址与调试中的不一致,我们这里直接搜索这个函数头部的字节码信息。(记得勾选Find all occurrences)
定位到函数的头部信息
找到对应代码后,双击进入查看,IDA没有识别出这个函数的信息,我们要按C手动转换数据为代码,再按P转换为函数,接着按F5观察伪代码
这一块实际上就是操作key的逻辑,我们整理后编写代码,其中0xDEADBEEFAA114514是key的初始值。
|
|
Get Flag
运行程序后后得到0xEA8451453BD5B7DD,我们再用CE将游戏中的key修改即可得到flag。
MR_CheckIN
一道安卓题目,出题人很贴心的给了x86的so文件,使得可以让调试器也能做这题。直接拖入JEB进行分析
程序逻辑分析
第一部分
观察程序逻辑,首先程序要求用户名为MRCTF,接着对密码进行检测,要求密码以MRCTF{开头和}结尾,并且在这之间的内容分为两部分进行检测,第一部分是进行了一个md5加密。
我们只需要对md5字符进行查询即可
第二部分
第二部分的检测看似很简单
但实际上随机数种子在开始执行的时
jni.test()中被偷偷替换了,而这个函数指向的是so中的native函数。所以我这里采用动态调试分析来得到xor异或数组的内容。
最后编写程序来解密flag,由于刚开始编写代码的时候用的是java,所以最后也懒得换了,用的也是java。
解题代码
|
|
程序输出了_check1n_welc0me_to_MRCTF,我们和之前得到的进行合并,得到flag MRCTF{Andr01d_check1n_welc0me_to_MRCTF}
MR_Register
这道题蛮有意思的,所以这里会写的详细一些。
程序是一个典型的双进程保护(Debug Blocker)
双进程保护,实际上是程序本身作为一个进程或一个调试器,并且在调试模式下运行自身程序。所以这种保护通常就会存在两个进程。 这种程序的技术特点是 1.无法被调试,因为程序本身也是一个调试器。我们又知道一般情况下一个程序只能被一个调试器所调试,如果他的程序先抢占作为了调试器,那么就无法进行调试。所以解决办法只能是在他的调试器附加之前你先开始调试。 2.一般来说,为了防止你直接抢占调试来绕过,他还会加一个异常处理函数,程序中原本存在一些不合理的代码或者INT3断点,当他的调试器处理的时候会去做一些指定的流程,而你作为调试者,在调试过程中就无法处理那些代码。
调试器代码逻辑
根据上面所说的我们首先应该找到他的调试器代码,并手动还原代码
我们顺着调试器逻辑找下去,可以很轻松的找到调试器逻辑相关的代码
我们可以注意到一处在调试器代码中用于解密的代码,这个异或操作使得我们的静态分析出现问题,我们在这里可以尝试使用IDAPython来还原这部分的异或代码。
|
|
主程序代码逻辑
还原后,我们就可以得到一份比较舒服的伪代码。
伪代码虽然有些地方分析的有些问题,但是大致逻辑是正确的。
大致逻辑
1.输入邮箱地址和验证代码
2.把编码后的邮箱地址作为xor数据来异或两次编码后的验证代码,并储存在temp目录下的sign文件中。
储存前的异或:
取出时的异或:
因为异或的内容是编码后的邮箱,而且经过两次的异或,邮箱的变量已经消去了,邮箱的信息为什么都不影响到最后的结果。
于是我们只需要考虑的是如何解密两次编码后的验证代码即可。
3.取出sign文件的内容,异或编码后的邮箱地址,并对验证代码进行比对,查看与程序中储存的是否一致。
其中编码后的邮箱和异或后的代码用横杠进行分割。
编码操作
第一次编码
实际上就是把当前编码的值与前面两个的值相加(如果当前编码的是前两项则不考虑),并且用%x#的格式输出。
第二次编码
这个类似于base编码,但是我发现这个实际上可以直接爆破,因为每次只有一个encode1_data[i]来进行运算,所以我这里为了省力,对其直接进行了爆破。
异或编码
根据上面所说的,这里的异或加密并不是重点。
数据块解密
这部分内容实际上应该放在调试器代码逻辑中的,但为了更好的模拟做题过程,我放在了这里,实际上在做题过程中,我分析到这里之后卡了很长时间,因为根据我的理解,这部分内容进行加密之后进行对比的信息(dword_405020)应该是%x#的格式,但实际上并不是。
在我研究之后发现,实际上是我漏看了一部分的调试器代码逻辑
这个函数无法直接查看伪代码,需要手动在这里把这块地方定义为函数即可查看
在调试器逻辑的这一部分中存在对dword_405020的异或解密。
我们可以简单的编写解密代码,不过注意要考虑到隐含的类型转换。
解题代码
|
|
以上程序执行后可以得到%x#格式的序列,我们再用python进行解密还原
|
|
PWN
比赛中遇到了很多其他事情,导致PWN部分都没怎么看,只做了一道题,赛后有时间我再复现一下
8bit adventure
题目分析
这道题我觉得质量很高,很有意思的一道题目
整体逻辑就是这样,在运行时候会用mmap申请一块可执行的内存,并且memset填充成0x90(NOP)
同时沙箱限制了无法使用execve,我们只能考虑使用orw。
题目的大致意思就是,使用单字节的指令(除了int 0x80)来执行shellcode来操作出实现orw。并且关闭了输入流,使得我们无法使用shellcode读入一段新的shellcode来执行这种非预期。
可用的汇编指令
指令的翻译我使用的是https://github.com/wjhwjhn/DisAsmVS这个项目,如果喜欢的话可以点个Star。
|
|
实际上,大部分对寄存器的操作我们都有,并且我们可以借助原有的NOP指令来构造出像
ADD AL, 90h这样的指令,不过这道题由于对长度限制很宽泛,所以这样的技巧实际上无关紧要。
构造OPEN
首先open的第一个参数就是要输入一个内存上的地址,并且这个地址保存的内容是flag。
这里使用MOVS 和 STOS都能够实现这个操作,我这里使用的是STOS,实际上这个指令所干的事情就是把eax的内容赋值给[edi],并且让edi + 1这样我们就实现了从寄存器地址到内存的转换。
我代码是在shellcode的一块已用的区域写入flag\x00字符串(注意需要写入\x00,否则字符串读出不正确)
OPEN参数对应的寄存器信息
|
|
构造READ
实际上难点就在构造OPEN上,构造READ的时候需要注意,由于之前代码中的close(0),所以上面open打开的fd实际上是0,我们需要使用0来当做fd,实际上我们也可以直接利用shellcode来push eax;pop ebx。
READ参数对应的寄存器信息
|
|
构造WRITE
这一部分是最省力的,因为只需要修改fd的值即可,其他信息和READ中的一致
WRITE参数对应的寄存器信息
|
|
EXP
|
|
总结
就我所做的方向而言,题目质量非常高,能够感受到出题人是花了很多时间去琢磨和完善的。也能够体会到题目非常的具有技术含量(难),但是打比赛不就是为了学习嘛,我个人来说是更喜欢难一些的题目的,通过打比赛来学习到更多的知识。非常希望明年还能够参加MRCTF 2022,祝MRCTF越办越好,战队实力也越来越强!