illustration by Craig Simmons

Octopus-Rex. Evolution of a multi task Botnet


During the last decade, different types of malware have been targeting Linux servers; Elknot, Encoder, Mirai, LuaBot, NyaDrop, Gayfgt etc. Most of them are used for DDoS purpose but there are some exceptions. Rex is one of them.
In this article we’ll try to present a detailed analysis of Rex.
Rex is a new malware developed in Go. Monitoring its activity over the last seven months brought out the efforts for developing various features.

Malware overview

Rex is a hybrid between a malware and a tool. The behavior depends on a list of arguments.
You can use it in two different ways:
– Scan mode: with the “scan” command line argument, the binary file uses embedded exploits to infect new Linux servers.
– Without scan mode: Rex contacts other bots through P2P protocol (DHT over HTTPS) and waits for commands.
Rex is always installed as a hidden file in the directory /tmp/, the malware does not have persistence mechanisms or any other hiding features. Quite the contrary, a help menu is available (-h).

benkow@stormshield:/home/rex/tmp$ ./.Z9g5aas0p0 -h
Usage of ./.Z9g5aas0p0:
        enable debugging
  -elevate.ignore string
        credentials to ignore during elevation (default "root")
        skip elevation (default true)
        enable stdio ipc
        log DHT requests
        log HTTP requests
  -socks string
        SOCKS5 proxy address
  -strategy string
        scan strategy [random, sequential] (default "random")
  -target string
        target(s) (default "")
  -wait int
        wait for PID to exit before starting (0: disable)
        enable WordPress Pingback

The help menu describes all the features available for both modes (scan or c&c).
Arguments details:
– Debug/log: launch the malware in debug mode, it is useful for analysis.
– Elevate: Rex can try to run itself as root by bruteforcing SSH service, you can ignore specific credentials with elevate.ignore pwd
– Ipc: we have not seen this feature used yet
– Socks: launch Rex through a socks proxy
– Strategy: configure how Rex scan IPs (random or sequential)
There are also some hidden arguments. You can use Rex as a DDoS tool with the argument “–stresser target”.
The main process is used for malware communication, when the bot master sends a command, the main process forks with the command in argument.
This is why, when you look at an infected host, Rex uses several processes:

Development cycle

Rex is a very active botnet. The binary file is updated on a daily basis. We’ll try to give an overview seven months of new features (click to enlarge).

Once upon a time… Rex – April 2016

The first version (a808a6e45d4f3837fcf30a28f6594ffff320f9b994eb35f7e915dd9d954c912c) was spotted at the end of April 2016.
Due to debug logs, we know that the malware is built on “/home/ubuntu/src/rex/”.


The first version was mainly used for infecting a first group of servers. It contained several exploits but no useful features.
Rex tries to infect other servers via Web based exploits (WordPress, Drupal…).
In order to exploit a remote file inclusion vulnerability, the remote file is hosted on infected machines on port 5099. I.E.: https://%s:5099/payload/php/%s/wp-gwollegb/ for gwollegb RFI exploit.


Rex infects Drupal websites via CVE-2014-3704, a SQLi that allows an attacker to change the admin password. It serves two purposes, first getting access to the server and second locking the website in order to ask for a ransom.
After exploitation, Rex wrote a blogpost on the homepage with the following message:
“Website is locked. Please transfer 1.4 BitCoin to address 3M6SQh8Q6d2j1B4JRCe2ESRLHT4vTDbSM9 to unlock content.”
In the first version, Drupal locker was the only “visible” feature.


Rex embeds the following WordPress plugins exploits
– Revslider
– Site-import
– Brandfolder
– Squirrel
– Robo-gallery
– Gwolle
– Woocommerce
– Issu panel

Hereafter is an example of infection:


In this example, Rex exploits a Revslider WordPress module in order to upload a zip file / which contains a PHP script used for PHP verification:

