MoeCTF 2023 “天网“ 题解

“天网”

DESCRIPTION:
“狗子拿到了一个毛线球。狗子把毛线球抛了出去。毛线球在空中乱飞(?)。毛钱织成了一张网。网把狗子困住了。”

请找到合适的输入时机显示Good Job即为flag正确,

忽略其余异常!
忽略其余异常!
忽略其余异常!

C#创新逆向by koito,豆瓣不敢评分,dr3建议新生不要尝试此题,老手也请轻喷,咱们文明一点

给他一个C#,他能够摧毁整个地球。美国著名的五星上将麦克阿瑟曾说:“如果当时我面对的是天网,我的百万雄师将无人生还。”大型纪录片《天网传奇》即将播出。


​ 个人觉得是很有意思的一道C#逆向题,也学到了不少东西,故专门一篇WriteUp来记录一下这道题的解题思路,已经尽可能写得特别详细了,非常适合刚入门的同学阅读。

解包/反编译

​ 拿到题目后,64M的程序看着挺大。

image-20230822150404655

​ 如果不了解的话会认为这个程序就是这么大,估计直接给劝退了。但是熟悉C#的话其实能明白,这实际上是把.NET运行时和运行时库打包到一起了,等同于Java程序把JDK打包,也等同于:你的同学想让你运行他写的Python代码,结果把他写的代码和整个Python解释器打包发给你了(bushi),把库去掉的话其实程序本身并不大,下图为程序入口。

(如若列子不恰当,请师傅们轻喷)

image-20230822151017988

​ 那么要才能把他单独拎出来呢?C#专属的逆向工具特别多,最常见的就有DnSpyILSpyNet Reflector以及JetBranis的dotPeek。但是怎么选工具也得看具体情况来分析,这里我很自然的选择了ILSpy,不为什么,单纯因为在这道题用ILSpy更方便,可以很自然的用它分析出来,而DnSpy并没有办法很自然的识别到这个程序,后面用DnSpy分析程序本体也是很眼瞎…恰好这道题用ILSpy逆向出来的项目工程文件无限接近原项目工程文件。

image-20230822152532871

​ 到这里,如果你选择了正确的工具,导出得到了想要的程序本体或者项目文件,那么恭喜你,你已经成功拿到了这道题的入场券

题目分析

​ 为了方便更改和调试,我在做这道题的时候直接把逆向出来的re-1项目整个拉下来了。

image-20230822151650211

​ 直接用Visual Studio打开项目,然后直接对着代码边改边调试

image-20230822152738920

​ 那么逐步分析,先看看是什么逻辑。

​ 第一步,这里要输入一个Flag,然后判断

image-20230822152956034

​ 跟进FlagHelper类很明显可以发现Flag就是一个Base64编码的字符串
image-20230822153041233

​ 这里我把全部编码好的都还原了,方便代码审计

image-20230822153127259

​ 那么,我们要的Flag就是这个了?

moectf{D0_y0U_6e1ieve_that_this_is_the_riGht_f1aG?_iYlJf!M3rux9G9Vf!Jox}

​ 很明显不是,我们先回到Main函数,看看我们把这个输入正确后,后面程序发生了什么。

image-20230822153252891

​ 代码执行到这里的时候,不用运行,我们都能知道,这个死循环,只有在当num2随机到0的时候,程序会抛出异常,按理说,他没有捕获异常,因此程序在这个时候就会抛出异常然后结束运行了。不过我们先尝试运行一遍,看看是不是这样的:

image-20230822153539981

​ 输入过后,程序还在运行,这个时候还在继续让我们输入,说明没完,不过逻辑并不在Main函数。但是我们刚刚不是把FlagHelper类里面的所有字符串都解码了吗?我们刚刚发现这里输出的oh my god很明显是刚才FlagHelper类里的代码,那它是怎么跳转过去的?我们继续审计一下代码

