C#的逆向


简介

C# 是微软公司发布的一种由 C 和 C++ 衍生出来的面向对象的编程语言,运行于 .NET Framework和 .NET Core(完全开源)之上的高级程序设计语言。

C#程序特征

使用 .NET 框架提供的编译器开源直接将源程序编译为 .exe 或 .dll 文件,但此时编译出来的程序代码并部署 CPU 可以直接执行的机器代码,而是一种中级语言 IL 的代码。

我们把这样的 exe 拖进 IDA 里,能看到一个没见过的加载项pe64.dll

而且反汇编窗口里只能看到 IL 的代码(类似 pyc 字节码)

C#基础

C# 语法和 Java 很像,仅仅搞逆向的话如果有 C++ 以及 Java 基础就没必要太过深入的 C# 的语法。

就算一个方法你不懂它的功能,但是仅仅通过名字也能大概猜出来。

不过简单的基础我们还是要了解下的。

开发 C# 程序一般我都用 MSVC 编译器进行开发。

C# 程序文件是以cs为后缀名结尾的。

创建一个名称为 RE 的 C# 项目

基本的 C# 代码结构:

1
2
3
4
5
6
7
8
9
10
11
12
using System; // 引入命名空间

namespace RE // 默认的命名空间
{
class Program // 定义程序类
{
static void Main(string[] args) // 主方法,程序入口
{
Console.WriteLine("Hello, World!"); // 输出到控制台
}
}
}

C#中常用的类和库:

  • Console

Write方法:输出文本。

1
Console.Write("hello");

WriteLine方法:输出文本并换行ReadLine方法:读取一行文本。

1
Console.WriteLine("hello");
  • String类:字符串类型类,提供了许多方法来处理字符串。

连接字符串:

1
string demo="hello"+" "+"world"

字符串格式化:

1
2
string formatted = string.Format("Hello, {0}!", "Alice");  // 格式化字符串
Console.WriteLine(formatted);

字符串替换

1
2
3
string str = "Hello, World!";
string replaced = str.Replace("World", "C#"); // 替换部分文本
Console.WriteLine(replaced); // 输出 "Hello, C#!"

子串提取(类似于切片):

1
2
string sub = "Hello, World!".Substring(7, 5);  // 提取子串 "World"
Console.WriteLine(sub);

判断字符串包含:

1
bool contains = "Hello, World!".Contains("World");  // 判断是否包含 "World"

字符串拆分:

1
2
string sentence = "apple,banana,cherry";
string[] fruits = sentence.Split(','); // 拆分为数组 ["apple", "banana", "cherry"]

C#逆向工具

dnspy 工具是专门用于逆向 C# 程序的工具。

我们首先根据程序位数选择 dnSpy32 或 dnSpy64 打开程序。

展开命名空间查看代码

dnSpy同样也支持交叉引用

C#打包

在项目命名空间下可以看到我们编写的代码。

Form1图形开发

  • 调试:调试程序的话程序必须是 debug 编译的才行
  • 打补丁:支持源码级别编辑类属性,类方法,并保存成新文件。
  • 分析:交叉引用

脱壳与去混淆

去混淆工具:de4dot(.NET程序脱壳,反混淆工具)

程序并没有发布版本,所以要我们自己进行编译。

通过 Visual Studio 进行编译。

  • 编译

下载的包中有两个解决方案,一个是基于 .NET Core 的,一个是基于 .NET Frameword 的。

如果使用 .net core版本,需要安装 netcoreapp3.1 和 netcoreapp2.1。

如果使用 .net framework 版本,需要安装 net 35和 net45。

通过 Visual Studio 打开解决方案,然后右键解决方案选择重新生成解决方案即可进行编译。

de4dot 支持对于以下工具混淆的代码的去混淆:

  • Agile.NET (aka CliSecure)
  • Babel.NET
  • CodeFort
  • CodeVeil
  • CodeWall
  • CryptoObfuscator
  • DeepSea Obfuscator
  • Dotfuscator
  • .NET Reactor
  • Eazfuscator.NET
  • Goliath.NET
  • ILProtector
  • MaxtoCode
  • MPRESS
  • Rummage
  • Skater.NET
  • SmartAssembly
  • Spices.Net
  • Xenocode

常见混淆的特征:

  • 随机字符串混淆

