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