背景
最近的SolarWinds事件展示了软件供应链攻击的过程。供应链攻击的范围很广,本文重点关注涉及代码和数据的应用程序安全。随着DevOps的成熟,大量的代码是通过本地或通过SaaS方案自动构建和发布的。企业将大部分精力放在保护运行此代码的生产系统上,而对构建系统的重视程度不足,或者将这种责任转移给服务提供商。在软件构建过程中,需要使用安全工具针对软件组成或者依赖进行检查,及时发现并删除废弃的软件组件以及存在已知漏洞的软件包,并及时升级为不包含已知漏洞的软件版本。
构建过程中植入恶意代码
构建系统通常在构建和部署或软件发布的过程中动态创建和销毁。攻击者的切入点则是在软件构建过程中植入恶意的程序代码。
如果用户安装了包含恶意代码的软件包,攻击者将可以执行以下一些活动:
- 窃取代码和敏感数据。
- 在代码中植入后门并部署到生产环境中。
- 利用计算机资源实现挖矿等目的。
- 窃取敏感数据,包括但不限于环境变量、敏感文件、凭据、证书等。
- 执行横向移动和特权提升。
由于软件构建过程中的安全检测内容比较多,这里以python程序环境变量的获取与使用为例进行分享,其他类型的可以使用相同的防范思路进行扩展。
获取环境变量
- 使用python API获取 os.environ。
return os.environ
- 运行ENV命令。
subprocess.check_output(['env'])
- 运行shell内置的set命令。
subprocess.check_output(['sh', '-c', 'set'])
- 从/proc//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//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工具为例来进行环境变量获取的跟踪分析。
- 命令执行
考虑到一些恶意代码是通过执行系统命令来获取系统环境信息的,可以通过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 with 0 +++
- 文件读取
针对通过读取文件方式获取系统环境变量的情况,可以通过监视openat()或 open() 系列系统调用进行分析与判定。
strace -f -e trace=open,openat -o strace python -c 'from pathlib import Path; Path("~/.bashrc").expanduser().read_text()'
$ cat strace | grep bashrc
432709 openat(AT_FDCWD, "/home/ajin/.bashrc", O_RDONLY|O_CLOEXEC) = 3
- 网络连接
攻击者窃取的数据会通过网络方式向外发送,可以监视connect()系统调用来发现外联的情况。
$ strace -f -e trace=connect -o strace python -c 'import urllib.request;urllib.request.urlopen("http://python.org/")'
$ cat strace | grep 'htons(80)'
435764 connect(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("45.55.99.72")}, 16) = 0
- 实施跟踪分析
针对新的安装包,可以通过strace工具跟踪其所有的敏感系统调用,并查找与恶意行为对应的模式。strace的命令如下:
strace -s 1000 -fqqe trace=openat,execve,connect --seccomp-bpf
参数说明如下:
-s strsizelimit length of print strings to STRSIZE chars (default 32)
-ffollow forks
-qsuppress messages about attaching, detaching, etc.
-e expra qualifying expression: option=[!]all or option=[!]val1[,val2]...
--seccomp-bpf Enable seccomp-bpf filtering to improve performance.
cmdthe command for process 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 in ENV_LOCATIONS:
if Path(loc).expanduser().as_posix() == path:
print(pkg +" tried to access sensitive environment location "+loc+" during installation.')
def check_cmd(pkg, syscall):
args = EXEC.search(syscall)
match_str = args.group()
if(cmd in match_str):
for cmd in BAD_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 to connect 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 i and '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()
return subprocess.check_output(args, stderr=subprocess.STDOUT).decode('utf-8', 'ignore')
def check():
pkg = "-e git://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
格物实验室
绿盟科技格物实验室专注于工业互联网、物联网和车联网三大业务场景的安全研究。实验室以“格物致知”的问学态度,致力于以智能设备为中心的漏洞挖掘和安全分析,提供基于业务场景的安全解决方案。积极与各方共建万物互联的安全生态,为企业和社会的数字化转型安全护航。
绿盟威胁情报中心
绿盟威胁情报中心(NSFOCUS Threat Intelligence center, NTI)是绿盟科技为落实智慧安全2.0战略,促进网络空间安全生态建设和威胁情报应用,增强客户攻防对抗能力而组建的专业性安全研究组织。其依托公司专业的安全团队和强大的安全研究能力,对全球网络安全威胁和态势进行持续观察和分析,以威胁情报的生产、运营、应用等能力及关键技术作为核心研究内容,推出了绿盟威胁情报平台以及一系列集成威胁情报的新一代安全产品,为用户提供可操作的情报数据、专业的情报服务和高效的威胁防护能力,帮助用户更好地了解和应对各类网络威胁。