JamesMenetrey / MemorySharp

A C# based memory editing library targeting Windows applications, offering various functions to extract and inject data and codes into remote processes to allow interoperability.
Other
631 stars 134 forks source link

64-bit support #5

Open A-VORONKIN opened 8 years ago

A-VORONKIN commented 8 years ago

When it will support 64bit processes?

JamesMenetrey commented 8 years ago

The 64-bit support is my next target for MemorySharp. No ETA yet :)

zcanann commented 8 years ago

It shouldn't be too much work to do. at least for the memory side of things. Need a new struct for VirtualQueryEx.

As for Fasm, spin up a 32 bit process and use IPC to ask the process to assemble the bytes (easier said than done)

I'd raise a PR, but I only pulled in certain parts of the project and refactored them heavily -- so there is no easy way for me to add my contributions back to this project.

Here is what I did. Essentially either populate the MemoryBasicInformation32 or MemoryBasicInformation64 struct depending on the environment, then just always return the 64 bit struct. If its a 32 bit process, just copy the data into the 64 bit struct manually.

 [StructLayout(LayoutKind.Sequential)]
    public struct MemoryBasicInformation64
    {
        /// <summary>
        /// A pointer to the base address of the region of pages.
        /// </summary>
        public IntPtr BaseAddress;
        /// <summary>
        /// A pointer to the base address of a range of pages allocated by the VirtualAlloc function. The page pointed to by the BaseAddress member is contained within this allocation range.
        /// </summary>
        public IntPtr AllocationBase;
        /// <summary>
        /// The memory protection option when the region was initially allocated. This member can be one of the memory protection constants or 0 if the caller does not have access.
        /// </summary>
        public MemoryProtectionFlags AllocationProtect;
        /// <summary>
        /// Required in the 64 bit struct. Blame Windows.
        /// </summary>
        public UInt32 __alignment1;
        /// <summary>
        /// The size of the region beginning at the base address in which all pages have identical attributes, in bytes.
        /// </summary>
        public long RegionSize;
        /// <summary>
        /// The state of the pages in the region.
        /// </summary>
        public MemoryStateFlags State;
        /// <summary>
        /// The access protection of the pages in the region. This member is one of the values listed for the AllocationProtect member.
        /// </summary>
        public MemoryProtectionFlags Protect;
        /// <summary>
        /// The type of pages in the region.
        /// </summary>
        public MemoryTypeFlags Type;
        /// <summary>
        /// Required in the 64 bit struct. Blame Windows.
        /// </summary>
        public UInt32 __alignment2;
    };
public static MemoryBasicInformation64 Query(SafeMemoryHandle ProcessHandle, IntPtr BaseAddress)
        {
            MemoryBasicInformation64 MemoryInfo64 = new MemoryBasicInformation64();

            if (!Environment.Is64BitProcess)
            {
                // 32 Bit struct is not the same
                MemoryBasicInformation32 MemoryInfo32 = new MemoryBasicInformation32();

                // Query the memory region
                if (NativeMethods.VirtualQueryEx(ProcessHandle, BaseAddress, out MemoryInfo32, MarshalType<MemoryBasicInformation32>.Size) != 0)
                {
                    // Copy from the 32 bit struct to the 64 bit struct
                    MemoryInfo64.AllocationBase = MemoryInfo32.AllocationBase;
                    MemoryInfo64.AllocationProtect = MemoryInfo32.AllocationProtect;
                    MemoryInfo64.BaseAddress = MemoryInfo32.BaseAddress;
                    MemoryInfo64.Protect = MemoryInfo32.Protect;
                    MemoryInfo64.RegionSize = MemoryInfo32.RegionSize;
                    MemoryInfo64.State = MemoryInfo32.State;
                    MemoryInfo64.Type = MemoryInfo32.Type;

                    return MemoryInfo64;
                }
            }
            else
            {
                // Query the memory region
                if (NativeMethods.VirtualQueryEx(ProcessHandle, BaseAddress, out MemoryInfo64, MarshalType<MemoryBasicInformation64>.Size) != 0)
                {
                    return MemoryInfo64;
                }
            }

            return MemoryInfo64;
}