<?php print(ini_get('safe_mode').'|'.ini_get('safe_mode_exec_dir').'|'.ini_get('disable_functions').'|'.ini_get('open_basedir'));;die('ok - h5tmVOxiMH');?>

If everything is ok, Rex binary file is uploaded and the server is infected.


Rex embeds a module called “Kerner” in reference to blog “Kerner on security”. This module is a Remote Code Execution in CCTV-DVR


Rex embeds 2 Jetspeed vulnerabilities (CVE-2016-0709 CVE-2016-0710). These exploits are flagged as “TODO” and are not functional yet.

“We are armada collective” – May 2016

After one month, the bot master has uploaded the first big update with an interesting feature: a Ransom note sent to the Drupal admin. (21-05-2016) 92651d4a11a43a9043a8126f2ada1e5bf1e00cb506d46c939e20f3ece93cb81d

We are Armada Collective.
All your servers will be DDoS-ed starting {{ .Time.Weekday.String }} ({{ .Time.Format "Jan 2 2006" }}) if you don't pay {{ .Amount }} Bitcoins @ {{ .Address }}
When we say all, we mean all - users will not be able to access sites host with you at all.
If you don't pay by {{ .Time.Weekday.String }}, attack will start, price to stop will increase by {{ .Step }} BTC for every day of attack.
If you report this to media and try to get some free publicity by using our name, instead of paying, attack will start permanently and will last for a long time.
This is not a joke.
Our attacks are extremely powerful - sometimes over 1 Tbps per second. So, no cheap protection will help.
Prevent it all with just {{ .Amount }} BTC @ {{ .Address }}
Do not reply, we will probably not read. Pay and we will know its you. AND YOU WILL NEVER AGAIN HEAR FROM US!
Bitcoin is anonymous, nobody will ever know you cooperated.

Interesting fact with this ransom note, CloudFlare reported detection of this threat in March 2016. But we spot the first version of Rex with this ransom note at the end of May 2016.
A deeper look at the ransom note shows that it is not exactly the same; we have the same bullshit about 1Tb DDoS attacks but sender email is different (we’ve seen / and CloudFlare see ).
This coincidence lets us thinks that Rex developers have done some tests with this threat before creating Rex. At this time no real DDoS feature were present in the binary file.
Three days after (24-05-2016), another update came with one real DDoS implementation, DnsAmpl.

Optimizations time – June 2016.

During June 2016 we did not notice important updates, but we have seen that the bot master has refactored the source code until the end of June.
At the end of June, Rex has implemented a complete “stresser” module. Now the malware supports many different DDoS types (HTTP, SlowLoris, DNSAmp…) and the builder moved on another machine “/home/user/src/rex/”.

“We are anonymous” – July 2016

Some days after (09-07-2016) Rex added 3 new exploits:
– Drupal RESTWS REC exploit
– Magento RCE exploit (CVE-2015-1397)
– Airos Arbitrary File Upload Exploit
The ransom note has been rewritten. Now they did not mention Armada Collective anymore but call themself “anonymous”.

We are Anonymous.
All your servers will be DDoS-ed starting {{ .Time.Weekday.String }} ({{ .Time.Format "Jan 2 2006" }}) 
if you don't pay {{ .Amount }} Bitcoins @ {{ .Address }}
When we say all, we mean all - users will not be able to access sites host with you at all.
Right now we will start 15 minutes attack on your site's IP {{ .IP }}. It will not be hard, 
we will not crash it at the moment to try to minimize eventual damage, 
which we want to avoid at this moment. It's just to prove that this is not a hoax. Check your logs!
If you don't pay by {{ .Time.Weekday.String }}, 
attack will start, price to stop will increase by {{ .Step }} BTC for every day of attack.
If you report this to media and try to get some free publicity by using our name, 
instead of paying, attack will start permanently and will last for a long time.
This is not a joke.
Our attacks are extremely powerful - sometimes over 1 Tbps per second. So, no cheap protection will help.
Prevent it all with just {{ .Amount }} BTC @ {{ .Address }}
Do not reply, we will probably not read. Pay and we will know its you. AND YOU WILL NEVER AGAIN HEAR FROM US!
Bitcoin is anonymous, nobody will ever know you cooperated.

