1

dotnet host startup hook

 1 year ago
source link: https://y4er.com/posts/dotnet-host-startup-hook/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

DOTNET_STARTUP_HOOKS

dotnet core提供了一个底层的hook钩子,通过环境变量设置DOTNET_STARTUP_HOOKS=aaa.dll就可以在Main函数之前运行一些自定义代码

System.Private.CoreLib.dll!System.StartupHookProvider类中

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using System.Diagnostics.Tracing;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;

namespace System
{
    internal static class StartupHookProvider
    {
        private const string StartupHookTypeName = "StartupHook";
        private const string InitializeMethodName = "Initialize";
        private const string DisallowedSimpleAssemblyNameSuffix = ".dll";

        private static bool IsSupported => AppContext.TryGetSwitch("System.StartupHookProvider.IsSupported", out bool isSupported) ? isSupported : true;

        private struct StartupHookNameOrPath
        {
            public AssemblyName AssemblyName;
            public string Path;
        }

        // Parse a string specifying a list of assemblies and types
        // containing a startup hook, and call each hook in turn.
        private static void ProcessStartupHooks()
        {
            string? startupHooksVariable = AppContext.GetData("STARTUP_HOOKS") as string;
            ...
            // Parse startup hooks variable
            string[] startupHookParts = startupHooksVariable.Split(Path.PathSeparator);
            StartupHookNameOrPath[] startupHooks = new StartupHookNameOrPath[startupHookParts.Length];
            for (int i = 0; i < startupHookParts.Length; i++)
            {
                string startupHookPart = startupHookParts[i];
                ...
                if (Path.IsPathFullyQualified(startupHookPart))
                {
                    startupHooks[i].Path = startupHookPart;
                }
                else
                {
                    // The intent here is to only support simple assembly names, but AssemblyName .ctor accepts
                    // lot of other forms (fully qualified assembly name, strings which look like relative paths and so on).
                    // So add a check on top which will disallow any directory separator, space or comma in the assembly name.
                    for (int j = 0; j < disallowedSimpleAssemblyNameChars.Length; j++)
                    {
                        if (startupHookPart.Contains(disallowedSimpleAssemblyNameChars[j]))
                        {
                            throw new ArgumentException(SR.Format(SR.Argument_InvalidStartupHookSimpleAssemblyName, startupHookPart));
                        }
                    }

                    if (startupHookPart.EndsWith(DisallowedSimpleAssemblyNameSuffix, StringComparison.OrdinalIgnoreCase))
                    {
                        throw new ArgumentException(SR.Format(SR.Argument_InvalidStartupHookSimpleAssemblyName, startupHookPart));
                    }

                    try
                    {
                        // This will throw if the string is not a valid assembly name.
                        startupHooks[i].AssemblyName = new AssemblyName(startupHookPart);
                    }
                    catch (Exception assemblyNameException)
                    {
                        throw new ArgumentException(SR.Format(SR.Argument_InvalidStartupHookSimpleAssemblyName, startupHookPart), assemblyNameException);
                    }
                }
            }

            // Call each hook in turn
            foreach (StartupHookNameOrPath startupHook in startupHooks)
            {
                CallStartupHook(startupHook);
            }
        }

        // Load the specified assembly, and call the specified type's
        // "static void Initialize()" method.
        [RequiresUnreferencedCode("The StartupHookSupport feature switch has been enabled for this app which is being trimmed. " +
            "Startup hook code is not observable by the trimmer and so required assemblies, types and members may be removed")]
        private static void CallStartupHook(StartupHookNameOrPath startupHook)
        {
            Assembly assembly;
            try
            {
                if (startupHook.Path != null)
                {
                    Debug.Assert(Path.IsPathFullyQualified(startupHook.Path));
                    assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(startupHook.Path);
                }
                else if (startupHook.AssemblyName != null)
                {
                    Debug.Assert(startupHook.AssemblyName != null);
                    assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(startupHook.AssemblyName);
                }
                ...
            Type type = assembly.GetType(StartupHookTypeName, throwOnError: true)!;

            // Look for a static method without any parameters
            MethodInfo? initializeMethod = type.GetMethod(InitializeMethodName,
                                                         BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static,
                                                         null, // use default binder
                                                         Type.EmptyTypes, // parameters
                                                         null); // no parameter modifiers
            ...
            initializeMethod.Invoke(null, null);
        }
    }
}

截取了部分代码,其实就是在这个StartupHookProvider类中进行加载程序集,然后反射调用Initialize函数,堆栈如下

如果你跟不到这个地方,那么需要勾掉这个选项:工具-选项-启用“仅我的代码”

然后回溯栈帧的时候会让你选择符号服务器,勾上之后就可以跟进来了。

src\coreclr\vm\assembly.cpp在这个cpp文件中

调用了钩子

https://github.com/dotnet/runtime/blob/2619d1c8eeef4a881c3910c87c1a8903ed742c24/src/coreclr/vm/assembly.cpp#L1494

RunStartupHooks在RunMain函数之前运行。

这个环境变量该如何使用?和java agent有什么区别?

想了想有几个用途

对于程序监控而言,aws的lambda就已经用上了这个东西

https://docs.aws.amazon.com/zh_cn/lambda/latest/dg/runtimes-modify.html

除此之外还有GitHub的一些开源项目,比如 https://github.com/newrelic/newrelic-dotnet-agent

再比如elastic的agent代理 https://www.elastic.co/guide/en/apm/agent/dotnet/current/setup-dotnet-net-core.html#zero-code-change-setup

对于后门来说,加一个环境变量应该不是很敏感吧

而对于静态免杀来讲,程序集通过dotnet runtime加载,而并非自己Assembly.Load引入,并且执行点不在Main函数中,可能相对效果好一些?

这些思考都是猜测,并没有实践过,读者自测吧。

对于java agent来说,java提供了动态attach的功能,而dotnet只能通过环境变量引入,需要重启,内存马可能不太现实。

暂时没想到什么好的利用面,留给读者吧。

https://github.com/dotnet/runtime/blob/main/docs/design/features/host-startup-hook.md

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK