查看原文
其他

Hack ELF Loader:用户态execve

七夜安全 七夜安全博客 2021-01-04

“ 

阅读本文大概需要 10 分钟。

 ”

   2020年 第  13  篇文章 ,flag 继续 

每周至少更一篇

前言

良好的习惯是人生产生复利的有力助手

本篇文章晚了两三天发,说明一下原因哈,最近在搞TSRC和青藤云的webshell挑战赛,时间被挤压了,文章会晚到,但不会缺席,继续今年的Flag。突然发现写完凌晨三点了,赶紧睡觉。。。

上一篇文章中,我讲了认知方面的事情,主要包括 边界思想,复利模型和时间定价,算是这段时间读书的一个小分享,希望能对大家有一些启发,并应用到实践中。经常读一些思想方面的书,思考会逐渐变得深入全面,还是有很多不足,继续努力。

   

       

    

有一句话,我挺喜欢:

教育目的本质是改变自己,改变自己对经验的解读。


如果你买了很多书,买了很多课程, 想想这些到底有没有改变自己,自己是否在成长?

如果自己前后没有改变,那不就意味着为此付出的时间和金钱都浪费了吗?

因为焦虑去学习,只会让自己更加焦虑。

曾经读过曾国藩的传记,他对付太平天国的策略给我留下了深刻的印象,简单总结为六个字:

结硬寨,打呆仗


其实这是很多人不喜欢的“笨方法”,虽然注定能成功,可是觉得慢,觉得累,觉得克制,不过这确实是一条捷径:

认准目标,稳扎稳打,步步为营,不投机,始终掌握战略主动


不多说了,上周文章的最后预告了今天的内容是关于linux下的loader:用户态execve

接下来会用 黄金思维圈 why-how-what 的方式来拆解这个问题。

一.Why:为什么研究用户态execve

why

做一件事情背后总有它根本的原因,不要被事情的表象迷惑。举个例子,老板让你给招聘会贴海报,那你直接去贴海报,其实有点单纯了。

同样,我不能直接给你摆出个linux下的loader,给答案是最没用的,对你们没有帮助,你们需要知道我研究它的原因和应用场景,以及我的思考过程

研究用户态execve的实现,起初是从攻击的方向去思考的,在linux主机安全中,使用shell命令进行攻击是非常常见的场景,无论是横向移动,还是种马,很难不应用shell命令。

起初,我的想法是绕策略,可是想想这不通用,这家可以,到别家又不行。如果能避免被抓到shell命令,这应该是个通用的解决方案。

在之前的文章中,无"命令"反弹shell-逃逸基于execve的命令监控(上) 分享过关于shell命令的各种监控方式,其中最难绕过的是内核态的execve监控。


我的选择是抛弃execve系统调用来执行命令,而是思考自己实现用户态execve,这样就可以彻底摆脱命令监控,如果再延展一下,还会有更深层次的操作。

二.How:如何设计linux elf loader

how

用户态execve 是仿照linux内核中execve syscall的原理 ,在应用层实现程序的加载和运行,如果做过windows pe loader的话,可以知道这其实是 linux下的elf loader,由于篇幅有限,只能捡最相关的进行讲解了。

旧知识与资料储备

旧知识

在设计之前,需要回顾一下之前的旧知识,可以帮助你建立知识体系,同时指明接下来的方向。与之相关的技术栈:

1.shellcode执行

shellcode是一段可以直接在内存运行的二进制代码,执行shellcode的流程是首先申请一段可读可写可执行的内存,然后将这段代码复制到内存中,最后将eip指向内存的的首地址,即可完成运行。

elf loader的实现方式应该与此类似,通过复制文件到申请的内存,并将eip指针指向elf入口地址e_entry。

2.PE 加载器

之前发过一篇文章 PE to shellcode,其中讲到了如何将exe文件转化为可以在内存中执行的shellcode,里面用到的技术手段就是给普通的exe文件加一个shellcode壳,壳的作用是将exe文件加载到内存中,并完成一系列库的加载,最后将eip指针指向exe文件的执行入口。

 


初步的猜测,elf loader基本上也是完成这样的功能。

资料储备

在对原来的相似知识进行回顾和思考后,心里大致有了实现方法,但这是不够的,还需要搜集一些这方面的资料,对心中的实现方法进行校准和补充。根据上述的思考,我们需要从以下四个方向去搜集资料和分析:

  1. elf文件结构

  2. elf 装载

  3. 依赖库的加载(动态链接)

  4. 开源项目