The ransom note tries to be more credible, It ask for log checking. Something it could not do before because of the lack of DDoS feature. But it is not enough to earn money. We checked some bitcoin addresses and all these wallets were empty.

BTCBrute and Clicky – August 2016.

Early in August, two new important updates came. The malware size has increased of 1.5mo and now embeds a bitcoin miner based on Btcsuite and a click fraud module called “clicky”.
The click fraud part is really interesting. Rex uses the botnet to display ads hosted on The game here is to use each bot for clicking on ads and earn money from advertiser. The good news is that it is easy to track ads campaign of a-ads and to retrieve nice statistics.
We have spotted three ad units: 218355 (code name “Unicorns!”), 261029 (code name “Porkupines!”) and 251270 (code name “Ferries!”). Two of them are associated to the bitcoin address 1HebiSQX2WfE2kXUuva79US4zNUxcYrHjZ and the last one used 1Q6mA6ERbwmaHX1nYwkrKuDiVjCYe2xma3.

unit 218355 - income details
unit 218355 – income details

unit 218355 - impressions details
unit 218355 – impressions details

The ads displayed looks like:

At the time we wrote this article, the clicky module has generated ~1€.

History of a fail – September 2016.

At the end of August, the first big fail of Rex starts (91164673cda591a9a4dec91ecda6dbb515d48df7b56108b5fa0053395c733188). Rex implements a feature for creating a lot of Instagram accounts, probably for social network fraud. But bypassing Instagram anti-spam is not so easy 🙂
First, Rex tries to use the botnet to create Instagram account via
Each bot used his own IP to create these fakes accounts. But Instagram has some anti-spam features and all nodes of the botnet have been blacklisted in a few minutes.

{ID: Name:oTmJzK6p Username:oTmJzK6p Password:DU9vD} 
(via &{Addr:XXX.XXX.XX.XXX:443 Type:2 Node:<nil> 
Created:0001-01-01 00:00:00 +0000 UTC Updated:0001-01-01 00:00:00 +0000 UTC}): token
{ID: Name:Sin4a Username:Sin4a Password:eVdU6} 
(via &{Addr:XXX.XXX.XX.XXX:443 Type:2 Node:<nil> 
Created:0001-01-01 00:00:00 +0000 UTC Updated:0001-01-01 00:00:00 +0000 UTC}): ip blacklisted

One week later, due to node blacklist, Bot master has implemented a proxy socks feature in order to bypass the Instagram blacklist.
This new feature results again in 2 fails:
– First implementation failed due to the length of the password.

{"status": "ok", "errors": {"password": ["Create a password at least 6 characters long."]}, 
"account_created": false}instagram.AccountCreate 
&{ID: Name:ZRSlnk3uH Username:ZRSlnk3uH Password:A1EtB} 
(via &{Addr:X.XXX.XXX.XX:80 Type:2 Node:<nil> 
Created:0001-01-01 00:00:00 +0000 UTC Updated:0001-01-01 00:00:00 +0000 UTC}): not created

– Second fails resides in the fact that Rex uses known proxy socks list that is already blocked by Instagram.

