Looney Tunables 本地权限升级 (CVE-2023-4911) 研讨会(仅用于教育目的)
在计算中,动态链接器是操作系统的一部分,通过将库内容从持久存储复制到 RAM、填充跳转表和重定位指针,加载和链接可执行文件执行时所需的共享库。
例如,我们有使用 openssl 库计算 md5 哈希的程序:
$ head md5_hash.c
#include
#include
#include
ld.so 解析二进制文件并尝试查找与
$ ldd md5_hash
linux-vdso.so.1 (0x00007fffa530b000)
libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3 (0x00007f19cda00000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f19cd81e000)
/lib64/ld-linux-x86-64.so.2 (0x00007f19ce032000)
正如我们所看到的,它在/lib/x86_64-linux-gnu/libcrypto.so.3中找到了必要的加密库。在程序启动期间,它将该库的代码放入进程 RAM 中,并将所有引用链接到该库。
当程序启动时,该加载程序首先检查该程序以确定其所需的共享库。然后它搜索这些库,将它们加载到内存中,并在运行时将它们与可执行文件链接。在此过程中,动态加载器解析符号引用,例如函数和变量引用,确保为程序的执行做好一切准备。鉴于其作用,动态加载程序对安全性高度敏感,因为当本地用户启动 set-user-ID 或 set-group-ID 程序时,其代码会以提升的权限运行。
可调参数是 GNU C 库中的一项功能,允许应用程序作者和发行版维护人员更改运行时库行为以匹配其工作负载。它们被实现为一组可以以不同方式修改的开关。当前执行此操作的默认方法是通过 GLIBC_TUNABLES 环境变量,将其设置为冒号分隔的名称=值对的字符串。例如,以下示例启用 malloc 检查并将 malloc 修剪阈值设置为 128 字节:
GLIBC_TUNABLES=glibc.malloc.trim_threshold=128:glibc.malloc.check=3
export GLIBC_TUNABLES
将 --list-tunables 传递给动态加载程序以打印所有具有最小值和最大值的可调参数:
$ /lib64/ld-linux-x86-64.so.2 --list-tunables
glibc.rtld.nns: 0x4 (min: 0x1, max: 0x10)
glibc.elision.skip_lock_after_retries: 3 (min: 0, max: 2147483647)
glibc.malloc.trim_threshold: 0x0 (min: 0x0, max: 0xffffffffffffffff)
glibc.malloc.perturb: 0 (min: 0, max: 255)
glibc.cpu.x86_shared_cache_size: 0x100000 (min: 0x0, max: 0xffffffffffffffff)
glibc.pthread.rseq: 1 (min: 0, max: 1)
glibc.cpu.prefer_map_32bit_exec: 0 (min: 0, max: 1)
glibc.mem.tagging: 0 (min: 0, max: 255)
在执行的一开始,ld.so 调用 __tunables_init() 来遍历环境(第 279 行),搜索 GLIBC_TUNABLES 变量(第 282 行);对于它找到的每个 GLIBC_TUNABLES,它都会复制该变量(第 284 行),调用 parse_tunables() 来处理和清理该副本(第 286 行),最后用该清理后的副本替换原始 GLIBC_TUNABLES(第 288 行) ):
// (GLIBC ld.so sources in ./glibc-2.37/elf/dl-tunables.c)
269 void
270 __tunables_init ( char * * envp )
271 {
272 char * envname = NULL ;
273 char * envval = NULL ;
274 size_t len = 0 ;
275 char * * prev_envp = envp ;
...
279 while (( envp = get_next_env ( envp , & envname , & len , & envval ,
280 & prev_envp )) != NULL )
281 {
282 if ( tunable_is_name ( "GLIBC_TUNABLES" , envname )) // searching for GLIBC_TUNABLES variables
283 {
284 char * new_env = tunables_strdup ( envname );
285 if ( new_env != NULL )
286 parse_tunables ( new_env + len + 1 , envval ); //
287 /* Put in the updated envval. */
288 * prev_envp = new_env ;
289 continue ;
290 }
parse_tunables() (tunestr) 的第一个参数指向即将清理的 GLIBC_TUNABLES 副本,而第二个参数 (valstring) 指向原始 GLIBC_TUNABLES 环境变量(在堆栈中)。为了清理 GLIBC_TUNABLES 的副本(其形式应为 "tunable1= aaa:tunable2=bbb"
),parse_tunables() 会从unestr 中删除所有危险的可调参数(SXID_ERASE 可调参数),但保留 SXID_IGNORE 和 NONE 可调参数(位于第 221 行) 235):