ELF文件结构

ELF 视图

ELF,即 Executable and Linking Format,译为“可执行可连接格式”,具有这种格式的文件称为 ELF 文件。ELF 规范中把 ELF 文件宽泛地称为“目标文件 (object file)”,主要有三种类型:

  • 可重定位文件(Relocatable File .o)包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。

  • 可执行文件(Executable File .exe) 包含适合于执行的一个程序,此文件规定了exec() 如何创建一个程序的进程映像。

  • 共享目标文件(Shared Object File .so) 包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理, 生成另外一个目标文件。其次动态链接器(Dynamic Linker)可能将它与某个可执行文件以及其它共享目标一起组合,创建进程映像。

由此可见,ELF文件结构考虑了两方面的内容:一个是链接,一个是运行。为了反映了两种活动的不同需求,目标文件格式提供了两种并行视图。 


这两种视图都在一个文件里,可以简单理解为你写了程序,里面既有A功能也有B功能,执行链接的时候,使用A;实际运行的时候,使用B。

对我们elf loader 有用的是执行视图,毕竟我们是要将可执行文件加载到内存运行起来。在执行视图中,程序头部表对可执行文件是必须的,同时在执行的时候,是按照段进行加载的,节的意义不大。

程序头部表与段

程序头部表主要是用来告诉操作系统如何将可执行文件映射到内存。可执行文件程序头部是一个结构数组,每个结构描述了一个段或者系统准备程序执行所必需的其它信息。目标文件的“段”包含一个或者多个“节区”, 也就是“段内容(Segment Contents)”。程序头部仅对于可执行文件和共享目标文件有意义。

可执行目标文件在 ELF 头部的 ephentsize 和 ephnum 成员中给出其自身程序头部 的大小。程序头部的数据结构:

  1. /* Program Header */

  2. typedef struct {

  3. Elf32_Word p_type; /* segment type */

  4. Elf32_Off p_offset; /* segment offset */

  5. Elf32_Addr p_vaddr; /* virtual address of segment */

  6. Elf32_Addr p_paddr; /* physical address - ignored? */

  7. Elf32_Word p_filesz; /* number of bytes in file for seg. */

  8. Elf32_Word p_memsz; /* number of bytes in mem. for seg. */

  9. Elf32_Word p_flags; /* flags */

  10. Elf32_Word p_align; /* memory alignment */

  11. } Elf32_Phdr;


  12. typedef struct {

  13. Elf64_Half p_type; /* entry type */

  14. Elf64_Half p_flags; /* flags */

  15. Elf64_Off p_offset; /* offset */

  16. Elf64_Addr p_vaddr; /* virtual address */

  17. Elf64_Addr p_paddr; /* physical address */

  18. Elf64_Xword p_filesz; /* file size */

  19. Elf64_Xword p_memsz; /* memory size */

  20. Elf64_Xword p_align; /* memory & file alignment */

  21. } Elf64_Phdr;

其中各个字段说明:

  • p_type 此数组元素描述的段的类型,或者如何解释此数组元素的信息。具体如下图。

  • p_offset 此成员给出从文件头到该段第一个字节的偏移。

  • p_vaddr 此成员给出段的第一个字节将被放到内存中的虚拟地址。

  • p_paddr 此成员仅用于与物理地址相关的系统中。因为 System V 忽略所有应用程序的物理地址信息,此字段对与可执行文件和共享目标文件而言具体内容是指定的。

  • p_filesz 此成员给出段在文件映像中所占的字节数。可以为 0。

  • p_memsz 此成员给出段在内存映像中占用的字节数。可以为 0。

  • p_flags 此成员给出与段相关的标志。

  • p_align 可加载的进程段的 p_vaddr 和 p_offset 取值必须合适,相对于对页面大小的取模而言。此成员给出段在文件中和内存中如何 对齐。数值 0 和 1 表示不需要对齐。否则 palign 应该是个正整数,并且是 2 的幂次数,p_vaddr 和 p_offset 对 p_align 取模后应该相等。

可执行 ELF 目标文件中的段类型: 


这样看有点不直观,以ls为例子,我们使用readelf -l /bin/ls的方式查看程序头表,图中标红的部分是对我们有意义的。

