windowsmemorydevice-emulation

How to catch a memory write and call function with address of write


I would like to catch memory writes to specific memory ranges and call a function with the address of the memory location being written to. Preferably, after the write to memory has already happened.

I know this can be done by the operating system by twiddling with the page table entries. However, how can this be similar accomplished from within an application that wants to do this?


Solution

  • Well, you could do something like this:

    // compile with Open Watcom 1.9: wcl386 wrtrap.c
    
    #include <windows.h>
    #include <stdio.h>
    
    #ifndef PAGE_SIZE
    #define PAGE_SIZE 4096
    #endif
    
    
    UINT_PTR RangeStart = 0;
    SIZE_T RangeSize = 0;
    
    UINT_PTR AlignedRangeStart = 0;
    SIZE_T AlignedRangeSize = 0;
    
    
    void MonitorRange(void* Start, size_t Size)
    {
      DWORD dummy;
    
      if (Start &&
          Size &&
          (AlignedRangeStart == 0) &&
          (AlignedRangeSize == 0))
      {
        RangeStart = (UINT_PTR)Start;
        RangeSize = Size;
    
        // Page-align the range address and size
    
        AlignedRangeStart = RangeStart & ~(UINT_PTR)(PAGE_SIZE - 1);
    
        AlignedRangeSize = ((RangeStart + RangeSize - 1 + PAGE_SIZE) &
                            ~(UINT_PTR)(PAGE_SIZE - 1)) -
                           AlignedRangeStart;
    
        // Make the page range read-only
        VirtualProtect((LPVOID)AlignedRangeStart, 
                       AlignedRangeSize,
                       PAGE_READONLY,
                       &dummy);
      }
      else if (((Start == NULL) || (Size == 0)) &&
               AlignedRangeStart &&
               AlignedRangeSize)
      {
        // Restore the original setting
        // Make the page range read-write
        VirtualProtect((LPVOID)AlignedRangeStart,
                       AlignedRangeSize,
                       PAGE_READWRITE,
                       &dummy);
    
        RangeStart = 0;
        RangeSize = 0;
    
        AlignedRangeStart = 0;
        AlignedRangeSize = 0;
      }
    }
    
    // This is where the magic happens...
    int ExceptionFilter(LPEXCEPTION_POINTERS pEp,
                        void (*pMonitorFxn)(LPEXCEPTION_POINTERS, void*))
    {
      CONTEXT* ctx = pEp->ContextRecord;
      ULONG_PTR* info = pEp->ExceptionRecord->ExceptionInformation;
      UINT_PTR addr = info[1];
      DWORD dummy;
    
      switch (pEp->ExceptionRecord->ExceptionCode)
      {
      case STATUS_ACCESS_VIOLATION:
        // If it's a write to read-only memory,
        // to the pages that we made read-only...
        if ((info[0] == 1) &&
            (addr >= AlignedRangeStart) &&
            (addr < AlignedRangeStart + AlignedRangeSize))
        {
          // Restore the original setting
          // Make the page range read-write
          VirtualProtect((LPVOID)AlignedRangeStart,
                         AlignedRangeSize,
                         PAGE_READWRITE,
                         &dummy);
    
          // If the write is exactly within the requested range,
          // call our monitoring callback function
          if ((addr >= RangeStart) && (addr < RangeStart + RangeSize))
          {
            pMonitorFxn(pEp, (void*)addr);
          }
    
          // Set FLAGS.TF to trigger a single-step trap after the
          // next instruction, which is the instruction that has caused
          // this page fault (AKA access violation)
          ctx->EFlags |= (1 << 8);
    
          // Execute the faulted instruction again
          return EXCEPTION_CONTINUE_EXECUTION;
        }
    
        // Don't handle other AVs
        goto ContinueSearch;
    
      case STATUS_SINGLE_STEP:
        // The instruction that caused the page fault
        // has now succeeded writing to memory.
        // Make the page range read-only again
        VirtualProtect((LPVOID)AlignedRangeStart,
                       AlignedRangeSize,
                       PAGE_READONLY,
                       &dummy);
    
        // Continue executing as usual until the next page fault
        return EXCEPTION_CONTINUE_EXECUTION;
    
      default:
      ContinueSearch:
        // Don't handle other exceptions
        return EXCEPTION_CONTINUE_SEARCH;
      }
    }
    
    
    // We'll monitor writes to blah[1].
    // volatile is to ensure the memory writes aren't
    // optimized away by the compiler.
    volatile int blah[3] = { 3, 2, 1 };
    
    void WriteToMonitoredMemory(void)
    {
      blah[0] = 5;
      blah[0] = 6;
      blah[0] = 7;
      blah[0] = 8;
    
      blah[1] = 1;
      blah[1] = 2;
      blah[1] = 3;
      blah[1] = 4;
    
      blah[2] = 10;
      blah[2] = 20;
      blah[2] = 30;
      blah[2] = 40;
    }
    
    // This pointer is an attempt to ensure that the function's code isn't
    // inlined. We want to see it's this function's code that modifies the
    // monitored memory.
    void (* volatile pWriteToMonitoredMemory)(void) = &WriteToMonitoredMemory;
    
    void WriteMonitor(LPEXCEPTION_POINTERS pEp, void* Mem)
    {
      printf("We're about to write to 0x%X from EIP=0x%X...\n",
             Mem,
             pEp->ContextRecord->Eip);
    }
    
    int main(void)
    {
      printf("&WriteToMonitoredMemory() = 0x%X\n", pWriteToMonitoredMemory);
      printf("&blah[1] = 0x%X\n", &blah[1]);
    
      printf("\nstart\n\n");
    
      __try
      {
        printf("blah[0] = %d\n", blah[0]);
        printf("blah[1] = %d\n", blah[1]);
        printf("blah[2] = %d\n", blah[2]);
    
        // Start monitoring memory writes
        MonitorRange((void*)&blah[1], sizeof(blah[1]));
    
        // Write to monitored memory
        pWriteToMonitoredMemory();
    
        // Stop monitoring memory writes
        MonitorRange(NULL, 0);
    
        printf("blah[0] = %d\n", blah[0]);
        printf("blah[1] = %d\n", blah[1]);
        printf("blah[2] = %d\n", blah[2]);
      }
      __except(ExceptionFilter(GetExceptionInformation(),
                               &WriteMonitor)) // write monitor callback function
      {
        // never executed
      }
    
      printf("\nstop\n");
      return 0;
    }
    

    Output (run on Windows XP):

    &WriteToMonitoredMemory() = 0x401179
    &blah[1] = 0x4080DC
    
    start
    
    blah[0] = 3
    blah[1] = 2
    blah[2] = 1
    We're about to write to 0x4080DC from EIP=0x4011AB...
    We're about to write to 0x4080DC from EIP=0x4011B5...
    We're about to write to 0x4080DC from EIP=0x4011BF...
    We're about to write to 0x4080DC from EIP=0x4011C9...
    blah[0] = 8
    blah[1] = 4
    blah[2] = 40
    
    stop
    

    That's the idea.

    You will likely need to change things around to make the code work well in multiple threads, make it work with other SEH code (if any), with C++ exceptions (if applicable).

    And, of course, if you really want it, you can make it call the writes monitoring callback function after the write's been completed. For that you'll need to save the memory address from the STATUS_ACCESS_VIOLATION case somewhere (TLS?) so that the STATUS_SINGLE_STEP case can pick it up later and pass to the function.