static FlagHelper()
{
    Flag = "moectf{D0_y0U_6e1ieve_that_this_is_the_riGht_f1aG?_iYlJf!M3rux9G9Vf!Jox}";
    Console.WriteLine("You have entered a new world!");
    AppDomain.CurrentDomain.UnhandledException += delegate
    {
        Console.WriteLine("Supercat is trying to recover you! but it is hungry!");
        object obj = CatFoodSeller.DoSomething();
        Console.WriteLine("oh my god! where am i??? but can you v me 50 to eat kfc crazy thursday? [Yes/No]");
        string text = Console.ReadLine();
        string chosen = text.Substring(7, 4);
        if (Activator.CreateInstance((from item in AppDomain.CurrentDomain.GetAssemblies()
                                      from type in item.GetTypes()
                                      where type.Name.EndsWith(chosen)
                                      select type).FirstOrDefault((Type i) => i.Name.Contains("FlagMachine"), typeof(FlagMachine))) is IFlagMachine flagMachine)
        {
            flagMachine.SetFlag(CatOfRX.FeedCat((dynamic)obj));
            flagMachine.VmeFlag(text);
        }
        else
        {
            Console.WriteLine("hey bro, this is a fake flag machine! we are cheated!");
        }
    };
}

​ 我们来一步一步分析,首先输出了entered a new world是因为调用了当时在判断输入的时候,首次访问了FlagHelper类,并调用了它的构造方法,后面的AppDomain.CurrentDomain.UnhandledException += delegate这个地方,相当于是在这里使用了委托来捕获异常,也就是在之前出现了除数为0的异常,在这里被捕获了,因此执行了这个委托事件。按照逻辑,继续分析这个委托事件

​ 但是肯定会有不熟悉的师傅会问,什么是委托事件?,简述一下就是:委托事件是一种基于委托的编程模式,用于实现事件和回调方法。委托是一种引用类型,可以存储对某个方法的引用,并在运行时动态地调用该方法。事件是一种特殊的委托,可以在某个对象发生特定行为时通知其他对象。C# 提供了一些语法糖和库支持来简化委托事件的使用。

具体可以参阅文档:委托 - C# 编程指南 | Microsoft Learn

Console.WriteLine("Supercat is trying to recover you! but it is hungry!");
object obj = CatFoodSeller.DoSomething();
Console.WriteLine("oh my god! where am i??? but can you v me 50 to eat kfc crazy thursday? [Yes/No]");
string text = Console.ReadLine();
string chosen = text.Substring(7, 4);
if (Activator.CreateInstance((from item in AppDomain.CurrentDomain.GetAssemblies()
                              from type in item.GetTypes()
                              where type.Name.EndsWith(chosen)
                              select type).FirstOrDefault((Type i) => i.Name.Contains("FlagMachine"), typeof(FlagMachine))) is IFlagMachine flagMachine)
{
    flagMachine.SetFlag(CatOfRX.FeedCat((dynamic)obj));
    flagMachine.VmeFlag(text);
}
else
{
    Console.WriteLine("hey bro, this is a fake flag machine! we are cheated!");
}

​ 再一行一行看,首先这里的obj,调用了CatFoodSeller类的DoSomething方法,其实不用那么麻烦搞清楚他到底在干什么,看一下这个方法都会返回什么。

public static dynamic DoSomething()
{
    j.ii1();
    j.iii1();
    if (j.i() || j.ii() || j.iii() || j.iii() || j.iiii() || j.i1())
    {
        if (Process.GetCurrentProcess().ProcessName == "feed rx's cat")
        {
            return "bad food";
        }
        return null;
    }
    if (Process.GetCurrentProcess().ProcessName == "feed rx's cat")
    {
        return new Exception("unexpected food");
    }
    return new CatFood();
}

​ 分析一下,这里出现了4个类型,一个CatFood类,一个Exception类,一个null以及一个字符串,也就是说,obj可能是上面四个的任意一种。但是要注意,我这里是直接用ILSpy还原的项目,和原项目肯定有区别的,比如这里Process.GetCurrentProcess().ProcessName我们这里就无法直接得到,只能去分析原始的程序文件,至少在这里并不能确认会返回什么,就假设这里四种都有可能,可以暂时先不管到底是哪一个,我们继续往下分析上面的那个委托事件。

