Passing arguments via rundll32.exe to function exported by DLL

After researching ways to export functions with their actual names from DLLs [1] the next thing I asked myself was: How I provide arguments to a DLL I execute via rundll32.exe. Bits and pieces of information existed out there. This post assembles this information and presents it in a hopefully easy-to-digest way.

This post in sections:

rundll32.exe behind the scenes

Always fascinated about the mystery behind the windows native utility rundll32.exe was looking for more information on how it actually works behind the scenes. At that time I was aware that it could be used to execute an exported function from a DLL. Assuming we’re working with the DLL named test.dll, his can be achieved with the following syntax:

rundll32.exe test.dll,<exported function name>

The step forward is how to pass arguments to this exported function and that’s the reason why this post exists!

The research [2][3] revealed that arguments can be passed in this manner:

rundll32.exe test.dll,<exported function name> <argument1> <argument2> <…> <argumentN>

Assume you have a DLL you call test.dll and the function HelperFunc. This function takes two arguments - one and 2. This would be the way to call the function (notice there is no space between test.dll and HelperFunc):

rundll32.exe test.dll,HelperFunc one 2

The following list shows the actions rundll32.exe takes in order to execute the exported function:

  1. Parses the command line
  2. Loads the specified DLL via LoadLibrary
  3. Obtains the address of the exported function via GetProcAddress
  4. Calls the exported function passing the arguments provided via the command line
  5. Unloads the DLL and exits once the exported function returns

A short demo of debugging rundll32

In this section we use windbg to confirm/trace what rundll32.exe does in the background. We already know that the LoadLibrary APIs (exported by kernel32.dll) call the LdrLoadDll (exported by ntdll.dll). So the next logical step is to break on LdrLoadDll and inspect the arguments that are passed to it.

We setup the breakpoint:

bp ntdll!LdrLoadDll

The LdrLoadDll is called multiple times as the Windows loader loads the appropriate modules in the address space of rundll32. Thus, we expect to hit our breakpoint many times. In order to see exactly when the DLL we provided as input to rundll32 gets loaded, as we said earlier, we have to inspect the passed arguments. The LdrLoadDll API is officially undocumented but a look around brings us to documentation written by researchers [4]. We are testing this on a x64 Windows 10 operating system. This means that the arguments to functions are passed in the registers RCX, RDX, R8, R9 [5]. The R8 register holds the name of the module to be loaded. In order to see which module is loaded each time we hit the breakpoint, we will be inspecting the R8 register. This register contains the pointer to a _UNICODE_STRING struct that has the actual name of the DLL to be loaded.

Once we hit the breakpoint, we issue the command:

dt _UNICODE_STRING @r8

Here is the output we got on the test machine:

unicodestring

We can also inspect the call stack and confirm that the call to LdrLoadDll originates from LoadLibraryExW:

callstackrundll

Code to generate a DLL with exported function

In order to demonstrate that it is possible to pass arguments to a function that is exported from a DLL, I wrote this code that once is compiled exports the function HelperFunc. The API OutputDebugStringA was used to print output to Sysinternal’s utility DebugView and thus trace the execution of this code.

#include <windows.h>
#include <debugapi.h>

#define DllExport comment(linker, "/EXPORT:HelperFunc=?HelperFunc@@YGXPAUHWND__@@PAUHINSTANCE__@@PADH@Z")

void WINAPI HelperFunc(HWND hwnd, HINSTANCE hinst, LPSTR lpszCmdLine, int nCmdShow)
{
    #pragma DllExport
    OutputDebugStringA("HelperFunc was executed");
    OutputDebugStringA(lpszCmdLine);
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch(fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            OutputDebugStringA("DllMain was executed");
            break;
        case DLL_PROCESS_DETACH:
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
            break;
    }

    return TRUE;
}

Output of calling an exported function

The following screenshot shows output from DebugView when we execute the following:

rundll32.exe test.dll,HelperFunc one 2 three

outputdebug

Bonus 1: Additional observation

I’ve also checked if it would be possible to execute the DLL with extensions unrelated to PE images, like for example .txt .crt .random. The following executions were successful, even if the DLL didn’t have that extension.

runndll32.exe test.crt,HelperFunc one 2 three

runndll32.exe test.txt,HelperFunc one 2 three

runndll32.exe test.random,HelperFunc one 2 three

Bonus 2: Powliks malware leveraging rundll32

You can also check the analysis [6] of a technique the malware Powliks used back in 2014 to execute malicious JavaScript using rundll32.exe.

Microsoft published a really informative report on threat actors that targeted cybersecurity researchers. What is interesting in this report and lines up with the present article is how the attacker executed their payloads. Few examples listed in Microsoft’s report:

C:\Windows\System32\rundll32.exe C:\ProgramData\VirtualBox\update.bin,ASN2_TYPE_new 5I9YjCZ0xlV45Ui8 2907

In the above example the attakers executed the (exported) function ASN2_TYPE_new from the DLL update.bin. The arguments provided to this function were the strings 5I9YjCZ0xlV45Ui8 and 2907

In another example taken again from the same report:

Rundll32.exe dxgkrnl_poc.vcxproj.suo,CMS_dataFinal Bx9yb37GEcJNK6bt 4231

This time, the function CMS_dataFinal exported from the DLL dxgkrnl_poc.vcxproj.suo was executed with the arguments Bx9yb37GEcJNK6bt and 4231.

To identify if the provided arguments have been processes by the function further analysis of the malicious libraries is required.

References

[1] https://stmxcsr.com//exporting-from-dll.html

[2] https://renenyffenegger.ch/notes/Windows/dirs/Windows/System32/rundll32_exe/index#rundll32-syntax

[3] https://www.fireeye.com/content/dam/fireeye-www/global/en/blog/threat-research/Flare-On%202017/Challenge6.pdf

[4] https://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FExecutable%20Images%2FLdrLoadDll.html

[5] https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention

[6] https://thisissecurity.stormshield.com/2014/08/20/poweliks-command-line-confusion/

[7] https://www.microsoft.com/security/blog/2021/01/28/zinc-attacks-against-security-researchers/