作者 | 绿盟科技格物实验室 李东宏
一、背景
最近的SolarWinds事件展示了软件供应链攻击的过程。供应链攻击的范围很广,本文重点关注涉及代码和数据的应用程序安全。随着DevOps的成熟,大量的代码是通过本地或通过SaaS方案自动构建和发布的。企业将大部分精力放在保护运行此代码的生产系统上,而对构建系统的重视程度不足,或者将这种责任转移给服务提供商。在软件构建过程中,需要使用安全工具针对软件组成或者依赖进行检查,及时发现并删除废弃的软件组件以及存在已知漏洞的软件包,并及时升级为不包含已知漏洞的软件版本。
二、构建过程中植入恶意代码
构建系统通常在构建和部署或软件发布的过程中动态创建和销毁。攻击者的切入点则是在软件构建过程中植入恶意的程序代码。
如果用户安装了包含恶意代码的软件包,攻击者将可以执行以下一些活动:
1. 窃取代码和敏感数据。
2. 在代码中植入后门并部署到生产环境中。
3. 利用计算机资源实现挖矿等目的。
4. 窃取敏感数据,包括但不限于环境变量、敏感文件、凭据、证书等。
5. 执行横向移动和特权提升。
由于软件构建过程中的安全检测内容比较多,这里以python程序环境变量的获取与使用为例进行分享,其他类型的可以使用相同的防范思路进行扩展。
三、获取环境变量
● 使用python API获取 os.environ。
return os.environ
● 运行ENV命令。
subprocess.check_output(["env"])
● 运行shell内置的set命令。
subprocess.check_output(["sh","-c","set"])
● 从/proc/<pid>/environ读取环境变量。
loc = Path("/proc") /str(os.getpid()) / "environ"
return loc.read_text()
● 读取可能包含环境变量的文件。
data = [] commons = {"/etc/environment", "/etc/profile", "/etc/bashrc", "~/.bash_profile","~/.bashrc", "~/.profile", "~/.cshrc", "~/.zshrc", "~/.tcshrc", }
for i in commons:
env= Path(i).expanduser().read_text()
data.append(env)
● 利用libc.so共享库获取环境变量。
libc = ctypes.CDLL(None)
environ =ctypes.POINTER(ctypes.c_char_p).in_dll(libc,"environ")
还有其他的环境变量获取方法,这里就不再赘述了。
四、静态分析
通过对环境变量获取方法的总结,可以使用semgrep 进行静态分析,以下是用于检测对环境变量访问的semgrep静态分析规则。
rules:
- id: env-set
patterns:
- pattern-either:
- pattern: |
subprocess.check_output([..., "=~/env|set/", ...])
- pattern: |
subprocess.run([..., "=~/env|set/", ...])
- pattern: |
subprocess.Popen([..., "=~/env|set/", ...])
message: |
Reading from env or set commands
severity: ERROR
languages:
- python
- id: python-os-environ
patterns:
- pattern-not-inside: os.environ.get(...)
- pattern-not-inside: os.environ[...]
- pattern-either:
- pattern: |
os.environ
message: |
Reading from python"s os.environ()
severity: ERROR
languages:
- python
- id: python-proc-fs
patterns:
- pattern-either:
- pattern: |
pathlib.Path("/proc") / ... / "environ"
message: |
Reading python /proc/<pid>/environ
severity: ERROR
languages:
- python
- id: environ-files
patterns:
- pattern-inside: |
$X = {..., "=~/\/etc\/environment|\/etc\/profile|\/etc\/bashrc|~\/.bash_profile|~\/.bashrc|~\/.profile|~\/.cshrc|~\/.zshrc|~\/.tcshrc/", ...}
...
- pattern-either:
- pattern: |
Path(...)
- pattern: |
open(...)
message: |
Reading from sensitve files that contain environment variables
severity: ERROR
languages:
- python
- id: libc-environ
patterns:
- pattern-either:
- pattern: |
$LIB = ctypes.CDLL(...)
...
$Y.in_dll($LIB, "environ")
message: |
Reading from libc.environ
severity: ERROR
languages:
-python
这个规则的语法是比较简单的。关于规则编写以及相关的语法,可以参考 https://semgrep.dev/docs/。
编写python脚本static_analysis.py针对目标软件包进行检测。脚本代码如下:
import os
os.system("semgrep -f static_scan_rules.yml pkgs/")
在命令行运行脚本的检测结果如下:
静态分析可以帮助我们轻松地检测出一些代码中明显的安全问题,但是静态分析对于混淆的代码具有局限性,因为实现相同逻辑的代码和API有不同的指令排列方式。从长远来看,可能无法为所有内容编写检测规则。因此还需要进行动态分析,使检测更加精确。
五、动态分析
对于动态分析,可以跟踪比较核心的系统调用或者函数调用,从而判定软件是否存在一些安全风险。为了简单表示动态分析,这里以strace工具为例来进行环境变量获取的跟踪分析。
5.1命令执行
考虑到一些恶意代码是通过执行系统命令来获取系统环境信息的,可以通过strace监视execve系列函数调用来发现具体的命令调用,从而判定是否存在危害性。
$ strace -f -e trace=execve -o strace python -c "import subprocess;subprocess.call(["env"])"
$ cat strace
431765 execve("/home/ajin/package_scan/venv/bin/python", ["python", "-c", "import subprocess;subprocess.cal"...], 0x7ffee0ac8c48 /* 28 vars */) = 0
431766 execve("/home/ajin/package_scan/venv/bin/env", ["env"], 0x7fff8fa0b308 /* 28 vars */) = -1 ENOENT (No such file or directory)
431766 execve("/home/ajin/.local/bin/env", ["env"], 0x7fff8fa0b308 /* 28 vars */) = -1 ENOENT (No such file or directory)
431766 execve("/usr/local/sbin/env", ["env"], 0x7fff8fa0b308 /* 28 vars */) = -1 ENOENT (No such file or directory)
431766 execve("/usr/local/bin/env", ["env"], 0x7fff8fa0b308 /* 28 vars */) = -1 ENOENT (No such file or directory)
431766 execve("/usr/sbin/env", ["env"], 0x7fff8fa0b308 /* 28 vars */) = -1 ENOENT (No such file or directory)
431766 execve("/usr/bin/env", ["env"], 0x7fff8fa0b308 /* 28 vars */) = 0
431766 +++ exited with 0 +++
431765 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=431766, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
431765 +++ exited with0 +++
5.2文件读取
针对通过读取文件方式获取系统环境变量的情况,可以通过监视openat()或 open() 系列系统调用进行分析与判定。
strace -f -e trace=open,openat -o strace python -c "from pathlibimport Path; Path("~/.bashrc").expanduser().read_text()"
$ cat strace | grep bashrc
432709 openat(AT_FDCWD,"/home/ajin/.bashrc",O_RDONLY|O_CLOEXEC) =3
5.3网络连接
攻击者窃取的数据会通过网络方式向外发送,可以监视connect()系统调用来发现外联的情况。
$ strace -f -e trace=connect -o strace python -c "importurllib.request;urllib.request.urlopen("http://python.org/")"
$ cat strace | grep "htons(80)"
435764connect(3, {sa_family=AF_INET, sin_port=htons(80),sin_addr=inet_addr("45.55.99.72")},16) =0
5.4实施跟踪分析
针对新的安装包,可以通过strace工具跟踪其所有的敏感系统调用,并查找与恶意行为对应的模式。strace的命令如下:
strace -s 1000 -fqqe trace=openat,execve,connect --seccomp-bpf<cmd>
参数说明如下:
-s strsizelimit length ofprint strings to STRSIZE chars (default 32)
-f follow forks
-q suppress messagesabout attaching, detaching, etc.
-e expr a qualifying expression:option=[!]all or option=[!]val1[,val2]...
--seccomp-bpf Enable seccomp-bpf filtering to improve performance.
cmd the command forprocess running.
以检测软件包rogue为例,编写动态检测脚本如下:
import re
import subprocess
from pathlib import Path
EXEC = re.compile(r", \[.*\]")
IP = re.compile(r"inet_addr\(\".+\"\)")
PORT = re.compile(r"htons\([0-9]+\)")
ENV_LOCATIONS = {
"/etc/environment",
"/etc/profile",
"/etc/bashrc",
"~/.bash_profile",
"~/.bashrc",
"~/.profile",
"~/.cshrc",
"~/.zshrc",
"~/.tcshrc",
}
BAD_COMMANDS = {
""set"",
""env"",
}
def check_path(pkg, syscall):
path =syscall.split(""")[1]
for loc inENV_LOCATIONS:
ifPath(loc).expanduser().as_posix() == path:
print(pkg+" tried to access sensitive environment location "+loc+" duringinstallation.")
def check_cmd(pkg, syscall):
args =EXEC.search(syscall)
match_str = args.group()
if(cmd in match_str):
for cmd inBAD_COMMANDS):
print( pkg +"tried to access environment variables by executing "+match_str+"command during installation.")
def check_connect(pkg, syscall):
ipo = IP.search(syscall)
porto =PORT.search(syscall)
ip_addr =ipo.group().replace("inet_addr(", "").replace(""", "").replace(")", "")
port = porto.group().replace("htons(","").replace(")", "")
loc = ip_addr +":" + port
print(pkg+ " tried toconnect to "+ loc +" during installation.")
def lookup_env(pkg, syscalls):
calls =syscalls.splitlines()
for i in calls:
if "openat(" in i:
check_path(pkg,i)
elif "execve(" in i:
check_cmd(pkg,i)
elif "connect(" in iand "sin_addr=" in i:
check_connect(pkg, i)
def collect_syscalls(pkg):
print(f"Analyzing:{pkg}")
args = [
"strace", "-s","2000", "-fqqe",
"trace=openat,execve,connect","--seccomp-bpf",
"pip", "install","--no-cache"] + pkg.split()
returnsubprocess.check_output(args, stderr=subprocess.STDOUT).decode("utf-8","ignore")
def check():
pkg = "-egit://github.com/ajinabraham/poc-rogue.git"
syscalls =collect_syscalls(pkg)
lookup_env("rogue",syscalls)
if __name__ == "__main__":
check()
执行脚本运行结果如下:
六、例外情况
Python开发的软件包中并不是所有的行为都可以通过syscall监控到,例如利用外部函数接口从libc通过函数指针从内存中访问的方法,这种情况可以使用LD_PRELOAD跟踪libc符号和函数调用行为精确匹配。
七、总结
为了保证生产环境的安全,必须在构建系统中实施必要的安全控制流程,例如开启双重认证以及每次进行生产构建的时候进行环境安全检测等,避免成为攻击者攻击的目标。在现实世界中,攻击并不总是很复杂,而是针对目标系统最薄弱的环节进行攻击。本文旨在推动安全意识的提高,分享一些在软件供应链中需要主动安全分析的过程和工具等内容。
参考链接:
https://ajinabraham.com/blog/detecting-zero-days-in-software-supply-chain-with-static-and-dynamic-analysis
https://github.com/ajinabraham/package_scan
声明:本文来自关键基础设施安全应急响应中心,版权归作者所有。文章内容仅代表作者独立观点,不代表安全内参立场,转载目的在于传递更多信息。如有侵权,请联系 anquanneican@163.com。