rei*_*jin 4 c linux memory x86-64 aslr
设置:
我正在尝试编写即使启用 ASLR 也应该能够在内存中找到结构的代码。遗憾的是,我找不到对这些区域的任何静态引用,所以我猜我必须使用暴力方式并扫描进程内存。我试图做的是扫描应用程序的整个地址空间,但这不起作用,因为某些内存区域未分配,因此SIGSEGV在访问时会产生收益。现在我认为最好getpid()使用 pid 来访问/proc/$PID/maps并尝试从那里解析数据。
但我想知道,是否有更好的方法来识别分配的区域?也许甚至是一种不需要我访问 libc (= getpid, open, close) 或摆弄字符串的方法?
我认为没有任何标准的 POSIX API 可以实现这一点。
解析/proc/self/maps是你最好的选择。(可能有一个库可以帮助解决这个问题,但我不知道)。
不过,您已标记了此 ASLR。如果您只想知道文本/数据/bss 段在哪里,您可以在它们的开头/结尾放置标签,以便这些地址在 C 中可用。例如,这是extern const char bss_end[];引用您在末尾放置的标签的好方法使用链接器脚本和一些手写的汇编语言来构建 BSS。编译器生成的 asm 将使用 RIP 相关的 LEA 指令来获取寄存器中相对于当前指令地址的地址(CPU 知道该地址,因为它正在执行映射到那里的代码)。
或者可能只是一个链接器脚本并在自定义部分中声明虚拟 C 变量。
我不确定你是否可以为堆栈映射做到这一点。对于大型环境和/或 argv,入口时的初始堆栈main()甚至_start可能与堆栈映射中的最高地址不在同一页中。
要扫描,您需要SIGSEGV使用系统调用而不是用户空间加载或存储来捕获或扫描。
mmap并且mprotect无法查询旧设置,因此它们对于非破坏性的东西不是很有用。 mmap有提示但没有MAP_FIXED可以映射一个页面,然后就可以了munmap。如果实际选择的地址!=提示,那么您可以假设该地址正在使用中。
也许更好的选择是使用 进行扫描madvise(MADV_NORMAL)并检查EFAULT,但一次只能扫描一页。
您甚至可以使用 便携式地执行此操作errno=0; posix_madvise(page, 4096, POSIX_MADV_NORMAL)。然后检查errno::ENOMEM指定范围内的地址是否部分或完全在调用者的地址空间之外。
在 Linux 上,madvise(2)您可以使用MADV_DOFORK或 更不可能为每个页面设置非默认设置的东西。
但在 Linux 上,只读查询进程内存映射的更好选择是mincore(2)ENOMEM:它还使用查询范围内无效地址的错误代码。“addr包含addr + length未映射的内存”。(EFAULT用于指向未映射内存的结果向量,而不是 addr)。
只有errno结果才有用;结果vec显示 RAM 中的页面是否热。(我不确定它是否显示哪些页面已连接到硬件页表中,或者是否会计算内存映射文件的页面缓存中驻留在内存中但未连接的页面,因此访问将触发软页面错误)。
您可以通过使用更大的长度进行调用来对大型映射的末尾进行二分搜索mincore。
但不幸的是,我没有看到任何等效的方法可以在未映射的页面之后查找下一个映射,这会更有用,因为大多数地址空间都将被取消映射。特别是在具有 64 位地址的 x86-64 中!
对于稀疏文件,有lseek(SEEK_DATA). 我想知道这在 Linux 上是否有效/proc/self/mem?可能不会。
因此,也许大型(如 256MB)(tmp=mmap(page, blah blah)) == page调用将是扫描未映射区域以查找映射页面的好方法。 无论哪种方式munmap(tmp),无论是否mmap使用了提示地址,您都可以简单地进行操作。
解析/proc/self/maps几乎肯定更有效。
但最有效的方法是将标签放置在您想要静态地址的位置,并跟踪动态分配,以便您已经知道内存在哪里。如果您没有内存泄漏,这将有效。(glibcmalloc可能有一个 API 来遍历映射,但我不确定。)
请注意,如果您向任何系统调用errno=EFAULT传递一个应该指向某物的参数的未映射地址,则任何系统调用都会生成 。 (除非它实际上是在用户空间中实现的,例如通过Linux的 VDSO,比如gettimeofday(2)。然后尝试通过这些指针参数写入只会出现段错误。我认为 POSIX 仅指定函数是否将返回错误而不是实际的段错误,错误代码必须是 EFAULT。)
一种可能的候选者是access(2),它接受一个文件名并返回一个整数。它对其他任何状态(成功或失败)的影响为零,但如果指向的内存是有效的路径字符串,则缺点是文件系统访问。而且它正在寻找隐式长度的 C 字符串,因此如果传递一个指向内存的指针而0很快任何地方都没有字节,那么速度也可能会很慢。我想ENAMETOOLONG会启动,但它仍然肯定会读取您使用它的每个可访问页面,即使它被调出,也会出错。
如果您在 上打开文件描述符/dev/null,则可以使用write()它进行系统调用。 或者甚至使用writev(2):writev(devnull_fd, io_vec, count)在一个系统调用中向内核传递一个指针向量,如果其中任何一个是坏的,则获得 EFAULT。(每个长度为 1 个字节)。 但是(除非/dev/null驱动程序足够早地跳过读取)这实际上是从有效的页面读取的,与mincore(). 根据其内部实现方式,/dev/null驱动程序可能会尽早看到请求,以便其“返回 true”(无需执行任何操作),以避免在检查 EFAULT 后实际触摸页面。检查一下会很有趣。
| 归档时间: |
|
| 查看次数: |
2312 次 |
| 最近记录: |