~ruther/NosSmooth.Local

ref: 28eccafc46deef68704c57e5c182fb32c23d47e5 NosSmooth.Local/src/Inject/NosSmooth.Injector/NosInjector.cs -rw-r--r-- 7.8 KiB
28eccafc — Rutherther feat: update NosSmooth.Inject to show config path in failed error messages 2 years ago
                                                                                
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
//
//  NosInjector.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using Microsoft.Extensions.Options;
using NosSmooth.Injector.Errors;
using Reloaded.Memory.Sources;
using Remora.Results;

namespace NosSmooth.Injector;

/// <summary>
/// Nos smooth injector for .NET 5+ projects.
/// </summary>
/// <remarks>
/// If you want to inject your project into NosTale that
/// uses NosSmooth.LocalClient, use this injector.
/// You must expose static UnmanagedCallersOnly method.
/// </remarks>
public class NosInjector
{
    private readonly NosInjectorOptions _options;

    /// <summary>
    /// Initializes a new instance of the <see cref="NosInjector"/> class.
    /// </summary>
    /// <param name="options">The injector options.</param>
    public NosInjector(IOptions<NosInjectorOptions> options)
    {
        _options = options.Value;
    }

    /// <summary>
    /// Inject the given .NET 5+ dll into NosTale process.
    /// </summary>
    /// <remarks>
    /// The dll must also have .runtimeconfig.json present next to the dll.
    /// </remarks>
    /// <param name="processId">The id of the process to inject to.</param>
    /// <param name="dllPath">The absolute path to the dll to inject.</param>
    /// <param name="classPath">The full path to the class. Such as "MyLibrary.DllMain, MyLibrary".</param>
    /// <param name="methodName">The name of the method to execute. The method should return void and have no parameters.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result Inject
    (
        int processId,
        string dllPath,
        string classPath,
        string methodName = "Main"
    )
    {
        using var process = Process.GetProcessById(processId);
        return Inject(process, dllPath, classPath, methodName);
    }

    /// <summary>
    /// Inject the given .NET 5+ dll into NosTale process.
    /// </summary>
    /// <remarks>
    /// The dll must also have .runtimeconfig.json present next to the dll.
    /// </remarks>
    /// <param name="process">The process to inject to.</param>
    /// <param name="dllPath">The absolute path to the dll to inject.</param>
    /// <param name="classPath">The full path to the class. Such as "MyLibrary.DllMain, MyLibrary".</param>
    /// <param name="methodName">The name of the method to execute. The method should return void and have no parameters.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result Inject
    (
        Process process,
        string dllPath,
        string classPath,
        string methodName = "Main"
    )
    {
        try
        {
            dllPath = Path.GetFullPath(dllPath);
            if (!File.Exists(dllPath))
            {
                return new NotFoundError($"Could not find the managed dll file at \"{dllPath}\".");
            }

            using var injector = new Reloaded.Injector.Injector(process);
            var memory = new ExternalMemory(process);

            var netHostInjectionResult = InjectNetHostDll
            (
                injector,
                Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
                System.IO.Path.GetDirectoryName(dllPath),
                System.IO.Path.GetDirectoryName(process.MainModule?.FileName)
            );

            if (!netHostInjectionResult.IsSuccess)
            {
                return netHostInjectionResult;
            }

            var directoryName = Path.GetDirectoryName(dllPath);
            if (directoryName is null)
            {
                return new GenericError("There was an error obtaining directory name of the dll path.");
            }

            var runtimePath = Path.Combine
                (directoryName, Path.GetFileNameWithoutExtension(dllPath)) + ".runtimeconfig.json";

            if (!File.Exists(runtimePath))
            {
                return new NotFoundError($"Could not find the runtimeconfig.json file at \"{runtimePath}\".");
            }

            using var dllPathMemory = AllocateString(memory, dllPath);
            using var classPathMemory = AllocateString(memory, classPath);
            using var methodNameMemory = AllocateString(memory, methodName);
            using var runtimePathMemory = AllocateString(memory, runtimePath);

            if (!dllPathMemory.Allocated || !classPathMemory.Allocated || !methodNameMemory.Allocated
                || !runtimePathMemory.Allocated)
            {
                return new GenericError("Could not allocate memory in the external process.");
            }

            var loadParams = new LoadParams
            {
                LibraryPath = (int)dllPathMemory.Pointer,
                MethodName = (int)methodNameMemory.Pointer,
                RuntimeConfigPath = (int)runtimePathMemory.Pointer,
                TypePath = (int)classPathMemory.Pointer
            };

            var nosSmoothInjectPath = Path.GetFullPath(_options.NosSmoothInjectPath);
            if (!File.Exists(nosSmoothInjectPath))
            {
                return new NotFoundError($"Could not find the dll to inject at \"{_options.NosSmoothInjectPath}\".");
            }

            var injected = injector.Inject(nosSmoothInjectPath);
            if (injected == 0)
            {
                return new InjectionFailedError
                    (nosSmoothInjectPath, "Did you forget to copy nethost.dll into the process directory?");
            }

            var functionResult = injector.CallFunction(nosSmoothInjectPath, "LoadAndCallMethod", loadParams);

            injector.Eject(nosSmoothInjectPath);

            if (functionResult != 1)
            {
                return new InjectionFailedError
                (
                    dllPath,
                    $"Couldn't initialize the nethost or call the main function, did you specify the class and method correctly? Result: {functionResult}",
                    (InjectionResult)functionResult
                );
            }

            return Result.FromSuccess();
        }
        catch (UnauthorizedAccessException)
        {
            return new InsufficientPermissionsError(process.Id, process.ProcessName);
        }
        catch (SecurityException)
        {
            return new InsufficientPermissionsError(process.Id, process.ProcessName);
        }
        catch (Exception e)
        {
            return e;
        }
    }

    private Result InjectNetHostDll(Reloaded.Injector.Injector injector, params string?[] pathsToSearch)
    {
        string? foundPath = pathsToSearch
            .Select(x => Path.Join(x, "nethost.dll"))
            .FirstOrDefault(File.Exists);

        if (foundPath is null)
        {
            return new NotFoundError
            (
                "Could not find nethost.dll to inject (tried to look in executing process directory, injector directory, target process directory)"
            );
        }

        var handle = injector.Inject(foundPath);

        if (handle == 0)
        {
            return new InjectionFailedError(foundPath, "Only the devil knows why this happened.");
        }

        return Result.FromSuccess();
    }

    private ManagedMemoryAllocation AllocateString(IMemory memory, string str)
    {
        var bytes = Encoding.Unicode.GetBytes(str);
        var allocated = memory.Allocate(bytes.Length + 1);
        if (allocated == nuint.Zero)
        {
            return new ManagedMemoryAllocation(memory, allocated);
        }

        memory.SafeWriteRaw(allocated + (nuint)bytes.Length, new byte[] { 0 });
        memory.SafeWriteRaw(allocated, bytes);
        return new ManagedMemoryAllocation(memory, allocated);
    }
}
Do not follow this link