如何捕获内存写入并使用写入地址调用函数

发布于 2024-12-13 15:03:26 字数 121 浏览 4 评论 0原文

我想捕获对特定内存范围的内存写入,并使用正在写入的内存位置的地址调用函数。最好是在写入存储器已经发生之后。

我知道操作系统可以通过调整页表条目来完成此操作。然而,如何在想要执行此操作的应用程序中实现类似的效果呢?

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

沒落の蓅哖 2024-12-20 15:03:26

好吧,您可以执行如下操作:

// 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;
}

输出(在 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

这就是想法。

您可能需要进行一些更改,以使代码在多个线程中正常工作,使其与其他 SEH 代码(如果有)以及 C++ 异常(如果适用)一起工作。

当然,如果你真的想要它,你可以让它在写入完成后调用写入监控回调函数。为此,您需要将 STATUS_ACCESS_VIOLATION 案例中的内存地址保存在某处(TLS?),以便 STATUS_SINGLE_STEP 案例可以拾取它稍后并传递给该函数。

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.

眼泪也成诗 2024-12-20 15:03:26

或者,您可以使用Page Guards,这同样会导致访问异常,但会被系统自动清除(一次性)。这些也应该适用于只读内存。

在您的情况下,您仍然需要单步陷阱技巧来重新启用页面防护。

例如 vkTrace< 使用/a> 也可能由 OpenGL/Vulkan 持久映射缓冲区驱动程序实现本身实现。
vkTrace 源代码还展示了如何在 Linux 和 Android 上执行此类操作。

Alternatively you may use Page Guards which similarly cause an exception on access but are automatically cleared by the system (one-shot). Those should also work for read-only memory.

In your case you still need the single-step trap trick though to re-enable the page guard.

Used for example by vkTrace and potentially also by OpenGL/Vulkan Persistently Mapped Buffer driver implementations themselves.
vkTrace source code also shows how to do this kind of thing on Linux and Android.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文