导语:在所有版本的Mac上,虽然内存映射的结构基本上是相同的,但是获取它们会相当困难,下文将会介绍如何读取Mac进程的内存映射。
读取Mac的内存映射过程
首先需要2个变量的地址:ruby_version(用来布局结构)和ruby_current_thread(用来进行堆栈跟踪)。
由于它们就在我正在查看的Ruby二进制文件的符号表中,所以通常获取这两个变量的地址并不难,但是由于ASLR的存在,二进制文件会随机加载到内存中。所以我需要做到以下几点:
1. 在符号表中找到ruby_version的地址;
2. 找出Ruby二进制文件在内存中加载的位置(来自进程的内存映射);
3. 减去__mh_execute_header符号的值。
在Linux上,你可以通过查看/ proc / PID / maps文件来获得内存映射,它们的组成方式是这样的:地址范围,权限(例如r-xp),大小, inode编号以及一个映射到该文件的文件名。
00400000-00401000 r-xp 00000000 00:14 13644 /usr/bin/ruby1.9.1 00600000-00601000 r--p 00000000 00:14 13644 /usr/bin/ruby1.9.1 00601000-00602000 rw-p 00001000 00:14 13644 /usr/bin/ruby1.9.1 0060b000-00887000 rw-p 00000000 00:00 0 [heap] 7f1d44648000-7f1d4464a000 r-xp 00000000 00:14 14411 /usr/lib/ruby/1.9.1/x86_64-linux/enc/trans/transdb.so 7f1d4464a000-7f1d4484a000 ---p 00002000 00:14 14411 /usr/lib/ruby/1.9.1/x86_64-linux/enc/trans/transdb.so 7f1d4484a000-7f1d4484b000 r--p 00002000 00:14 14411 /usr/lib/ruby/1.9.1/x86_64-linux/enc/trans/transdb.so 7f1d4484b000-7f1d4484c000 rw-p 00003000 00:14 14411 /usr/lib/ruby/1.9.1/x86_64-linux/enc/trans/transdb.so
而在所有版本的Mac上,虽然内存映射的结构基本上是相同的,但是获取它们会相当困难!起初我试图在Mac上使用适用于Linux的方法(就是上文提到的/ proc文件),但花了3天时间都没有得到。为此,我进行了以下2种尝试:
尝试1——使用vmmap
VMMap是一个免费的工具,可以用来分析应用程序使用虚拟和物理内存的情况。除了内存使用图形来表示,VMMap也显示摘要信息和详细进程的内存映射。强大的过滤和刷新功能可以确定进程的内存使用情况和应用功能内存成本的来源。
不过,由于某种原因vmmap速度很慢,获取一个进程的内存映射需要2秒,这可能是因为vmmap在获取内存映射之前暂停了这个过程。
我不想直接使用vmma的原因,除了不满意vmmap的速度以外,另外一个原因就是如果vmmap真的是在获取内存映射之前暂停了这个过程,那这就意味着内存映射的分析过程被干扰了。
尝试2——在Rust中重新使用vmmap
Rust是针对多核体系提出的语言,并且吸收一些其他动态语言的重要特性,比如不需要管理内存,比如不会出现Null指针等等。由于不想直接使用vmmap,所以我考虑在Rust中重新实现它的功能(至少是我需要的部分)。我在Rust中写了一个简单的vmmap复制功能,代码就在main.rs。
为了做到这一点,我使用了mach crate,它具有Rust绑定功能,可以调用一些Mac内核函数。我还了解到,在Mac / BSD上有一个“端口(port)”的概念:
‘端口’是一个受保护的消息队列,用于在任务之间进行通信,任务拥有发送权限并接收每个端口的权限
以下是如何从一个程序获得一个内存映射的方法,不过这个函数的接口有一点奇怪,就是你给它一个端口ID和一个地址,它会给你该地址之后的第一个内存映射。基本上这个函数只是将mach_vm_region函数从Mach微内核中封装起来(所有Mach函数的头文件都在/usr/include/mach/*.h中)。
我已经注释了一些代码,它使用https://github.com/andrewdavidmackenzie/libproc-rs crate作为regionfilename函数(它给出了与内存映射关联的库的文件名)。我就不得不在github master上使用该版本的crate,因为其发布的版本有一个Use-After-Free(UAF)漏洞。
fn mach_vm_region(target_task: mach_port_name_t, mut address: mach_vm_address_t) -> Option<Region> { let mut count = mem::size_of::<vm_region_basic_info_data_64_t>() as mach_msg_type_number_t; let mut object_name: mach_port_t = 0; // we need to create new `size` and `info` structs for the function we call to read the data // into let mut size = unsafe { mem::zeroed::<mach_vm_size_t>() }; let mut info = unsafe { mem::zeroed::<vm_region_basic_info_data_t>() }; let result = unsafe { // Call the underlying Mach function mach::vm::mach_vm_region( target_task as vm_task_entry_t, &mut address, &mut size, VM_REGION_BASIC_INFO, &mut info as *mut vm_region_basic_info_data_t as vm_region_info_t, &mut count, &mut object_name, ) }; if result != KERN_SUCCESS { return None; } // this uses let filename = match regionfilename(41000, address) { Ok(x) => Some(x), _ => None, }; Some(Region { size: size, info: info, address: address, count: count, filename: filename, }) }
以上是我编写的Rust程序,比vmmap要快很多!整个内存映射的时间要在80毫秒左右完成,比vmmap快大约15倍。
不过到现在为止,我的Rust vmmap复制还存在一个主要问题,就是它实际上只是给我一些现在我的进程中的内存映射。而对于任何动态链接库(包括一个Ruby库以及我需要的地址和文件名),它们被存储在一个叫做“dyld_shared_cache”的地方。
在下面的链接中有一堆关于这个“dyld”的代码,我打算试着利用一下。
用于从Mac进程读取内存映射的有用资源
以下是我在Mac上阅读内存映射时发现的4个最有用的资源:
2.“使用Mach-O二进制文件和dyld”,用于在Mac进程中查找共享库的地址;
3.来自Chromium的dynamic_images.cc,用它读取Mac进程中的信息共享库;
4.psutil的OS X C代码,ppsutil是一个跨平台库,能够轻松实现获取系统运行的进程和系统利用率(CPU,内存,磁盘,网络等)信息,主要应用于系统监控,分析和限制系统资源及进程的管理,它实现了同等命令行工具提供的功能,所以它的源代码有助于了解Mac内部的结构