Linux memory manipulation using .NET Core 2

Recently, I wanted to port my .NET Framework Windows application to Linux. Obviously, since .NET Core 2 is there, I decided to use it instead of Mono. It wasn’t that much of pain until I had to re-implement a class to manipulate the memory of a remote process. I did a quick research and at first it looked pretty complicated. Apparently, most of the guides I found on the internet were implementing memory manipulation using ptrace. Luckily, since Linux 3.2 we can use process_vm_readv and process_vm_writev.

Implementation

Referring to the man page, you can see that first of all we have to implement the iovec struct.

struct iovec {
  void  *iov_base;    /* Starting address */
  size_t iov_len;     /* Number of bytes to transfer */
};

The first field is a pointer. As a replacement for size_t, I’ll be using int because that’s what Unsafe.SizeOf<T>() returns. Here’s the final C# implementation:

[StructLayout(LayoutKind.Sequential)]
unsafe struct iovec
{
  public void* iov_base;
  public int iov_len;
}

Let’s implement process_vm_readv now. The man page shows the following method signature:

ssize_t process_vm_readv(pid_t pid,
                        const struct iovec *local_iov,
                        unsigned long liovcnt,
                        const struct iovec *remote_iov,
                        unsigned long riovcnt,
                        unsigned long flags);

pid is the Process ID accessible from the Id property of the System.Diagnostics.Process class. Next parameter is a pointer to the local iovec struct that will be holding the result of the operation. liovcnt is the count of local structs. This parameter is useful when we want to perform multiple memory operations, but my implementation doesn’t do that. The following param is again a pointer to the iovec struct, but this time it’s the remote one that holds information about the memory fragment we’re trying to read. riovcnt is the remote equivalent to liovcnt. According to the man page, the flags argument is currently unused and does nothing. process_vm_readv on success returns the number of bytes read.

Here’s the C# Pinvoke implementation:

[DllImport("libc")]
private static extern unsafe int process_vm_readv(int pid,
    iovec* local_iov,
    ulong liovcnt,
    iovec* remote_iov,
    ulong riovcnt,
    ulong flags);

process_vm_writev has exactly the same signature as process_vm_readv. The big difference lies in what these two calls do. process_vm_readv transfers the data from remote_iov to local_iov while process_vm_writev does the opposite.

[DllImport("libc")]
private static extern unsafe int process_vm_readv(int pid,
    iovec* local_iov,
    ulong liovcnt,
    iovec* remote_iov,
    ulong riovcnt,
    ulong flags);

Usage

Now as we got the API ready, let’s create some methods to access it.

Read:

public unsafe bool Read<T>(IntPtr address, out T value) where T : unmanaged
{
  var size = Unsafe.SizeOf<T>();
  var ptr = stackalloc byte[size];

  var localIo = new iovec
  {
    iov_base = ptr,
    iov_len = size
  };

  var remoteIo = new iovec
  {
    iov_base = address.ToPointer(),
    iov_len = size
  };
                
  var res = process_vm_readv(_process.Id, &localIo, 1, &remoteIo, 1, 0);

  value = *(T*)ptr;

  return res != -1;
}

Write:

public unsafe bool Write<T>(T value, IntPtr address) where T : unmanaged
{
  var ptr = &value;
  var size = Unsafe.SizeOf<T>();

  var localIo = new iovec
  {
    iov_base = ptr,
    iov_len = size
  };

  var remoteIo = new iovec
  {
    iov_base = address.ToPointer(),
    iov_len = size
  };

  var res = process_vm_writev(_process.Id, &localIo, 1, &remoteIo, 1, 0);

  return res != -1;
}

Make sure to enable unsafe code for these examples to work.