[DllImport("kernel32.dll", SetLastError = true)]
        public static extern int VirtualQueryEx(SafeMemoryHandle hProcess, IntPtr lpAddress, out MemoryBasicInformation32 lpBuffer, int dwLength);
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern int VirtualQueryEx(SafeMemoryHandle hProcess, IntPtr lpAddress, out MemoryBasicInformation64 lpBuffer, int dwLength);
JamesMenetrey commented 8 years ago

Hey @zcanann,

Interesting! I've searched a way to use fasm in x86-64 for long time and basically come to the conclusion that either we request to the developers of Fasm to provide a compatible code (even not worth trying) or we isolate the assembler in a separated process, as you suggested.

I don't really like the idea of having an executable next to the library but we don't have a lot of choice in this situation. As you seem to use this way, did you notice some perf issues around IPC ?

I think I'll go for a lightweight usage of WCF, as Microsoft was nice enough to provide this high-level API. :)

Cheers Zen

zcanann commented 8 years ago

@ZenLulz

I didn't notice any performance issues, but I only used it lightly (assembling < 50 bytes of data at a time very rarely)

I tried desperately to get 64 bit to work, but some form of IPC looks like it was the only solution. I was also considering exploring Nasm and Masm to see if there was similar capabilities, but I gave up because I was going insane. I ended up going with WCF as well (or at least I think so -- Microsoft has so many damn IPC options). I'll post the relevant code from my project, just in case there are any pieces you want to use.

Ended up with 3 projects. A main project, a shared library to define shared interfaces, and a 32 bit project. Main project -> AnyCPU FasmProxy -> AnyCPU FasmProxy32 -> x86

Pardon the PascaleCase in advance as well as the use of full structure names for primitives (ie Int32), I know these aren't the same convention as your project

In the project FasmProxy I had 2 classes and 1 interface:

public class FasmProxy
{
    private const Int32 ParentCheckDelayMs = 500;

    public FasmProxy(Int32 ParentProcessId, String PipeName, String WaitEventName)
    {
        // Create an event to have the client wait until we are finished starting the service
        EventWaitHandle ProcessStartingEvent = new EventWaitHandle(false, EventResetMode.ManualReset, WaitEventName);

        InitializeAutoExit(ParentProcessId);

        ServiceHost ServiceHost = new ServiceHost(typeof(ProxyService));
        ServiceHost.Description.Behaviors.Remove(typeof(ServiceDebugBehavior));
        ServiceHost.Description.Behaviors.Add(new ServiceDebugBehavior { IncludeExceptionDetailInFaults = true });
        NetNamedPipeBinding Binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
        ServiceHost.AddServiceEndpoint(typeof(IProxyService), Binding, PipeName);
        ServiceHost.Open();

        ProcessStartingEvent.Set();

        Console.WriteLine("Fasm proxy library loaded");
        Console.ReadLine();
    }

    public static Boolean IsRunning(Int32 ParentProcessId)
    {
        try
        {
            Process.GetProcessById(ParentProcessId);
        }
        catch (ArgumentException)
        {
            return false;
        }

        return true;
    }

    private void InitializeAutoExit(Int32 ParentProcessId)
    {
        Task.Run(() =>
        {
            while (true)
            {
                if (!IsRunning(ParentProcessId))
                    break;

                Thread.Sleep(ParentCheckDelayMs);
            }

            Environment.Exit(0);
        });
    }

} // End class
[ServiceContract()]
public interface IProxyService
{
    [OperationContract]
    Byte[] Assemble(Boolean IsProcess32Bit, String Assembly, UInt64 BaseAddress);
}  // End interface
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public class ProxyService : IProxyService
{
    private const Int32 AttachTimeout = 5000;

    public ProxyService() { }

    public Byte[] Assemble(Boolean IsProcess32Bit, String Assembly, UInt64 BaseAddress)
    {
        if (Assembly == null)
            return null;

        // Add header information about process
        if (IsProcess32Bit)
            Assembly = String.Format("use32\n" + "org 0x{0:X8}\n", BaseAddress) + Assembly;
        else
            Assembly = String.Format("use64\n" + "org 0x{0:X16}\n", BaseAddress) + Assembly;

        // Print fully assembly to console
        Console.WriteLine("\n" + Assembly + "\n");

        Byte[] Result;
        try
        {
            // Call C++ FASM wrapper which will call the 32-bit FASM library which can assemble all x86/x64 instructions
            Result = FasmNet.Assemble(Assembly);

            // Print bytes to console
            Array.ForEach(Result, (X => Console.Write(X.ToString() + " ")));
        }
        catch
        {
            Result = null;
        }
        return Result;
    }
}  // End class

In the project FasmProxy32:

class Program
{
    private static FasmProxy.FasmProxy FasmProxy;

    static void Main(String[] Args)
    {
        if (Args.Length < 3)
            return;

        Console.WriteLine("Initialized FasmProxy 32-bit helper process");
        FasmProxy = new FasmProxy.FasmProxy(Int32.Parse(Args[0]), Args[1], Args[2]);
    }

} // End class

Then finally a static singleton class in the main project

class ProxyCommunicator
{
    // Singleton instance of proxy communication class
    private static Lazy<ProxyCommunicator> ProxyCommunicatorInstance = new Lazy<ProxyCommunicator>(() => { return new ProxyCommunicator(); }, LazyThreadSafetyMode.PublicationOnly);

    private const String FasmProxy32Executable = "FasmProxy32.exe";
    private const String WaitEventName = @"Global\Fasm";
    private const String UriPrefix = "net.pipe://localhost/";

    private IProxyService FasmProxy32;

    private ProxyCommunicator() { }

    public static ProxyCommunicator GetInstance()
    {
        return ProxyCommunicatorInstance.Value;
    }

    public void InitializeServices()
    {
        // Initialize channel names
        String FasmProxy32ServerName = UriPrefix + Guid.NewGuid().ToString();

        // Start 32 bit proxy service
        FasmProxy32 = StartProxyService(FasmProxy32Executable, FasmProxy32ServerName);
    }

    private IProxyService StartProxyService(String ExecutableName, String ChannelServerName)
    {
        // Start the proxy service
        EventWaitHandle ProcessStartEvent = new EventWaitHandle(false, EventResetMode.ManualReset, WaitEventName);
        ProcessStartInfo ProcessInfo = new ProcessStartInfo(Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), ExecutableName));
        ProcessInfo.Arguments = Process.GetCurrentProcess().Id.ToString() + " " + ChannelServerName + " " + WaitEventName;
        ProcessInfo.UseShellExecute = false;
        ProcessInfo.CreateNoWindow = true;
        Process.Start(ProcessInfo);
        ProcessStartEvent.WaitOne();

        // Create connection
        NetNamedPipeBinding Binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
        Binding.MaxReceivedMessageSize = Int32.MaxValue;
        Binding.MaxBufferSize = Int32.MaxValue;

        EndpointAddress Endpoint = new EndpointAddress(ChannelServerName);
        IProxyService ProxyService = ChannelFactory<IProxyService>.CreateChannel(Binding, Endpoint);

        return ProxyService;
    }

    public IProxyService GetProxyService()
    {
            return FasmProxy32;
    }

} // End class
JamesMenetrey commented 8 years ago

Excellent @zcanann, many thanks for your code. No worries about the naming convention, you code is easy to read and well commented. I think this is a nice approach. In all the case, if it suffers from performance issues, I will add some caching in MemorySharp in order to keep high performance. From what I could read, some software need to heavily rely on assembly injection, which I would like to promote, as a out-of-process memory editing library.

