Why are strings in C++ so freakin’ complicated?!?!
OK, let’s back up a bit and I’ll explain what’s going on.
I am in the process of moving all of the scripts we use for accounts and such off of UMRA (which is going the way of the Dodo) to PowerShell. I had to find a way to capture password changes so they can be synced out to other systems, and was delighted when I found out that I could write my own password filter DLL to do the trick.
Now, I’ve played around with a number of programming languages, and I’m sure C++ was in there somewhere, so even though it’s been years since I’ve worked with C++ I figured that, with a little help from Google, I should be able to write something that triggers a PowerShell scrip that can take care of the rest.
When I looked up the best way to trigger a PowerShell script, I found the system() command. Looks simple enough. I see that it takes a char string, and the DLL is getting a PUNICODE_STRING, but it shouldn’t be a problem to convert one string to another… right?
So I decided to start with a very simple DLL that just passes everything it gets to a simple PowerShell script as command line arguments (which I tested with CMD to make sure that would work). Then, once I get that working, I plan to add some actual password filtering and maybe encrypting the password (in case the system() command is logged somewhere) before passing it to PS.
That was 2 weeks ago, and I can’t even figure out how to translate PUNICODE_STRING to the char type that system() expects. And I’ve tried so many things, I can’t remember them all. Yesterday I spent some time re-trying some of the ones I could remember, so I could document them here.
I’m working in Visual Studio 2022, on Windows Server 2016.
In the first code example, I have shown the entire code, but in subsequent examples I have commented out DllMain, InitializeChangeNotify, and PasswordFilter to save room in this post.
I would also like to point out that I haven’t tried compiling anything yet, I’m trying to get the code to a point where the debugger is happy first. So I have included the debugger errors as comments on the line that triggered them.
OK, my first attempt was to pack all of the text and variables into the system() command… which didn’t work, so I started trying to make a char variable that I could pass to system().
#include "pch.h"
#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <SubAuth.h>
#include <format>
#include <iostream>
#include <cstring>
#include <winternl.h>
#include <string.h>
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
BOOLEAN __stdcall InitializeChangeNotify(void)
{
return TRUE;
}
NTSTATUS __stdcall PasswordChangeNotify(PUNICODE_STRING UserName, ULONG RelativeId, PUNICODE_STRING NewPassword)
{
char PSCommand[] = "start powershell.exe SD60PWS.ps1 -Section 'ChangeNotify' -UserName ";
strcat(PSCommand, UserName); //Error - argument of type "PUNICODE_STRING" is incompatible with parameter of type "const char *"
//strcat(PSCommand, " -RelativeId ");
//strcat(PSCommand, RelativeId);
//strcat(PSCommand, " -NewPassword ");
//strcat(PSCommand, NewPassword);
system(PSCommand);
return 0;
}
BOOLEAN __stdcall PasswordFilter(PUNICODE_STRING AccountName, PUNICODE_STRING FullName, PUNICODE_STRING Password, BOOLEAN SetOperation)
{
return 1;
}
So I started Googling and found a reference to using RtlUnicodeStringToAnsiString() to do the conversion. Of course they didn’t say how so I had to do a bunch more searching and eventually came up with this…
#include "pch.h"
#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <SubAuth.h>
#include <format>
#include <iostream>
#include <cstring>
#include <winternl.h>
#include <string.h>
//DllMain and InitializeChangeNotify
NTSTATUS __stdcall PasswordChangeNotify(PUNICODE_STRING UserName, ULONG RelativeId, PUNICODE_STRING NewPassword)
{
ANSI_STRING ConvUserName;
RtlUnicodeStringToAnsiString(&ConvUserName, UserName, TRUE);
//Note: the example I found had an & in front of both variables, but the debugger didn't like that
// So I took them both away, and it didn't like that either. But the debugger was OK with this.
char PSCommand[] = "start powershell.exe SD60PWS.ps1 -Section 'ChangeNotify' -UserName ";
strcat(PSCommand, ConvUserName); //Error - no suitable conversion function from "ANSI_STRING" to "const char *"
//strcat(PSCommand, " -RelativeId ");
//strcat(PSCommand, RelativeId);
//strcat(PSCommand, " -NewPassword ");
//strcat(PSCommand, NewPassword);
system(PSCommand);
return 0;
}
//PasswordFilter
In my poking around on the interweb, I came across this excellent blog ‘The Complete Guide to C++ Strings, Part I and Part II‘ by Michael Dunn. Very informative… but also melted my brain.
One thing I found in it that I thought I should try was ATL conversion macros.
#include "pch.h"
#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <SubAuth.h>
#include <format>
#include <iostream>
#include <cstring>
#include <winternl.h>
#include <string.h>
#include <atlconv.h>
//DllMain and InitializeChangeNotify
NTSTATUS __stdcall PasswordChangeNotify(PUNICODE_STRING UserName, ULONG RelativeId, PUNICODE_STRING NewPassword)
{
USES_CONVERSION;
using std::string;
string ConvUserName;
ConvUserName = W2CA(UserName); //Error - a value of type "PUNICODE_STRING" cannot be assigned to an entity of type "LPCWSTR"
char PSCommand[] = "start powershell.exe SD60PWS.ps1 -Section 'ChangeNotify' -UserName ";
strcat(PSCommand, ConvUserName); //Error - no suitable conversion function from "std::string" to "const char *" exists
//strcat(PSCommand, " -RelativeId ");
//strcat(PSCommand, RelativeId);
//strcat(PSCommand, " -NewPassword ");
//strcat(PSCommand, NewPassword);
system(PSCommand);
return 0;
}
//PasswordFilter
After that, I went back to Microsoft’s post How to: Convert Between Various String Types. I though I’d picked up enough to be able to try their suggestion. I think I copied (and personalized) it all correctly, maybe I missed something?
#include "pch.h"
#include "atlbase.h"
#include "atlstr.h"
#include "comutil.h"
#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <SubAuth.h>
#include <format>
#include <iostream>
#include <cstring>
#include <winternl.h>
#include <string.h>
#include <atlconv.h>
using namespace std;
using namespace System; //Error - name must be a namespace name
//DllMain and InitializeChangeNotify
NTSTATUS __stdcall PasswordChangeNotify(PUNICODE_STRING UserName, ULONG RelativeId, PUNICODE_STRING NewPassword)
{
size_t origsize = wcslen(UserName) + 1; //Error - argument of type "PUNICODE_STRING" is incompatible with parameter of type "const wchar_t *"
size_t convertedChars = 0;
char strConcat[] = " (char *)";
size_t strConcatsize = (strlen(strConcat) + 1) * 2;
const size_t newsize = origsize * 2;
char* ConvUserName = new char[newsize + strConcatsize];
wcstombs_s(&convertedChars, ConvUserName, newsize, UserName, _TRUNCATE); //Error - no instance of overloaded function "wcstombs_s" matches the argument list
_mbscat_s((unsigned char*)ConvUserName, newsize + strConcatsize, (unsigned char*)strConcat);
char PSCommand[] = "start powershell.exe SD60PWS.ps1 -Section 'ChangeNotify' -UserName ";
strcat(PSCommand, ConvUserName);
//strcat(PSCommand, " -RelativeId ");
//strcat(PSCommand, RelativeId);
//strcat(PSCommand, " -NewPassword ");
//strcat(PSCommand, NewPassword);
system(PSCommand);
return 0;
}
//PasswordFilter
Lastly, while searching Stack Overflow specifically (before asking this question) I came across this post that wasn’t exactly what I was looking for, but they were passing the PUNICODE_STRING to a function that accepted it as a char* variable. So I figured I’d give that a try.
#include "pch.h"
#include "atlbase.h"
#include "atlstr.h"
#include "comutil.h"
#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <SubAuth.h>
#include <format>
#include <iostream>
#include <cstring>
#include <winternl.h>
#include <string.h>
#include <atlconv.h>
using namespace std;
//DllMain and InitializeChangeNotify
NTSTATUS __stdcall PasswordChangeNotify(PUNICODE_STRING UserName, ULONG RelativeId, PUNICODE_STRING NewPassword)
{
WriteToPowerShell("ChangeNotify", UserName, NewPassword); //Error - argument of type "PUNICODE_STRING" is incompatible with parameter of type "const char *"
return 0;
}
//PasswordFilter
void WriteToPowerShell(const char* PSSection, const char* PSUserName, const char* PSPassWord)
{
char PSCommand[] = "start powershell.exe SD60PWS.ps1 -Section ";
strcat(PSCommand, PSSection);
strcat(PSCommand, " -UserName ");
strcat(PSCommand, PSUserName);
strcat(PSCommand, " -NewPassword ");
strcat(PSCommand, PSPassWord);
system(PSCommand);
}
One last note. I know I probably have a lot of #includes that I don’t need and things like that. That’s because every time I tried something I would add any that I needed for that test, but I didn’t bother removing them after. I plan to comment them out one-by-one later, and discard the ones that don’t cause an error, and uncomment the ones that do.
I hope someone out there can help. And I hope that all of these failures weren’t just because I didn’t know I needed to #include <this> or using namespace that.
Alternately, if there’s some other way to trigger the PowerShell script that accepts PUNICODE_STRING, that would be helpful too.
Thank you.
2
Answers
UNICODE_STRING
contains awchar_t*
string pointer.You can convert a
wchar_t*
string to achar*
string using the Win32WideCharToMultiByte()
function. Or, you can use the (deprecated)std::wstring_convert
class, orwcsrtombs()
. Or, an equivalent 3rd party library, like ICU (which is now built-in to Windows 10 and later). There are MANY different ways (and MANY answered questions on StackOverflow) related to string conversions.Rather than using
system()
at all, you can instead use the Win32CreateProcessW()
function (which is whatsystem()
calls internally).system()
merely runs"cmd.exe /C <command>"
. In this case, your<command>
is usingstart
to runPowerShell.exe
. You can runPowerShell.exe
directly withCreateProcessW()
using awchar_t*
string, eg:at first of course you need use here not
system
butCreateProcessW
() for start new process. about string manipulations – one simply way – use c style format functions. here_snwprintf
most suitable and use%wZ
format forPUNICODE_STRING
. example: