• 我们在哪一颗星上见过 ,以至如此相互思念 ;我们在哪一颗星上相互思念过,以至如此相互深爱
  • 我们在哪一颗星上分别 ,以至如此相互辉映 ;我们在哪一颗星上入睡 ,以至如此唤醒黎明
  • 认识世界 克服困难 洞悉所有 贴近生活 寻找珍爱 感受彼此

恶意代码技术理论:ELF恶意代码分析

基于平台 云涯 4年前 (2020-04-03) 2463次浏览

ELF执行流程

start()  ->  __libc_start_main( )  ->  init( )  ->  main( )  ->  fini( )  ->  rtld_fini( )  ->  exit( )

init( ):用于C++全局对象构造

main( ):你自己写的函数

fini( ):用于C++全局对象析构

查看linux系统调用:http://asm.sourceforge.net/syscall.html

1 创建进程

在Linux上,要启动一个新的进程,一般通过fork + exec系列函数来实现,fork 将当前进程“分叉”出一个孪生子进程,exec负责替换这个子进程的执行文件,来执行子进程的新程序文件。

这里的forkexec系列函数,是操作系统提供给应用程序的API函数,在其内部最终都会通过系统调用,进入操作系统内核,通过内核中的进程管理机制,来完成一个进程的创建。

操作系统内核将负责进程的创建,主要有下面几个工作要做:

  • 创建内核中用于描述进程的数据结构,在Linux上是task_struct
  • 创建新进程的页目录、页表,用于构建新进程的内存地址空间

在Linux内核中,由于历史原因,Linux内核早期并没有线程的概念,而是用任务:task_struct来描述一个程序的执行实例:进程

在内核中,一个任务对应就是一个task_struct,也就是一个进程,内核的调度单元也是一个个的个task_struct

后来,多线程的概念兴起,Linux内核为了支持多线程技术,task_struct实际上表示的变成了一个线程,通过将多个task_struct合并为一组(通过该结构内部的组id字段)再来描述一个进程。因此,Linux上的线程,也称为轻量级进程

系统调用fork的一个重要使命就是要去创建新进程的task_struct结构,创建完成后,进程就拥有了调度单元。随后将开始可以参与调度并有机会获得执行。

2 加载可执行文件

通过fork成功创建进程后,此时的子进程和父进程相当于一个细胞进行了有丝分裂,两个进程“几乎”是一模一样的。

而要想子进程执行新的程序,在子进程中还需要用到exec系列函数来实现对进程可执行程序的替换。

exec系列函数同样是系统调用的封装,通过调用它们,将进入内核sys_execve来执行真正的工作。

这个工作细节比较多,其中有一个重要的工作就是加载可执行文件到进程空间并对其进行分析,提取出可执行文件的入口地址

我们使用C、C++等高级语言编写的代码,最终通过编译器会编译生成可执行文件,在Linux上,是ELF格式,在Windows上,称之为PE文件。

无论是ELF文件还是PE文件,在各自的文件头中,都记录了这个可执行文件的指令入口地址,它指示了程序该从哪里开始执行。

进程创建后,是如何来到这个入口地址的?

不管在Windows还是Linux上,应用线程都会经常在用户空间和内核空间来回穿梭,这可能出现在以下几种情况发生时:

  • 系统调用
  • 中断
  • 异常

在进入内核空间时,线程将自动保存上下文(其实就是一些寄存器的内容,比如指令寄存器EIP)到线程的堆栈上,记录自己从哪里来的,等到从内核返回时,再从堆栈上加载这些信息,回到原来的地方继续执行。

子进程是通过sys_execve系统调用进入到内核中的,在后面完成可执行文件的分析后,拿到了ELF文件的入口地址,将会去修改原来保存在堆栈上的上下文信息,将EIP指向ELF文件的入口地址。这样等sys_execve系统调用结束时,返回到用户空间后,就能够直接转到新的程序入口开始执行代码。

所以,一个非常重要的特点是:exec系列函数正常情况下是不会返回的,一旦进入,完成使命后,执行流程就会转向新的可执行文件入口

另外需要提一下的是,在Linux上,除了ELF文件,还支持一些其他格式的可执行文件,如MS-DOS、COFF除了二进制的可执行文件,还支持shell脚本,这个情况下将会将脚本解释器程序作为入口来启动

3 从ELF入口到main函数

例如这个程序

#include 
int main() {
    printf("hello, world!\n");
    return 0;
}

通过gcc编译后,生成了一个ELF可执行文件,通过readelf指令,可以实现对ELF文件的分析,这里可以看到ELF文件的入口地址是0x400430:

随后,我们通过反汇编神器,IDA打开分析这个文件,看一下位于0x400430入口的地方是什么函数?
可以看到,入口地方是一个叫做 _start 的函数,并不是我们的main函数。
在_start的结尾,调用了 __libc_start_main 函数,而这个函数,位于libc.so中。在进入main函数之前,还有一个重要的工作要做,这就是:C/C++运行时库的初始化。上面的 __libc_start_main 就是在完成这一工作。
在通过GCC进行编译时,编译器将自动完成运行时库的链接,将我们的main函数封装起来,由它来调用。

glibc是开源的,我们可以在GitHub上找到这个项目的libc-start.c文件,一窥 __libc_start_main 的真面目,我们的main函数正是被它在调用。

4 分析程序实践

