Skip to content

Commit 8218eeb

Browse files
authored
Read a 32-bit process's command line from a 64-bit host (#798)
NtQueryInformationProcess with ProcessBasicInformation answers a 64-bit caller with the 64-bit PEB, even when the target runs under WOW64. GetCommandLine then handed that address to ReadCommandLine32, which reads it with 32-bit offsets and gets nothing usable. So no 32-bit diff tool ever had a command line - and that is most of them, since %ProgramFiles(x86)% is where the resolver goes looking. The consequences are the two things a command line is for: such a tool was never seen as already running, so AutoRefresh opened a second window instead of reusing the first and every relaunch spent another MaxInstance slot, and Kill never matched it, so it was never closed. Ask for the 32-bit PEB by name, through ProcessWow64Information. Also return null rather than a misread when a 32-bit host looks at a 64-bit target, which is the same confusion the other way round and equally unreadable. The test starts SysWOW64\cmd.exe, shaped like a diff tool invocation so FindAll keeps it, and looks for its command line. It fails without the fix.
1 parent bb37d2c commit 8218eeb

2 files changed

Lines changed: 142 additions & 3 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#if NET10_0
2+
/// <summary>
3+
/// Reading the command line of a 32-bit process from this 64-bit one.
4+
/// <para>
5+
/// NtQueryInformationProcess with ProcessBasicInformation answers a 64-bit caller with the 64-bit
6+
/// PEB, even when the target is running under WOW64. Reading that with 32-bit offsets produced
7+
/// nothing, so every 32-bit diff tool - which is most of the %ProgramFiles(x86)% installs the
8+
/// resolver goes out of its way to find - had no command line: never seen as already running, and
9+
/// never killed. The 32-bit PEB has to be asked for by name.
10+
/// </para>
11+
/// </summary>
12+
[NotInParallel]
13+
[RunOn(TUnit.Core.Enums.OS.Windows)]
14+
public class Wow64CommandLineTests
15+
{
16+
[Test]
17+
public async Task AThirtyTwoBitProcessHasAReadableCommandLine()
18+
{
19+
var wow = Path.Combine(
20+
Environment.GetFolderPath(Environment.SpecialFolder.Windows),
21+
"SysWOW64",
22+
"cmd.exe");
23+
if (!Environment.Is64BitProcess ||
24+
!File.Exists(wow))
25+
{
26+
// A 32-bit host, or a Windows with no WOW64 layer. Nothing to say here
27+
return;
28+
}
29+
30+
// Distinctive, so this cannot match any other cmd on the machine
31+
var marker = $"DiffEngineWow64Probe{Guid.NewGuid():N}";
32+
var process = Process.Start(
33+
new ProcessStartInfo
34+
{
35+
FileName = wow,
36+
// Shaped like a diff tool invocation, because FindAll only keeps command
37+
// lines with two file path arguments
38+
Arguments = $"/c ping -n 30 127.0.0.1 >nul & rem C:\\probe\\{marker}.received.txt C:\\probe\\{marker}.verified.txt",
39+
UseShellExecute = false,
40+
CreateNoWindow = true
41+
})!;
42+
43+
try
44+
{
45+
await Assert.That(await WaitForCommandLine(marker)).IsTrue();
46+
}
47+
finally
48+
{
49+
try
50+
{
51+
if (!process.HasExited)
52+
{
53+
process.Kill();
54+
}
55+
}
56+
catch
57+
{
58+
// Nothing useful to do if it has already gone
59+
}
60+
61+
process.Dispose();
62+
}
63+
}
64+
65+
static async Task<bool> WaitForCommandLine(string marker)
66+
{
67+
for (var attempt = 0; attempt < 40; attempt++)
68+
{
69+
var found = WindowsProcess
70+
.FindAll([with(StringComparer.OrdinalIgnoreCase), "cmd.exe"])
71+
.Any(_ => _.Command.Contains(marker, StringComparison.Ordinal));
72+
if (found)
73+
{
74+
return true;
75+
}
76+
77+
await Task.Delay(250);
78+
}
79+
80+
return false;
81+
}
82+
}
83+
#endif

src/DiffEngine/Process/WindowsProcess.cs

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ private static partial int NtQueryInformationProcess(
2929
int size,
3030
out int returnLength);
3131

32+
/// <summary>
33+
/// The same export, asked for a pointer sized answer rather than a struct: the ProcessWow64
34+
/// Information class returns the address of a WOW64 target's 32-bit PEB.
35+
/// </summary>
36+
[LibraryImport("ntdll.dll", EntryPoint = "NtQueryInformationProcess")]
37+
private static partial int NtQueryWow64Peb(
38+
SafeProcessHandle handle,
39+
int processInformationClass,
40+
ref IntPtr info,
41+
int size,
42+
out int returnLength);
43+
3244
[LibraryImport("kernel32.dll", SetLastError = true)]
3345
[return: MarshalAs(UnmanagedType.Bool)]
3446
private static partial bool IsWow64Process(
@@ -89,6 +101,14 @@ static extern bool ReadProcessMemory(
89101
IntPtr size,
90102
out IntPtr bytesRead);
91103

104+
[DllImport("ntdll.dll", EntryPoint = "NtQueryInformationProcess")]
105+
static extern int NtQueryWow64Peb(
106+
SafeProcessHandle handle,
107+
int processInformationClass,
108+
ref IntPtr info,
109+
int size,
110+
out int returnLength);
111+
92112
[DllImport("kernel32.dll", SetLastError = true)]
93113
static extern bool IsWow64Process(SafeProcessHandle handle, out bool isWow64);
94114
#endif
@@ -99,6 +119,12 @@ static extern bool ReadProcessMemory(
99119
const int processTerminate = 0x0001;
100120
const int processBasicInformation = 0;
101121

122+
/// <summary>
123+
/// ProcessWow64Information. Returns the address of the 32-bit PEB for a WOW64 target, or zero
124+
/// for one that is not running under WOW64.
125+
/// </summary>
126+
const int processWow64Information = 26;
127+
102128
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
103129
struct PROCESSENTRY32W
104130
{
@@ -298,16 +324,46 @@ static int FindDotSeparatedPath(CharSpan span)
298324
return null;
299325
}
300326

301-
return Environment.Is64BitProcess && !isTarget32Bit
302-
? ReadCommandLine64(handle, pbi.PebBaseAddress)
303-
: ReadCommandLine32(handle, pbi.PebBaseAddress);
327+
if (!Environment.Is64BitProcess)
328+
{
329+
// A 32-bit caller cannot reach a 64-bit target's PEB, so there is nothing useful to
330+
// read rather than something wrong to read
331+
if (Environment.Is64BitOperatingSystem &&
332+
!isTarget32Bit)
333+
{
334+
return null;
335+
}
336+
337+
return ReadCommandLine32(handle, pbi.PebBaseAddress);
338+
}
339+
340+
if (!isTarget32Bit)
341+
{
342+
return ReadCommandLine64(handle, pbi.PebBaseAddress);
343+
}
344+
345+
// A WOW64 target seen from a 64-bit caller. ProcessBasicInformation answers with the
346+
// 64-bit PEB the system keeps for it, and reading that with 32-bit offsets yields
347+
// nothing usable - so every 32-bit diff tool, which is most of the %ProgramFiles(x86)%
348+
// installs the resolver goes out of its way to find, had no command line at all: never
349+
// detected as already running, and never killed. The 32-bit PEB has to be asked for
350+
return TryGetWow64Peb(handle, out var wow64Peb)
351+
? ReadCommandLine32(handle, wow64Peb)
352+
: null;
304353
}
305354
catch
306355
{
307356
return null;
308357
}
309358
}
310359

360+
static bool TryGetWow64Peb(SafeProcessHandle handle, out IntPtr peb)
361+
{
362+
peb = IntPtr.Zero;
363+
return NtQueryWow64Peb(handle, processWow64Information, ref peb, IntPtr.Size, out _) == 0 &&
364+
peb != IntPtr.Zero;
365+
}
366+
311367
static string? ReadCommandLine64(SafeProcessHandle handle, IntPtr pebAddress)
312368
{
313369
// In 64-bit PEB, ProcessParameters is at offset 0x20

0 commit comments

Comments
 (0)