A side note about your code, I'm a bit concerned by the attribute ServiceBehavior, more specifically the parameter InstanceContextMode = InstanceContextMode.PerSession (to not confuse with ConcurrencyMode). With this parameter, we configure the server to have a thread per client. The consequence is when you have two clients, the server can handle their requests simultaneously. I've made a test with this configuration based on [this article](http://www.codeproject.com/Articles/89858/WCF-Concurrency-Single-Multiple-and-Reentrant-and#Instance mode = per session and Concurrency = single). After a bit of pimping in order to display when the connections start and end, I got the following result:

persession

As you can see, the server indeed manage multiple requests at the same time (multiple consecutive [start] flags). The issue here is the Fasm assembler is not thread-safe, meaning your hosted Fasm service can lead undefined results. I've tried with the parameter InstanceContextMode = InstanceContextMode.Single afterwards. As illustrated below, the server is handling request synchronously for all the clients this time:

single

Thank you again @zcanann. I'll add a contributor file to reference everybody who help/helped me to design the library. :)

Cheers, Zen

zcanann commented 8 years ago

@ZenLulz

Nice catch, thanks for letting me know. And no problem, it's the least I can do to give back.

JamesMenetrey commented 8 years ago

64-bit support.

lolp1 commented 8 years ago

@ZenLulz
Some thoughts on x64/random stuff

From what I could read, some software need to heavily rely on assembly injection What reasonable argument is there for some software to rely heavily on assembly injection? which I would like to promote, as a out-of-process memory editing library. I personally think the project is much more interesting when it is able to do both in a managed language, especially if the in-process implementation has high-quality and simplicity that the external parts of MemorySharp brings.

Some things can make "in-process" C# much more interesting to the casual programmer, mostly the same things that attract external users, increased simplicity.

Here are some of the main issues that turn people off about in-process c# code, and my solutions so far.

-Injecting a C# app into a process.

The answer to this is rather simple. If you review this project I updated https://github.com/lolp1/DomainWrapper you can see the process is simplified to injecting the DLL and calling the HostDomain export with the path to the C# program to host.

Everything from WinForms, Console apps, and even class library's can be hosted easily. In the case of C# projects with out an "entry point", the injector can use an attribute [such as STAThread] to search for in reflection for the "entry" method to call when the c# domain is hosted. All you really need is to write a user-friendly injector.

-Using pointers (both for delegates/functions and data) Again, this can be answered by stealing some tricks from other libs. https://github.com/lolp1/Process.NET/blob/master/src/Process.NET/Extensions/UnsafeMemoryExtensions.cs

-Making use of in-process code (such as calling functions in the process via unmanaged function pointer delegates) in a thread-safe way. Thanks to Jadd, this is also pretty easy to make user friendly. Use this WndProc override https://github.com/lolp1/Process.NET/blob/master/src/Process.NET/Windows/WndProcHook.cs to forward messages to have the main UI thread run code youw ant to execute.

A little example of some of my syntax sugar..

    internal class Function<T> where T : class
    {
        internal Function(IntPtr address)
        {
            Instance = address.ToDelegate<T>();
        }

        internal T Instance { get; }

        internal void Execute(params object[] objects)
        {
           Process.Instance.Window.Invoke(Instance, objects);
        }
        internal TT Execute<TT>(params object[] objects)
        {
            return (TT) Process.Instance.Window.Invoke(Instance, objects);
        }
    }

This lets you do stuff like this to invoke code inside the main thread in a safe way using the WndProc messages to communicate via a wrapper that makes use of a queue and the WndProc override linked.

        private Function<IsAutoTrackingDelegate> _isSomethingTrueFunction;
    // _isSomethingTrueFunction = new Function<IsSomethingTrue>(IntPtr.Zero);
        // bool isItTrue = _isSomethingTrueFunction.Execute<bool>((IntPtr)0x500);       
        [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
        private delegate bool IsSomethingTrue(IntPtr thisSomething);

-Making use of hooks You can read my recent blog http://blog.stylesoftware.net/2016/09/09/tl-dr-hooks/ to see how simple hooks can be used when the right code is in place.

using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace TestApp
{
    internal class Program
    {
        internal static readonly List TimeStamps = new List();
        internal static readonly Process Process = Process.GetCurrentProcess();

        private static D3DHook _d3DHook;

        private static void Main(string[] args)
        {
            _d3DHook = new D3D9(new DetourManager(Process.Handle));

            _d3DHook.Initialize();
            _d3DHook.Frame += D3DHookOnFrame;
        }

        private static void D3DHookOnFrame(object sender, EventArgs eventArgs)
        {
            TimeStamps.Add(DateTime.Now);
        }
    }
}

There is more, but if you ask me, using a well written user-friendly C# injector and a well-designed C# library that supports in-process can make in-process stuff a lot more attractive and simple. Being entirely done in C# while doing that is a nice bonus.

Aside from that.. I supported x64 in my memory sharp clone by turning everything into interfaces that will be used forever, and implementing anything that broke as I went on the fly. I had the same issue with bringing the ASM code execution/injection support MemorySharp brought in x32 to x64.

In the end I just went full C# in-process, but in my efforts, I had the same solutions thought of here. I used a third party to handle assembling things. Microsoft has great built in IPC stuff as well as amazing interop with c# in general.

I'd love to see an effort to bring MemorySharp to the in-process world along side its out-of-process world that is already mostly built.

JamesMenetrey commented 8 years ago

Hey @lolp1,

The main reason of being out-of-process is to reduce the footprint of memory editing in the targeted process, keeping it as low as possible and have the full control of what is altered in the target process. This is why I oriented MemorySharp to be out-of-process. As soon as you end up injecting managed code in a process, you require to start the CLR runtime in that process. That is not a bad practice by essense of course, nonetheless, this can restrain developers to use it in several scenarios, where the target process is wise enough to detect such modifications.

The code you posted is very interesting. I'm going to have a look to the abstraction you used in your clone of the library. :)

Most of the stuff (detour, hooking, etc.) can also be achieved while being out-of-process, without having the users of the library to write assembly code. That's obviously more work in the library and much more interesting to do. :D

MemorySharp was initially written to be efficient as an out-of-process library and the implementation of memory management/type conversion was thought in that way. A lot of stuff in that library should be rewritten differently to make it worth using MemorySharp in a context of in-process. Obviously, those changes would be only applicable if the library is in-process. This is why I'm not so keen to inject MemorySharp in the target process.

I'll add you in Skype this week-end. :)

Cheers Zen

lolp1 commented 8 years ago

@ZenLulz

Sounds good :) I have more thoughts but of course this place is not the best for such communication. JacobKemple@outlook.com is my skype :) add that one when you have a chance.

