
MoeCTF 2023 "天网" 题解
“天网”
DESCRIPTION:
“狗子拿到了一个毛线球。狗子把毛线球抛了出去。毛线球在空中乱飞(?)。毛钱织成了一张网。网把狗子困住了。”
请找到合适的输入时机显示Good Job即为flag正确,
忽略其余异常!
忽略其余异常!
忽略其余异常!
C#创新逆向by koito,豆瓣不敢评分,dr3建议新生不要尝试此题,老手也请轻喷,咱们文明一点
给他一个C#,他能够摧毁整个地球。美国著名的五星上将麦克阿瑟曾说:“如果当时我面对的是天网,我的百万雄师将无人生还。”大型纪录片《天网传奇》即将播出。
个人觉得是很有意思的一道C#逆向题,也学到了不少东西,故专门一篇WriteUp来记录一下这道题的解题思路,已经尽可能写得特别详细了,非常适合刚入门的同学阅读。
解包/反编译
拿到题目后,64M的程序看着挺大。
如果不了解的话会认为这个程序就是这么大,估计直接给劝退了。但是熟悉C#的话其实能明白,这实际上是把.NET运行时和运行时库打包到一起了,等同于Java程序把JDK打包,也等同于:你的同学想让你运行他写的Python代码,结果把他写的代码和整个Python解释器打包发给你了(bushi),把库去掉的话其实程序本身并不大,下图为程序入口。
(如若列子不恰当,请师傅们轻喷)
那么要才能把他单独拎出来呢?C#专属的逆向工具特别多,最常见的就有DnSpy、ILSpy、Net Reflector以及JetBranis的dotPeek。但是怎么选工具也得看具体情况来分析,这里我很自然的选择了ILSpy,不为什么,单纯因为在这道题用ILSpy更方便,可以很自然的用它分析出来,而DnSpy并没有办法很自然的识别到这个程序,后面用DnSpy分析程序本体也是很眼瞎…恰好这道题用ILSpy逆向出来的项目工程文件无限接近原项目工程文件。
到这里,如果你选择了正确的工具,导出得到了想要的程序本体或者项目文件,那么恭喜你,你已经成功拿到了这道题的入场券。
题目分析
为了方便更改和调试,我在做这道题的时候直接把逆向出来的re-1项目整个拉下来了。
直接用Visual Studio打开项目,然后直接对着代码边改边调试
那么逐步分析,先看看是什么逻辑。
第一步,这里要输入一个Flag,然后判断
跟进FlagHelper
类很明显可以发现Flag
就是一个Base64编码的字符串
这里我把全部编码好的都还原了,方便代码审计
那么,我们要的Flag就是这个了?
1 | moectf{D0_y0U_6e1ieve_that_this_is_the_riGht_f1aG?_iYlJf!M3rux9G9Vf!Jox} |
很明显不是,我们先回到Main函数,看看我们把这个输入正确后,后面程序发生了什么。
代码执行到这里的时候,不用运行,我们都能知道,这个死循环,只有在当num2随机到0的时候,程序会抛出异常,按理说,他没有捕获异常,因此程序在这个时候就会抛出异常然后结束运行了。不过我们先尝试运行一遍,看看是不是这样的:
输入过后,程序还在运行,这个时候还在继续让我们输入,说明没完,不过逻辑并不在Main函数。但是我们刚刚不是把FlagHelper
类里面的所有字符串都解码了吗?我们刚刚发现这里输出的oh my god很明显是刚才FlagHelper
类里的代码,那它是怎么跳转过去的?我们继续审计一下代码
1 | static FlagHelper() |
我们来一步一步分析,首先输出了entered a new world是因为调用了当时在判断输入的时候,首次访问了FlagHelper
类,并调用了它的构造方法,后面的AppDomain.CurrentDomain.UnhandledException += delegate
这个地方,相当于是在这里使用了委托来捕获异常,也就是在之前出现了除数为0的异常,在这里被捕获了,因此执行了这个委托事件。按照逻辑,继续分析这个委托事件。
但是肯定会有不熟悉的师傅会问,什么是委托事件?,简述一下就是:委托事件是一种基于委托的编程模式,用于实现事件和回调方法。委托是一种引用类型,可以存储对某个方法的引用,并在运行时动态地调用该方法。事件是一种特殊的委托,可以在某个对象发生特定行为时通知其他对象。C# 提供了一些语法糖和库支持来简化委托事件的使用。
具体可以参阅文档:委托 - C# 编程指南 | Microsoft Learn
1 | Console.WriteLine("Supercat is trying to recover you! but it is hungry!"); |
再一行一行看,首先这里的obj,调用了CatFoodSeller
类的DoSomething
方法,其实不用那么麻烦搞清楚他到底在干什么,看一下这个方法都会返回什么。
1 | public static dynamic DoSomething() |
分析一下,这里出现了4个类型,一个CatFood
类,一个Exception
类,一个null
以及一个字符串,也就是说,obj可能是上面四个的任意一种。但是要注意,我这里是直接用ILSpy还原的项目,和原项目肯定有区别的,比如这里Process.GetCurrentProcess().ProcessName
我们这里就无法直接得到,只能去分析原始的程序文件,至少在这里并不能确认会返回什么,就假设这里四种都有可能,可以暂时先不管到底是哪一个,我们继续往下分析上面的那个委托事件。
1 | string text = Console.ReadLine(); |
注意这两行,这里有一个chosen
,是选择了第7
个往后的4个字符,如果没错的话,我们这里还要输入flag,盲猜一个前7个字符是moectf{
,也就是,这里的chosen
是截取了第二次输入的flag
(不算”moectf{
“)的前四个字符,那么这个chosen
是用来选择什么呢?
1 | if (Activator.CreateInstance((from item in AppDomain.CurrentDomain.GetAssemblies() |
这个地方使用了一个LINQ查询语句,来遍历当前的所有声明的类,注意where type.Name.EndsWith(chosen)
和i.Name.Contains("FlagMachine")
,这个地方相当于是在选择这个类名包含了flag
的前四位的这个类,比如说,我输入的flag是moectf{abcdedfhijk......
,chosen是从flag的第7
位开始选4个出来,也就是abcd
,那么他这里相当于是在选择一个FlagMachine_abcd
这个类来执行,如果能找到这个类,就调用这个类的SetFlag
和VmeFlag
方法;如果找不到这样的类,就输出hey bro, this is a fake flag machine! we are cheated!
现在回头来看,这样的类,在这里一共有9906个类,并且每个类的基类都是FlagMachine
,实现接口是IFlagMachine
是不是觉得非常麻烦?我怎么知道是哪个?不急,继续往下看代码他就究竟在干什么
1 | flagMachine.SetFlag(CatOfRX.FeedCat((dynamic)obj)); |
这不就联系上了嘛,此处调用了刚才的obj,那么我们继续看CatOfRx
类的FeedCat
返回了什么
1 | public static byte[] FeedCat(dynamic catFood) |
现在看来,其实他就是在判断我们的obj是什么类型,obj的4种可能类型就代表了这里FeedCat
方法可能会返回的4个值,也就是说,在SetFlag
方法传入的参数有4种可能,还是暂时放在这里,继续看接下来的VmeFlag
。
因为这里的VmeFlag
只能看到接口,但在实际调用的时候是一个类,也就是说,我们要去找VmeFlag
的实现。但是VmeFlag
的实现特别多…并不能确认具体是哪一个,我们先随便找一个类来假设是这种情况,比如这里找一个FlagMachine_zyoP
一直跟着每一个类的父类往下找,每一个类其实都把传入的这个flag
给异或了一次,
!可能会有人困惑这里的异或方法有些奇怪,这个异或不应该是直接用^吗?但是这里的异或操作实际上是正确的。因为异或的两个数据,一个类型是byte[],一个类型是long,而C#作为强类型语言,这俩不同类型的数据是没有办法直接运算的,这里出题人是为了方便,自行定义了一种方法来让这两个数据异或。
一直跟一直跟,到后面会发现这个东西:
是的,这里的IsRealFlag
非常显眼无不是在暗示你这个地方是校验真实flag
的方法。
那就看看它是怎么校验的叭~
1 | using System.Linq; |
先直接看一看IsRealFlag
方法回结果那一行,也就是在判断array
的元素要全部等于0
,才说明是真flag
。不过他这个异或方式写得也更迷惑了,如果我们简化一下,大概是这样一个算法:
1 | ResetState(paramaters); |
如果要返回去的算flag
话,我们其实只需要求出paramaters
参数,算法逻辑可以写成下面这样:
1 | public static byte[] GetFlag(byte[] paramaters){ |
不过这里有点绕,IsRealFlag
定义是这样的:
1 | bool IsRealFlag(byte[] flag, byte[] paramaters) |
但是在VmeFlag
调用它的时候,是这样调用的:
1 | KoitoMagicalShop.IsRealFlag(Encoding.UTF8.GetBytes(token), Flag) |
非常混乱是不是?怎么这么多flag
?主要是这里确实有点误导人,但是再往回看的话,其实IsRealFlag
这里定义的参数名称才是正确的。
先理清顺序。在IsRealFlag
当中的flag
参数,其实是对应了在委托事件里面的flagMachine.VmeFlag(text);
这一行,也就是我们输入的flag,而paramaters
参数对应了flagMachine.SetFlag(CatOfRX.FeedCat((dynamic)obj));
这一行。在KoitoMagicalShop
这个类里面,我们不难发现,paramaters
长度为8,恰好对应了FeedCat
方法返回的四种结果,长度均为8。
但是在VmeFlag
当中,传入的参数flag
,其实在N个FlagMachine
类经过异或后的结果。所以到了IsRealFlag
这里,这个flag
,其实是原来的 flagMachine.SetFlag(CatOfRX.FeedCat((dynamic)obj))
。
思路已经整理清楚了,也搞明白了我们究竟要输入的东西是哪些,现在要搞明白的其实要如何让IsRealFlag
的返回结果为true?
解题过程
这里我先提供我的解题思路:爆破。
爆破,爆什么?怎么爆?
我的选择是:两个都爆。
继续回到委托事件,我选择爆破这里的VmeFlag
和SetFlag
的传入的参数,为什么?
我们这样来看,在IsRealFlag
函数这里,已经非常清楚的写明了最后结果array
的所有成员均等于0,但是我们刚刚猜测了flag是moectf{xxxx
开头的,那么我们就直接找出这里的moectf{xxxx
刚好前11位结果等于0就行,别看11/72这个比例不是很大,但是用来限制筛选出我们想要的答案还是够用了,就算多解也不会出现9906个。也就是说,我们这里传入给SetFlag
的参数是可以爆破的。
在我们刚刚讨论了所有的可能性,SetFlag
有9906种情况,而VmeFlag
传入的参数却区区只有4种,总共要爆破的次数也就只有4*9906=39624次,这个规模还是算比较小的,短时间内是可行的。
所以,这里把Program.cs
改成这样来写:
- 注:C#在9.0过后引入了顶级语句,顶级语句从许多应用程序中删除了不必要的流程,具体参考顶级语句 - 不使用 Main 方法的程序 - C# | Microsoft Learn
1 | using System.Text; |
然后再把IsRealFlag
函数改成这样:
1 | public static bool IsRealFlag(byte[] flag, byte[] paramaters){ |
这里需要注意一点的是,建议把ButAnotherFlagMachine
和YetAnotherFlagMachine
里面的输出给删掉,不然在遍历的时候会很吵,就像这样:
建议修改成如下:
改好了,那么现在再运行一次:
运气非常好,只爆破出了一种可能性,那就是当flag以nUyn
开头,并且SetFlag
的参数是**mEow????**时,可以得到我们想要的答案,并且也得到了最后在校验flag时的paramaters
的参数。
根据之前我们对算法的分析,现在很简单了,再算回去我们就可以得到我们想要的flag了。再把下面这段代码添加到KoitoMagicalShop
类里
1 | public static byte[] GetFlag(byte[] paramaters) |
然后再到程序主函数里调用一下他就可以算出我们的flag了。
1 | using KoitoCoco.MoeCtf; |
总结
十分的创新,对新生来说也是一道很有意义的题,在做这道题的过程中也可以学到非常多的编程概念,例如异常处理、面向对象、委托事件等等,同时还能感受到C#的魅力~ 看似毫无关联的东西,但最后串联起来的感觉十分过瘾~~个人愿意打满分的一道.NET逆向题~希望可可能再多来点这种题QwQ