Home > OS >  How can a WOW64 program overwrite its command-line arguments, as seen by WMI?
How can a WOW64 program overwrite its command-line arguments, as seen by WMI?

Time:06-20

I'm trying to write a program that can mask its command line arguments after it reads them. I know this is stored in the PEB, so I tried using the answer to "How to get the Process Environment Block (PEB) address using assembler (x64 OS)?" by Sirmabus to get that and modify it there. Here's a minimal program that does that:

#include <wchar.h>
#include <windows.h>
#include <winnt.h>
#include <winternl.h>

// Thread Environment Block (TEB)
#if defined(_M_X64) // x64
PTEB tebPtr = reinterpret_cast<PTEB>(__readgsqword(reinterpret_cast<DWORD_PTR>(&static_cast<NT_TIB*>(nullptr)->Self)));
#else // x86
PTEB tebPtr = reinterpret_cast<PTEB>(__readfsdword(reinterpret_cast<DWORD_PTR>(&static_cast<NT_TIB*>(nullptr)->Self)));
#endif

// Process Environment Block (PEB)
PPEB pebPtr = tebPtr->ProcessEnvironmentBlock;

int main() {
    UNICODE_STRING *s = &pebPtr->ProcessParameters->CommandLine;
    wmemset(s->Buffer, 'x', s->Length / sizeof *s->Buffer);
    getwchar();
}

I compiled this both as 32-bit and 64-bit, and tested it on both 32-bit and 64-bit versions of Windows. I looked for the command line using Process Explorer, and also by using this PowerShell command to fetch it via WMI:

Get-WmiObject Win32_Process -Filter "name = 'overwrite.exe'" | Select-Object CommandLine

I've found that this works in every combination I tested it in, except for using WMI on a WOW64 process. Summarizing my test results in a table:

Architecture Process Explorer WMI
64-bit executable on 64-bit OS (native) ✔️ xxxxxxxxxxxxx ✔️ xxxxxxxxxxxxx
32-bit executable on 64-bit OS (WOW64) ✔️ xxxxxxxxxxxxx ❌ overwrite.exe
32-bit executable on 32-bit OS (native) ✔️ xxxxxxxxxxxxx ✔️ xxxxxxxxxxxxx

How can I modify my code to make this work in the WMI WOW64 case too?

CodePudding user response:

wow64 processes have 2 PEB (32 and 64 bit) and 2 different ProcessEnvironmentBlock (again 32 and 64). the command line exist in both. some tools take command line correct (from 32 ProcessEnvironmentBlock for 32bit processes) and some unconditional from 64bit ProcessEnvironmentBlock (on 64 bit os). so you want zero (all or first char) of command line in both blocks. for do this in "native" block we not need access TEB/PEB/ProcessEnvironmentBlock - the GetCommandLineW return the direct pointer to the command-line string in ProcessEnvironmentBlock. so next code is enough:

PWSTR psz = GetCommandLineW();
while (*psz) *psz   = 0;

or simply

*GetCommandLineW() = 0;

is enough

as side note, for get TEB pointer not need write own macro - NtCurrentTeb() macro already exist in winnt.h

access 64 bit ProcessEnvironmentBlock from 32 bit process already not trivial. one way suggested in comment. another way more simply, but not documented - call NtQueryInformationProcess with ProcessWow64Information

When the ProcessInformationClass parameter is ProcessWow64Information, the buffer pointed to by the ProcessInformation parameter should be large enough to hold a ULONG_PTR. If this value is nonzero, the process is running in a WOW64 environment. Otherwise, the process is not running in a WOW64 environment.

so this value receive some pointer. but msdn not say for what he point . in reality this pointer to 64 PEB of process in wow64 process.

so code can be next:

#ifndef _WIN64
    PEB64* peb64;
    if (0 <= NtQueryInformationProcess(NtCurrentProcess(), 
        ProcessWow64Information, &peb64, sizeof(peb64), 0) && peb64)
    {
        // ...
    }
#endif

but declare and use 64 bit structures in 32bit process very not comfortable (need all time check that pointer < 0x100000000 )

another original way - execute small 64bit shellcode which do the task.

the code doing approximately the following:

#include <winternl.h>
#include <intrin.h>

void ZeroCmdLine()
{
    PUNICODE_STRING CommandLine = 
        &NtCurrentTeb()->ProcessEnvironmentBlock->ProcessParameters->CommandLine;
    if (USHORT Length = CommandLine->Length)
    {
        //*CommandLine->Buffer = 0;
        __stosw((PUSHORT)CommandLine->Buffer, 0, Length / sizeof(WCHAR));
    }
}

you need create asm, file (if yet not have it in project) with the next code

.686

.MODEL FLAT

.code

@ZeroCmdLine@0 proc
    push ebp
    mov ebp,esp
    and esp,not 15
    push 33h
    call @@1
    ;         x64          
    sub esp,20h
    call @@0
    add esp,20h
    retf
@@0:
    DQ 000003025048b4865h
    DQ 0408b4860408b4800h
    DQ 00de3677048b70f20h
    DQ 033e9d178788b4857h
    DQ 0ccccc35fab66f3c0h
    ;-------- x64 ---------
