Passing arguments via rundll32.exe to function exported by DLL
After researching ways to export functions with their actual names from DLLs  the next thing I asked myself was: How I provide arguments to a DLL I execute via rundll32.exe. (When this post was first written) Bits and pieces of information existed out there. The post assembles this scattered information and presents it in a hopefully easy-to-digest way.
This post in sections:
- rundll32.exe behind the scenes
- Code to generate a DLL with exported function
- Output of calling an exported function
- Bonus 1: Additional Observation
- Bonus 2: Powliks malware leveraging rundll32
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, this 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!
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:
- Parses the command line
- Loads the specified DLL via LoadLibrary
- Obtains the address of the exported function via GetProcAddress
- Calls the exported function passing the arguments provided via the command line
- 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:
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.
Sadly and as it happens with many Windows APIs, LdrLoadDll is (at the time of writing) officially undocumented but a look around brings us to documentation written by researchers . 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 . The R8 register holds the name of the module to be loaded. 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:
We can also inspect the call stack and confirm that the call to LdrLoadDll originates from LoadLibraryExW:
Someone can jump to the conclusion that rundll32.exe can be leveraged to execute any function from any DLL. This conclusion is not correct. Certain conditions apply for it to be true.
If the DLL that is called from rundll32.exe implements DllMain, this function is run as soon as the DLL is loaded in the address space of rundll32 (see the screenshot from DebugView later in this post).
The definition of the called function (exported from the DLL that is given as input to rundll32.exe) has to include certain parameters. The following example shows the definition of an example function exported from an unmanaged DLL:
void WINAPI ExportedFunctionExample(HWND hwnd, HINSTANCE hinst, LPSTR lpszCmdLine, int nCmdShow);
If you have or want to create an exported function from a managed DLL (a DLL created with .NET Framework, written in C# - Csharp), the function needs to be defined similarly. The following definition is the C# equivalent that will work for this purpose:
public static void ExportedFunctionExample(IntPtr hwnd, IntPtr hinst, string lpszCmdLine, int nCmdShow);
Code to generate a DLL with exported function
A straightforward approach to create export functions is described in this section of my post “Exporting functions from DLL using the actual function name”.
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.
#define DllExport comment(linker, "/EXPORT:HelperFunc=?HelperFunc@@YGXPAUHWND__@@PAUHINSTANCE__@@PADH@Z")
void WINAPI HelperFunc(HWND hwnd, HINSTANCE hinst, LPSTR lpszCmdLine, int nCmdShow)
OutputDebugStringA("HelperFunc was executed");
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
OutputDebugStringA("DllMain was executed");
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
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
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 processed by the function further analysis of the malicious libraries is required.
* If you are interested to explore this corner of the web have a look at other posts
 https://www.microsoft.com/security/blog/2021/01/28/zinc-attacks-against-security-researchers/tags: #reversing