Due to my curiosity on the rundll32.exe that will trigger a COM execution via -sta/-localserver switch, I decided to dig into the rundll32.exe code further to understand why does it so.
Investigation
During the initial phase, rundll32.exe itself will parse the argument passed into it via RunDLL_ParseCommand. This function will determine which part of code will be execute based on the output of switch_flag that been processed by RunDLL_ParseCommand.
There are only 3 switch type for rundll32.exe:
- -sta (represented by decimal value 1)
- -localserver (represented by decimal value 2)
- non of above
Inside the RunDLL_ParseCommand, it will start to determine the switch type by getting the first character of the passed value.
__int64 __fastcall RunDLL_ParseCommand(
unsigned __int16 *lpstrCmdline,
enum SWITCH_FLAGS *switch_flag,
unsigned __int16 **lpsz,
unsigned __int16 **ordinal_num,
unsigned __int16 **command_arg)
{
unsigned __int16 **v5; // r12
unsigned __int16 **first_char; // rdi
unsigned __int16 *lpstrCmdline_2; // rbx
unsigned __int16 first_char_after_switch; // ax
unsigned __int16 *command_arg_1; // rcx
__int64 arg_len; // rax
int Switch; // eax
__int64 v15; // rax
int v16; // eax
unsigned __int16 v17; // ax
unsigned __int16 v18; // cx
unsigned __int16 v19; // ax
unsigned __int16 first_char_of_argv2; // ax
__int64 result; // rax
__int16 v22; // ax
unsigned __int16 *v23; // rsi
LPWSTR v24; // rcx
unsigned __int16 v25; // ax
unsigned __int16 v26; // dx
WCHAR i; // ax
bool is_localserver; // zf
int is_localserver_2; // ecx
v5 = command_arg;
*(_DWORD *)switch_flag = 0;
first_char = (unsigned __int16 **)lpstrCmdline;
*lpsz = 0i64;
lpstrCmdline_2 = lpstrCmdline;
*v5 = 0i64;
*ordinal_num = 0i64;
if ( *lpstrCmdline )
{
while ( 1 ) // GOTO: when_not_switch_symbol_found
{
when_space_found_2:
while ( *(_WORD *)first_char == ' ' )
{ // will return here once a space character is found in order to separate the argv correctly
first_char = (unsigned __int16 **)((char *)first_char + 2);// since it wide char, it need +2 to get into "{"
// "<space> \x00 {"
lpstrCmdline_2 = (unsigned __int16 *)first_char; // assigning the clisd into lpstrCmdline_2
}
if ( *(_WORD *)first_char != '/' && *(_WORD *)first_char != '-' )// windows swtich symbol
goto when_not_switch_symbol_found;
first_char = (unsigned __int16 **)(lpstrCmdline_2 + 1);
first_char_after_switch = lpstrCmdline_2[1];
lpstrCmdline_2 = (unsigned __int16 *)first_char;
command_arg = first_char;
if ( first_char_after_switch )
break;
when_space_found:
if ( !*(_WORD *)first_char )
goto when_space_found_2;
}
command_arg_1 = (unsigned __int16 *)first_char;
while ( 1 ) // switch string scanning loop here
{
lpstrCmdline_2 = command_arg_1;
scanning_first_char: // This should be the correct one
if ( first_char_after_switch == ' ' )
goto when_space_found;
if ( first_char_after_switch == 'L' )
break;
switch ( first_char_after_switch )
{
case 'S': // Sta
goto sta_switch;
case 'l': // localserver
goto LABEL_21;
case 's': // sta
sta_switch:
arg_len = -1i64; // arg_len without the "-"
do
++arg_len;
while ( command_arg_1[arg_len] );
if ( (int)arg_len < 3 )
{
Switch = 0;
}
else
{
Switch = _GetSwitch(L"sta", (unsigned __int16 **)&command_arg, 3);
first_char = command_arg; // first_char will point to "a" becuz of "*a2 += cchCount2 - 1;"
}
if ( Switch )
*(_DWORD *)switch_flag |= 1u; // set the switch_flag to 1
break;
}
get_sta_arg:
first_char = (unsigned __int16 **)((char *)first_char + 2);// get argv[2] with space, arg value after the switch (argv[1])
// NOTE: remember its wide char ya
lpstrCmdline_2 = (unsigned __int16 *)first_char;
command_arg = first_char;
command_arg_1 = (unsigned __int16 *)first_char;
first_char_after_switch = *(_WORD *)first_char;
if ( !*(_WORD *)first_char )
goto scanning_first_char;
}
LABEL_21:
v15 = -1i64;
do
++v15;
while ( command_arg_1[v15] );
if ( (int)v15 < 11 )
{
v16 = 0;
}
else
{
v16 = _GetSwitch(L"localserver", (unsigned __int16 **)&command_arg, 11);
first_char = command_arg;
}
if ( v16 )
*(_DWORD *)switch_flag |= 2u;
goto get_sta_arg;
}
when_not_switch_symbol_found:
if ( !*(_WORD *)first_char )
return 0i64;
if ( *(_WORD *)first_char == '"' )
{
first_char = (unsigned __int16 **)(lpstrCmdline_2 + 1);
v17 = lpstrCmdline_2[1];
++lpstrCmdline_2;
if ( v17 )
{
v18 = v17;
do
{
v19 = v18;
if ( v18 == 34 )
break;
v19 = *++lpstrCmdline_2;
v18 = *lpstrCmdline_2;
}
while ( *lpstrCmdline_2 );
if ( v19 )
{
LABEL_37:
*lpstrCmdline_2 = 0;
do
v22 = *++lpstrCmdline_2;
while ( *lpstrCmdline_2 && (v22 == ' ' || v22 == ',') );
goto LABEL_48;
}
}
return 0i64;
}
while ( 1 )
{
first_char_of_argv2 = *lpstrCmdline_2;
if ( !*lpstrCmdline_2 || first_char_of_argv2 == ' ' || first_char_of_argv2 == ',' )
break;
++lpstrCmdline_2;
}
if ( *lpstrCmdline_2 )
goto LABEL_37;
if ( (*(_BYTE *)switch_flag & 3) == 0 )
return 0i64;
LABEL_48:
v23 = lpstrCmdline_2;
v24 = lpstrCmdline_2;
if ( (*(_BYTE *)switch_flag & 3) == 0 )
{
v25 = *lpstrCmdline_2;
if ( !*lpstrCmdline_2 )
return 0i64;
do
{
v26 = v25;
if ( v25 == 32 )
break;
v25 = *++lpstrCmdline_2;
v26 = *lpstrCmdline_2;
}
while ( *lpstrCmdline_2 );
if ( v26 )
{
*lpstrCmdline_2 = 0;
do
++lpstrCmdline_2;
while ( *lpstrCmdline_2 && *lpstrCmdline_2 <= 0x20u );
}
for ( i = *v24; i; i = *v24 )
{
if ( i == '\\' || i == '/' )
goto LABEL_62;
v24 = CharNextW(v24);
}
v24 = 0i64;
LABEL_62:
if ( v24 )
return 0i64;
}
result = 1i64;
is_localserver = (*(_DWORD *)switch_flag & 3) == 0;
is_localserver_2 = *(_DWORD *)switch_flag & 3;
*lpsz = (unsigned __int16 *)first_char; // if -sta, then first_char will be clsid
if ( !is_localserver )
lpstrCmdline_2 = v23;
if ( is_localserver_2 )
v23 = 0i64;
*ordinal_num = v23;
*v5 = lpstrCmdline_2;
return result;
}
Note that once the switch strings were matched, the code will scan through any non-space char that appended behind until it reach any sentinel value such as space char. So you can add something like -staabcdefg or -sta
After getting the switch_type, it will determine which part of code will be execute later.
Switch -sta
It will execute code _DoComObjectSta it the switch_flag is 1. This is the part where the CoCreateInstance will get call and trigger the COM execution 😏
After the CoCreateInstance being called with a unknown CLSID cause the IF condition become false and jump to the CoUnintialize().
Switch -localserver
Wanted to try with -localserver switch to trigger a COM call. However, it does not work because the CLSID has been hard-coded as shown in the screenshot below. 😥
(Probably MS has patched the rundll32 in newer windows version)
Re-implementation of -sta switch COM call
You can find my re-implemented c++ code for rundll32 COM execution in github ✨🎉🥳🎇🎆
References:
https://pentestlab.blog/2020/05/20/persistence-com-hijacking/
https://github.com/ghoulgy/RandomCodes/blob/master/cpp/com_hijack_progid_clsid.cpp