@@1:
    call fword ptr [esp]
    leave
    ret
@ZeroCmdLine@0 endp

end

the code in the DQs came from this:

    mov rax,gs:30h
    mov rax,[rax 60h]
    mov rax,[rax 20h]
    movzx ecx,word ptr [rax 70h]
    jecxz @@2
    push rdi
    mov rdi,[rax 78h]
    shr ecx,1
    xor eax,eax
    rep stosw
    pop rdi
@@2:
    ret
    int3
    int3

custom build: ml /c /Cp $(InputFileName) -> $(InputName).obj

declare in c

#ifdef __cplusplus
extern "C"
#endif
void FASTCALL ZeroCmdLine(void);

and call it.

#ifndef _WIN64
    BOOL bWow;
    if (IsWow64Process(GetCurrentProcess(), &bWow) && bWow)
    {
        ZeroCmdLine();
    }
#endif

CodePudding user response:

To be explicit about the reason for the difference, it's that for a WOW64 process, Process Explorer will read from the 32-bit PEB, while WMI will read from the 64-bit PEB. For completeness, here's a WOW64 program written in NASM and C that will change its command line to all 3s as seen by Process Explorer, and to all 6s as seen by WMI:

global _memcpy64, _wmemset64, _readgsqword64
section .text

BITS 32
_memcpy64:
    push edi
    push esi
    call 0x33:.heavensgate
    pop esi
    pop edi
    ret

BITS 64
.heavensgate:
    mov rdi, [esp   20]
    mov rsi, [esp   28]
    mov rcx, [esp   36]
    mov rdx, rdi
    rep movsb
    mov eax, edx
    shr rdx, 32
    retf

BITS 32
_wmemset64:
    push edi
    call 0x33:.heavensgate
    pop edi
    ret

BITS 64
.heavensgate:
    mov rdi, [esp   16]
    mov eax, [esp   24]
    mov rcx, [esp   28]
    mov rdx, rdi
    rep stosw
    mov eax, edx
    shr rdx, 32
    retf

BITS 32
_readgsqword64:
    call 0x33:.heavensgate
    ret

BITS 64
.heavensgate:
    mov rdx, [rsp   12]
    mov rdx, gs:[rdx]
    mov eax, edx
    shr rdx, 32
    retf
#include <windows.h>
#include <winternl.h>
#include <stdint.h>

typedef struct _TEB64 {
    PVOID64 Reserved1[12];
    PVOID64 ProcessEnvironmentBlock;
    PVOID64 Reserved2[399];
    BYTE Reserved3[1952];
    PVOID64 TlsSlots[64];
    BYTE Reserved4[8];
    PVOID64 Reserved5[26];
    PVOID64 ReservedForOle;  // Windows 2000 only
    PVOID64 Reserved6[4];
    PVOID64 TlsExpansionSlots;
} TEB64;

typedef struct _PEB64 {
    BYTE Reserved1[2];
    BYTE BeingDebugged;
    BYTE Reserved2[21];
    PVOID64 LoaderData;
    PVOID64 ProcessParameters;
    BYTE Reserved3[520];
    PVOID64 PostProcessInitRoutine;
    BYTE Reserved4[136];
    ULONG SessionId;
} PEB64;

typedef struct _UNICODE_STRING64 {
    USHORT Length;
    USHORT MaximumLength;
    PVOID64 Buffer;
} UNICODE_STRING64;

typedef struct _RTL_USER_PROCESS_PARAMETERS64 {
    BYTE Reserved1[16];
    PVOID64 Reserved2[10];
    UNICODE_STRING64 ImagePathName;
    UNICODE_STRING64 CommandLine;
} RTL_USER_PROCESS_PARAMETERS64;

PVOID64 memcpy64(PVOID64 dest, PVOID64 src, uint64_t count);
PVOID64 wmemset64(PVOID64 dest, wchar_t c, uint64_t count);
uint64_t readgsqword64(uint64_t offset);

PVOID64 NtCurrentTeb64(void) {
    return (PVOID64)readgsqword64(FIELD_OFFSET(NT_TIB64, Self));
}

int main(void) {
    UNICODE_STRING *pcmdline = &NtCurrentTeb()->ProcessEnvironmentBlock->ProcessParameters->CommandLine;
    wmemset(pcmdline->Buffer, '3', pcmdline->Length / sizeof(wchar_t));

    TEB64 teb;
    memcpy64(&teb, NtCurrentTeb64(), sizeof teb);
    PEB64 peb;
    memcpy64(&peb, teb.ProcessEnvironmentBlock, sizeof peb);
    RTL_USER_PROCESS_PARAMETERS64 params;
    memcpy64(&params, peb.ProcessParameters, sizeof params);
    wmemset64(params.CommandLine.Buffer, '6', params.CommandLine.Length / sizeof(wchar_t));
    
    getwchar();
}

(A real program doing this should probably include some error checking and sanity tests to make sure it's running on the architecture it expects.)

  • Related