吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 651|回复: 7
上一主题 下一主题
收起左侧

[.NET逆向] .NET NativeAOT 程序基础分析

[复制链接]
跳转到指定楼层
楼主
palonsong 发表于 2026-1-26 14:12 回帖奖励
本帖最后由 palonsong 于 2026-1-26 17:21 编辑

记一次对NativeAOT程序的逆向分析

前言

NativeAOT 是 .NET 框架自 .NET 8 正式发布以来逐渐走向成熟的一项关键技术。它通过在编译期将 IL 代码直接编译为本地机器码,生成一个不依赖 外部 CLR / CoreCLR 的原生可执行文件,从而在启动速度、程序大小方面带来了显著提升。

正文

编译

本篇文章采用 .NET 9 运行时进行编译,
代码:

namespace NativeAOTSample
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Welcome to .NET Test!");
#if NATIVE_AOT
            Console.WriteLine("This application is compiled with Native AOT.");
#else
            Console.WriteLine("This application is running under .NET Framework.");
#endif
            string Password = "Password";

            Console.WriteLine("Enter the 'Password'...");

            string? input = Console.ReadLine();

            if (input == Password)
            {
                Console.WriteLine("Access Granted!");
            }
            else
            {
                Console.WriteLine("Access Denied!");
            }

        }
    }
}

编译参数为:

<!-- if you want compiles with .Net Framework, comment this 'TrimmerRootConfiguration' -->
<PropertyGroup Label="TrimmerRootConfiguration" Condition="$(Configuration) == 'Release'">
        <PublishAot>true</PublishAot>
        <StackTraceSupport>false</StackTraceSupport>
        <TrimMode>full</TrimMode>
        <BuiltInComInteropSupport>false</BuiltInComInteropSupport>
        <StaticExecutable>true</StaticExecutable>
        <DefineConstants>$(DefineConstants);NATIVE_AOT</DefineConstants>
</PropertyGroup>

程序外部分析

如果我们不使用NativeAOT编译,携带框架的程序将会达到如图大小:

而如果我们使用NativeAOT编译,其程序大小会显著降低:

而很明显的,你可以注意到 PDB 变大了,从 11 KB 变成了 6.38 MB。
这正是说明,在进行 NativeAOT 之后,其程序确确实实的脱离了 .NET 运行时环境,最后的结果也是用 Linker 进行链接的 Native Code 产物。

程序内部分析

使用的工具 : IDA 9.2 , x64dbg

我们现在使用 IDA 带PDB对程序进行分析。
在加载PDB后,我们注意到程序的入口点如图所示:

其Main函数很明显是在建立一些东西,简言之:

native main()
 ├─ RhInitialize            // 启动 .NET Runtime
 ├─ Get module handle       // 找到当前模块
 ├─ RhRegisterOSModule      // 注册托管代码段
 ├─ InitializeModules       // 初始化 CoreLib + 程序
 └─ call managed Main()     // 进入 C#

也就是说,对于所有未加壳,未混淆,未魔改的 .NET 9 NativeAOT x64程序,其真正的入口点都是main函数的最后一个函数,前面的我们一般来说不关注。

当我们进入这个 _managed__Main 后,我们又可以发现地狱绘图:

这个托管的Main函数流程示意图:

┌────────────────────────────────────┐
│ 进入托管世界(Reverse P/Invoke)    │
│ RhpReversePInvoke                  │
└───────────────┬────────────────────┘
                │
                ▼
┌────────────────────────────────────┐
│ Runtime 基础准备                   │
│ • 预分配 OutOfMemoryException      │
│ • ClassConstructorRunner.Init      │
│ • RuntimeAugments / TypeLoader     │
│ • ReflectionExecution.Init         │
└───────────────┬────────────────────┘
                │
                ▼
┌────────────────────────────────────┐
│ 命令行参数初始化                   │
│ argc / argv → string[] args        │
│ InitializeCommandLineArgs           │
└───────────────┬────────────────────┘
                │
                ▼