string text = Console.ReadLine();
string chosen = text.Substring(7, 4);

​ 注意这两行,这里有一个chosen,是选择了第7个往后的4个字符,如果没错的话,我们这里还要输入flag,盲猜一个前7个字符是moectf{,也就是,这里的chosen是截取了第二次输入的flag(不算"moectf{")的前四个字符,那么这个chosen是用来选择什么呢?

if (Activator.CreateInstance((from item in AppDomain.CurrentDomain.GetAssemblies()
                              from type in item.GetTypes()
                              where type.Name.EndsWith(chosen)
                              select type).FirstOrDefault((Type i) => i.Name.Contains("FlagMachine"), typeof(FlagMachine))) is IFlagMachine flagMachine)
{
    flagMachine.SetFlag(CatOfRX.FeedCat((dynamic)obj));
    flagMachine.VmeFlag(text);
}

​ 这个地方使用了一个LINQ查询语句,来遍历当前的所有声明的类,注意where type.Name.EndsWith(chosen)i.Name.Contains("FlagMachine"),这个地方相当于是在选择这个类名包含了flag的前四位的这个类,比如说,我输入的flag是moectf{abcdedfhijk......,chosen是从flag的第7位开始选4个出来,也就是abcd,那么他这里相当于是在选择一个FlagMachine_abcd这个类来执行,如果能找到这个类,就调用这个类的SetFlagVmeFlag方法;如果找不到这样的类,就输出hey bro, this is a fake flag machine! we are cheated!

​ 现在回头来看,这样的类,在这里一共有9906个类,并且每个类的基类都是FlagMachine,实现接口是IFlagMachine

image-20230822155256602

​ 是不是觉得非常麻烦?我怎么知道是哪个?不急,继续往下看代码他就究竟在干什么

flagMachine.SetFlag(CatOfRX.FeedCat((dynamic)obj));
flagMachine.VmeFlag(text);

​ 这不就联系上了嘛,此处调用了刚才的obj,那么我们继续看CatOfRx类的FeedCat返回了什么

public static byte[] FeedCat(dynamic catFood)
{
    if ((object)catFood == null)
    {
        return Encoding.UTF8.GetBytes("Meow~!?!");
    }
    if (catFood is string text)
    {
        if (text != catFood)
        {
            return Encoding.UTF8.GetBytes("meoW?!~~");
        }
        return Encoding.UTF8.GetBytes("me0w~?!!");
    }
    return Encoding.UTF8.GetBytes("mEow????");
}

​ 现在看来,其实他就是在判断我们的obj是什么类型,obj的4种可能类型就代表了这里FeedCat方法可能会返回的4个值,也就是说,在SetFlag方法传入的参数有4种可能,还是暂时放在这里,继续看接下来的VmeFlag

image-20230822221429531

​ 因为这里的VmeFlag只能看到接口,但在实际调用的时候是一个类,也就是说,我们要去找VmeFlag的实现。但是VmeFlag的实现特别多…并不能确认具体是哪一个,我们先随便找一个类来假设是这种情况,比如这里找一个FlagMachine_zyoP

image-20230823152616106

​ 一直跟着每一个类的父类往下找,每一个类其实都把传入的这个flag给异或了一次,

!可能会有人困惑这里的异或方法有些奇怪,这个异或不应该是直接用^吗?但是这里的异或操作实际上是正确的。因为异或的两个数据,一个类型是byte[],一个类型是long,而C#作为强类型语言,这俩不同类型的数据是没有办法直接运算的,这里出题人是为了方便,自行定义了一种方法来让这两个数据异或。

​ 一直跟一直跟,到后面会发现这个东西:

image-20230823152309162

​ 是的,这里的IsRealFlag非常显眼无不是在暗示你这个地方是校验真实flag的方法。

​ 那就看看它是怎么校验的叭~

using System.Linq;
namespace KoitoCoco.MoeCtf;
public static class KoitoMagicalShop{
	public static int[] Params = new int[8];
	public static int[] States = new int[512];
	public static int[] MagicalDust = new int[72];
	public static void ResetState(byte[] p){
		int[] array = new int[512]{
            //省略数据
		};
		for (int k = 0; k < 512; k++){
			States[k] = array[k];
		}
		for (int l = 0; l < 8; l++){
			Params[l] = p[l];
		}
		for (int m = 0; m < 233; m++){
			GetNextSpellcard();
		}
	}
	public static byte GetNextSpellcard(){
		int num = 233;
		int[] @params = Params;
		foreach (int num2 in @params){
			num ^= States[num2];
			num++;
		}
		for (int l = 0; l < 511; l++){
			States[l] = States[l + 1];
		}
		States[511] = num;
		return (byte)num;
	}
	public static bool IsRealFlag(byte[] flag, byte[] paramaters){
		if (flag.Length != 72){
			return false;
		}
		ResetState(paramaters);
		int[] array = new int[72]{
            //省略数据
		};
		int[] array2 = new int[72];
		for (int k = 0; k < 72; k++){
			array2[k] ^= GetNextSpellcard();
		}
		for (int l = 0; l < 72; l++){
			array[l] ^= flag[l] ^ array2[l];
		}
		MagicalDust = array;
		return array.All((int i) => i == 0);
	}
}

​ 先直接看一看IsRealFlag方法回结果那一行,也就是在判断array的元素要全部等于0,才说明是真flag。不过他这个异或方式写得也更迷惑了,如果我们简化一下,大概是这样一个算法:

ResetState(paramaters);
for(int i = 0; i < 72; i++){
    var t = flag[i] ^ GetNextSpellcard();
    if(array[i] ^ t != 0){
        return false;
    }
}

​ 如果要返回去的算flag话,我们其实只需要求出paramaters参数,算法逻辑可以写成下面这样:

public static byte[] GetFlag(byte[] paramaters){
    byte[] flag = new byte[72];
    ResetState(paramaters);
    int[] array = new int[72]{
        //省略数据
    };
    for (int k = 0; k < 72; k++){
        flag[k] = (byte)(array[k] ^ GetNextSpellcard());
    }
    return flag;
}

​ 不过这里有点绕,IsRealFlag定义是这样的:

bool IsRealFlag(byte[] flag, byte[] paramaters)

​ 但是在VmeFlag调用它的时候,是这样调用的:

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

解题过程

​ 这里我先提供我的解题思路:爆破

爆破,爆什么?怎么爆?

我的选择是:两个都爆。

继续回到委托事件,我选择爆破这里的VmeFlagSetFlag的传入的参数,为什么?

我们这样来看,在IsRealFlag函数这里,已经非常清楚的写明了最后结果array的所有成员均等于0,但是我们刚刚猜测了flag是moectf{xxxx开头的,那么我们就直接找出这里的moectf{xxxx刚好前11位结果等于0就行,别看11/72这个比例不是很大,但是用来限制筛选出我们想要的答案还是够用了,就算多解也不会出现9906个。也就是说,我们这里传入给SetFlag的参数是可以爆破的。

在我们刚刚讨论了所有的可能性,SetFlag有9906种情况,而VmeFlag传入的参数却区区只有4种,总共要爆破的次数也就只有4*9906=39624次,这个规模还是算比较小的,短时间内是可行的。

所以,这里把Program.cs改成这样来写:

using System.Text;
using KoitoCoco.MoeCtf;
using System.IO;
using System.Linq;
using System;

string[] s = File.ReadAllLines("./dic.txt");
//这里的dic.txt是FlagMachine_xxxx后面的xxxx,遍历所有.cs文件得到的所有类。
string[] s3 = { "Meow~!?!", "meoW?!~~", "me0w~?!!", "mEow????" };
foreach (var s4 in s3)
{
    Console.Write(s4 + ":");
    int i = 0;
    foreach (var s2 in s)
    {
        i++;
        Console.Title = i + "/" + s.Length;
        string text = "moectf{" + s2;
        string chosen = text.Substring(7, 4);
        if (Activator.CreateInstance((from item in AppDomain.CurrentDomain.GetAssemblies()
                                      from type in item.GetTypes()
                                      where type.Name.EndsWith(chosen)
                                      select type).FirstOrDefault((Type i) => i.Name.Contains("FlagMachine"), typeof(FlagMachine))) is IFlagMachine flagMachine)
        {


            flagMachine.SetFlag(Encoding.UTF8.GetBytes(s4));
            flagMachine.VmeFlag(text);
        }
    }
    Console.WriteLine("done!");
}

然后再把IsRealFlag函数改成这样:

public static bool IsRealFlag(byte[] flag, byte[] paramaters){
    ResetState(paramaters);
    int[] array = new int[72]{
        //忽略数据
    };
    int[] array2 = new int[72];
    for (int k = 0; k < 11; k++){
        array2[k] ^= GetNextSpellcard();
    }
    for (int l = 0; l < 11; l++){
        array[l] ^= flag[l] ^ array2[l];
    }
    MagicalDust = array;
    var b = true;
    for (int i = 0; i < 11; i++){
        if (array[i] != 0){
            b = false;
            break;
        }
    }
    if (b){
        Console.WriteLine($"Find it!{Encoding.UTF8.GetString(flag)}");
        foreach (var x in paramaters){
            Console.Write($"{x}, ");
        }
        Console.WriteLine();
    }
    return b;
}

这里需要注意一点的是,建议把ButAnotherFlagMachineYetAnotherFlagMachine里面的输出给删掉,不然在遍历的时候会很吵,就像这样:

image-20230906215253215

u=91482142,3652982568&fm=253&app=138&f=JPEG

建议修改成如下:

image-20230906215049732

image-20230906220036398

改好了,那么现在再运行一次:

image-20230906221238027

运气非常好,只爆破出了一种可能性,那就是当flag以nUyn开头,并且SetFlag的参数是**mEow???**时,可以得到我们想要的答案,并且也得到了最后在校验flag时的paramaters的参数。

根据之前我们对算法的分析,现在很简单了,再算回去我们就可以得到我们想要的flag了。再把下面这段代码添加到KoitoMagicalShop类里

    public static byte[] GetFlag(byte[] paramaters)
    {
        byte[] flag = new byte[72];
        ResetState(paramaters);
        int[] array = new int[72]
        {
            143, 75, 130, 35, 251, 51, 51, 49, 92, 145,
            151, 13, 30, 200, 47, 14, 231, 100, 49, 169,
            56, 25, 94, 176, 116, 11, 128, 10, 186, 63,
            185, 45, 216, 55, 190, 72, 130, 200, 139, 252,
            58, 250, 37, 151, 179, 220, 200, 35, 111, 41,
            100, 87, 203, 54, 7, 81, 59, 153, 165, 71,
            255, 195, 220, 144, 112, 243, 227, 251, 228, 232,
            246, 251
        };
        for (int k = 0; k < 72; k++)
        {
            flag[k] = (byte)(array[k] ^ GetNextSpellcard());
        }
        return flag;
    }

然后再到程序主函数里调用一下他就可以算出我们的flag了。

using KoitoCoco.MoeCtf;
using System;
using System.Text;

byte[] para = { 231, 239, 247, 240, 136, 161, 120, 14 };
Console.WriteLine(Encoding.UTF8.GetString(KoitoMagicalShop.GetFlag(para)));
//moectf{nUynafeaaz_gOoD_jo6!_you_have_fed_The_CaT_in_The_neT!_xSMrlYDuuM}

image-20230906221902373

总结

​ 十分的创新,对新生来说也是一道很有意义的题,在做这道题的过程中也可以学到非常多的编程概念,例如异常处理、面向对象、委托事件等等,同时还能感受到C#的魅力~ 看似毫无关联的东西,但最后串联起来的感觉十分过瘾~~个人愿意打满分的一道.NET逆向题~希望可可能再多来点这种题QwQ

  • 12
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值