严正声明:本文仅限于技术讨论与学术学习研究之用,严禁用于其他用途(特别是非法用途,比如非授权攻击之类),否则自行承担后果,一切与作者和平台无关,如有发现不妥之处,请及时联系作者和平台
译者:ForrestX386
预估稿费:200RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
0x00. 前言
在正式开始本篇文章之前,我有必要阐述一下我的观点:我反对在没有得到个人或者组织明确授权的情况下对目标使用任何形态的恶意软件。 本篇文章的目的旨在通过演示示例教育读者恶意的PHP模块带来危害。
我会向大家简单介绍一下有关Rootkit的知识,然后解释一下我为什么编写一个攻击PHP解释器的PHP rootkit模块, 当然我会提供实验中涉及到的rootkit的PoC, 包括它的源码。如果你仅仅对这个PoC源码感兴趣,你可以直接猛击这里访问。
0x01. Rootkit 简介
早在上个世纪90年代初,Rootkits 技术就已经存在了,传统意义上的Rootkit是指运行于内核态的程序代码。 我们日常使用的word 和浏览器都是运行于用户态的进程。 运行于内核态的进程拥有完全的系统权限。
在内核态中运行的代码很任性,可以被允许做任何事情,但是任性也是有代价的,如果你的代码在运行中出现一点点小错误都可能导致系统的崩溃或者数据丢失等严重问题。
大多数的Rootkit 工作原理都很类似, 他们会寻找操作系统处理内核态和用户态通信的地方,然后在这个地方注入代码,充当系统内核态和用户态之间通信的代理人,这样Rootkit就可以读取并修改内核态和用户态之间通信的任何数据。 在软件开发中,这种代理技术也被称之为HooK技术, 这种HooK技术可以带给我们很多好处,有点像程序员临时修复问题的monkey-path技术。
rootkit是如何Hook 函数调用的
0x02. 我为什么要编写一个PHP 模块形态的rootkit
至于为什么要为PHP编写一个Rootkit 模块? 原因其实有很多,其中最重要的一条就是:
PHP 也许是世界上最好的语言之一(在PHP开发人员眼中是没有之一的 O(∩_∩)O~)
当然其他原因还包括:
1、PHP易上手
第一个也是最明显的原因就是:PHP 语言很容易上手。 以我的经验,学习如何使用Zend Engine(整个PHP语言构建的基石)并为其编写扩展 比学习如何编写系统内核模块容易多了,相比系统内核模块, Zend Engine 的代码本身就小很多,文档也非常棒,复杂度也小了很多。
退一步说,即使没有好的文档和教程, 我也能用一天的时间就学会关于如何编写PHP模块的基础知识,作为一个C 语言的入门新手尚可做到,那些术业有专攻的坏蛋肯定是没问题的。
2、php 模块形式的rootkit稳定性好
我们知道传统的rootkit 一般被设计用于运行在系统内核态,那么一个写的不是很好的rootkit导致系统崩溃的概率就很大,但是PHP 模块形态的rootkit就没有这个问题。 即使,PHP rootkit写的很烂,也不会导致整个系统崩溃。顶多会导致当前的请求被返回一串堆栈错误信息。
3、伪装性好
坦白地说,你上一次检测PHP 模块文件完整性是什么时候,如果我把我的PHP Rootkit模块命名成具有欺骗性的名字,比如’curl.so’, 你会较真的去检查一下它是否为真的curl 模块吗?
以 PHP 模块形态存在的rootkit 一般不会要求进行反病毒签名,也不会触发IDS报警,事实上,一些没有经验的PHP 开发人员一般只会安装PHP ,其他的基本不懂,这些有利条件都会使得我们的PHP 模块 rootkit 不会被轻易发觉。
此外,基于内核的rootkit 一般都会要求你HooK 所有进程的系统调用,这会大幅降低系统的性能,也更容易引起人们的警觉。
4、可移植性
编写PHP 模块形态的rootkit 不仅能享受到用户态编程的好处,还以让你的rootkit 跨平台使用(因为PHP 就是一个平台独立的开发语言)。在一个平台编写的Rootkit可以很容易在另一个平台编译、运行(比如Linux 平台下编写的PHP 模块 rootkit可以在windows 下编译、运行)。
0x03. 关于试验中涉及到的rootkit详情
现在到了最令人感兴趣的部分了,我会向您展示php 模块形态的 rootkit到底是怎样的,它到底有多危险。 在我的演示示例中,用户可以很明显的观察到他们正在被窥视。 通过一些小的调整和设置,甚至对于管理员来说,你都不会轻易观察到这些现象。
Hook 加密方法
PHP rootkit 一般由两个比较重要的部分组成, 一个部分是注册rootki自身,另一个部分就是Hook 目标函数。 接下来的截图描绘了我实现的rootkit 中rootki注册自身并hook相关函数的代码,这些代码加起来只有80行之多(包括注释行在内)
从代码中,我们可以了解到PHP 扩展是如何被注册到Zend 引擎(PHP 语言的基石)中的。 不知道细心的你是否注意到 PHP_MINIT_FUNCTION 方法中的两行奇怪的代码?
其中rootkit_hook_fuction 函数的实现如下:
rootkit_hook_fuction 会在全局函数表中搜索目标函数,然后将其保存在original 变量中,最后将函数引用指向Hook函数(也就实现了将恶意Hook函数注入到了全局函数表中的目的)。
演示示例中,rootkit选择Hook hash 和sha1这两个PHP内置方法。
总结一下,rootkit 工作流程如下:
首先在全局函数表中搜索目标函数, 如果找到了目标函数,那么rootkit会将目标函数保存在一个名为original的变量中,然后将全局函数表中的目标函数的引用替换成Hook函数的地址。
如果一切顺利的话,Hook函数会在目标函数之前被调用,这样rootkit 就可以完全控制目标函数了,这就意味着rootkit可以读取目标函数的入参信息和目标函数的返回值,或者完全绕过目标函数的调用,几乎可以做任何事情。
在写完rootkit中基本的Hook代码之后,我决定给rootkit模块增加日志功能,代码如下:
用于记录被Hook函数的参数值,下图会向您描绘这些日志是如何被记录的。
1、第一个命令显示/tmp/php-module-rootkit.txt 文件 不存在,这个文件用于保存rookit劫持到的敏感信息, 如果不存在,rootkit将在运行时新创建一个。
2、第二个命令显示rootkit是怎样通过参数 -dextension={module} 被载入到php解释器中的。 一般情况下这个可以在php.ini中通过配置实现。
3、我们通过执行 php -r ‘code’命令,告诉php解释器去执行双引号之间的代码。 在示例中,我将执行sha1()函数 并将执行结果打印到屏幕。
4、在最后一个命令中,我将会读取新创建的/tmp/php-module-rootkit.txt 文件内容,这个文件中包含了rootkit Hook到的sha1 函数参数值,也就是我们要计算hash的密码内容。
0x04. 我为什么不完全公开源代码
我觉得很有必要解释一下, 我仍然能够看到有些人还在使用一些弱加密的函数,比如md5 和sha1, 去存储他们的密码。 记住: 永远不要使用这些弱加密的函数,现代的计算机处理器可以很轻松地破解这些密码hash值(目前bcrypt函数是个比较好的可替代的加密方法, 如果你的登录认证方式是通过类似OAuth的方式实现,这样会更加安全,因为你不用存储加密后的密码。)
一些网友告诉我说,他们希望可以得到rootkit的源码,于是我将rootkit的源码放在了Github上,这样大家都可以看到了。不过, 为了防止一些脚本小子拿这个rootkit去作恶,我已经删除了编译指令和Hook 方法的实现。 而且我也不会公布编译好的二进制模块。
一个稍微有点经验的 C 开发者可以很容易地实现PHP 模块的编译以及去实现hook 函数功能。
0x05. 如何避免使用到恶意的PHP 模块
检测你的PHP 模块是否为恶意模块的最简单方法就是:安装PHP 之后保存一份这些安装好的php模块文件的hash值,有了这些hash值之后,你就可以设置一个定时任务去计算所有PHP 扩展目录中模块的hash值,然后比较它们的值和原始的值是否一样。下面是一个简单的实现此目的的python脚本:
from argparse import ArgumentParser
from subprocess import Popen, PIPE
from hashlib import sha1
from os import path, walk
import re
import os
from sys import stderr
def extension_dir():
return Popen(["php-config", "--extension-dir"], stdout=PIPE).stdout.read().decode().strip()
def hash_files():
for root, dirs, files in walk(extension_dir()):
for file in files:
file = path.join(root, file)
yield file, hash_file(file)
def hash_file(file):
with open(file, "rb") as data:
return sha1(data.read()).hexdigest()
def check_hashes(hashes):
with open(hashes) as file:
for expected_path, expected_hash in (line.strip().split(", ") for line in file):
if hash_file(expected_path) != expected_hash:
yield expected_path
def main():
parser = ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("-l", "--list", help="List hashes, save these to a file which you will use later", action="store_true")
group.add_argument("-c", "--check", help="Check that all hashes are equal in the given file")
args = parser.parse_args()
if args.list:
for full_path, file in hash_files():
print("{}, {}".format(full_path, file))
else:
print("[!] Unable to find any extensions", file=stderr)
if args.check:
for expected_path in check_hashes(args.check):
print("[!] Potentially malicious extension detected: {}".format(expected_path), file=stderr)
else:
print("[+] No changes detected")
if __name__ == "__main__":
main()