LOAD段是需要操作系统加载到内存的部分,而INTERP段则是存储链接器的位置,我们的ls所需要的链接器为/lib64/ld-linux-x86-64.so.2。大家会不会很奇怪为什么需要链接器?下面会揭晓。

ELF 装载

程序装载是操作系统创建或扩充进程镜像的过程。进程空间如何构造,内存页面如何管理,以及进程如何被处理,不同的操作系统有不同的作法。

当系统创建或者扩充一个进程镜像时,逻辑上,它要把文件中的段复制成为虚拟内存中的一个段。但是系统不一定立刻真正地去读文件,什么时候读,还要依赖于程序的行为、系统负载等等。

为了提高了系统的性能和效率,可执行文件和共享目标文件中镜像在文件中的偏移量或者内存虚拟地址尽量是面向页面大小对齐。

对于 Intel 架构来说,页面的最大尺寸为 4KB,所以段的虚拟地址和文件内偏移量要向 4KB(0x1000)或者 4KB 的整数倍对齐,这样便于整页的换入换出,可以提高效率。以一个elf文件的程序头部表为例,包括代码段和数据段的内容,对齐属性p_align为4KB。


通过上表中的数据,计算出的文件偏移与内存偏移之间的关系如下图所示,其中不足一页的地方用0填充。


依赖库的加载(动态链接)

链接有两种方式,一种是静态链接,另一种是动态链接,这两种链接方式各有好处。

所谓静态链接就是在编译链接时直接将需要的执行代码拷贝到调用处,优点是在程序发布的时候就不需要依赖库,也就是不再需要带着库一块发布,程序可以独立执行,但是体积可能会相对大一些。

所谓动态链接就是在编译的时候不直接拷贝可执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统,操作系统负责将需要的动态库加载到内存中,然后程序在运行到指定的代码时,去共享执行内存中已经加载的动态库可执行代码,最终达到运行时连接的目的。

       

假如是静态链接产生的elf可执行文件,则不需要这一步,完成装载阶段即可,只需最后将eip指向e_entry。

但是静态链接的程序相对较少,更多地是需要动态链接的程序,在linux 中很多程序都会依赖glibc,那谁来负责完成glibc的加载呢?这就涉及INTERP段了,里面包含了动态链接器的路径

动态链接器

当创建一个可执行文件时,如果依赖其它的动态链接库,那么链接编辑器会在可执行文件的程序头中加入一个 PT_INTERP 项,告诉系统这里需要使用动态链接器,一般链接器为ld。

可执行文件与动态链接器一起创建了进程的镜像,这个过程包含以下活动(很重要!!!)

  • 添加可执行文件的段到进程空间;

  • 添加共享目标文件的段到进程空间;

  • 为可执行文件和共享目标文件进行重定位;

  • 如果动态链接器使用了可执行文件的文件描述符,应关闭它;

  • 把控制权交给程序。

因此如果程序是动态链接的,那在可执行程序运行之前,首先需要完成对链接器的装载并执行,之后的工作都交给链接器就好了。

程序与链接器交互

上文说到链接器会对程序进行处理,并且最后将控制权交还给程序,那站在链接器的位置思考就会出现问题:

当操作系统把控制权交给链接器时,它将开始进行链接工作,那么它至少需要知道关于可执行文件与进程的一些信息,不然无法对已经映射好的可执行文件镜像进行重定位。

这就涉及一个对动态链接器很重要的信息:辅助信息数组。辅助信息的格式也是一个结构数组,它的结构被定义在“elf.h":

  1. typedf struct

  2. {

  3. uint32_t a_type;

  4. union {

  5. uint32_t a_val;

  6. } a_un;

  7. } Elf32_auxv_t;

摘录几个比较重要的类型值,这几个类型值都是比较常见的,而且是动态链接器在启动时所需要的:

在程序运行的时候,这些辅助信息很容易看到,通过设置环境变量 LD_SHOWAUXV=1: 

总结

通过上文的思考,可以知道elf loader需要实现三个方面的内容:

  1. 映射可执行文件的LOAD段到内存中,并判断是否对齐

  2. 搜索INTERP段,如果存在,则像装载可执行文件一样装载链接器

  3. 初始化数据包括设置辅助信息数组,环境变量和参数,最后将eip指向程序基地址。


三. What:show the code

微信扫一扫付费阅读本文

    Preview the first 67% of the content for free.

    微信扫一扫付费阅读本文

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存