{"status": "ok", "errors": 
{"ip": ["The IP address you are using has been flagged as an open proxy. 
If you believe this to be incorrect, please visit"]}, 
"account_created": false}instagram.AccountCreate 
&{ID: Name:LOT8mWL Username:LOT8mWL Password:yF7QO3} 
(via &{Addr:XXX.XX.XX.XX:80 Type:2 Node:<nil> 
Created:0001-01-01 00:00:00 +0000 UTC Updated:0001-01-01 00:00:00 +0000 UTC}): ip blacklisted

After one month of fails, we have not seen this feature used anymore by the bot master.

When Rex meets Mirai – October 2016

After seven months of life, the main problem with Rex is the low number of bots. Without a large botnet, it is difficult to make a real return on investment.
In September 2016 (4b513dfc68fe825e5f83c51fc1a023c15bf1039e48e025a0a4f4b034dbf443b9), media put light on the Mirai botnet (IoT botnet used for DDoS).
After the leak of the source code of Mirai, Rex developer tried to implement the Mirai telnet scanner in Rex.

*scanner.telnet.mirai - trying ubnt:ubnt
*scanner.telnet.mirai - prompt at 36 in "ubnt\r\nUser name is incorrect\r\n\rLogin: "
*scanner.telnet.mirai - prompt at 38 in "enable\r\nUser name is incorrect\r\n\rLogin: "
*scanner.telnet.mirai - prompt at 38 in "system\r\nUser name is incorrect\r\n\rLogin: "
*scanner.telnet.mirai - prompt at 37 in "shell\r\nUser name is incorrect\r\n\rLogin: "
*scanner.telnet.mirai - prompt at 34 in "sh\r\nUser name is incorrect\r\n\rLogin: "
*scanner.telnet.mirai - credentials incorrect "/bin/busybox MIRAI\r\nUser name is incorrect\r\n\rLogin: "
*scanner.telnet.mirai - trying 888888:888888
*scanner.telnet.mirai - prompt at 38 in "888888\r\nUser name is incorrect\r\n\rLogin: "
*scanner.telnet.mirai - prompt at 38 in "enable\r\nUser name is incorrect\r\n\rLogin: "
*scanner.telnet.mirai - prompt at 38 in "system\r\nUser name is incorrect\r\n\rLogin: "
*scanner.telnet.mirai - prompt at 37 in "shell\r\nUser name is incorrect\r\n\rLogin: "
*scanner.telnet.mirai - prompt at 34 in "sh\r\nUser name is incorrect\r\n\rLogin: "
*scanner.telnet.mirai - credentials incorrect "/bin/busybox MIRAI\r\nUser name is incorrect\r\n\rLogin: "
*scanner.telnet.mirai - trying root:xc3511
*scanner.telnet.mirai - prompt at 35 in "\r\n\rPassword is incorrect\r\n\rPassword: "
*scanner.telnet.mirai - prompt at 35 in "\r\n\rPassword is incorrect\r\n\rPassword: "
*scanner.telnet.mirai - prompt at 35 in "\r\n\rPassword is incorrect\r\n\rPassword: "
*scanner.telnet.mirai - prompt at 35 in "\r\n\rPassword is incorrect\r\n\rPassword: "
*scanner.telnet.mirai - prompt at 35 in "\r\n\rPassword is incorrect\r\n\rPassword: "

As usual, this first buggy version of Rex Telnet scanner was tested directly in the wild. Unfortunately for the bot master, after one week of telnet scanning, only few new victims were infected (less than 10). But now, when you want to retrieve Mirai sample via Honeypots, you have to be sure that it is not Rex ;).
At the end of October (25-10-2016) (1058cce9f28c2a3522c31b67e913f00f229c2e00977c979dd68237e184c6df79) an update now include an SSH scanner. The malware scan Internet for SSH and try to brute force services with the same passwords list than Mirai.

*ssh.Scanner.Scan - ssh
*ssh.Scanner.Scan - ssh
*ssh.Scanner.Scan - ssh
*ssh.Scanner.Scan - ssh
*ssh.Scanner.Scan - ssh
*ssh.Scanner.Scan - ssh
*ssh.Scanner.Scan - ssh
*ssh.Scanner.Scan - ssh
*ssh.Scanner.Scan - ssh
*ssh.Scanner.login root anko - version "SSH-2.0-dropbear_0.52"
*ssh.Scanner.Scan - ssh
*ssh.Scanner.Scan - ssh
*ssh.Scanner.login [root anko]: wait: remote command exited without exit status or exit signal
*ssh.Scanner.Scan - ssh
*ssh.Scanner.Scan - ssh
*ssh.Scanner.Scan - ssh

Last funny fact, this version includes a set of commands used for QA and benchmarking purpose. Maybe they hired a Quality Engineer.

benkow_@stormshield:/home/rex# ./rex -h
Usage of ./rex:
        enable debugging
  -elevate.ignore string
        credentials to ignore during elevation (default "root")
        skip elevation (default true)
        enable stdio ipc
        log DHT requests
        log HTTP requests
  -socks string
        SOCKS5 proxy address
  -strategy string
        scan strategy [random, sequential] (default "random")
  -target string
        target(s) (default "")
  -test.bench string
        regular expression per path component to select benchmarks to run
        print memory allocations for benchmarks
  -test.benchtime duration
        approximate run time for each benchmark (default 1s)
  -test.blockprofile string
        write a goroutine blocking profile to the named file after execution
  -test.blockprofilerate int
        if >= 0, calls runtime.SetBlockProfileRate() (default 1)
  -test.count n
        run tests and benchmarks n times (default 1)
  -test.coverprofile string
        write a coverage profile to the named file after execution
  -test.cpu string
        comma-separated list of number of CPUs to use for each test
  -test.cpuprofile string
        write a cpu profile to the named file during execution
  -test.memprofile string
        write a memory profile to the named file after execution
  -test.memprofilerate int
        if >=0, sets runtime.MemProfileRate
  -test.outputdir string
        directory in which to write profiles
  -test.parallel int
        maximum test parallelism (default 8) string
        regular expression to select tests and examples to run
        run smaller test suite to save time
  -test.timeout duration
        if positive, sets an aggregate time limit for all tests
  -test.trace string
        write an execution trace to the named file after execution
        verbose: print additional output
  -wait int
        wait for PID to exit before starting (0: disable)
        enable WordPress Pingback

We’ll continue to monitor all these features, the developer seems to be creative.

Crawling the botnet

As reminding, Rex use DHT P2P over HTTPS for communication. Due to certificate pining failure it is easy for us to do some man-in-the-middle on the malware and then implement a crawler.
This is how looks like Rex DHT request

As you can see, Rex uses the default Go User-Agent “Go-http-client/1.1” and sends gzip encoded requests.
We know that DHT supports the following commands:
So, it is easy to implement a quick crawler.
At the time of writing, despite the efforts of the bot master, the botnet is still harmless (~150 bots). Not enough for doing any significant DDoS.

We try to identify the most affected country but due to the random scan strategy this do not allow us to conclude something useful.


Linux malware is a trendy topics, we can find new families every week. The huge amount of vulnerable servers available and the absence of anti-virus attracts crooks on the Linux side. They can stay on a compromised server for several months without being detected. In the case of Rex, if they did not implement “visible” features like Drupal locker, the malware would still be hidden.
Regarding how the bot master uses this botnet, we can easily conclude that it may not be part of a big cyber gang, Rex Botnet looks more like an experimental botnet.
2017 promises us some funny crapware on Linux.


Quick and dirty yara rules for VTi

rule Rex {
    description = "Quick and dirty rule for Rex malware"
    author = "Benkow_@Stormshield"
    $string1= {6d 61 69 6e 2e 67 6f}
    $string2 = {72 65 78}
    $string3= {64 72 75 70 61 6c}
    all of them

List of hashes (unpacked version only)


illustration by Craig Simmons

How to run userland code from the kernel on Windows – Version 2.0



2 years ago, Thierry F. wrote an article in this blog about a technique that could allow a driver to inject a DLL in a process ( This was based on the reverse engineering of the field PEB.KernelCallbackTable, which is untyped and completely undocumented.

You may have discovered, through the article mentioned above that, behind this opaque pointer, there is a big table of pointers related to User32.dll. This means that a process that does not load User32.dll will not have the field PEB.KernelCallbackTable initialized.

Furthermore, because this is completely undocumented by Microsoft, it’s also not supported at all. The pointer to User32!__ClientLoadLibrary has always been located in the PEB.KernelCallbackTable since Windows XP at least, but its index in there changes for each new version of Windows though.

What if I tell you that there is a way a bit more documented to do the same, that doesn’t rely on any reverse engineering or assembly code at all? And if I add the fact that this DLL is invisible for any API (GetModuleHandle for example) and even for the !peb or !dlls in WinDBG? Is that enough to tickle your curiosity?

Just a note before we start: all the code you’ll see here is not bullet proof, or production-ready. This is just a sample, a proof-of-concept just to illustrate this technique. I am aware that there are some loopholes that need to be fixed, but consider them as an exercise left to the reader. You can find all the code detailed in this article on this repository:
Note that the code in this article will also be stripped of some details (like the WoW64 processes’ injection support) for readability purposes but the github repository will hold the full code.

Peeping through the keyhole

When a program loads a DLL, there is a cascade of several events happening to this DLL. We can basically list them below:

  • A handle is opened on this DLL
  • A section of the mapped size of the DLL is created
  • The DLL is mapped into this section.
  • The loader does some internal work, such as taking care of some alignments, sections’ rights, etc.
  • Eventually, the DLL main is called

Again, this is a very rough description; the goal here is not to dive into each detail of this process. Anyway, what prevents a driver from doing exactly this? Opening a handle can easily be done with ZwCreateFile, mapping the DLL with ZwCreateSection/ZwMapViewOfSection. That leaves 2 problematic steps: the job done by the the loader and the execution of a function in this DLL. Hopefully, we can ask the Windows kernel to do that for us.

Before diving into the kernel code, let’s talk about the final objective here: the DLL. This DLL must NOT have any dependency. The loader will not resolve them for us (at least with the technique described here) and it could be complicated (and even unsafe since we are talking about userland space) to do it manually. The loader will also not fill the IAT (Import Address Table) of the DLL, so it’s a nice plus if the DLL can run without any imports. Obviously, for that kind of situation, kernel32!LoadLibrary and kernel32!GetProcAddress are our best friends (well, you will need to find those manually though).

I also promised there would be no assembly, so it must be done fully in C. Now let’s take a look at the kernel code.

Dive into the rabbit hole

To inject a DLL in every process, we need to be in each process’ context. For that, nothing is better than a notification callback. Let’s shoot two birds with one stone here and setup the LoadImage notification callback. Why this one? Because you’re sure it is called every time a process is created, and, cherry on the pie, it gives you the addresses of some interesting DLLs, like kernel32.dll. I suggest you call the function that will inject the DLL in the current process when you’re notified of the kernel32 mapping in this callback.

In this function, we’ll retrieve a handle on the current process:

Status = ObOpenObjectByPointer(PsGetCurrentProcess(),
if (!NT_SUCCESS(Status))
    return Status;

Now, we’re going to create and map the section in the current process memory:

InitializeObjectAttributes(&ObjectAttributes, NULL, OBJ_KERNEL_HANDLE, NULL, NULL);
Status = ZwCreateSection(&DllSectionHandle,
if (!NT_SUCCESS(Status))
    // cleanup
    return Status;

Status = ZwMapViewOfSection(DllSectionHandle,
if (!NT_SUCCESS(Status))
    // cleanup
    return Status;

We’re mapping the DLL with read and execution rights only. This will prevent any modification of the DLL in any way possible by the userland. We also set the SEC_IMAGE flag. This will tell the kernel loader to map the image as an executable one. This means that it will make all the required fixups and alignment. But it will not resolve the imports of this DLL!
Now that our DLL is mapped, we can easily call an exported function of our DLL. But first, we may need to give to this function some parameters in order to run properly. In order to do that, we will first allocate some userland memory that will hold those parameters:

InitializeObjectAttributes(&ObjectAttributes, NULL, OBJ_KERNEL_HANDLE, NULL, NULL);
MappingSize.QuadPart = PAGE_SIZE;
Status = ZwCreateSection(&InputSectionHandle,
                         SECTION_MAP_READ | SECTION_QUERY,
                         SEC_COMMIT | SEC_NO_CHANGE,
if (!NT_SUCCESS(Status))
    // cleanup
    return Status;

InputMappingAddress = NULL;
ViewSize = PAGE_SIZE;
Status = ZwMapViewOfSection(InputSectionHandle,
if (!NT_SUCCESS(Status))
    return Status;

This page will only have the read right. At least, that will prevent the userland to temper with your input parameters. To deny even further any modification, I set the SEC_NO_CHANGE flag. Now, even VirtualProtect or NtMapViewOfSection will fail if it targets our page. Note that you can also call MmSecureVirtualMemory on top of that. I suggest that, if you need to set some output parameter from your DLL, to allocate another page with the write right so the input parameters remain unmodified no matter what.

Okay, one last step, we need to setup the input parameters of our DLL. Just create a custom structure with the input parameters and fill it. Here, I just need some information about kernel32.dll. A MDL is a nice way to get around the PAGE_READONLY right set previously on the input parameters. This will allow us to map the same page in kernel address space with both read and write rights enabled.

ParamMDL = IoAllocateMdl(InputMappingAddress, PAGE_SIZE, FALSE, FALSE, NULL);
if (ParamMDL == NULL)
    // cleanup

    MmProbeAndLockPages(ParamMDL, UserMode, IoReadAccess);
    // cleanup
SystemAddress = MmGetSystemAddressForMdlSafe(ParamMDL, NormalPagePriority);
if (SystemAddress == NULL)
    // cleanup

RtlZeroMemory(SystemAddress, PAGE_SIZE);
DllParam = (PDLL_PARAMS)SystemAddress;

DllParam->Kernel32Address = Kernel32Address;
DllParam->Kernel32Size = Kernel32Size;


Last piece of the puzzle, the execution of our DLL. In order to make this DLL as small as possible, I didn’t set any export, but only an entry point fixed at 0x1000. Just use RtlCreateUserThread and you’re good to go. I suggest you to retrieve the entry point’s offset dynamically by parsing the PE header though.

Status = RtlCreateUserThreadPtr(ProcessHandle,
                                (PUCHAR)DllMappingAddress + 0x1000,
if (!NT_SUCCESS(Status))
    // cleanup
    return Status;

RtlCreateUserThread has one major drawback: it’s not available on Windows 7. It is exported in XP and since Windows 8 though. So, if you need to support Windows 7, you will have to find another solution. I may or may not have something up in my sleeve that I will reveal a bit later about this situation 😉

I’m the one who knocks

Now, a thread has been created, even before the main thread of the process has reached the entry point of the program. This thread will not be active right now, since you’re in the middle of some DLL initialization. So, I cannot wait for my new thread to finish since it will deadlock the current process (and the workstation in the end). And I need to wait until my thread is done in order to clean up the sections or read the output left by my userland code, etc. If only there was some kind of notification where I could register a custom callback that would be called whenever a thread has finished…

Oh wait!
Yup, you guessed it! PsSetCreateThreadNotifyRoutine will be our friend here. You just need to keep a context when you create the thread with RtlCreateUserThread, seek any terminating thread in your callback registered through PsSetCreateThreadNotifyRoutine that matches the CLIENT_ID received as output parameter of RtlCreateUserThread. You’ll end up with something like that:

PINJECT_CONTEXT SearchForContext(__in HANDLE ProcessID, __in HANDLE ThreadID)
    PLIST_ENTRY CurrentElement = NULL;
    PCTX_LIST   CurrentContext = NULL;

    ExAcquireResourceExclusiveLite(&ContextListLock, TRUE);

    for (CurrentElement = ContextHeadList.Flink;
         CurrentElement != &ContextHeadList;
         CurrentElement = CurrentElement->Flink)
        CurrentContext = (PCTX_LIST)CONTAINING_RECORD(CurrentElement,
        if (CurrentContext == NULL || CurrentContext->InjectContext == NULL)
        if (CurrentContext->InjectContext->ClientID.UniqueProcess == ProcessID &&
            CurrentContext->InjectContext->ClientID.UniqueThread == ThreadID)
        CurrentContext = NULL;

    if (CurrentContext)


    if (CurrentContext)
        return CurrentContext->InjectContext;
    return NULL;

VOID ThreadNotification(__in HANDLE ProcessID,
                        __in HANDLE ThreadID,
                        __in BOOLEAN Create)
    PINJECT_CONTEXT InjectContext = NULL;

    if (Create == TRUE)
    InjectContext = SearchForContext(ProcessID, ThreadID);
    if (InjectContext == NULL)

    // cleanup

    ExFreePoolWithTag(InjectContext, 'ewom');

You will notice that the thread created by RtlCreateUserThread can finish even before the process has started! In some situations, this can be very useful. Note that you can also call PsGetThreadExitStatus to retrieve the exit code of your thread.

There is no spoon

Now, the final step: the DLL itself. It must be compiled in a certain way if you want it without any IAT.
First, remove all dependencies and default libraries.


Then, disable some security checks that requires some CRT. This mean no /GS and no /RTC


Setup your entry point to whatever function you want


And voilà! You’re all set. You can obviously remove some options in order to make it smaller, but that’s up to you. It is possible to get a DLL smaller than 4KB while still able to retrieve LdrLoadDll or GetProcAddress by itself. Now your function designed as entry point in your DLL will have this prototype:

INT main(PDLL_PARAMS DllParams)
    if (DllParams == NULL ||
        DllParams->Kernel32Address == NULL ||
        DllParams->Kernel32Size == 0)
        return 0;

	// do whatever you want here

    return 1;

PDLL_PARAMS is a structure I defined earlier. It’s the 7th parameter of RtlCreateUserThread. Yup, you can share any kind of data blob between your driver and your DLL. Now, you’re free to parse kernel32 in order to search for LoadLibrary and GetProcAddress, all using plain C code. Even better, you don’t have to worry about things like relocations or relative addresses. For example, using strlen or strcmp like below on a constant string works like a charm, all thanks to the DLL format and loader fixing everything for us. You can even use globals without any issue.

if (strlen(CurrentFunctionName) == (sizeof("LdrLoadDll") - 1) &&
    !strcmp(CurrentFunctionName, "LdrLoadDll"))

And, last but not least, if you want to use functions like strcmp, strlen, etc. like the example above but don’t want any imports (which is recommended here), set the /Oi options in your DLL’s project. You can find the list of useable intrinsic functions here:


The cake is a lie

I must confess, I lied a bit in the introduction. We are not really loading a DLL, but using the structure of a DLL to wrap up some code. This allows us to create a file-backed section and map it into the userland memory of the current process while using documented functions. Even the loader helps us (a little bit at least) in our journey!
For me, this technique is more reliable than the one using KeUserModeCallback(). It even works like a charm with 32bits or 64bits processes. You just need to recompile in the desired architecture. WoW64 processes can be injected too, but take care of your pointers size then when filling the input parameters since your driver will be 64bits and your DLL will be 32bits. The code above might need some adjustments here and there, but nothing dramatic. The only major drawback is that it won’t work on Windows 7 since RtlCreateUserThread is not exported. In the end, it’s not perfect but that’s pretty close. In fact, it may even be the best technique to inject code from a driver you’ve ever seen.