Bug 27471 - GLIBC_TUNABLES aren't parsed properly in AT_SECURE binaries
Summary: GLIBC_TUNABLES aren't parsed properly in AT_SECURE binaries
Status: RESOLVED FIXED
Alias: None
Product: glibc
Classification: Unclassified
Component: libc (show other bugs)
Version: 2.34
: P2 normal
Target Milestone: 2.34
Assignee: Siddhesh Poyarekar
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2021-02-26 01:08 UTC by Max
Modified: 2021-12-08 02:53 UTC (History)
2 users (show)

See Also:
Host:
Target:
Build:
Last reconfirmed: 2021-03-01 00:00:00
siddhesh: security+


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Max 2021-02-26 01:08:09 UTC
In AT_SECURE binaries, the parse_tunables() function in elf/dl-tunables.c will not correctly read a tunable's name if it's preceded by one labelled TUNABLE_SECLEVEL_SXID_ERASE.

In the worst case scenario an AT_SECURE binary may pass tunables labelled TUNABLE_SECLEVEL_SXID_ERASE to child processes.


Here's an example:

$ GLIBC_TUNABLES=glibc.malloc.tcache_count=16 sudo -E env | grep TUNABLES                             
GLIBC_TUNABLES=

$ GLIBC_TUNABLES=glibc.malloc.tcache_count=16:glibc.malloc.tcache_count=32 sudo -E env | grep TUNABLES
GLIBC_TUNABLES=glibc.malloc.tcache_count=32

The 1st invocation of env via sudo attempts to use the glibc.malloc.tcache_count tunable, which is labelled TUNABLE_SECLEVEL_SXID_ERASE and therefore correctly ignored by sudo and erased from its environment before invoking the env child process.

The 2nd invocation does the same thing, but includes another copy of the same tunable. Although it is still correctly ignored by sudo, it is not erased from the environment and subsequently passed to the env child process.

This example is benign but shows that TUNABLE_SECLEVEL_SXID_ERASE isn't behaving correctly.


The following lines of parse_tunables() are always executed before its while loop starts over:

if (p[len] == '\0')
    return;
else
    p += len + 1;

'p' is assumed to be a char*, currently pointing to the start of a tunable's value string. 'len' is assumed to hold the length of said value string. In the above examples, having processed the 1st tunable, p is expected to point to the string "16" and len is assumed to be 2.

If the tunable wasn't labelled TUNABLE_SECLEVEL_SXID_ERASE then these assumptions would be correct, the code above would adjust p to point at the start of the next tunable's name and the while loop would start over, parsing the next tunable.

However, tunables labelled TUNABLE_SECLEVEL_SXID_ERASE are erased by the following code in the same function:

char *q = &p[len + 1];
p = name;
while (*q != '\0')
    *name++ = *q++;
name[0] = '\0';
len = 0;

Where "name" is a pointer to the start of the current tunable's name. When a tunable is erased, the 'p' and 'len' variables differ from what they're expected to be when the 1st code snippet is reached, resulting in p getting incremented by the line:
p += len + 1;

In the 2nd example above, p ends up pointing to the 2nd character of the 2nd tunable's name, losing the "g" in "glibc", so the tunable gets incorrectly read as "libc.malloc.tcache_count=32". Non-existent tunables are ignored, so it remains in the environment where it's subsequently passed to child processes, despite the TUNABLE_SECLEVEL_SXID_ERASE label specifying that it shouldn't be.
Comment 1 Siddhesh Poyarekar 2021-03-01 14:21:29 UTC
I've posted a candidate fix that should rewrite the tunable string rewriting so that it only writes out valid tunables into the environment that the current setuid program and its children inherit:

https://patchwork.sourceware.org/project/glibc/patch/20210301141732.3433685-1-siddhesh@sourceware.org/
Comment 2 Siddhesh Poyarekar 2021-03-02 05:06:43 UTC
Some notes on the security impact after taking a closer look at this.  I don't think there are any direct security implications (to the extent of it introducing a *new* vulnerability in the environment) of tunables going across the setxid boundary.  It definitely does not alter behaviour of the setxid program itself since all tunables except those explicitly set as SXID_NONE are ignored.  As a result, there's no privilege escalation.

At worst, tunables give some amount of control on the environment of non-setxid children of setxid programs where it previously couldn't.  This could potentially improve feasibility of exploiting existing bugs.

For example, one could force usage of specific versions of routines (using, say, glibc.cpu.name) known to have the right timing characteristics to improve chances of hitting a data race.  Alternatively, one could use glibc.rtld.optional_static_tls to block away address space to maybe reduce effectiveness of ASLR.
Comment 3 Max 2021-03-02 05:16:49 UTC
Indeed the only threat I can think of is when a suid binary sets uid = euid before invoking a child process (like sudo does), in which case the child won't ignore the SXID_ERASE tunable but will still have elevated privileges.

A (somewhat contrived) example could be a user having sudo access to a regular binary, in which case they could, as you mentioned, use the tunables to facilitate exploitation of said binary with elevated privileges.
Comment 4 Siddhesh Poyarekar 2021-12-08 02:53:11 UTC
This was fixed in 2.34.

commit 2ed18c5b534d9e92fc006202a5af0df6b72e7aca
Author: Siddhesh Poyarekar <siddhesh@sourceware.org>
Date:   Tue Mar 16 12:37:55 2021 +0530

    Fix SXID_ERASE behavior in setuid programs (BZ #27471)