01里面,我们看到了修改后的 ls 程序在 maps 里面的布局:
在 01 里面,我们说到,r—p 这个段是一个 padding 段,但是实际上不是的。
我们一行行介绍,从第一行看起:
5fde6d4000-5fde731000 r-xp 00000000 103:13 1450012 /data/local/tmp/ls
这个显然是对应的:
第二行:
5fde731000-5fde734000 r--p 0005c000 103:13 1450012 /data/local/tmp/ls
这个段的权限标志是 r--
,这个与010中两个可加载段都不匹配,这是为啥呢?
其实,这个段是010中第二个可加载段的一部分,只不过它的权限被修改了。是被下面这个段修改的:
这个段描述的就是,在 elf 被重定位之后,将虚拟位置 5D660 且大小为 10656 字节的区域的权限改成只读的。
我们算一下:
且由于4kb对齐,所以5fde731000
就是需要被修改为只读的起始地址。
10656 的十六进制是29A0,对齐后是 3000,所以5fde731000-5fde734000
就是一个 r--
段。
第三行:
5fde734000-5fde736000 rw-p 0005f000 103:13 1450012 /data/local/tmp/ls
这就就是对应第二个可加载段中中间的一部分。
第四行:
5fde736000-5fde73a000 rw-p 00000000 00:00 0
这个就是第二个可加载段的结尾部分,为啥呢?而且它为啥没有对应的文件路径呢?
起始可以比对一下第二个可加载段的 RAM 大小,就知道这个段是第二个可加载段的结尾部分。至于为啥它没有名字,那是因为它的文件大小要小于映射后的 RAM 大小,所以RAM超出文件的部分就没有文件映射了,很奇特是不是?!!
下面进入正题。
Program Header
这个段里面就2块数据有用:
文件偏移与虚拟地址,它们都是40H,而且这个值与 elf_header 必须是相等的。这个值某种意义上就是 elf_header 的大小。那为啥还需要在这里再储存一次呢?
可以简单理解,是因为 linker 里面有一些指针指向的是 elf_header,而有些指针指向的是 program_header,互相转换的时候,会使用指针偏移来计算,偏移大小就是这里的 40H,所以就在这里也记录了值。
p_data 就是整个 program_header_table 的内容。
p_type 不知道有没有用,没试过改这里,假定有用吧。
Interpreter Path
这个段描述的是解释器路径:
这个段与上面的 Program Header 段必须要出现在可加载段前面。
这个段里面,我们只需要关注其 p_data,会发现它是一个字符串:
/system/bin/linker64
我们的手机上是有这个文件的。
ELF 的可加载段就是有这个文件来加载的,包括重定位操作等等。
既然ELF里面使用了一个路径来描述它,那么是不是意味着我们可以修改这个字符串,使用我们自定义的 linker 来加载 ELF 文件呢?
做如下操作:
-
将 /system/bin/ls cp 到 /data/local/tmp/ls 位置
-
将 /system/bin/linker64 copy 到 /data/local/tmp/er64 位置
-
将 /data/local/tmp/ls 的这个段的加载器路径改为 /data/local/tmp/er64
-
将 /data/local/tmp/er64 的入口地址改为死循环
注意加载器路径长度不能超过 p_filesz_SEGMENT_FILE_LENGTH 这个限制,而且要以 00 字节结尾。
修改完之后,我们再运行 /data/local/tmp/ls 程序,查看其 maps 文件信息:
5d2ab79000-5d2abd6000 r-xp 00000000 103:13 1450012 /data/local/tmp/ls
5d2abd6000-5d2abdb000 rw-p 0005c000 103:13 1450012 /data/local/tmp/ls
5d2abdb000-5d2abdf000 rw-p 00000000 00:00 0
73e9eee000-73e9eef000 r--p 00000000 00:00 0 [vvar]
73e9eef000-73e9ef0000 r-xp 00000000 00:00 0 [vdso]
73e9ef0000-73e9fe7000 r-xp 00000000 103:13 1450018 /data/local/tmp/er64
73e9fe8000-73e9fed000 rw-p 000f7000 103:13 1450018 /data/local/tmp/er64
73e9fed000-73ea061000 rw-p 00000000 00:00 0
7fcd826000-7fcd847000 rw-p 00000000 00:00 0 [stack]
发现,相比之前少了很多东西,什么 libc 相关的东西都没有。
这个就是因为现在进程卡在了非常早的时机,连这个 ELF 文件自身的段的重定位都没有做,只是光将 ELF 文件中的东西放到内存里面了。
那么上面的信息中,前面两行里面,我们发现 ls 文件被加载了,这又是为啥?我们明明已经在 linker 的入口函数加了一个死循环,linker 根本不会执行加载逻辑才对。
其实是因为,这两个可执行段不是由 ls 中指定的 linker 加载的,而是其他进程中的 linker 加载的,现在先简单的理解为是 init 进程干的吧(虽然也不一定对,等后面搞os再研究,不知要到猴年马月)。剩下的其他 ELF 依赖文件,比如 libc 等将由路径中指定的 linker 来加载。
我们使用 IDA 来验证一下,先附加到进程:
可以看到,这里面就两个 so 被加载。
我们还原一下 linker 的入口地址指令,让其继续执行,并且在 ls 的入口地址加上断点。
按 F8 步过 linker_init,发现此时,linker 加载了很多的 so:
再按 F9 让程序执行到 ls 入口地址:
linker 的特点
它没有 imports:
因为它不依赖其他 so,它也不能依赖其他 so,因为它运行的时候,其他 so 都还没加载呢。比如 open 函数,它就不能使用 libc 中的 open 函数,它必须自己实现 open 函数才行。它必须实现所有自己需要的函数。