作者简介 /Profile/
耿建航,平安科技银河实验室高级经理,曾在多家安全公司任职,擅长逆向。
-
本系列文章会分成以下三个部分进行讲解:
-
第一章 介绍C#逆向基础知识、C#编译原理、常用工具等
-
第二章 讲解CLR机制
-
第三章 讲解IL语言
第一章
准备工作:需要安装Windows的VisualStudio用于写C#程序对照,同时VS提供命令行环境,能够提供ildasm,ilasm等工具。单纯依靠dnspy、net reflector等自动化工具,遇到壳就得变sb了,基本功还是要学好。
本章需要了解C#的编译运行原理。在逆向过程中会遇到的很多技术点,就不一一展开,感兴趣的同学可根据关键字自行检索学习。
C#编程使用的Windows库主要就是.net平台库,常用版本2.5-4.5,对C#程序的逆向主要也是对.net平台库的熟悉与逆向,所以先让我们看一下这个平台。
1、.net平台简介
.net 是微软提供的用于Windows系统上软件开发的平台。常与VisualStudio组合进行含有gui界面app的开发。
.net 对于微软自定义的各种网络认证协议都有很好的支持,是在Windows系统上编程的省事之选。可以用于Web服务的开发,但是用户集中在北美,应用在微软自家的Azure云上。
支持语言:
-
C#(单机app,IIS-web服务)
-
F#(使用量少)
-
Visual Basic(多用于病毒)
2、C#逆向常见目的
最常见的情况有三种:
-
.net app 逆向,原理分析或修改,功能增加(目标exe,dll)
-
com组件dll,原理分析或修改,功能增加(目标dll)
-
.net visual basic 脚本分析(病毒行为检测)
3、C#程序特征
C#程序基本为Windows系统使用,方便写界面或使用Windows系统接口,所以我们见到的大多是dll或者exe,其中dll主要用在com组件服务,Web服务上,exe就是常见的Windows软件。
使用常用逆向工具ghidra打开我们自己编写的一个C#exe程序,得到如下信息:
这里红框标注的编译器cli就是C#的程序特。
4、C#编译流程
C#编译的过程中会进行两次编译:
第一次是使用的IDE,比如VScode自带的编译器进行编译,生成exe或dll文件。
源代码被编译为一种符合 CLI 规范的中间语言 (IL)。IL 代码与资源(例如位图和字符串)一起作为一种称为程序集的可执行文件存储在磁盘上,通常具有的扩展名为 .exe 或 .dll。程序集包含清单,它提供有关程序集的类型、版本、区域性和安全要求等信息。
第二次是JIT编译,把exe或dll文件编译成二进制代码。
执行 C# 程序时,程序集将加载到 CLR 中,这可能会根据清单中的信息执行不同的操作。然后,如果符合安全要求,CLR 就会执行实时 (JIT) 编译以将 IL 代码转换为本机机器指令。CLR 还提供与自动垃圾回收、异常处理和资源管理有关的其他服务。
由 CLR 执行的代码有时称为“托管代码”,它与编译为面向特定系统的本机机器语言的“非托管代码”相对应。下图阐释了C#源代码文件、.NET Framework 类库、程序集和 CLR 的编译时与运行时的关系。
5、C#反编译流程-手工
1).使用编译完成的exe、dll进行逆向推导,获取c#源代码。
2).ILDASM工具 在安装了visualStudio之后就会出现在 vs2022的程序文件夹里面:
直接运行ildasm 会出现对话框
3)把编译好的exe拖进去就能看到代码结构和IL源码:
6、自动化逆向工具
1) .Net Reflector
.Net Reflector具有良好的用户体验、强大的插件功能和反编译能力,使用它不仅能看到反编译后的IL源码甚至能直接反编译出C#源码,而且和编写时的代码几无二致。此外还可以直接另存为工程文件用Visual Studio打开,方便二次开发。
官方网址:http://www.red-gate.com/products/dotnet-development/reflector/
2) ILSpy/dnSpy
-
开源免费
-
无Visual Studio集成
-
从ILSpy 产生的dnSpy工具更加方便逆向与编译工程
官方网址:https://github.com/0xd4d/dnSpy/releases
3)JetBrains dotPeek
-
尝试到源代码服务器上抓取代码
-
导航功能和快捷键非常便捷
-
精确查找符号的使用,同时支持插件
官方网址:http://www.jetbrains.com/decompiler/
4)Telerik JustDecompile
-
无Visual Studio集成
-
可以为反编译程序集得到的代码创建一个项目
-
提供了健壮的查找功能,能够支持全文查找和符号使用查找
官方网址:http://www.telerik.com/products/decompiler.aspx
推荐使用dnSpy 工具作为主逆向工具使用,其优点如下:
-
开源免费,自己编译没有小礼物
-
界面友好,反编译代码质量高
-
可重新打包,方便二次开发
7、相关实验室材料
本章中写了一个简单的C# WindowsFormsApp1源码如下,大家可以看下这个程序经过ildasm后出现的代码作为对照,了解下C#编译成功的il代码到底是什么样式:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show(this,"确认提交么","提示");
}
}
}
il的main代码:
.method private hidebysig static void Main() cil managed
{
.entrypoint
.custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )
// 代码大小 26 (0x1a)
.maxstack 8
IL_0000: nop
IL_0001: call void [System.Windows.Forms]System.Windows.Forms.Application::EnableVisualStyles()
IL_0006: nop
IL_0007: ldc.i4.0
IL_0008: call void [System.Windows.Forms]System.Windows.Forms.Application::SetCompatibleTextRenderingDefault(bool)
IL_000d: nop
IL_000e: newobj instance void WindowsFormsApp1.Form1::.ctor()
IL_0013: call void [System.Windows.Forms]System.Windows.Forms.Application::Run(class [System.Windows.Forms]System.Windows.Forms.Form)
IL_0018: nop
IL_0019: ret
} // end of method Program::Main
onclick的代码:
.method private hidebysig instance void button1_Click(object sender,
class [mscorlib]System.EventArgs e) cil managed
{
// 代码大小 19 (0x13)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldstr bytearray (6E 78 A4 8B D0 63 A4 4E 48 4E ) // nx...c.NHN
IL_0007: ldstr bytearray (D0 63 3A 79 ) // .c:y
IL_000c: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(class [System.Windows.Forms]System.Windows.Forms.IWin32Window,
string,
string)
IL_0011: pop
IL_0012: ret
} // end of method Form1::button1_Click
参考来源:
微软msdn资料库
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/
第二章
第二章节需要了解C#的运行原理、过程,编译1产生文件的结构与内容。
1、CLR简介
CLR是一个可以由多编程语言使用的运行时,CLR的核心功能:内存管理,程序集加载,安全性,异常处理,线程同步等等。可以被很多属于微软系列的开发语言使用。
事实上,在运行时,CLR根本不关心开发运用什么语言编写源代码,这意味着选择编程语言的时候,应该选择最容易表达自己意图的语言。只要编译器是面向CLR的即可。
2、CLR流程
从源码到应用程序执行CLR 流程:
1)源码编译成托管模块
2)托管模块合并成程序集
3)加载CLR
3、源码第一次编译-托管模块
使用C#,vb,il等语言编写代码后,可以使用对应语言的编译器将代码编译成托管模块。
4、托管模块结构
托管模块由四部分组成:PE32头、CLR头、元数据(Metadata)、IL代码
1) PE32头用来决定托管模块运行的系统环境(32位、64位)
2) CLR头用来描述CLR版本等信息
3) 元数据:元数据其实是一些用来描述:
程序集、托管模块、类型、类型的成员之间的关系的表(tables)。我们可以将这些表分为三类:定义表、引用表、Manifest,我们通常看到的托管模块不包含Manifest。
4)IL代码:中间语言代码,提供给JIT执行的脚本,类似JAVA 编译后的class
四个结构中,元数据、IL代码是逆向需要重点关注的点。
元数据结构-定义表
元数据结构-引用表
元数据结构-引用表-遍历流程
-
通过模块的入口 找到所有的类型
-
通过类型的入口 找到对应类型的所有的成员的入口
-
成员方法的入口会有指向IL代码的索引,只要有模块的入口就可以拿到入口中所有元素了。
成员方法和成员属性以外其他的元素都是用元数据描述出来的,只有这两者是有IL代码的描述的。
元数据结构-Manifest
模块结构明确后,通过链接托管模块,可以获取到程序集,程序集有多种形态:
-
单文件程序集:只包含一个物理文件
-
多文件程序集:包含多个物理文件
使用VS创建的项目都是单文件程序集,这个程序集与一般托管模块不同,会包含Manifest类型的表。
Manifest表:描述程序集中托管模块的分布,将托管模块从逻辑上关联成为一个程序集。
元数据结构-Manifest表
5、程序集生成方式
程序集文件的Manifest 中 包含一些引用表,是用来描述程序集中所有模块引用的程序集的入口的,这样在我们加载程序集的时候,就可以根据这个表知道有哪些程序集被引用了。程序集结构大概有如下几种:
或者下面这种形式:
清单元数据放到一个空的托管模块中。最终的dll文件本身没有内容,但是其中包含了两个.netmodule托管模块。
6、CLR加载执行程序集
1)初次调用
这里指的是CLR在本次启动中第一次调用程序,程序集中的被调用函数没有初始化的情况。
2)再次调用
7、相关实验材料
本章中写了一个简单的C#ConsoleApp2源码如下,大家可以看下这个程序经过ildasm后出现的代码作为对照,了解下c#编译成功的il代码到底是什么样式:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp2
{
internal class Program
{
static void Main(string[] args)
{
string inputStr = Console.ReadLine();
if (inputStr == "12243")
{
Console.WriteLine("成功");
}
else {
Console.WriteLine("失败");
}
string inputStr2 = Console.ReadLine();
}
}
}
第三章
1、IL简介
IL是.NET框架中中间语言(Intermediate Language)的缩写。
使用.NET框架提供的编译器可以直接将源程序编译为.exe或.dll文件,但此时编译出来的程序代码并不是CPU能直接执行的机器代码,而是一种中间语言IL(Intermediate Language)的代码。
运行时流程如下:
2、ildasm使用
可以使用vs提供的ildasm进行代码获取。
3、程序il执行样例演示
C#源码
namespace ConsoleApp2
{
internal class Program
{
static void Main(string[] args)
{
string inputStr = Console.ReadLine();
if (inputStr == "12243")
{
Console.WriteLine("成功");
}
else {
Console.WriteLine("失败");
}
string inputStr2 = Console.ReadLine();
}
}
}
IL源码
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// 代码大小 46 (0x2e)
.maxstack 8
IL_0000: call string [mscorlib]System.Console::ReadLine()
IL_0005: ldstr "12243"
IL_000a: call bool [mscorlib]System.String::op_Equality(string,
string)
IL_000f: brtrue.s IL_001d
IL_0011: ldstr bytearray (10 62 9F 52 ) // .b.R
IL_0016: call void [mscorlib]System.Console::WriteLine(string)
IL_001b: br.s IL_0027
IL_001d: ldstr bytearray (31 59 25 8D ) // 1Y%.
IL_0022: call void [mscorlib]System.Console::WriteLine(string)
IL_0027: call string [mscorlib]System.Console::ReadLine()
IL_002c: pop
IL_002d: ret
} // end of method Program::Main
执行流程1:
语句执行之后 在终端输入 “12243”,评估栈的情况如下:
解释:
IL语言运行时在内存中会一直保存一个区域 :评估栈(evaluation stack )
作为函数调用的参数保存。
评估栈的开辟会在函数开始进行 .maxstack 8 代表了这个main函数中会最多保存8个数据。
然后是程序的第一条语句
IL_0000: call string [mscorlib]System.Console::ReadLine()
这条语句表示程序调用库mscorlib 里的 System.Console类型ReadLine函数,这个函数的返回值是 string,这条语句的地址是 IL_0000。
执行流程2:
(1) IL_0005: ldstr “12243”
把用于比较的字符串“12243”加载到评估栈上
(2) IL_000a: call bool [mscorlib]System.String::op_Equality(string, string)
调用对比函数,如果字符串相等,在栈上放入bool值 true
(3) IL_000f: brtrue.s IL_001d 根据评估栈上的值判断是否需要跳到指令流程IL_001d。
执行流程3:
(1) IL_0011: ldstr bytearray (10 62 9F 52 ) // .b.R
加载数据到评估栈上
(2) IL_0016: call void [mscorlib]System.Console::WriteLine(string)
调用系统函数,打印数据
(3) IL_001b: br.s IL_0027 直接跳转到 IL_0027
(4) IL_001d: ldstr bytearray (31 59 25 8D ) // 1Y%
(5) IL_0022: call void [mscorlib]System.Console::WriteLine(string)
(6) IL_0027: call string [mscorlib]System.Console::ReadLine()
(7) IL_002c: pop
(8) IL_002d: ret main函数返回
银河实验室
往期回顾
技术
技术
技术
技术
长按识别二维码关注我们
微信号:PSRC_Team
球分享
球点赞
球在看
原文始发于微信公众号(平安集团安全应急响应中心):C# 逆向入门