最可靠方式是检查pe头+cli头:确认image_nt_headers存在,再定位image_cor20_header(可选头第15项数据目录),若majorruntimeversion>0即为.net程序集;避免直接loadfile触发jit或静态构造器。

怎么判断一个二进制文件是不是 C# 程序集(.dll/.exe)
直接看文件是否能被 System.Reflection.Assembly.LoadFile 加载,是最可靠的方式。但别急着调用——它会触发 JIT、可能执行静态构造器,甚至抛出异常阻塞线程。
更安全的做法是先检查 PE 文件头 + CLI 头:用 System.IO.FileStream 读前几个字节,确认存在 IMAGE_NT_HEADERS,再跳到 IMAGE_COR20_HEADER 偏移(通常在可选头数据目录第15项),如果该结构存在且 MajorRuntimeVersion > 0,基本就是 .NET 程序集。
- 常见错误:用
File.ReadAllText或默认编码尝试“读文本”,二进制乱码后误判为非托管文件 - 注意:.NET Core / .NET 5+ 的单文件发布包(
singlefilehost)不是程序集,而是原生 PE 包裹了压缩 payload,需先解包 - 兼容性:.NET Framework 1.0–4.8 和 .NET 5+ 的 CLI 头布局一致,但某些混淆工具(如 ConfuserEx)会清空或伪造
MetaDataRoot,导致反射加载失败但文件仍是合法程序集
用 dnlib 解析未知程序集的元数据结构
dnlib 比原生 Assembly.ReflectionOnlyLoad 更适合逆向分析:它不执行代码、不依赖运行时版本、能处理损坏/混淆/强名称验证失败的程序集。
关键操作是打开模块后遍历 Module.Types 和 Module.GetTypes(),再逐个检查 TypeDef.Fields、TypeDef.Methods、MethodDef.Body(如有 IL)。
- 容易踩的坑:
Module.Load默认启用完整性校验,遇到修改过的强名称会抛BadImageFormatException;应改用ModuleDefMD.Load(stream, new ModuleCreationOptions { CheckSumEnabled = false }) - 性能影响:加载大型程序集(如 Unity 的
UnityEngine.dll)时,dnlib默认解析所有元数据表,可设options.MetadataOptions.Flags |= MetadataFlags.NoTypeRef跳过不必要引用解析 - 混淆识别:若
TypeDef.Name是a、b这类单字母,或MethodDef.HasBody == false但有MethodDef.ImplMap,大概率用了重命名+IL 合并混淆
如何从原始字节中定位嵌入资源或序列化对象
C# 程序集里藏东西,常见位置就三个:.resources 节、ManifestResource 元数据项、或直接写死在某个方法的 byte[] 字段里。
先用 dnlib 查 Module.Resources,看有没有非空的 EmbeddedResource;再查 Module.ManifestResources,过滤掉 IsPublic == false 的私有资源(常被用来存密钥、配置片段)。
- 典型现象:反编译看到类似
private static readonly byte[] smth = new byte[] { 0x46, 0x4C, 0x56, ... };—— 这串很可能是个加密的配置或自定义二进制格式,得结合调用它的方法体(MethodDef.Body.Instructions)看解密逻辑 - 路径陷阱:
ManifestResource的Name是逻辑名,实际数据在 PE 的.rsrc节或独立数据目录,需用ResourceSection或手动解析 PE 结构提取 - 注意:.NET 6+ 的
Single-file bundles把资源打进了.data节,dnlib无法识别,得切回PEParser类库定位 raw section 数据
遇到 IL 指令乱序、控制流平坦化怎么办
这不是标准 C# 编译器行为,而是混淆器(如 Dotfuscator、SmartAssembly)的手笔。此时 dnlib 能读出指令,但直接看 Instructions 列表毫无意义——跳转目标被替换成查表函数,真实逻辑被拆成几十个无意义的小方法。
真正有效的做法是:用 dnlib 提取所有含 Call 指令的方法体 → 找出高频调用的“调度器”方法 → 反推其参数和返回值映射关系 → 再对每个分支目标做局部控制流还原。
- 常见错误:试图用
ICSharpCode.Decompiler直接反编译,结果输出满屏goto Label_XXXX,因为 decompiler 没配适配器处理平坦化 - 实操建议:先禁用所有优化(
DecompilerSettings.OptimizeLoops = false),再人工标注几个已知功能点(比如登录校验入口),顺藤摸瓜找分支条件对应的字段/常量 - 性能代价:全自动还原平坦化控制流需要符号执行或约束求解,目前没成熟开源方案;手工还原一个中等混淆 DLL 通常要 2–8 小时,取决于调度器复杂度
逆向 C# 二进制最耗时间的从来不是读字节,而是分辨哪些是编译器生成的噪音、哪些是开发者写的逻辑、哪些是混淆器硬塞的干扰项——三者混在一起,没有银弹,只能一层层剥。