sesey commented 7 years ago

When will 64-bit support come?

JamesMenetrey commented 7 years ago

Hey !

@sesey Still no ETA currently ^^

@lolp1 Hey man ! Sorry I forgot you ! Just added you now. :)

JamesMenetrey commented 7 years ago

I created the branch x86-64 to work on this feature. This enables you to follow the progress as well.

Cheers

JamesMenetrey commented 7 years ago

Side note for me.

PEB 32 and 64-bit support

PEB structures vary depending on the OS and architecture. A research around all the PEB from XP is available here: http://blog.rewolf.pl/blog/?p=573.

This leads to the Terminus Project, that enables us to compare Windows structures here: http://blog.rewolf.pl/blog/?p=1438. The PEB structures on Terminus Project can be found here: http://terminus.rewolf.pl/terminus/structures/ntdll/_PEB_combined.html. MemorySharp will certainly only maintain the independent OS fields.

TEB 32 and 64-bit support

Similar to the PEB, the TEB's are available here: http://terminus.rewolf.pl/terminus/structures/ntdll/_TEB_combined.html

sesey commented 7 years ago

What about the workings for 64 bit support?

JamesMenetrey commented 7 years ago

This is in progress ! :)

sesey commented 7 years ago

I want to help but I know little of c# language.

Kruithne commented 7 years ago

Any update/ETA on this milestone for the project?

lolp1 commented 7 years ago

Just so people know @ZenLulz is a professional dev who does this for a living and AFAIK this is a bit of a hobby project that he takes great pride in it being quality work so it takes some time for releases.

In the meantime, feel free to suggest anything you want in MemorySharp and I'll do my best to get some of that done in ways that meets the standards ZenLulz desires and pull request it. You may also check out my spin-off (FULL CREDITS to @ZenLulz for my project too!) in the mean-time which does support x64.

https://github.com/lolp1/Process.NET

KairuByte commented 7 years ago

Was this ever finalized? I tried using the files built from the x86-64 and no matter what I do I keep getting 'Couldn't get the information from the thread, error code '-1073741820'.'

lolp1 commented 7 years ago

@KairuByte That error is due to an issue in his structures with x32-64, will look into fixing it later and pull requesting.

ghost commented 7 years ago

@lolp1 I still have this error, hasn't this been fixed yet?

roboserg commented 6 years ago

I too have the error code '-1073741820'.' one year later, this is sad :(

zcanann commented 6 years ago

For those interested, I have 64-bit support working for some features (just read/write and the x86/x64 assembler)

https://github.com/Squalr/Squalr

This project was never really meant to be used as a library though -- but it may be possible to repurpose it for that.

EDIT: My backend has been made available via NuGet

roboserg commented 6 years ago

Thanks, but I need to inject my c# dll to another unmanaged process. For read / write there already many libs. RIP

MohamedAlaaJameel commented 2 years ago

