ELF文件分析(五) ELF文件的内存映射

0x01 Linux进程内存布局

linux下进程的内存布局(Memory layout)如下图所示,和其他图不同的是我从上到下地址是从低到高,个人习惯罢了,不过这样排布在处理栈和数组的时候都会带来一定程度上的便利。其次,内存布局展示的是64位下的而非32位下的,32位的可以自行类别或者搜索。其中大致布局是这样的,其实也不全对,后面对具体进程分析的时候我们会看到这个概念图和实际的区别。

elf-5-1

note

图片中所示段只是个代表,不过需要记住的是段中节的权限是相同的,把相同权限的节封装成段是对加载过程的性能优化。

0x02 什么是内存映射

要理解内存映射前,先得了解计算机科学中这几个基本的概念:进程、物理内存、虚拟内存、页表。

进程:进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。从内存管理的角度来看,进程是拥有独立虚拟地址空间*的程序执行实例。操作系统会为每个进程分配一套独立的页表,确保进程间的内存隔离。

物理内存:指物理存在的数据区域,存在于我们的内存条中。它的地址是唯一的、真实的、物理存在的

虚拟内存:每个进程认为的数据区域。操作系统会让每个进程认为其独占系统的所有数据区域,而实际上是通过页表和MMU伪造出来的。

页表:保存着虚拟地址到物理地址的映射关系的结构,每个进程都有自己独立的页表。页表本身存在于物理内存之中,并有一个特别的寄存器PTBR存储顶级页表的物理地址

内存映射实际上是虚拟内存到物理内存的映射关系,当进程要访问某个虚拟地址时,会发生下面的操作:

  • CPU把虚拟地址 V_addr 交给 MMU
  • MMU 查询该进程的页表
    • 如果存在,通过页表可以由V_addr得到物理地址P_addr
    • 如果不存在,发生缺页中断,内核进行处理,最后回到该页存在的状态
  • CPU访问到真实的物理地址 P_addr

0x03 ELF文件的内存映射

来到本次的核心部分,关于ELF文件是如何被映射到内存中的。这个部分涉及到我们在前面提过的加载器(Loader),加载器的加载过程现在可以简单理解成下面几个过程:

  • 内核加载器

    • 文件校验(魔数、架构、格式)
    • 建立新的地址空间
    • 遍历程序头中的PT_LOAD段
    • 创建内存映射
    • 创建堆栈
    • 检查程序头,看有没有PT_INTERP,如果有就把控制权交给动态链接器,没有就将控制权交给程序并跳转到入口点(一般是_start函数)
  • 动态链接器

    • 解析程序头,获得PT_DYNAMIC段,获得所需so文件的列表
    • 系统中寻找这些so文件
    • 将这些so文件建立内存映射到该进程的内存映射区域
    • 执行重定位功能,遍历动态重定位表并进行相应修改
    • 将控制权交给程序并跳转到入口点(一般是_start函数)

info

注意加载器是创建内存映射而不是直接把数据拷贝到内存上。如果你弄清楚了页表、物理内存和虚拟内存的关系,理解这件事就不是困难。这通常是通过写时复制(Copy-on-Write)按需分页(Demand Paging)技术高效实现的,内核只在进程实际访问某块内存时,才将对应的数据从磁盘加载到物理内存中。

实例

该实例源码可以在我的远程仓库下载到,这里给出链接,可以自行查看。

1. 运行程序,并查看其内存布局

程序大致的功能是打印PID和一些数据信息,然后进入一个无限循环中,每10s输出一次心跳确保其正常运行。

elf-5-2

现在我们来看看其内存布局,是不是感觉可以和上面的内存布局图片对应上?

elf-5-3

info

/proc/<pid>/maps记录了可执行文件的内存映射关系,可以通过文件的方式读取。毕竟linux下"万物皆文件"嘛。还有一个后面要用到的是/proc/<pid>/mem,是可执行文件的虚拟内存的数据

2. 解析ELF文件到内存的映射

我们需要搞清楚到底加载了哪些节,位置情况是怎么样的。可以借助readelf工具来看看,如下图所示

elf-5-4
elf-5-5

现在我们就可以依次解析出内存映射关系中前5行的内容。

1
2
3
4
5
00400000-00401000 r--p 00000000 103:04 24283437    # 文件头和程序头 02段                    
00401000-00402000 r-xp 00001000 103:04 24283437 # 03段,包含.text节
00402000-00403000 r--p 00002000 103:04 24283437 # 04段,包含.rodata节
00403000-00404000 r--p 00002000 103:04 24283437 # 05段部分,包含.got节
00404000-00405000 rw-p 00003000 103:04 24283437 # 05段部分,包含.data节和.bss节

note

看这个地方的.got的权限和之前提过的内存布局不同,主要是安全机制RELRO的作用,后面会详细讲解。主要是加载后会设置为只读权限来防止GOT表被劫持。

3. 验证文件头和程序头的存在

这里要使用一个linux下的危险命令dd,由于其使用基本上会带上sudo来完成,而且进行的是复制或者转换单个文件或整个设备的功能。由于使用不当可能会导致一些重要的数据出现问题(如某个所有磁盘数据丢失等),所以人送外号”数据毁灭者”data destoryer(dd)。不过数据丢失的问题绝大多数情况下都是使用者的疏忽导致的,dd只是菜和不严谨的替罪羊。感兴趣的话可以看看这篇reddit帖子

1
2
# 替换[PID]为该进程的PID
sudo dd if=/proc/[PID]/mem bs=1 skip=$((0x400000)) count=$((0x1000)) of=header.bin

放心,这条指令是安全的,因为它不涉及写入到磁盘等内容上,不过如果你if敲成of了,我感觉我也不能帮您什么了,找个医生治治吧。

导出来的数据包含文件头和程序头,甚至readelf都还能识别,只不过会出现错误罢了。

elf-5-6

我们hexdump看一下前面:

1
2
3
4
5
6
> hexdump -n 64 -C header.bin
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............|
00000010 02 00 3e 00 01 00 00 00 f0 10 40 00 00 00 00 00 |..>.......@.....|
00000020 40 00 00 00 00 00 00 00 38 40 00 00 00 00 00 00 |@.......8@......|
00000030 00 00 00 00 40 00 38 00 0d 00 40 00 25 00 24 00 |....@.8...@.%.$.|
00000040

note

这个操作叫做内存转储(dump),主要用于数字取证(Digital Forensics)或者脱壳分析。对于一些可执行文件会进行加壳,只有程序运行起来才会在内存中进行展开。而通过dump可以把展开后的文件获取出来,进行重构,甚至是使其运行(限于比较简单的,复杂的成本过高、成功率低)。同时,从内存中提取数据的过程也是dump,所以你会听到dump出某个数组之类、密钥等的说法。

info

linux下的dump工具,除了dd这种底层工具外,还有调试器如GDB来实现内存转储。
在GDB中附加到进程后,使用dump memory <文件名> <起始地址> <结束地址>命令可以更安全、便捷地完成同样的工作。

0x04 小问题

关于内存布局

  • 链接器和加载器到底是如何影响内存布局的?
  • 为什么文件映射到内存是容易的,而从内存回到文件确实异常困难的?
  • 自行了解linux的/proc虚拟文件系统,这是linux下很多监控软件实现的基础。