这里对符号进行了随机字符串混淆,那些以\u开头的都是 Unicode 编码。

  • 使用方法

直接去混淆

1
./de4dot/bin/Debug/de4dot.exe myApp.dll

指定混淆类型去混淆

1
./de4dot/bin/Debug/de4dot.exe myApp.dll -P sa

Unity应用

Unity3D 是一款强大的游戏引擎,它的核心优势之一就是能够让开发者一次开发,然后将游戏部署到多个平台。Mono 是 Unity3D 实现这一跨平台能力的关键,它允许 Unity 在多种平台上运行,特别是在支持 .NET 的平台上,Mono 提供了兼容的运行时和类库。

然而,Mono 并不是完全安全的,它会将源代码编译成 IL(中间语言)代码,这使得逆向工程变得相对容易。为了增强安全性,Unity 从 2014 年开始引入 IL2CPP(Intermediate Language to C++),这一技术将中间语言(IL)代码转化为 C++ 代码,然后再编译成机器码,从而提高了代码的安全性和性能。

  • Mono、IL2CPP的逆向
  • CE Mono功能
  • uniref框架

例题

[CFI-CTF 2018]IntroToPE

Exeinfo 查看程序发现为 C# 程序。

dnSpy打开反编译

进入程序命名空间,打开ValidatePasswd类查看。

  • ValidatePasswd类代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System;
using System.Text;

namespace IntroToPe
{
// Token: 0x02000004 RID: 4
internal class ValidatePasswd
{
// Token: 0x0600000A RID: 10 RVA: 0x0000262B File Offset: 0x0000082B
public ValidatePasswd(string passwd)
{
this.passwd = passwd;
}

// Token: 0x0600000B RID: 11 RVA: 0x0000263C File Offset: 0x0000083C
public bool verifyPasswd()
{
bool result = false;
bool flag = Convert.ToBase64String(Encoding.UTF8.GetBytes(this.passwd)) == "Q0ZJey5OZXRDI18xc19AdzNzMG0zfQ==";
if (flag)
{
result = true;
}
return result;
}

// Token: 0x0400000B RID: 11
private string passwd;
}
}
  • 程序逻辑

分析发现程序将输入通过Convert.ToBase64String方法编码为 base64,然后与一串密文进行比较。

很明显那串密文就是 base64 编码。

如果输入内容与密文比较相等,则resulttrue即返回相等。

直接那密文 base64 解密拿到flag

