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):
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:
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:
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:
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.