@zcanann I want to execute calls on x64 remote process , is that possible&how ?

JamesMenetrey commented 2 years ago

Hey @MohamedAlaaJameel,

The branch deepening-project implements some support for x64 calling convention, notably here: https://github.com/JamesMenetrey/MemorySharp/blob/deepening-project/src/MemorySharp/Assembly/CallingConvention/MicrosoftX64CallingConvention.cs.

Feel free to go to that branch, compile the library and check to see if that fits your needs :)

JamesMenetrey commented 2 years ago

A sidenote for everyone here, I'm still planning in revamping this library with the latest C# features (e.g., span, etc.). :)

MohamedAlaaJameel commented 2 years ago

@JamesMenetrey

I have compiled the library , but I get these errors /

on 32-Compile : can't open 64 bit proc from 32 . on 64-Compile :System.ApplicationException: 'Couldn't get the information from the process, error code '-1073741820'.' I have recorded a video please watch : https://youtu.be/CMO2uwc0tLs

JamesMenetrey commented 2 years ago

@MohamedAlaaJameel Try to force your .NET app to be compiled as 64-bit. This is usually done in Visual Studio by changing AnyCPU into 64bit.

MohamedAlaaJameel commented 2 years ago

@JamesMenetrey the problem is solved it seems I have cloned the old repo , thank you for the lib and your efforts .

JamesMenetrey commented 2 years ago

Great! To answer your other question: I have dropped Fasm for Keystone for 64-bit assembler support :)

lolp1 commented 2 years ago

Great! To answer your other question: I have dropped Fasm for Keystone for 64-bit assembler support :)

@JamesMenetrey

Off-topic, but I forgot your discord name could you throw me a private message over on there :D?

MohamedAlaaJameel commented 2 years ago

MR @JamesMenetrey image I am using 4.7.2 .net framework extension methods of intptr are not resolved at run time . I have add IsEqual manually and fixed . image is there any good solution ? instead of implementing all extension methods of IntPtr manually?

