-1

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:

  1. Compile code
  2. Launch notepad
  3. Use Task Manager to Process Explorer to identify the PID of Notepad
  4. Execute 'Inject.exe PID'
  5. You should see a MessageBox with correct results from ipconfig
  6. You should see a MessageBox validating that strcat worked as expected
  7. Notepad crashes...
  8. 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.
Scooter
  • 41
  • 1
  • 7
  • You didn't show your modified `CaptureConsoleOutput()`, in particular how it is accessing the `AnsiChar[]` array you are passing to it. The original code took `UnicodeString` parameters, so how did you modify its code to work with ANSI data? Sounds like you didn't handle that correctly, but we can't see what you actually did – Remy Lebeau Dec 26 '19 at 04:20
  • Can you show a whole runnable program, boiled down to it's simplest case? – Sean B. Durkin Dec 26 '19 at 06:52
  • Thank you for the replies. I will pull out the appropriate code and create a version that demonstrates the crash. I'm travelling for the holidays today, so this may take a day. – Scooter Dec 26 '19 at 13:31
  • Travelled home from Niagara Falls and back at the keyboard :) – Scooter Dec 27 '19 at 15:46
  • 1
    I'm having a hard time understanding the code. You have a handle to another process. You determine the base address of your own executable. You want to release some memory of the other application which begins at an address that corresponds to the base address of your executable. That doesn't seem to make sense. Did you try and run your POC? – Sertac Akyuz Dec 27 '19 at 16:50
  • Hi @Sertac - the code has been stripped to a point that I’m able to demonstrate the crash, so please don’t try to analyze the code because that detracts from my problem. The issue I’m having is that I need help passing an array of AnsiChar to a procedure that is expecting a string. The PoC code does compile and works as described on RAD Studio 10.3, run on Windows 10 x64. I realize the code logic seems weird, but that’s because I’ve stripped it down to the base minimum. – Scooter Dec 27 '19 at 17:27
  • I think I may be close to solving my own problem. After more research I focused on the fact that WinAPI calls use WideString and not AnsiString. I switched from using CreateProcess() to CreateProcessA(), and changed the procedure to require a PAnsiChar instead of a string. This allows me to build up the parameters as an array of AnsiChar and pass a pointer to the array to the procedure. The results are promising, and If it works I'll post my code incase anyone else runs into the same issue. Boils down to my need to get better at variables and pointers. – Scooter Dec 28 '19 at 01:13
  • @Scooter Windows is a Unicode based OS. Why are you dealing with ANSI at all? Why wouldn't you want to use Unicode APIs? You are using a Unicode version of Delphi. – Remy Lebeau Dec 28 '19 at 04:37
  • FYI my reproduction crashes earlier, which does not surprise me at all. Both Virtual.. calls fail, which conveniently you don't check the return at all. That results in WriteProcessMemory to fail too which results with the crash of target process as soon as the remote thread is created. – Sertac Akyuz Dec 28 '19 at 13:17
  • @Remy - I've been using Delphi for 15+ years, but up to this point have focused on standard RTL type applications and never had to deal with Unicode/Ansi/Wide etc to this level. So to be honest, I'm still coming to grips with how Delphi interfaces with WinAPI calls. I started with Delphi 6, and I'm now using RAD Studio Rio 10.3. I'm using ANSI right now simply to get past this hurtle, because I'm not able to pass a String to the procedure. Changing to CreateProcessA() and passing a PAnsiChar is working. I'm going to spend today cleaning up the code and will post later. – Scooter Dec 28 '19 at 14:22
  • @Sertac - It's unfortunate that you were not able to reproduce the crash, but I appreciate your interest and trying to help. I'm not sure why it would crash earlier, but clearly we have different development setups. I acknowledge the code lacks checks, but in the interest of limiting the amount of code I posted here, I removed as much code as I could, while still demonstrating the crash, so that anyone wishing to help could concentrate on the issue at hand. If it helps to clarify the difference, I'm developing on Win 10 Pro Ver 1903 x64. The code I posted above definitely works on my system. – Scooter Dec 28 '19 at 14:32
  • You would need to make a "UniqueString" of the passed variable parameter (inside your `CaptureConsoleOutput` procedure), I think. Don't have a compiler with me to check this, but try this. Declare `var sCopy:string; ` and just before your CreateProcess `sCopy := ACommand; UniqueString(sCopy); if CreateProcess(nil, PChar(sCopy),...` – Nani Jan 01 '20 at 00:14

1 Answers1

0

To convert from array of AnsiChar to string, just use:

function ArrayToString(const a: array of AnsiChar): string;
var
  i: integer;
begin
  for I := Low(a) to High(a) do
    Result := Result + a[i]
end;
JoelFan
  • 37,465
  • 35
  • 132
  • 205