Edit 1: Changed title from 'AnsiString' to 'AnsiChar', as it was a typo.
Edit 2: Added POC code that demonstrates crash.
This question involves the same project from a previous question: Converting Integer to String crashes Injected Parent Process [Delphi].
The challenge carried over from my previous question is that I am limited to using only WinAPI functions because this code is executing within an injected process. My WinAPI skills are slowly getting better, but I'm still falling into a lot of pitfalls.
My problem is that I need to dynamically generate the command line parameters for a procedure that will execute cmd.exe and return the output.
I am using a slightly modified version of a procedure called CaptureConsoleOutput() that I found on StackOverflow that executes cmd.exe with command line arguments and then returns the output. The original code is located here: How do I run a command-line program in Delphi?
When using hardcoded parameters this procedure works as desired. However, as soon as I try to pass in a dynamically generated string the parent process crashes. I'm confident that the issue is how I'm converting the array of AnsiString to string. The CaptureConsoleOutput() procedure requires a string, and I'm using an array of AnsiChar to build up my command line parameters.
The modified prototype for the procedure is as follows:
procedure CaptureConsoleOutput(const ACommand);
Here are some of my attempts:
{ this causes a crash }
var cmdStr : array [0..254] of AnsiChar;
strcat(@cmdStr, 'cmd.exe /C ipconfig');
MessageBoxA(0, cmdStr, 'Generated Parameters', 0);
CaptureConsoleOutput(@cmdStr[0]);
Note that in the above example I'm even calling MessageBoxA() to validate that strcat function worked and filled cmdStr with the required command and parameters. I assume that referencing the pointer of the array should pass the value as a string. It's not working and I'm not sure how else to pass the value of an array.
{ this works - hard coded parameters }
CaptureConsoleOutput('cmd.exe /C ifconfig');
{ this also works - hard coded string variable }
var testStr : string = 'cmd.exe /C ifconfig';
CaptureConsoleOutput(testStr);
As soon as I either try to change testStr, or use cmdStr the process crashes.
To try and convert an Array of AnsiChar to a String I've tried strcat(), strcpy(), StrCopy(), StrPCopy(), Move(), SetString(). Clearly, some of these functions are Delphi specific and naturally are not expected to work due to the context of running within an injected process. I think the issue is not with strcat(), but with how I'm trying to pass the value.
Edit 2: Proof of Concept code that demonstrates the crash
- This is an x64 console app running on Delphi RAD Studio 10.3
- Set Target Platform to 'Windows: 64-bit'
- Code reduced to include only enough to demonstrate crash conditions.
- The application injects itself into a running process, and then launches cmd.exe to return the results of ipconfig.
- When using hard coded parameters the application works.
- Trying to pass an Array of AnsiChar crashes the process, as I'm not sure how to pass an Array of AnsiChar as a string.
Steps to recreate crash:
- Compile code
- Launch notepad
- Use Task Manager to Process Explorer to identify the PID of Notepad
- Execute 'Inject.exe PID'
- You should see a MessageBox with correct results from ipconfig
- You should see a MessageBox validating that strcat worked as expected
- Notepad crashes...
- Third MessageBox is never displayed
I've done research and acknowledge that Delphi 2009+ has changed how it treats variable types, and that I think my next step is to invest in a decent book.
program Inject;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
System.Variants,
System.Classes,
Winapi.Windows,
Winapi.Messages,
ShellAPI,
System.Win.crtl,
shlobj;
var
ClassName: string;
ProcessHandle: Thandle;
Active : Integer;
PID : integer;
Module, NewModule: Pointer;
Size: SIZE_T;
BytesWritten: SIZE_T;
TID: DWORD;
User : array [0..1024] of AnsiChar; // Used when getting Host UserName
procedure CaptureConsoleOutput(ACommand : String);
const
CReadBuffer = 2400;
var
saSecurity: TSecurityAttributes;
hRead: THandle;
hWrite: THandle;
suiStartup: TStartupInfo;
piProcess: TProcessInformation;
pBuffer: array[0..CReadBuffer] of AnsiChar;
dRead: DWord;
dRunning: DWord;
str : string;
begin
saSecurity.nLength := SizeOf(TSecurityAttributes);
saSecurity.bInheritHandle := True;
saSecurity.lpSecurityDescriptor := nil;
if CreatePipe(hRead, hWrite, @saSecurity, 0) then
begin
FillChar(suiStartup, SizeOf(TStartupInfo), #0);
FillChar(piProcess, SizeOf(TProcessInformation), #0);
suiStartup.cb := SizeOf(TStartupInfo);
suiStartup.hStdInput := hRead;
suiStartup.hStdOutput := hWrite;
suiStartup.hStdError := hWrite;
suiStartup.dwFlags := STARTF_USESTDHANDLES or STARTF_USESHOWWINDOW;
suiStartup.wShowWindow := SW_HIDE;
if CreateProcess(nil, PChar(ACommand), nil, nil, True, CREATE_NO_WINDOW + CREATE_NEW_PROCESS_GROUP + NORMAL_PRIORITY_CLASS, nil, nil, suiStartup, piProcess) then
begin
repeat
dRunning := WaitForSingleObject(piProcess.hProcess, 100);
repeat
dRead := 0;
ReadFile(hRead, pBuffer[0], CReadBuffer, dRead, nil);
pBuffer[dRead] := #0;
OemToAnsi(pBuffer, pBuffer);
until (dRead < CReadBuffer);
until (dRunning <> WAIT_TIMEOUT);
strcpy(User, pBuffer);
CloseHandle(piProcess.hProcess);
CloseHandle(piProcess.hThread);
end
else
MessageBoxA(0, 'CreateProcess() didnt work','Alert',0);
CloseHandle(hRead);
CloseHandle(hWrite);
end
else
MessageBoxA(0, 'CreatePipe() didnt work','Alert',0);
end;
procedure Main;
var
x : Integer;
s : string;
cmdStr : array [0..255] of AnsiChar;
begin
LoadLibrary('kernel32.dll');
LoadLibrary('user32.dll');
LoadLibrary('msvcrt.dll');
LoadLibrary('win32.dll');
{ Hardcoded command and parameters works }
CaptureConsoleOutput('cmd.exe /C ipconfig');
MessageBoxA(0, User, 'Result from hard coded ipconfig', 0);
{ Use strcat to copy the command and parameters into the variable cmdStr }
{ MessageBoxA() used to validate that the strcat() function worked as expected }
strcat(cmdStr, 'cmd.exe /C ipconfig');
MessageBoxA(0, cmdStr, 'Result from strcat', 0);
{ Problem is here... }
{ I'm trying to pass an array of AnsiChar to a procedure that is expecting a String }
CaptureConsoleOutput(cmdStr); // <--- Crashes
CaptureConsoleOutput(cmdStr[1]); // <--- crashes
CaptureConsoleOutput(@cmdStr[0]); // <--- Incompatible types 'string' and 'pointer'
MessageBoxA(0, User, 'Result from string copy ipconfig', 0);
ExitThread(0);
end;
begin
try
if (ParamCount() > 0) then
begin
PID := StrToInt(ParamStr(1));
ProcessHandle := OpenProcess(PROCESS_ALL_ACCESS, False, PID);
Module := Pointer(GetModuleHandle(nil));
Size := PImageOptionalHeader64(Pointer(integer(Module) + PImageDosHeader(Module)._lfanew + SizeOf(dword) + SizeOf(TImageFileHeader))).SizeOfImage;
VirtualFreeEx(ProcessHandle, Module, 0, MEM_RELEASE);
NewModule := VirtualAllocEx(ProcessHandle, Module, Size, MEM_COMMIT or MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(ProcessHandle, NewModule, Module, Size, BytesWritten);
CreateRemoteThread(ProcessHandle, nil, 0, @Main, Module, 0, TID);
WaitForSingleObject(ProcessHandle, 1000);
end
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.