【问题标题】:mprotect always returns invalid argumentsmprotect 总是返回无效参数
【发布时间】:2013-12-21 07:40:49
【问题描述】:

我正在尝试使用保护修改 .text 段中的值以授予我写入权限:

 int pageSize = sysconf(_SC_PAGE_SIZE);

 int *toModify = (int *)(foo+5);
 if (mprotect(toModify, pageSize, PROT_WRITE) < 0 ) {
      perror("mprotect failed with error:");
      return -1;
  }
  *toModify = 5;
  printf("Modify :%i",foo());

mprotect 从不工作。它总是返回一个mprotect failed with error:: Invalid argument 错误。

foo 是一种返回 int 的方法,该 int 存储在函数之后 5 个字节(这就是 foo+5 的原因)

【问题讨论】:

  • 来自documentation: "mprotect() 更改调用进程的内存页的保护,该内存页包含区间[addr, addr+len-1] 中的地址范围的任何部分。addr 必须是与页面边界对齐。"。

标签: c linux unix memory mprotect


【解决方案1】:

我在 OS X 10.9 上执行了以下代码,它似乎具有所需的行为。输出是“foo 返回 23”。

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <unistd.h>
#include <sys/mman.h>


extern int foo(void);


int main(void)
{
    //  New value to write into foo+5.
    int NewValue = 23;

    //  Find page size for this system.
    size_t pagesize = sysconf(_SC_PAGESIZE);

    //  Calculate start and end addresses for the write.
    uintptr_t start = (uintptr_t) &foo + 5;
    uintptr_t end = start + sizeof NewValue;

    //  Calculate start of page for mprotect.
    uintptr_t pagestart = start & -pagesize;

    //  Change memory protection.
    if (mprotect((void *) pagestart, end - pagestart,
            PROT_READ | PROT_WRITE | PROT_EXEC))
    {
        perror("mprotect");
        exit(EXIT_FAILURE);
    }

    //  Write new bytes to desired location.
    memcpy((void *) start, &NewValue, sizeof NewValue);

    //  Some systems could require an invalidate of instruction cache here.

    //  Try modified function.
    printf("foo returns %d.\n", foo());

    return 0;
}

对于foo,我使用了这个汇编代码。这两个源都是用cc -arch i386 构建的。

    .globl  _foo
_foo:
    nop
    nop
    nop
    nop
    mov $42, %eax
    ret

您应该以这种方式修改代码仅作为学习练习,而不是在任何已部署的应用程序中使用它。

【讨论】:

  • 我目前不在电脑附近。这听起来不错,但是当 foo 是同一个文件中声明的函数时,它是否也有效?
  • @AzzUrr1:C 标准通常不能保证它完全有效;您正在尝试做 C 标准不支持且 C 实现通常不支持的事情。这就是为什么你一再被建议不要这样做的原因。如果您将foo 与更改它的代码放在同一个源文件中,编译器可能会优化代码,以便根本不会调用fooprintf 在编译时完全准备好),所以对foo 的任何更改都不会产生任何影响。关闭优化可能会失败。
  • @EricPostpischil 谢谢!为我工作。这究竟是做什么的:uintptr_t pagestart = start &amp; -pagesize;?
  • @SujayPhadke:它找到包含start 的页面的开头。 pagesize 包含页面中的字节数,它应该是 2 的幂。它有一个无符号类型,所以-pagesize 是大小的二进制补码。在二进制二进制补码中,所有高位都打开,低位关闭。例如,在八位算术中,-4 的二进制是 11111100。当与 AND 运算符一起使用时,&amp;,它是一个关闭低位的掩码。因此,start 中地址的低位被关闭,只留下高位。这是页面的开始。
【解决方案2】:

来自man mprotect

   EINVAL addr is not a valid pointer, or not a multiple of PAGESIZE.

您没有注意addr 需要是PAGESIZE 的倍数的部分,显然......尽管在手册页的至少一个版本中,该要求并没有特别明确,只是简单地说明"为包含部分或全部区间 [addr,addr+len-1] 的内存页指定所需的保护"。

找到包含特定地址的页面地址并不是特别难,因为您已经完成了pageSize = sysconf(_SC_PAGE_SIZE); 位:

static inline void *pageof(const void* p)
{ return (p & ~(pageSize - 1));
}

然后将您的mprotect 调用修改为mprotect(pageof(toModify), pageSize, ...)。不过,请参阅@Zack 的答案,以获取有关您指定的权限的警告。一定要回过头来阅读mprotect() 的手册页,并确保您真正了解自己在做什么...

【讨论】:

  • 我怎样才能知道使用哪个页面边界?
  • @AzzUrr1 也许您需要阅读页面。您正在编写什么应用程序,您想让文本页面可写但不知道如何确定页面边界?
【解决方案3】:

mprotect 的地址参数需要是页面对齐的,大小参数是整数页数。此外,将页面单独设置为PROT_WRITE 意味着您不再允许阅读它——而且这个特定页面将在文本段中,所以它也需要是PROT_EXEC ,否则程序会在从mprotect 返回时崩溃(因为该页面上的代码不再被视为可执行文件)。

您的程序的这种修改可以满足您的要求:

/* file A */
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/mman.h>

extern const int foo;

int
main (int argc, char *argv[])
{
  printf("Before modification: foo = %d @ %p\n", foo, (void *)&foo);

  size_t pagesize = sysconf(_SC_PAGESIZE);
  void *foo_page = (void *) (((uintptr_t)&foo) & ~(pagesize - 1));

  if (mprotect(foo_page, pagesize, PROT_READ|PROT_WRITE|PROT_EXEC)) {
    perror("mprotect");
    return 1;
  }

  *(int *)&foo = 42; /* this is still undefined behavior! */

  printf("After modification: foo = %d @ %p\n", foo, (void *)&foo);
  return 0;
}

/* file B */
const int foo = 23;

警告:写入const 数据会触发未定义的行为,无论您是否使用操作系统原语来禁用包含它的页面的写保护。当我第一次测试这段代码时,我自己的文件中没有const int foo = 23;,并且GCC 将printf 调用重写为printf("...", 23, &amp;foo)这是一个有效的优化。如果我打开链接时优化,我希望会发生这种情况即使常量的定义被移动到它自己的文件中。此外,GCC 也有权将*(int *)&amp;foo = 42; 替换为无条件的abort() 或陷阱指令。

【讨论】:

  • 如果foo 恰好位于页面的最后一个字节中,这可能会失败,因此要写入的字节位于下一页中。
  • 另请注意,在foo 之外的五个字节处需要写入,而不是直接在foo 处。
  • 通过使用memcpy(DesiredAddress, &amp; (int) {42}, sizeof(int));而不是使用带有转换指针的赋值,可以在没有指针别名问题的情况下完成写入。
  • @EricPostpischil 我特意简化代码直接写入foo,并将foo设为全局常量而不是函数地址,重点关注mprotect的正确使用。
  • @EricPostpischil 另外,不是指针别名问题导致我将写入foo 标记为“仍然未定义的行为”,而是对象声明const 的更基本点不得通过任何方式修改。特别是,即使您使用操作系统原语使包含const 数据的页面可写,编译器仍然有权假设您不对其进行写入,并且可能例如在寄存器中缓存以前加载的值。
猜你喜欢
  • 2018-04-06
  • 1970-01-01
  • 2013-05-04
  • 2015-11-02
  • 2011-05-20
  • 2012-09-28
  • 2020-08-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多