加载的 ELF 段的重叠映射
Overlapping mappings for loaded ELF segments
我想详细了解动态加载程序如何为 ELF 段创建映射。
考虑一个与 GNU ld 链接的小型共享库。程序头是:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x00095c 0x00095c R E 0x200000
LOAD 0x000df8 0x0000000000200df8 0x0000000000200df8 0x000250 0x000258 RW 0x200000
DYNAMIC 0x000e08 0x0000000000200e08 0x0000000000200e08 0x0001d0 0x0001d0 RW 0x8
GNU_EH_FRAME 0x000890 0x0000000000000890 0x0000000000000890 0x00002c 0x00002c R 0x4
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x000df8 0x0000000000200df8 0x0000000000200df8 0x000208 0x000208 R 0x1
此共享对象可以打印加载它的进程的映射 (/proc/self/maps
),代码段:
7fd1f057b000-7fd1f057c000 r-xp 00000000 fe:00 12090538 /path/libmy.so
7fd1f057c000-7fd1f077b000 ---p 00001000 fe:00 12090538 /path/libmy.so
7fd1f077b000-7fd1f077c000 r--p 00000000 fe:00 12090538 /path/libmy.so
7fd1f077c000-7fd1f077d000 rw-p 00001000 fe:00 12090538 /path/libmy.so
如果我打印可变全局变量的地址,打印的地址在第四个映射中。
- 这四个映射各自的用途是什么?
- 为什么动态加载器会创建一个没有权限的"padding"映射?
解构映射:
Base address == 7fd1f057b000
Mapping 1: virtual offset 0x000000, size 0x001000, R-X, from file offset 0x0000
Mapping 2: virtual offset 0x001000, size 0x1ff000, ---, from file offset 0x1000
Mapping 3: virtual offset 0x200000, size 0x001000, R--, from file offset 0x0000
Mapping 4: virtual offset 0x201000, size 0x001000, RW-, from file offset 0x1000
我目前的理解:
广告 1.
- 第一个映射是 "text" 段(第一个程序头)。
- 第二个映射看起来像一种填充形式(没有权限和大小使得下一段的虚拟偏移量为 0x200000)。
- ???
- "data" 段。除了我会说它应该从文件偏移量 0 开始并且大小为 0x2000。该段从文件中的 0xdf8 开始,它仍在第一页上。另外,"text"段的文件大小怎么能大于"data"段的偏移量呢?
广告 2. 链接器不能只在确切的虚拟地址 7fd1f077b000 请求映射,从而创建一个漏洞?为什么要为这个映射烦恼?
$ readelf -d libmy.so
Dynamic section at offset 0xe08 contains 25 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x5a8
0x000000000000000d (FINI) 0x848
0x0000000000000019 (INIT_ARRAY) 0x200df8
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x200e00
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x0000000000000004 (HASH) 0x190
0x000000006ffffef5 (GNU_HASH) 0x1e0
0x0000000000000005 (STRTAB) 0x380
0x0000000000000006 (SYMTAB) 0x218
0x000000000000000a (STRSZ) 172 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0x201000
0x0000000000000002 (PLTRELSZ) 120 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x530
0x0000000000000007 (RELA) 0x470
0x0000000000000008 (RELASZ) 192 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x450
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x42c
0x000000006ffffff9 (RELACOUNT) 3
0x0000000000000000 (NULL) 0x0
$ readelf -Wl libmy.so
Elf file type is DYN (Shared object file)
Entry point 0x630
There are 6 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x00095c 0x00095c R E 0x200000
LOAD 0x000df8 0x0000000000200df8 0x0000000000200df8 0x000250 0x000258 RW 0x200000
DYNAMIC 0x000e08 0x0000000000200e08 0x0000000000200e08 0x0001d0 0x0001d0 RW 0x8
GNU_EH_FRAME 0x000890 0x0000000000000890 0x0000000000000890 0x00002c 0x00002c R 0x4
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x000df8 0x0000000000200df8 0x0000000000200df8 0x000208 0x000208 R 0x1
Section to Segment mapping:
Segment Sections...
00 .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
01 .init_array .fini_array .dynamic .got .got.plt .data .bss
02 .dynamic
03 .eh_frame_hdr
04
05 .init_array .fini_array .dynamic .got
What is the purpose of each of those four mappings?
Why does the dynamic loader create a "padding" mapping with no permissions?
为了了解最终状态,我们需要跟踪动态链接器执行的操作。它的“指示”是什么?它需要在内存中的随机地址(由 OS 选择)加载 ET_DYN
对象。映射必须满足这些“命令”(我省略了 PhysAddr,因为它与 VirtAddr 相同):
Offset VirtAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000000000 0x00095c 0x00095c R E 0x200000
LOAD 0x000df8 0x0000000000200df8 0x000250 0x000258 RW 0x200000
现在,对于所有 ELF 二进制文件来说,第一件重要的事情是,为了正常工作,两个 LOAD
段 必须 由相同的“基准偏移”。它不会做例如。 mmap
第一个 LOAD
片段位于 0x1000000
,第二个片段位于 0x2000000+0x200df8 == 0x2200df8
。
因此,动态链接器(我将使用 rtld
收缩)必须 执行 的 mmap
both 段作为单个 mmap
(否则,不能保证第二个映射不会干扰已经映射到那里的其他东西)。所以它执行:
size_t len = 0x200df8 + 0x258;
void *base = mmap(0, len, PROT_READ|PROT_EXEC, MAP_PRIVATE, fd, 0);
在您的特定情况下,base == 0x7fd1f057b000
,我们有一个映射,涵盖 .text
和 .data
:
7fd1f057b000-7fd1f077d000 r-xp 0 libmy.so
但是 rtld
远未完成。它现在必须超过 mmap
.data
(第二个)LOAD
段到正确的位置并具有所需的权限(省略错误检查):
mmap(base + 0x200000, 0xdf8 + 0x258, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
我们的映射现在看起来像这样:
7fd1f057b000-7fd1f077b000 r-xp 0 libmy.so
7fd1f077b000-7fd1f077d000 rw-p 0 libmy.so
其次,我们的文件非常短(小于 4K),将地址留在 [0x7fd1f057c000, 0x7fd1f077b000)
映射范围内可能会产生 SIGBUS
或其他令人困惑的错误,而我们更喜欢简单的 SIGSEGV
.
我们可以 munmap
这个区域,但是有缺点(其他一些小型图书馆可能会落入几乎 2MiB 的区域,并混淆 rtld
寻找最近的基础映射的其他部分)。相反,rtld
保护该区域不可访问,同时保持映射不变:
mprotect(0x7fd1f057c000, 0x1ff000, PROT_NONE);
现在我们的内存映射看起来几乎与您观察到的最终结果一样:
7fd1f057b000-7fd1f077b000 r-xp 0 libmy.so
7fd1f057c000-7fd1f077b000 ---p 0 libmy.so
7fd1f077b000-7fd1f077d000 rw-p 0 libmy.so
但是 rtld
还有一件事要做:您的对象请求(由于具有 GNU_RELRO
段)它的一部分 writable 数据受到保护搬迁后写作。所以rtld
执行重定位,然后执行最后的mprotect
:
mprotect(base + 0x200000, 0xdf8 + 0x208, PROT_READ);
这会产生最终的内存映射(与您观察到的完全匹配):
7fd1f057b000-7fd1f077b000 r-xp 0 libmy.so
7fd1f057c000-7fd1f077b000 ---p 0 libmy.so
7fd1f077b000-7fd1f077c000 r--p 0 libmy.so
7fd1f077c000-7fd1f077d000 rw-p 0 libmy.so
I'm having some issues finding documentation on GNU_RELRO.
有一个很好的讨论 here。
I guess its VirtAddr and FileSize specify which parts should be read-only?
正确,除了它是 MemSize
(但它应该始终匹配 FileSize
)。
So the section table is not used?
table 部分在动态链接期间从不使用,它可以在删除部分 table 的完全剥离的二进制文件上工作。 table 部分保留在二进制文件中(默认情况下)只是为了帮助调试。
我想详细了解动态加载程序如何为 ELF 段创建映射。
考虑一个与 GNU ld 链接的小型共享库。程序头是:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x00095c 0x00095c R E 0x200000 LOAD 0x000df8 0x0000000000200df8 0x0000000000200df8 0x000250 0x000258 RW 0x200000 DYNAMIC 0x000e08 0x0000000000200e08 0x0000000000200e08 0x0001d0 0x0001d0 RW 0x8 GNU_EH_FRAME 0x000890 0x0000000000000890 0x0000000000000890 0x00002c 0x00002c R 0x4 GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10 GNU_RELRO 0x000df8 0x0000000000200df8 0x0000000000200df8 0x000208 0x000208 R 0x1
此共享对象可以打印加载它的进程的映射 (/proc/self/maps
),代码段:
7fd1f057b000-7fd1f057c000 r-xp 00000000 fe:00 12090538 /path/libmy.so 7fd1f057c000-7fd1f077b000 ---p 00001000 fe:00 12090538 /path/libmy.so 7fd1f077b000-7fd1f077c000 r--p 00000000 fe:00 12090538 /path/libmy.so 7fd1f077c000-7fd1f077d000 rw-p 00001000 fe:00 12090538 /path/libmy.so
如果我打印可变全局变量的地址,打印的地址在第四个映射中。
- 这四个映射各自的用途是什么?
- 为什么动态加载器会创建一个没有权限的"padding"映射?
解构映射:
Base address == 7fd1f057b000 Mapping 1: virtual offset 0x000000, size 0x001000, R-X, from file offset 0x0000 Mapping 2: virtual offset 0x001000, size 0x1ff000, ---, from file offset 0x1000 Mapping 3: virtual offset 0x200000, size 0x001000, R--, from file offset 0x0000 Mapping 4: virtual offset 0x201000, size 0x001000, RW-, from file offset 0x1000
我目前的理解:
广告 1.
- 第一个映射是 "text" 段(第一个程序头)。
- 第二个映射看起来像一种填充形式(没有权限和大小使得下一段的虚拟偏移量为 0x200000)。
- ???
- "data" 段。除了我会说它应该从文件偏移量 0 开始并且大小为 0x2000。该段从文件中的 0xdf8 开始,它仍在第一页上。另外,"text"段的文件大小怎么能大于"data"段的偏移量呢?
广告 2. 链接器不能只在确切的虚拟地址 7fd1f077b000 请求映射,从而创建一个漏洞?为什么要为这个映射烦恼?
$ readelf -d libmy.so
Dynamic section at offset 0xe08 contains 25 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x5a8
0x000000000000000d (FINI) 0x848
0x0000000000000019 (INIT_ARRAY) 0x200df8
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x200e00
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x0000000000000004 (HASH) 0x190
0x000000006ffffef5 (GNU_HASH) 0x1e0
0x0000000000000005 (STRTAB) 0x380
0x0000000000000006 (SYMTAB) 0x218
0x000000000000000a (STRSZ) 172 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0x201000
0x0000000000000002 (PLTRELSZ) 120 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x530
0x0000000000000007 (RELA) 0x470
0x0000000000000008 (RELASZ) 192 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x450
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x42c
0x000000006ffffff9 (RELACOUNT) 3
0x0000000000000000 (NULL) 0x0
$ readelf -Wl libmy.so
Elf file type is DYN (Shared object file)
Entry point 0x630
There are 6 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x00095c 0x00095c R E 0x200000
LOAD 0x000df8 0x0000000000200df8 0x0000000000200df8 0x000250 0x000258 RW 0x200000
DYNAMIC 0x000e08 0x0000000000200e08 0x0000000000200e08 0x0001d0 0x0001d0 RW 0x8
GNU_EH_FRAME 0x000890 0x0000000000000890 0x0000000000000890 0x00002c 0x00002c R 0x4
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x000df8 0x0000000000200df8 0x0000000000200df8 0x000208 0x000208 R 0x1
Section to Segment mapping:
Segment Sections...
00 .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
01 .init_array .fini_array .dynamic .got .got.plt .data .bss
02 .dynamic
03 .eh_frame_hdr
04
05 .init_array .fini_array .dynamic .got
What is the purpose of each of those four mappings?
Why does the dynamic loader create a "padding" mapping with no permissions?
为了了解最终状态,我们需要跟踪动态链接器执行的操作。它的“指示”是什么?它需要在内存中的随机地址(由 OS 选择)加载 ET_DYN
对象。映射必须满足这些“命令”(我省略了 PhysAddr,因为它与 VirtAddr 相同):
Offset VirtAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000000000 0x00095c 0x00095c R E 0x200000
LOAD 0x000df8 0x0000000000200df8 0x000250 0x000258 RW 0x200000
现在,对于所有 ELF 二进制文件来说,第一件重要的事情是,为了正常工作,两个 LOAD
段 必须 由相同的“基准偏移”。它不会做例如。 mmap
第一个 LOAD
片段位于 0x1000000
,第二个片段位于 0x2000000+0x200df8 == 0x2200df8
。
因此,动态链接器(我将使用 rtld
收缩)必须 执行 的 mmap
both 段作为单个 mmap
(否则,不能保证第二个映射不会干扰已经映射到那里的其他东西)。所以它执行:
size_t len = 0x200df8 + 0x258;
void *base = mmap(0, len, PROT_READ|PROT_EXEC, MAP_PRIVATE, fd, 0);
在您的特定情况下,base == 0x7fd1f057b000
,我们有一个映射,涵盖 .text
和 .data
:
7fd1f057b000-7fd1f077d000 r-xp 0 libmy.so
但是 rtld
远未完成。它现在必须超过 mmap
.data
(第二个)LOAD
段到正确的位置并具有所需的权限(省略错误检查):
mmap(base + 0x200000, 0xdf8 + 0x258, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
我们的映射现在看起来像这样:
7fd1f057b000-7fd1f077b000 r-xp 0 libmy.so
7fd1f077b000-7fd1f077d000 rw-p 0 libmy.so
其次,我们的文件非常短(小于 4K),将地址留在 [0x7fd1f057c000, 0x7fd1f077b000)
映射范围内可能会产生 SIGBUS
或其他令人困惑的错误,而我们更喜欢简单的 SIGSEGV
.
我们可以 munmap
这个区域,但是有缺点(其他一些小型图书馆可能会落入几乎 2MiB 的区域,并混淆 rtld
寻找最近的基础映射的其他部分)。相反,rtld
保护该区域不可访问,同时保持映射不变:
mprotect(0x7fd1f057c000, 0x1ff000, PROT_NONE);
现在我们的内存映射看起来几乎与您观察到的最终结果一样:
7fd1f057b000-7fd1f077b000 r-xp 0 libmy.so
7fd1f057c000-7fd1f077b000 ---p 0 libmy.so
7fd1f077b000-7fd1f077d000 rw-p 0 libmy.so
但是 rtld
还有一件事要做:您的对象请求(由于具有 GNU_RELRO
段)它的一部分 writable 数据受到保护搬迁后写作。所以rtld
执行重定位,然后执行最后的mprotect
:
mprotect(base + 0x200000, 0xdf8 + 0x208, PROT_READ);
这会产生最终的内存映射(与您观察到的完全匹配):
7fd1f057b000-7fd1f077b000 r-xp 0 libmy.so
7fd1f057c000-7fd1f077b000 ---p 0 libmy.so
7fd1f077b000-7fd1f077c000 r--p 0 libmy.so
7fd1f077c000-7fd1f077d000 rw-p 0 libmy.so
I'm having some issues finding documentation on GNU_RELRO.
有一个很好的讨论 here。
I guess its VirtAddr and FileSize specify which parts should be read-only?
正确,除了它是 MemSize
(但它应该始终匹配 FileSize
)。
So the section table is not used?
table 部分在动态链接期间从不使用,它可以在删除部分 table 的完全剥离的二进制文件上工作。 table 部分保留在二进制文件中(默认情况下)只是为了帮助调试。