┌────────────────────────────────────┐
│ 当前模块与类型系统绑定             │
│ typeof(<Module>) RuntimeTypeHandle │
└───────────────┬────────────────────┘
                │
                ▼
┌────────────────────────────────────┐
│ 线程系统初始化                     │
│ • Thread.CurrentThread             │
│ • ThreadStatic / TLS               │
│ • ApartmentState(STA)              │
└───────────────┬────────────────────┘
                │
                ▼
┌────────────────────────────────────┐
│ Module Initializer                 │
│ [ModuleInitializer]                │
│ RunModuleInitializers              │
└───────────────┬────────────────────┘
                │
                ▼
┌────────────────────────────────────┐
│ Main 参数构造                      │
│ 根据 Main 签名构造参数对象         │
│ GetMainMethodArguments             │
└───────────────┬────────────────────┘
                │
                ▼
┌────────────────────────────────────┐
│ MainMethodWrapper                  │
│ • 适配不同 Main 签名               │
│ • async / Task / int / void        │
└───────────────┬────────────────────┘
                │
                ▼
┌────────────────────────────────────┐
│ ★★★ 用户 C# Main ★★★              │
│ Program.Main(...)                  │
│ 你的业务逻辑                       │
└───────────────┬────────────────────┘
                │
                ▼
┌────────────────────────────────────┐
│ Main 返回处理                      │
│ • await Task                       │
│ • 设置 Environment.ExitCode        │
└───────────────┬────────────────────┘
                │
                ▼
┌────────────────────────────────────┐
│ 前台线程收尾                      │
│ Thread.WaitForForegroundThreads    │
└───────────────┬────────────────────┘
                │
                ▼
┌────────────────────────────────────┐
│ 进程退出事件                      │
│ AppContext.OnProcessExit           │
│ ProcessExit / Dispose              │
└───────────────┬────────────────────┘
                │
                ▼
┌────────────────────────────────────┐
│ 托管 → Native 返回                │
│ RhpReversePInvokeReturn            │
└────────────────────────────────────┘

也就是说,到了这一层真正对我们有用的被封装在了 MainMethodWrapper 内,我们应当去查看 MainMethodWarpper 才能看到程序的真实逻辑。
经过观察,这个 MainMethodWarpper 通常在 _managed__Main 的倒数段,且传入一个参数,
我们放一张没有加载 PDB 的图作为对比:

现在我们就可以进入我们Warpper了,Warpper浅显易懂,我们一笔带过。

可以注意到,此时,Main函数的参数被创建成了个托管对象,传入我们真正的逻辑。

进入用户 Main 真实逻辑层,我们按有 PDB 信息和无 PDB 信息放图:


这里的逻辑很像C层,但是好像又不太一样,尤其是当我们点击这些 unk_xxxxx 变量,我们会发现其实他们在静态是空的:

这是因为这些引用并非真正的指针,而是逻辑引用,引用的是C# System.String 对象,那我们没办法了,静态我们拿不到信息,只能去动态碰碰运气了。

因此我们启动 x64dbg 对程序进行调试。

我们按照IDA中的进入顺序,按照如下函数进行:
wmain

SetupCodeMain

然后我们终于看到熟悉的 MainMethodWarpper

然后就能进Main了,因为MainWarpper就一个Call,直接进去即可,所以不给图。

Main函数上下文汇编:

我们断在这个 lea 上,然后步进,可以注意到此时的rcx指针指向了我们在IDA看到的那个引用地址:

我们查看该内存区域:

可以发现一个规律:
对于所有的托管对象,其相同类型的MethodTable指针必然相同,
图示为System.String托管对象,且在 .NET 中,数据存储按照小端排列。
等价 C++ 代码:

namespace System {

    // 所有托管引用类型的对象头
    struct Object
    {
        void* MethodTable;   // +0x00  类型信息(CLR MethodTable)
    };