1
CFI{.NetC#_1s_@w3s0m3}

[ISC+2016]Classical+CrackMe

dnSpy 打开发现程序被加了混淆。

我们通过上面讲过的 de4dot 工具来去混淆

  • 去混淆后:

去混淆后的主要逻辑代码

直接解密 base64 密文拿到 flag

1
PCTF{Ea5y_Do_Net_Cr4ck3r}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
using System;
using System.ComponentModel;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
// Token: 0x02000002 RID: 2
public class Form1 : Form
{
// Token: 0x06000004 RID: 4 RVA: 0x00002057 File Offset: 0x00000257
public Form1()
{
this.InitializeComponent();
}
// Token: 0x06000005 RID: 5 RVA: 0x0000206C File Offset: 0x0000026C
private void Form1_Load(object sender, EventArgs e)
{
}

// Token: 0x06000006 RID: 6 RVA: 0x000021EC File Offset: 0x000003EC
private void button1_Click(object sender, EventArgs e)
{
string s = this.textBox1.Text.ToString();
byte[] bytes = Encoding.Default.GetBytes(s);
string a = Convert.ToBase64String(bytes);
string b = "UENURntFYTV5X0RvX05ldF9DcjRjazNyfQ==";
if (a == b)
{
MessageBox.Show("注册成功!", "提示", MessageBoxButtons.OK);
}
else
{
MessageBox.Show("注册失败!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Hand);
}
}
// Token: 0x06000007 RID: 7 RVA: 0x0000206C File Offset: 0x0000026C
private void textBox1_TextChanged(object sender, EventArgs e)
{
}

// Token: 0x06000008 RID: 8 RVA: 0x0000206E File Offset: 0x0000026E
private void button2_Click(object sender, EventArgs e)
{
base.Close();
}
// Token: 0x06000009 RID: 9 RVA: 0x00002258 File Offset: 0x00000458
protected virtual void Dispose(bool disposing)
{
if (disposing && this.icontainer_0 != null)
{
this.icontainer_0.Dispose();
}
base.Dispose(disposing);
}

2024Moectf dotNet

下载附件拿到一个dll文件,根据题目名称判定为 C# 逆向。

我们用 dnSpy 打开可执行文件。

在项目的命名空间中我们可以看到Main函数,进入查看。

程序的主要逻辑就在Main函数中

  • Main函数代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// Program
// Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
private static void <Main>$(string[] args)
{
// 定义一个字节数组,猜测为密文
byte[] array = new byte[]
{
173, 146, 161, 174, 132, 179, 187, 234, 231, 244, 177, 161, 65, 13, 18, 12,
166, 247, 229, 207, 125, 109, 67, 180, 230, 156, 125, 127, 182, 236, 105, 21,
215, 148, 92, 18, 199, 137, 124, 38, 228, 55, 62, 164
};

// 提示输入 flag,并以 text 接收
Console.WriteLine("Input Your Flag:");
string text = Console.ReadLine();

// 判断输入的 flag 长度是否与密文长度一样
if (text.Length != array.Length)
{
Console.WriteLine("Flag is WRONG!!!");
return;
}

//flag 的验证标志
int num = 1;

// 遍历输入的每个字符并进行验证
for (int i = 0; i < array.Length; i++)
{
// 检查输入字符是否符合加密规则
if ((byte)((int)((byte)text[i] + 114 ^ 114) ^ i * i) != array[i])
{
num &= 0; // 如果条件不满足,num 会变为 0
}
}

// 如果 num 保持为 1,说明 flag 是正确的
if (num == 1)
{
Console.WriteLine("Correct Flag!!!");
return;
}

// 如果 flag 不正确,输出错误信息
Console.WriteLine("Flag is WRONG!!!");
}

  • 核心逻辑

这里对输入的内容进行了操作,如果操作后的内容与密文相等则输入的内容就是正确的。

1
2
3
4
5
6
7
8
for (int i = 0; i < array.Length; i++)
{
// 先加114然后再异或114,之后与i*i异或再与密文进行比较
if ((byte)((int)((byte)text[i] + 114 ^ 114) ^ i * i) != array[i])
{
num &= 0; // 如果条件不满足,num 会变为 0
}
}

根据加密逻辑编写解密脚本

将加密逻辑逆过来就是:先与i*i异或,然后再与114异或,最后减去114

1
2
3
4
5
6
7
8
9
10
11
12
13
enc = [
    173, 146, 161, 174, 132, 179, 187, 234, 231, 244, 177, 161, 65, 13, 18, 12,
    166, 247, 229, 207, 125, 109, 67, 180, 230, 156, 125, 127, 182, 236, 105, 21,
    215, 148, 92, 18, 199, 137, 124, 38, 228, 55, 62, 164
]

flag = ""
for i in range(len(enc)):
#这里有一个重要的点,C#和python的int精度是不一样的,python的精读是无限的
#所以我们这里要与上0xff将它限制在一字节范围内
    flag += chr(((enc[i] ^ (i * i)) ^ 114) - 114 & 0xff)

print(flag)

[2024极客大挑战] 玩就行了

[FlareOn5]Ultimate Minesweeper

题目描述:You hacked your way into the Minesweeper Championship, good job. Now its time to compete. Here is the Ultimate Minesweeper binary. Beat it, win the championship, and we’ll move you on to greater challenges.
Hint:本题解出相应字符串后请用flag{}包裹,形如:flag{123456@flare-on.com}

题目描述中文翻译:你闯入了扫雷锦标赛,干得好。现在是比赛的时候了。这是终极扫雷二进制文件。打败它,赢得冠军,我们将带你去迎接更大的挑战。

运行程序发现为扫雷游戏。

Exeinfo查看发现程序为 C# 编写

接下来我们通过 dnSpy 打开反编译

查看发现程序命名空间中很多类名我们通过名字就能猜出其作用。

比如:FailurePopup(失败后弹出)、SucessPopup(成功后弹出)、MainForm(主窗口)、MineField(雷区)、MineFieldControl(雷场控制)、Program(程序)。

我们先进入Program类查看,发现其中创建了MainForm的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System; 
using System.Windows.Forms;

namespace UltimateMinesweeper {
// Token: 0x02000006 RID: 6
internal static class Program
{ // Token: 0x06000034 RID: 52 RVA: 0x00003219 File Offset: 0x00001419
[STAThread]
private static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
}

我们点击MainForm进入查看

分析其逻辑,加上注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public MainForm()        
{            
//创建Windows窗口
this.InitializeComponent();            
//MineField是雷区的封装,游戏的核心
this.MineField = new MineField(MainForm.VALLOC_NODE_LIMIT);            
//创建雷区
this.AllocateMemory(this.MineField);            
this.mineFieldControl.DataSource = this.MineField;
//回调         
this.mineFieldControl.SquareRevealed += this.SquareRevealedCallback;            
this.mineFieldControl.FirstClick += this.FirstClickCallback;            
this.stopwatch = new Stopwatch();            
this.FlagsRemaining = this.MineField.TotalMines;            
this.mineFieldControl.MineFlagged += this.MineFlaggedCallback;            
this.RevealedCells = new List<uint>();        
}

进入AllocateMemory函数分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void AllocateMemory(MineField mf)
{
for (uint num = 0U; num < MainForm.VALLOC_NODE_LIMIT; num += 1U)
{
for (uint num2 = 0U; num2 < MainForm.VALLOC_NODE_LIMIT; num2 += 1U)
{
bool flag = true;
uint r = num + 1U; // 行索引
uint c = num2 + 1U; // 列索引

// 判断是否需要更改 flag 的值
if (this.VALLOC_TYPES.Contains(this.DeriveVallocType(r, c)))
{
flag = false;
}

// 根据计算结果更新 GarbageCollect 数组
mf.GarbageCollect[(int)num2, (int)num] = flag;
}
}
}

进入SquareRevealedCallback函数分析

第一部分的if程序是触发地雷的显示。

第二部分的if程序中存在GetKey函数,猜测是成功后的显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void SquareRevealedCallback(uint column, uint row)
{
// 检查是否触发了地雷
if (this.MineField.BombRevealed)
{
this.stopwatch.Stop(); // 停止计时
Application.DoEvents(); // 处理任何挂起的 Windows 消息
Thread.Sleep(1000); // 等待 1 秒
new FailurePopup().ShowDialog(); // 显示失败弹窗
Application.Exit(); // 退出应用程序
}

// 记录已揭示的格子
this.RevealedCells.Add(row * MainForm.VALLOC_NODE_LIMIT + column);

// 检查是否所有空白格子都已揭示
if (this.MineField.TotalUnrevealedEmptySquares == 0)
{
this.stopwatch.Stop(); // 停止计时
Application.DoEvents(); // 处理任何挂起的 Windows 消息
Thread.Sleep(1000); // 等待 1 秒
new SuccessPopup(this.GetKey(this.RevealedCells)).ShowDialog(); // 显示成功弹窗
Application.Exit(); // 退出应用程序
}
}

GetKey函数

利用revealedCells前三个元素作为种子,之后将arrayarray2的值进行异或运算,最后将结果转化为 ASCII 字符的数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private string GetKey(List<uint> revealedCells)
{
revealedCells.Sort();

// 使用前三个 revealedCells 元素生成随机种子
Random random = new Random(Convert.ToInt32(revealedCells[0] << 20 | revealedCells[1] << 10 | revealedCells[2]));

// 创建一个长度为 32 的字节数组
byte[] array = new byte[32];

// 预定义字节数组 array2
byte[] array2 = new byte[]
{
245, 75, 65, 142, 68, 71, 100, 185, 74, 127, 62, 130, 231, 129, 254, 243,
28, 58, 103, 179, 60, 91, 195, 215, 102, 145, 154, 27, 57, 231, 241, 86
};

// 填充 array 数组
random.NextBytes(array);

uint num = 0U;

// 对 array2 数组进行异或操作
while ((ulong)num < (ulong)((long)array2.Length))
{
byte[] array3 = array2;
uint num2 = num;
array3[(int)num2] = (array3[(int)num2] ^ array[(int)num]);
num += 1U;
}

// 返回转换为 ASCII 字符串的 array2 数组
return Encoding.ASCII.GetString(array2);
}

解题思路:

  • 删除第一个if部分
  • 修改校验部分

[2019红帽杯]Snake