从进程内部查找映射的内存

时间:2018-10-27 13:54:59

标签: c linux memory x86-64 aslr

设置:

  • Ubuntu 18x64
  • x86_64应用程序
  • 从内部执行任意代码 应用程序

我正在尝试编写即使启用了ASLR也应该能够在内存中找到结构的代码。可悲的是,我找不到对这些区域的任何静态引用,所以我猜测我必须使用蛮力方式并扫描进程内存。我试图做的是扫描应用程序的整个地址空间,但是由于某些内存区域未分配,因此无法访问,因此在访问时会产生SIGSEGV。现在,我认为getpid()是一个好主意,然后使用pid访问/proc/$PID/maps并尝试从那里解析数据。

但是我想知道,有没有更好的方法来识别分配的区域?甚至甚至不需要我访问libc(= {getpid, open, close)或弄乱字符串的方式?

1 个答案:

答案 0 :(得分:3)

我认为没有任何标准的POSIX API。

解析/proc/self/maps是您最好的选择。 (可能有一个库可以帮助您解决这个问题,但IDK)。

不过,您标记了此ASLR。如果您只想知道text / data / bss段的位置,则可以在它们的开头/结尾放置标签,以便这些地址在C中可用。 extern const char bss_end[];是使用链接程序脚本和一些手写的asm引用您在BSS末尾放置的标签的好方法。编译器生成的asm将使用相对于RIP的LEA指令来获取相对于当前指令地址的寄存器中的地址(CPU知道该地址,因为它正在执行映射到该地址的代码)。

或者只是一个链接描述文件,并在自定义部分中声明伪C变量。

我不确定您是否可以对堆栈映射进行操作。对于大型环境和/或argv,进入main()甚至_start的初始堆栈可能与堆栈映射中的最高地址不在同一页中。


要进行扫描,您需要捕获SIGSEGV或使用系统调用而不是用户空间加载或存储进行扫描。

mmapmprotect无法查询旧设置,因此它们对非破坏性内容的用处不大。带有提示但没有mmap的{​​{1}}可以映射页面,然后可以MAP_FIXED映射页面。如果实际选择的地址是==提示,则可以假定该地址已被使用。

也许更好的选择是使用munmap扫描并检查madvise(MADV_NORMAL),但一次只扫描一页。

您甚至可以通过errno=0; posix_madvise(page, 4096, POSIX_MADV_NORMAL)轻松地执行此操作。然后检查EFAULTerrno:指定范围内的地址部分或完全不在呼叫者的地址空间之外。

在具有madvise(2)的Linux上,您可以使用ENOMEM或每页非默认设置的可能性。

但是在Linux上,用于只读查询进程内存映射的一个更好的选择是mincore(2) :它还将错误代码MADV_DOFORK用于错误的地址查询范围。 “ ENOMEMaddr包含未映射的内存”。 (addr + length是指向未映射内存的结果向量,而不是addr)。

只有EFAULT结果有用; errno的结果向您显示RAM中的页面是否很热。 (我不确定是否显示您已将哪些页面链接到硬件页表中,或者是否会计算驻留在页面高速缓存中的页面是否是内存映射文件但未链接的页面,所以访问将触发软链接页面错误)。

您可以通过调用较大长度的vec来对大型映射的结尾进行二进制搜索。

但是不幸的是,在未映射的页面之后,我没有找到用于查找下一个映射的等效项,这将更加有用,因为大多数地址空间都将被映射。尤其是在具有64位地址的x86-64中!

对于稀疏文件,有mincore。我想知道这是否适用于Linux的lseek(SEEK_DATA)吗?可能不是。

因此,大容量(例如256MB)/proc/self/mem调用是浏览未映射区域以查找映射页面的好方法。不管您使用哪种(tmp=mmap(page, blah blah)) == page,无论{ {1}}是否使用了您的提示地址。

解析munmap(tmp)几乎可以肯定是更有效率的。

但是最有效的方法是将标签放在您希望它们用于静态地址的位置,并跟踪动态分配,以便您已经知道内存的位置。如果没有内存泄漏,则此方法有效。 (glibc mmap可能有一个API可以遍历映射,但是我不确定。)


请注意,如果您向 any 系统调用传递未映射的地址以指向应该指向某个参数的参数,则会产生/proc/self/maps

一个可能的候选者是access(2),它使用文件名并返回一个整数。它对其他任何状态(成功或失败)的影响为零,但是如果指向的内存是有效的路径字符串,则不利的是文件系统访问。而且它正在寻找一个隐式长度的C字符串,因此如果很快将指针传递给没有malloc字节的内存指针,它也会很慢。我猜想errno=EFAULT会加入,但它肯定仍会读取您在其上使用的每个可访问页面,即使页面被调出也会出错。

如果您在0上打开文件描述符,则可以进行ENAMETOOLONG系统调用。 或者甚至使用writev(2)/dev/null在一个系统调用中将内核的指针向量传递给内核,并在其中任何一个错误的情况下获得EFAULT。 (每个长度为1个字节)。 但是(除非write()驱动程序足够早地跳过读取操作),这实际上是从有效页面读取的,从而使它们出现故障,与writev(devnull_fd, io_vec, count) 不同。根据内部实现的方式,/dev/null驱动程序可能会尽早看到请求以实现其“返回真”(不执行任何操作),从而避免在检查EFAULT后实际触摸页面。检查会很有趣。