    struct String : Object
    {
        int32_t Length;      // +0x08  UTF-16 字符数
        int32_t Padding;     // +0x0C  对齐填充
        char16_t FirstChar;  // +0x10  UTF-16 字符起点(柔性数组)

        // 逻辑上存在的柔性字符数组,实际长度由 Length 决定;
        // 该字段在运行期紧随对象头存储,
        // 但 C++ 标准不支持以运行期变量作为数组长度,因此此处仅作概念性标注。
        // char16_t Chars[Length]; 
    };

} // namespace System

其他说明:
Console.ReadLine 内部实现涉及多层封装,其核心流程为:
调用操作系统提供的标准输入接口(Windows 下通常为 ReadFile),
将读取到的字节序列按当前控制台编码(如 UTF-16 / OEM Code Page)进行解码,
并在托管堆上构造对应的 System.String 对象作为返回值。

总结

通过以上分析可以看出,NativeAOT 程序在表现形式上已经完全脱离了传统的 .NET Framework / CoreCLR 运行模式,其最终产物更接近一个包含精简运行时的纯原生程序。
在逆向视角下,虽然其代码形式与普通 C/C++ 程序极为相似,但其对象模型、类型系统以及字符串等基础结构仍然严格遵循 CLR 的设计规范。

在本文中,我们以 System.String 为切入点,从静态反编译到运行期调试,验证了托管对象在 NativeAOT 下的真实内存布局,并总结了通过 MethodTable 判定托管对象类型的通用方法。这一思路同样适用于 System.Array、object[] 以及其他常见托管类型,虽然这些东西如果有RTTI,那你基本上睁一只眼闭一只眼都能知道是个啥。

需要注意的是,本文所分析的示例仅涉及同步 Main 逻辑。在实际应用中,若程序使用 async Main、Task、线程池或更复杂的异步模型,其在 MainMethodWrapper 层我还没分析过,所以我不知道。

全文完。

下附示例代码与编译示例程序文件:
https://gitee.com/MartionSimon/workshop/releases/download/NAOTSample/NativeAOTSample.7z

免费评分

参与人数 2吾爱币 +2 热心值 +2 收起 理由
coreide290 + 1 + 1 鼓励转贴优秀软件安全工具和文档!
xzqsr + 1 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
PercyDan 发表于 2026-1-27 08:12
推荐阅读:https://blog.washi.dev/posts/recovering-nativeaot-metadata/
推荐
woaipojie2014 发表于 2026-1-27 17:33
NativeAOT支持率太低了 哈哈  还有很长的路要走 特别是桌面程序 截至目前基本上没戏 除了那个框架
推荐
qq465881818 发表于 2026-1-26 20:01
本帖最后由 qq465881818 于 2026-1-26 20:25 编辑

确实在没有pdb的情况下可以猜到 真正的main的具体位置,不过我还有更快的方法 就是查找特征码 E827FDFFFF  一步就到真正的main入口

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
palonsong + 1 + 1 我很赞同!

查看全部评分

沙发
Hmily 发表于 2026-1-26 16:22
图片太小了太慢了,直接上传论坛本地编辑下主题吧。
3#
 楼主| palonsong 发表于 2026-1-26 17:10 |楼主
Hmily 发表于 2026-1-26 16:22
图片太小了太慢了,直接上传论坛本地编辑下主题吧。

好的,马上整改,谢谢H大提醒
5#
bester 发表于 2026-1-26 21:22
NativeAOT 最低运行环境是不是仅支持w10 x64,那还需要安装desktop runtime  or c# sdk ?
6#
 楼主| palonsong 发表于 2026-1-26 21:52 |楼主
bester 发表于 2026-1-26 21:22
NativeAOT 最低运行环境是不是仅支持w10 x64,那还需要安装desktop runtime  or c# sdk ?

实测,NativeAOT编译出来的程序win7都能跑,什么都不用装
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-1-28 05:30

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表