dotnet core提供了一个底层的hook钩子,通过环境变量设置DOTNET_STARTUP_HOOKS=aaa.dll
就可以在Main函数之前运行一些自定义代码
System.Private.CoreLib.dll!System.StartupHookProvider
类中
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
| // 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
文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。