在start函数,将main函数地址入栈,然后执行__libc_start_main( )

 

恢复静态编译程序的符号

使用kali的file命令查看一下文件,发现是静态编译,剥离(stripped)

再使用readelf命令查看一下,并没有.comment这个段,这个段通常是用来保存编译器信息的

在IDA中并不能识别出函数

接下来使用lscan(https://github.com/maroueneboubakri/lscan),这款工具用来识别样本中使用的库文件版本,并且自带了sig文件。

如果要使用lscan,则必须先安装pyelftoolspefile

找到libcrypto-1.0.1e.sig、libm-2.23.sig导入到IDA的/sig/pc目录里,然后shift+F5,添加sig文件,然后发现一个函数都没识别出来

有两个签名库可以使用:

https://github.com/Maktm/FLIRTDB

https://github.com/push0ebp/sig-database

发现效果并不理想,使用strings命令,或者找样本字符串,发现了一下加密相关字符。

然后试试libcrypt相关的签名文件。

 

ELF动态调试

1 IDA远程动态调试ELF文件

在IDA的文件夹中,有一个“agent”文件夹,可以与ida建立远程连接。

images

在linux下分析elf文件,使用的是linux_server64(版本由待分析程序决定)。将linux_server64复制到ubuntu中,然后运行linux_server64,它就处于一个监听23946端口的状态。

images

打开IDA,不附加任何程序。选择Debugger-Run-Remote Linux Debugger,之后会进入到下面的界面。

images

Application:待分析程序地址,例: “watchdog”

Input file:上传到linux的文件,例: “watchdog”

Directory:待分析程序所在的文件夹,例:“/home/ubuntu/eyidaima”

Parameters:运行程序附加的参数

Hostname:ubuntu主机IP

Password : ubuntu主机权限密码

填好之后就是如下所示。

images

点击确定之后就可以远程分析linux类的elf样本了。同样,在静态下可以使用的功能,如字符串,F5,这里依旧可以使用。images

 

2  radare2 进行动态分析

使用kali下的radare2进行elf样本分析。

  • rax2 ———> 用于数值转换
  • rasm2 ——-> 反汇编和汇编
  • rabin2 ——-> 查看文件格式
  • radiff2 ——> 对文件进行 diff
  • ragg2/ragg2­cc ——> 用于更方便的生成shellcode
  • rahash2 ——> 各种密码算法, hash算法
  • radare2 ——> 整合了上面的工具

 

官方文档:https://book.rada.re/index.html

常用命令:
信息搜集:
$ rabin2 -I ./program — 查看二进制信息
ii [q] – 查看导出表
?v sym.imp.func_name — 获取过程链接表中相应函数的地址(func_name@PLT)
?v reloc.func_name —获取全局偏移表中函数的地址(func_name@GOT)
ie [q] — 获取入口点地址

内存相关:
dmm — 列出模块 (库文件,内存中加载的二进制文件)
dmi [addr|libname] [symname] — 列出目标库的符号标识

搜索:
/? — 列出搜索子命令
/ string — 搜索内存/二进制文件的字符串
/R [?] —搜索ROP gadgets
/R/ — 使用正则表达式搜索ROP gadgets

调试:
dc — 继续执行
dcu addr – 继续执行直到到达指定地址
dcr — 继续执行直到到达ret (使用步过step over)
dbt [?] —基于 dbg.btdepth 和 dbg.btalgo显示backtrace追踪函数
doo [args] — 添加参数重新打开调试模式
ds — 步入一条指令(step on)
dso — 步过(Step over)

Visual Modes
pdf @ addr — 打印出相应偏移处的函数的汇编代码
V —视图模式,使用p/P to在不同模式间切换

具体可以查看-h来理解radare2的命令,也别机器翻译了,看英文更能理解一些。

开始调试

r2 -d watchdog  进入调试模式

aaa  进行分析,将不能识别的函数识别出来

vv 进入视图模式,类似于IDA的界面了,然后v或者!打开多个窗口

images

F8 单步 F9 执行 F7 步入等

 

3 cutter 进行动态分析

官方地址:https://cutter.re/

官方下载地址:https://cutter.re/download/

从源代码构建:https://cutter.re/docs/building.html

Releases格式:https://github.com/rizinorg/cutter/releases/

cutter2.0 :  https://cutter.re/cutter-2.0

cutter2.0是依靠radare2逆向工程框架的分支rizin作为引擎的

Rizin的github地址:https://github.com/rizinorg/rizin

Rizin的发行版:https://github.com/rizinorg/rizin/releases

使用发行版要全英文路径

cutter的静态分析针对elf的也比较不错,能识别main

动态调试使用Debug选项即可,2.0版本的动态调试处于beat阶段,也还能用。

附加的情况,必须加载一个程序,然后才能附加别的程序。

4 edb-debugger进行动态分析

github地址:https://github.com/eteran/edb-debugger

Releases: https://github.com/eteran/edb-debugger/releases

采用源码编译方式:

$ git clone –recursive git://github.com/eteran/edb-debugger.git  (使用https会报403)

$ mkdir build

$ cd build

$ cmake -DCMAKE_INSTALL_PREFIX=/usr/local/ ..   (这里缺少什么包就安装什么包)

$ make

$ make install

$ edb

动态调试和onlydbg一样


云涯历险记 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:恶意代码技术理论:ELF恶意代码分析
喜欢 (0)