See the question and my original answer on StackOverflow

What you can do is use COM+ Component Services. With .NET the easiest way is use Enterprise Services's ServicedComponent which has all sorts of wrappers and utility classes to interop with COM+ Component services.

So here are steps to do it:

1) Create a .NET Framework Class Library.

2) Add it a strong name and sign it with it

3) Add it a class like this for example (I've also put some utility method to diagnose things)

[ComVisible(true)]
public class AdminClass : ServicedComponent
{
    public int DoSomethingAsAdmin()
    {
        // test something that a normal user shouldn't see
        return Directory.GetFiles(Path.Combine(Environment.SystemDirectory, "config")).Length;
    }

    public string WindowsIdentityCurrentName => WindowsIdentity.GetCurrent().Name;
    public string CurrentProcessFilePath => Process.GetCurrentProcess().MainModule.FileName;

    // depending on how you call regsvcs, you can run as a 32 or 64 bit surrogate dllhost.exe
    public bool Is64BitProcess => Environment.Is64BitProcess;
}

4) Add the following to AssemblyInfo.cs

[assembly: ApplicationName("AdminApp")]
[assembly: SecurityRole("AdminAppUser")]
[assembly: ApplicationActivation(ActivationOption.Server)]

What this does is define a COM+ application named "AdminApp", add a role named "AdminAppUser" to it, and declare the app will run as a "server" which means "out-of-process".

5) Compile that and run this command as admin

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\regsvcs.exe AdminApp.dll

or this command:

C:\Windows\Microsoft.NET\Framework\v4.0.30319\regsvcs.exe AdminApp.dll

Both commands will create the the COM + application, and host the .NET library DLL in a surrogate .exe (dllhost.exe). If you choose the first, the hosted process will run as x64, and if you run the second, the hosted process will run as x86.

You can check the result of this registration if you run Component Services (from Windows/Run):

enter image description here

6) Right-click the app and you'll see a whole bunch of cool things you can configure. Note you can even run this as a service (in the 'Activation' tab), etc. What you must do is configure the identity which will run this process, something like this:

enter image description here

Here, I've used a custom admin account. You don't want to use any of the other builtin choices.

7) Now, since default security has been enabled, basically nobody can calls this component. So we just have to add a user to the role "AdminAppUser" we created earlier. You can of course do this using the UI as shown here:

enter image description here

but here is a piece of code that does this programmatically (we use the COM+ administration objects) :

AddUserInRole("AdminApp", "AdminAppUser", @"SMO01\simon");

....

static void AddUserInRole(string appName, string roleName, string userName)
{
    dynamic catalog = Activator.CreateInstance(Type.GetTypeFromProgID("COMAdmin.COMAdminCatalog"));

    // the list of collection hierarchy : https://learn.microsoft.com/en-us/windows/desktop/cossdk/com--administration-collections
    var apps = catalog.GetCollection("Applications");
    var app = GetCollectionItem(apps, appName);
    if (app == null)
        throw new Exception("Application '" + appName + "' was not found.");

    var roles = apps.GetCollection("Roles", app.Key);
    var role = GetCollectionItem(roles, roleName);
    if (role == null)
        throw new Exception("Role '" + roleName + "' was not found.");

    // UsersInRole collection
    // https://learn.microsoft.com/en-us/windows/desktop/cossdk/usersinrole
    var users = roles.GetCollection("UsersInRole", role.Key);
    var user = GetCollectionItem(users, userName);
    if (user == null)
    {
        user = users.Add();
        user.Value["User"] = userName;
        users.SaveChanges();
    }
}

static dynamic GetCollectionItem(dynamic collection, string name)
{
    collection.Populate();
    for (int i = 0; i < collection.Count; i++)
    {
        var item = collection.Item(i);
        if (item.Name == name)
            return item;
    }
    return null;
}

The result should be like this:

enter image description here

8) Now, for the client app, using the AdminApp facilities is easy. Don't reference the .DLL as a standard .NET reference, but use it as any other external COM component. You could reference the .TLB file that was created by regsvcs, or just use the magic dynamic keyword as I demonstrate here (the drawback is you don't get autocompletion):

using System;
using System.Security.Principal;

namespace UserApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Is64BitProcess " + Environment.Is64BitProcess);
            Console.WriteLine("Running As " + WindowsIdentity.GetCurrent().Name);

            var type = Type.GetTypeFromProgID("AdminApp.AdminClass");
            dynamic trustedClass = Activator.CreateInstance(type);

            Console.WriteLine("Admin App Process Path: " + trustedClass.CurrentProcessFilePath);
            Console.WriteLine("Admin App Running As: " + trustedClass.WindowsIdentityCurrentName);
            Console.WriteLine("Admin App Is64BitProcess: " + trustedClass.Is64BitProcess);
            Console.WriteLine("Admin App DoSomethingAsAdmin: " + trustedClass.DoSomethingAsAdmin());
        }
    }
}

Now, when you run it for example as "simon", you should see something like this, it works:

Is64BitProcess False
Running As SMO01\simon
Admin App Process Path: C:\WINDOWS\system32\dllhost.exe
Admin App Running As: SMO01\myAdmin
Admin App Is64BitProcess: True
Admin App DoSomethingAsAdmin: 71

and when you run it for example as "bob" who's not configured in the role, you should see something like this with an access denied, this is expected:

Is64BitProcess False
Running As SMO01\bob

Unhandled Exception: System.UnauthorizedAccessException: Retrieving the COM class factory for component with CLSID {0DC1F11A-A187-3B6D-9888-17E635DB0974} failed due to the following error: 80070005 Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED)).
   at System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean noCheck, Boolean& canBeCached, RuntimeMethodHandleInternal& ctor, Boolean& bNeedSecurityCheck)
   at System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark)
   at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark)
   at System.Activator.CreateInstance(Type type, Boolean nonPublic)
   at System.Activator.CreateInstance(Type type)
   at UserApp.Program.Main(String[] args) in C:\Users\simon\source\repos\TrustedSystem\UserApp\Program.cs:line 14

Note we've created a trusted system without setting any password anywhere. And, I've only scratched the surface of what you can do with COM+ component. For example, you can export the app as an .MSI for easy deployment, etc.