Re: [PATCH] elf: Open the normalized $ORIGIN rpath in AT_SECURE programs (BZ 34360)
Florian Weimer <[email protected]>
| Newsgroups | gmane.comp.lib.glibc.alpha |
|---|---|
| Message-ID | <[email protected]> |
* Adhemerval Zanella:
> +/* Lexically normalize the NUL-terminated PATH in place, collapsing "//",
> + "/./" and "/../" segments (a leading "/../" collapses to "/"). The
> + normalized string is a rearrangement of a prefix of PATH: the write
> + cursor never runs ahead of the read cursor and no trailing character is
> + appended, so this only ever touches bytes within the original
> + strlen (PATH) + 1 storage and can never access memory out of bounds.
> + Returns the length of the normalized path (excluding the terminating
> + NUL). */
> +static size_t
> +dst_normalize_path (char *path)
> {
> + char *wnp = path;
> + const char *rnp = path;
> + while (*rnp != '\0')
> {
> + if (rnp[0] == '/')
> {
> + /* Collapse a run of '/' to a single one before interpreting "/."
> + or "/..", so that the "." or ".." is applied to the real
> + preceding component rather than to an empty "//" segment: skip
> + this '/' whenever it is immediately followed by another one. */
> + if (rnp[1] == '/')
> {
> + ++rnp;
> + continue;
> }
>
> + if (rnp[1] == '.')
> {
> + if (rnp[2] == '.' && (rnp[3] == '/' || rnp[3] == '\0'))
> + {
> + while (wnp > path && *--wnp != '/')
> + ;
> + rnp += 3;
> + continue;
> + }
> + else if (rnp[2] == '/' || rnp[2] == '\0')
> + {
> + rnp += 2;
> + continue;
> + }
> }
> }
>
> + *wnp++ = *rnp++;
> }
>
> + *wnp = '\0';
> + return wnp - path;
> +}
This turns "/a/.." into "", which doesn't look right to me. "a/../"
ends up as "/", and "a/.." as "". And of course "" remains "". The ""
special case probably needs to be called out in the function comment.
The comment should also clarify the expected behavior regarding trailing
slashes (the caller cannot assume their presence or absence).
I suggest putting this function into a separate (header) file, so that
we can test it directly.
The integration test looks okay to me.
> /* Given a substring starting at INPUT, just after the DST '$' start
> @@ -327,6 +342,8 @@ _dl_dst_substitute (struct link_map *l, const char *input, char *result)
> }
> while (*input != '\0');
>
> + *wp = '\0';
> +
> /* In SUID/SGID programs, after $ORIGIN expansion the normalized
> path must be rooted in one of the trusted directories. The $LIB
> and $PLATFORM DST cannot in any way be manipulated by the caller
> @@ -335,15 +352,21 @@ _dl_dst_substitute (struct link_map *l, const char *input, char *result)
> checked for trust, the authors of the binaries themselves are
> trusted to have designed this correctly. Only $ORIGIN is tested in
> this way because it may be manipulated in some ways with hard
> - links. */
> - if (__glibc_unlikely (check_for_trusted)
> - && !is_trusted_path_normalize (result, wp - result))
> - {
> - *result = '\0';
> - return result;
> - }
> + links.
>
> - *wp = '\0';
> + Checking the normalized path but opening the raw one is not enough:
> + "a/b/../c" only names "a/c" when "b" is not a symbolic link, so an
> + attacker who controls a component of $ORIGIN (for example by
> + hard-linking the program into an attacker-owned directory) could
> + otherwise redirect the lookup outside the trusted directory. Replace
> + the expansion with its normalized, "../"-free form, so that the path
> + that is opened is exactly the path that was validated. */
> + if (__glibc_unlikely (check_for_trusted))
> + {
> + size_t nlen = dst_normalize_path (result);
> + if (!path_is_trusted (result, nlen))
> + *result = '\0';
> + }
It turns out this fixes a potential buffer overflow as well.
Previously, we called is_trusted_path_normalize on a potential
non-null-terminated byte array. The old loop iteration did not stop at
the specified length, but at the first null byte. So this could end up
writing beyond the end of the stack-allocated array.
I've attached the full report below.
I've been instructed to mention: Found by AISLE in partnership with Red Hat
I think under the current rules, these two bugs do not need separate in
CVE assignment even though they are very different in nature. They were
introduced in the same commit, and as your patch shows, it's not really
possible to fix them separately.
Thanks,
Florian
RHEL-215965.html
(text/html, 6.7 KB)
<title>EMBARGOED glibc: Potential stack buffer overflow in `is_trusted_path_normalize` from pre-termination call in `_dl_dst_substitute` [vulnerability-report]</title>
<p>Potential stack buffer overflow in `is_trusted_path_normalize` from pre-termination call in `_dl_dst_substitute`</p>
<p>AI_ONLY_REPORT<br/>
package: glibc-2.42-11.1.hum1<br/>
------<br/>
Summary: Potential stack buffer overflow in `is_trusted_path_normalize` <br/>
from pre-termination call in `_dl_dst_substitute`: in secure execution, the <br/>
dynamic loader validates a `$ORIGIN`-expanded path before NUL-terminating <br/>
it, which can let `is_trusted_path_normalize` read past the intended string <br/>
and potentially overflow its stack buffer.<br/>
Requirements to exploit: A real attack requires local execution plus an <br/>
existing `AT_SECURE` binary that reaches `_dl_dst_substitute` with <br/>
`check_for_trusted` set, which in practice means a setuid/setgid or <br/>
otherwise secure-execution executable whose `DT_RPATH` or `DT_RUNPATH` <br/>
begins with `$ORIGIN` and is followed by `\0` or `/`. The available <br/>
materials do not establish that this SRPM ships such a privileged binary by <br/>
default. Reproducibility also depends on adjacent bytes after the expanded <br/>
string not containing an early NUL byte.<br/>
Component affected: `glibc-2.42-11.1.hum1` dynamic loader path handling in <br/>
`elf/dl-load.c` (`_dl_dst_substitute`, `is_trusted_path_normalize`)<br/>
Version affected: `glibc-2.42-11.1.hum1`<br/>
Patch available: no released package fix established; proposed patch <br/>
included below<br/>
Version fixed: unknown<br/>
Upstream coordination: Not notified.<br/>
CVSS: CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:H - 6.8 (MEDIUM)<br/>
AV:L - Exploitation requires local execution of a binary using this <br/>
loader path.<br/>
AC:H - Triggering requires `AT_SECURE`, a leading `$ORIGIN` <br/>
`DT_RPATH`/`DT_RUNPATH`, and favorable adjacent memory contents.<br/>
PR:N - No prior privileges are required beyond running the target binary.<br/>
UI:N - No separate victim interaction is required once the attacker <br/>
invokes the target.<br/>
S:U - The impact is within the same process and security scope.<br/>
C:L - Out-of-bounds access may expose a limited amount of process memory.<br/>
I:L - Memory corruption may alter process state, but reliable arbitrary <br/>
modification is not established.<br/>
A:H - The most directly supported outcome is a loader crash or process <br/>
termination.<br/>
Impact: Moderate. This is a real memory-corruption flaw in privileged <br/>
loader code, but exploitation is not easy and depends on narrow <br/>
circumstances: local execution, secure mode, a specific `$ORIGIN` path <br/>
form, and surrounding memory layout. The available evidence supports denial <br/>
of service and limited memory corruption more clearly than easy, reliable <br/>
privilege escalation. Under Red Hat's guidance, that fits Moderate rather <br/>
than Important or Critical because the issue is harder to exploit and <br/>
appears configuration-dependent.<br/>
Embargo: no<br/>
Reason: The issue is local, configuration-sensitive, and the available <br/>
evidence does not show an easy or reliably weaponizable <br/>
privilege-escalation path. A minimal fix is straightforward and public <br/>
exposure risk appears limited.<br/>
Acknowledgement: Aisle Research<br/>
Vulnerability Details: In `elf/dl-load.c`, `_dl_dst_substitute` performs <br/>
trusted-path validation before it terminates the expanded path buffer:<br/>
```c<br/>
if (__glibc_unlikely (check_for_trusted)<br/>
&& !is_trusted_path_normalize (result, wp - result))<br/>
{<br/>
*result = '\0';<br/>
return result;<br/>
}<br/>
*wp = '\0';<br/>
```<br/>
`is_trusted_path_normalize` receives `len = wp - result`, allocates `len + <br/>
2` bytes on the stack, but iterates until a sentinel NUL byte instead of <br/>
enforcing the supplied length:<br/>
```c<br/>
if (len == 0)<br/>
return false;<br/>
char *npath = (char *) alloca (len + 2);<br/>
char *wnp = npath;<br/>
while (*path != '\0')<br/>
{<br/>
...<br/>
*wnp++ = *path++;<br/>
}<br/>
```<br/>
Observed fact: the call order is as shown above, so `result` is not yet <br/>
NUL-terminated when `is_trusted_path_normalize` starts walking it. <br/>
Reasonable inference: if bytes immediately following the intended string do <br/>
not contain `'\0'`, the copy loop can continue beyond the logical end of <br/>
`result` and then write beyond `npath`, causing stack corruption. The <br/>
provided materials clearly support out-of-bounds access and crash <br/>
potential; they do not establish a working arbitrary code-execution or <br/>
privilege-escalation exploit.<br/>
Steps to reproduce:<br/>
1. Build a test environment using `glibc-2.42-11.1.hum1` with ASan/UBSan <br/>
and debug symbols if possible.<br/>
2. Create a small setuid-root or setuid/setgid test executable whose <br/>
`DT_RUNPATH` or `DT_RPATH` starts with `$ORIGIN/`.<br/>
3. Place and execute it so the loader expands that entry under `AT_SECURE`, <br/>
reaching `_dl_dst_substitute` with `check_for_trusted` enabled.<br/>
4. Run repeatedly with varied allocator state to reduce the chance of an <br/>
accidental early NUL byte immediately after the expanded string.<br/>
5. Observe out-of-bounds access in `is_trusted_path_normalize`, <br/>
particularly around the `while (*path != '\0')` copy loop.<br/>
Mitigation: Avoid privileged executables whose `DT_RPATH` or `DT_RUNPATH` <br/>
begins with `$ORIGIN`; prefer absolute trusted library directories instead. <br/>
Where a binary does not need privileged execution, removing the <br/>
setuid/setgid bit avoids the secure-execution path that reaches this check.<br/>
Proposed Fix: NUL-terminate `result` before calling <br/>
`is_trusted_path_normalize`. As additional hardening, <br/>
`is_trusted_path_normalize` can be updated to enforce `len` as a strict <br/>
bound.<br/>
```diff<br/>
diff --git a/elf/dl-load.c b/elf/dl-load.c<br/>
— a/elf/dl-load.c<br/>
+++ b/elf/dl-load.c<br/>
@@ -352,14 +352,14 @@ _dl_dst_substitute (struct link_map *l, const char <br/>
*input, char *result)<br/>
checked for trust, the authors of the binaries themselves are<br/>
trusted to have designed this correctly. Only $ORIGIN is tested in<br/>
this way because it may be manipulated in some ways with hard<br/>
links. */<br/>
+ *wp = '\0';<br/>
+<br/>
if (__glibc_unlikely (check_for_trusted)<br/>
&& !is_trusted_path_normalize (result, wp - result))<br/>
{<br/>
*result = '\0';<br/>
return result;<br/>
}<br/>
-<br/>
*wp = '\0';</p>
<p>return result;<br/>
}<br/>
```<br/>
------<br/>
This report was generated using AI technology. Always review AI-generated <br/>
content prior to use</p>