edit : fix : open ThreadFactory.cs add using Binarysharp.MemoryManagement.Helpers; change var ret = ThreadCore.NtQueryInformationThread( to ThreadBasicInformation ret = ThreadCore.NtQueryInformationThread( I have found it here : https://stackoverflow.com/questions/7562205/why-is-an-expandoobject-breaking-code-that-otherwise-works-just-fine

I need your discord if that's possible

BinToss commented 1 year ago

@MohamedAlaaJameel Convert/cast the IntPtr values to Long or ULong.


About a year ago, I wrote my own 32-bit and 64-bit definitions of these structs. Instead of IntPtr which depends on the .NET Runtime for bit length, I wrote facades IntPtr32, IntPtr64, UIntPtr32, UIntPtr64. I also implemented generic variants (e.g. UIntPtr64<T>) to indicate the Type of the object at the pointer.

For structs with definitions dependent on bit-length, I wrote a managed definition (e.g. MemoryBasicInformation) which wrapped either 32-bit or 64-bit unmanaged structs. The Managed definition stores the length-specific struct or pointer as a nullable member of a tuple e.g.

public class ProcessBasicInformation
{
    public ProcessBasicInformation(PROCESS_BASIC_INFORMATION pbi)
    {
        ExitStatus = pbi.ExitStatus;
        unsafe { PebBaseAddress = Environment.Is64BitProcess ? (null, (ulong)pbi.PebBaseAddress) : ((uint)pbi.PebBaseAddress, null); }
        AffinityMask = Environment.Is64BitProcess ? (null, (ulong)pbi.AffinityMask) : ((uint)pbi.AffinityMask, null);
        BasePriority = pbi.BasePriority;
        ProcessId = pbi.ProcessId;
        ParentProcessId = pbi.ParentProcessId;
    }

    public ProcessBasicInformation(PROCESS_BASIC_INFORMATION32 pbi)
    {
        ExitStatus = pbi.ExitStatus;
        PebBaseAddress = (pbi.PebBaseAddress, null);
        AffinityMask = (pbi.AffinityMask, null);
        BasePriority = pbi.BasePriority;
        ProcessId = pbi.UniqueProcessId;
        ParentProcessId = pbi.InheritedFromUniqueProcessId;
    }

    public ProcessBasicInformation(PROCESS_BASIC_INFORMATION64 pbi)
    {
        ExitStatus = pbi.ExitStatus;
        PebBaseAddress = (null, pbi.PebBaseAddress);
        AffinityMask = (null, pbi.AffinityMask);
        BasePriority = pbi.BasePriority;
        ProcessId = (uint)pbi.UniqueProcessId;
        ParentProcessId = (uint)pbi.InheritedFromUniqueProcessId;
    }

    public (UIntPtr32<PEB32>? w32, UIntPtr64<PEB64>? w64) PebBaseAddress { get; }
    public ProcessEnvironmentBlock? ProcessEnvironmentBlock { get; private set; }

    public NTSTATUS ExitStatus { get; }
    public (uint? w32, ulong? w64) AffinityMask { get; }
    public KPRIORITY BasePriority { get; }
    public uint ProcessId { get; }
    public uint ParentProcessId { get; }

    /// <summary>Read the process's private memory to recursively copy the PEB.</summary>
    /// <param name="hProcess">A handle opened with <see cref="PROCESS_ACCESS_RIGHTS.PROCESS_VM_READ"/>. Requires Debug and/or admin privileges.</param>
    /// <exception cref="AccessViolationException">Read operation failed; The memory region is protected and Read access to the memory region was denied.</exception>
    /// <exception cref="NullReferenceException">Unable to copy PEB; The 32-bit and 64-bit pointers are both null.</exception>
    /// <exception cref="NTStatusException">NtWow64ReadVirtualMemory failed to copy 64-bit PEB from target process; (native error message)</exception>
    /// <exception cref="Exception">ReadProcessMemory failed; (native error message)</exception>
    public unsafe ProcessEnvironmentBlock GetPEB(SafeProcessHandle hProcess)
    {
        if (PebBaseAddress is (null, null))
            throw new NullReferenceException("Unable to copy PEB; The 32-bit and 64-bit pointers are both null.");

        using SafeBuffer<PEB64> buffer = new(numElements: 2); // We only use the type for allocation length. It's large enough for either PEB64 or PEB32.

        if (!Environment.Is64BitProcess && PebBaseAddress.w64 is not null)
        {
            ulong bytesRead64 = 0;
            NTSTATUS status;

            if ((status = PInvoke.NtWow64ReadVirtualMemory64(hProcess, (UIntPtr64)PebBaseAddress.w64, (void*)buffer.DangerousGetHandle(), buffer.ByteLength, &bytesRead64)).Code is Code.STATUS_PARTIAL_COPY)
                throw new AccessViolationException("NtWow64ReadVirtualMemory64 failed; The memory region is protected and Read access to the memory region was denied.", new NTStatusException(status));
            else if (status.Code is not Code.STATUS_SUCCESS)
                throw new NTStatusException(status, "NtWow64ReadVirtualMemory failed to copy 64-bit PEB from target process; " + status.Message);
            else
                return ProcessEnvironmentBlock = new ProcessEnvironmentBlock(buffer.Read<PEB64>(0));
        }
        else
        {
            nuint bytesRead = 0;
            if (PebBaseAddress.w32 is not null && PInvoke.ReadProcessMemory(hProcess, (void*)PebBaseAddress.w32, (void*)buffer.DangerousGetHandle(), (nuint)buffer.ByteLength, &bytesRead))
            {
                return ProcessEnvironmentBlock = new(buffer.Read<PEB32>(0));
            }
            else if (PebBaseAddress.w64 is not null && PInvoke.ReadProcessMemory(hProcess, (void*)PebBaseAddress.w64, (void*)buffer.DangerousGetHandle(), (nuint)buffer.ByteLength, &bytesRead))
            {
                return ProcessEnvironmentBlock = new(buffer.Read<PEB64>(0));
            }
            else
            {
                Win32ErrorCode err = (Win32ErrorCode)Marshal.GetLastPInvokeError();
                if (err is Win32ErrorCode.ERROR_PARTIAL_COPY)
                    throw new AccessViolationException("ReadProcessMemory failed; The memory region is protected and Read access to the memory region was denied.", new Win32Exception(err));
                else
                    throw new Exception("ReadProcessMemory failed; " + err.GetMessage(), new Win32Exception(err));
            }
        }
    }

Some relevant definitions and .dib gists: