Krótkie wprowadzenie do bibliotek w systemie Linux

nfsec.pl 9 miesięcy temu

W

programowaniu biblioteka jest zbiorem wstępnie skompilowanych fragmentów kodu. Bibliotekę programistyczną możemy ponownie wykorzystać w różnych programach, które mają mieć zaimplementowane obsługę tych samych zadań. Są bardzo przydatne, ponieważ zapewniają funkcje, klasy i struktury danych wielokrotnego użytku. Niektóre przykłady bibliotek w systemie Linux to glibc (wersja GNU standardowej biblioteki C), libc (standardowa biblioteka C). Biblioteki możemy podzielić na dwie główne kategorie: biblioteki statyczne (ang. static libraries) – związane statycznie z programem w czasie kompilacji; biblioteki współdzielone (ang. shared libraries) – ładowane podczas uruchamiania programu i ładowane do pamięci w czasie wykonywania.

Biblioteki statyczne:

Biblioteki te są „zabetonowane” w programie w czasie kompilacji. Mówi się również o nich jako biblioteki połączone statycznie. Po włączeniu biblioteki statycznej do programu podczas procesu kompilacji staje się ona integralną częścią wynikowego, samodzielnego pliku wykonywalnego:

[ program.c ] statycznie połączony z biblioteką [ biblioteka.a ]
– daje wynik w postaci tylko: [ program.out ]

[ biblioteka.a ] -> [ program.out #1 ] [ biblioteka.a ] -> [ program.out #2 ]

Biblioteki statyczne mają rozszerzenie „.a”, gdzie jest to skrót od „archive” (pol. archiwum). Programy z bibliotekami statycznymi mają znacznie większy rozmiar, ale ich czas wykonywania jest znacznie szybszy, ponieważ zestaw powszechnie używanych plików obiektowych i ich funkcji jest umieszczony w pojedynczym pliku wykonywalnym. Wadą korzystania z bibliotek statycznych, jest fakt, iż kod używany do budowania z ich udziałem jest zablokowany w końcowym pliku wykonywalnym. Dlatego jakakolwiek modyfikacja biblioteki wymaga ponownej kompilacji programu.

Przykład stworzenia i wykorzystania statycznej biblioteki (intruder.c) w programie zaczniemy od stworzenia pliku źródłowego zawierającego prostą funkcję wyświetlającą napis:

#include <stdio.h> void message_to_victim() { printf("Your decryption key is: addf120b430021c36c232c99ef8d926aea2acd6b\n"); }

Drugim krokiem jest stworzenie pliku nagłówkowego (intruder.h) dla naszej biblioteki, który będzie zawierał wszystkie prototypy funkcji:

void message_to_victim();

W celu stworzenia statycznej biblioteki, musimy przekazać kompilatorowi (tutaj: gcc), iż chcemy skompilować kod biblioteki (intruder.c) do pliku obiektowego (intruder.o) bez linkowania (-c):

agresor@darkstar:~$ gcc -c -Wall -Werror -Wextra intruder.c agresor@darkstar:~$ file intruder.o intruder.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

Podczas kompilacji programu w języku C kompilator generuje kod obiektowy. Jest on zawarty w pliku obiektowym (w naszym przypadku jest to plik intruder.o). Następnie kompilator wywołuje również konsolidator (linker) – jego głównym zadaniem jest udostępnienie programowi kodu funkcji z bibliotek (np. message_to_victim()) dla tworzonego programu. To zadanie może być wykonane na dwa sposoby: kopiując kod funkcji z bibliotek do kodu wynikowego (statycznie) lub dokonać pewnego układu, aby pełny kod funkcji z bibliotek nie był kopiowany do kodu, ale był udostępniony w czasie jego wykonywania (dynamicznie). Gdy mamy już nasz plik obiektowy, możemy go teraz spakować jako bibliotekę statyczną (można też wiele plików obiektowych pakować do jednej biblioteki). W celu utworzenia biblioteki statycznej lub dodania dodatkowych plików obiektowych do istniejącej biblioteki musimy użyć programu GNU ar (od archiver – pol. archiwizator):

agresor@darkstar:~$ ar rcs libintruder.a intruder.o agresor@darkstar:~$ file libintruder.a libintruder.a: current ar archive agresor@darkstar:~$ echo *intruder* intruder.c intruder.h intruder.o libintruder.a

Nasza biblioteka statyczna jest gotowa do użycia! jeżeli chcemy zobaczyć zawartość naszej biblioteki możemy użyć polecenia: ar -t. jeżeli interesują nas symbole możemy użyć polecenia: nm, które wyświetla: nazwę symboli, wartość symboli i typ symboli z plików obiektowych:

agresor@darkstar:~$ ar -t libintruder.a intruder.o agresor@darkstar:~$ nm libintruder.a intruder.o: 0000000000000000 T message_to_victim U puts

Na potrzeby tej demonstracji nie będziemy kopiować jeszcze naszej biblioteki do /usr/local/lib tylko zatrzymamy ją w naszym katalogu domowym. Stwórzmy teraz program testowy (ransomware.c), który skorzysta z utworzonej powyżej biblioteki:

#include "intruder.h" void main() { message_to_victim(); }

Połączmy teraz skompilowany program testowy z biblioteką statyczną:

agresor@darkstar:~$ gcc ransomware. c -L. -lintruder -o ransomware agresor@darkstar:~$ file ransomware ransomware: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV) agresor@darkstar:~$ nm ransomware | grep message_to_victim 000000000000115e T message_to_victim agresor@darkstar:~$ readelf -s ransomware | grep intruder 12: 0000000000000000 0 FILE LOCAL DEFAULT ABS intruder.c agresor@darkstar:~$ ./ransomware Your encryption key is: addf120b430021c36c232c99ef8d926aea2acd6b

Biblioteki współdzielone:

To biblioteki, które istnieją jako osobne pliki poza plikami wykonywalnymi, nazywane są bibliotekami współdzielonymi. Zaletą korzystania z biblioteki współdzielonej jest to, iż jedna biblioteka może być używana przez wiele aplikacji bez konieczności posiadania przez każdą aplikację własnej kopii biblioteki, jak to ma miejsce w przypadku bibliotek statycznych. Plik wykonywalny ma mniejszy rozmiar. Biblioteki współdzielone są dołączane w czasie wykonywania programu, dlatego nie wymagają ponownej kompilacji programu, gdy programista dokona w nich zmiany. Wadą bibliotek współdzielonych jest to, iż szanse na ich uszkodzenie są znacznie większe niż w przypadku bibliotek statycznych. Pliki z rozszerzeniem „.so” to nic innego jak biblioteki współdzielone, a sufiks SO oznacza „shared object” (pol. współdzielony obiekt).

[ program.c ] dynamicznie połączony z biblioteką [ biblioteka.so ]
– daje wynik w postaci: [ program.out ] oraz [ biblioteka.so ]

[ program.out #1 ] <- [ biblioteka.so ] -> [ program.out #2 ]

W celu stworzenia współdzielonej biblioteki, musimy cofnąć się do kroku, gdzie posiadamy już plik obiektowy intruder.o. Następnym ruchem będzie stworzenie współdzielonej biblioteki zamiast archiwum, jak to miało miejsce w przypadku biblioteki statycznej:

agresor@darkstar:~$ rm libintruder.a agresor@darkstar:~$ gcc intruder.o -shared -o libintruder.so

Konwencja nazewnictwa bibliotek współdzielonych jest taka, iż każda nazwa biblioteki współdzielonej musi zaczynać się od prefiksu lib i kończyć na sufiksie .so. Teraz podlinkujemy dynamicznie stworzoną bibliotekę to naszego programu:

agresor@darkstar:~$ gcc ransomware.c -L. -lintruder -o ransomware_so agresor@darkstar:~$ ./ransomware_so ./ransomware_so: error while loading shared libraries: libintruder.so: cannot open shared object file: No such file or directory agresor@darkstar:~$ sudo cp libintruder.so /usr/local/lib/ agresor@darkstar:~$ sudo ldconfig agresor@darkstar:~$ ./ransomware_so Your encryption key is: addf120b430021c36c232c99ef8d926aea2acd6b

lub

agresor@darkstar:~$ gcc ransomware.c -L. -lintruder -o ransomware_so agresor@darkstar:~$ ./ransomware_so ./ransomware_so: error while loading shared libraries: libintruder.so: cannot open shared object file: No such file or directory agresor@darkstar:~$ export LD_LIBRARY_PATH=/home/agresor agresor@darkstar:~$ ./ransomware_so Your encryption key is: addf120b430021c36c232c99ef8d926aea2acd6b

ld.so, ldd oraz LD_LIBRARY_PATH:

Dlaczego przy pierwszej próbie uruchomienia programu ransomware_so otrzymaliśmy błąd, iż nie może on znaleźć naszej współdzielonej biblioteki libintruder.so? jeżeli spojrzymy na FHS Filesystem Hierarchy Standard definiujący drzewiastą strukturę katalogów w systemie Linux to dowiemy się, iż biblioteki są trzymane w standardowych katalogach: /lib, /usr/lib oraz /usr/local/lib. Katalog /lib zawiera biblioteki używane podczas uruchamiania systemu, ale także używane przez programy z katalogu /bin. Podobnie katalog /usr/lib zawiera biblioteki używane przez programy z katalogu /usr/bin. Wreszcie, katalog /usr/local/lib zawiera biblioteki używane przez programy z katalogu /usr/local/bin. Dlatego uruchamiany program na początku sprawdzi te standardowe ścieżki, aby dowiedzieć czy zainstalowana jest określona biblioteka współdzielona.

Defacto, nie program, ale dynamiczny linker (ld.so) w czasie wykonywania programu. Konsolidator (linker) przeszukuje ścieżki wymienione w pliku konfiguracyjnym /etc/ld.so.conf. W dystrybucji Ubuntu plik ten zawiera pojedynczą linię odnoszącą się do innego katalogu z konfiguracją:

agresor@darkstar:~$ cat /etc/ld.so.conf include /etc/ld.so.conf.d/*.conf agresor@darkstar:~$ cat /etc/ld.so.conf.d/*.conf /usr/lib/x86_64-linux-gnu/libfakeroot # libc default configuration /usr/local/lib # Multiarch support /usr/local/lib/x86_64-linux-gnu /lib/x86_64-linux-gnu /usr/lib/x86_64-linux-gnu

Przeszukiwanie ścieżek zdefiniowanych w pliku /etc/ld.so.conf mogłoby opóźnić wykonywanie programów. Dlatego system Linux używa pliku konfiguracyjnego /etc/ld.so.cache, który buforuje listę wszystkich bibliotek współdzielonych i ich lokalizację w systemie. To dlatego po przekopiowaniu biblioteki libintruder.so do katalogu /usr/local/lib/ zostało wydane polecenie ldconfig – ponieważ buduje ono tę pamięć podręczną. Należy pamiętać, iż choćby jeżeli plik biblioteki istnieje w jednej ze ścieżek pliku konfiguracyjnego ld.so.conf, przez cały czas nie będzie używany, jeżeli pamięć podręczna go nie zarejestrowała. Aby dowiedzieć się, czy biblioteka współdzielona jest zarejestrowana, możemy użyć polecenia ldconfig z opcją -p:

agresor@darkstar:~$ ldconfig -p | grep intruder libintruder.so (libc6,x86-64) => /usr/local/lib/libintruder.so

Z kolei jeżeli chcemy wyświetlić biblioteki współdzielone wymagane przez program możemy użyć skryptu powłoki ldd:

agresor@darkstar:~$ ldd ransomware_so linux-vdso.so.1 (0x00007ffe643da000) libintruder.so => /usr/local/lib/libintruder.so (0x00007fe7009c5000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe70079d000) /lib64/ld-linux-x86-64.so.2 (0x00007fe7009d7000)

Należy jednak używać go z zachowaniem ostrożności, ponieważ narzędzie to może uruchomić program w celu uzyskania listy bibliotek. Nigdy nie powinniśmy uruchamiać polecenia ldd na niezaufanych plikach wykonywalnych. Innym poleceniem jakiego możemy użyć to objdump, które wyświetla informacje z plików obiektowych:

agresor@darkstar:~$ objdump -p ransomware_so | grep 'NEEDED' NEEDED libintruder.so NEEDED libc.so.6

Jeśli porównamy powyższe dane wyjściowe z danymi wyjściowymi polecenia ldd to zobaczymy, iż pierwsze narzędzie wyświetla znacznie więcej bibliotek niż drugie. Dzieje się tak, ponieważ polecenie objdump zrzuca to, co sam program wymienia jako biblioteki. Skrypt ldd natomiast podąża za topologią programu i wypisuje biblioteki, które ld.so załaduje. Dlatego w porównaniu z poleceniem objdump daje znacznie lepszy obraz tego, co musi być dostępne w czasie wykonywania programu. Podobnym poleceniem do objdump jest readelf – wyświetla informacje o plikach obiektowych w formacie ELF:

agresor@darkstar:~$ readelf --dynamic ransomware_so | grep 'NEEDED' 0x0000000000000001 (NEEDED) Shared library: [libintruder.so] 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]

Drugim sposobem na wskazanie, gdzie linker ma szukać naszych bibliotek jest użycie zmiennej środowiskowej LD_LIBRARY_PATH. Zmienna ta może zawierać niestandardowe ścieżki do bibliotek rozdzielone dwukropkami – wówczas konsolidator najpierw przeszuka katalogi zdefiniowane w tej zmiennej, a dopiero później te znajdujące się w pliku konfiguracyjnym /etc/ld.so.conf:

export LD_LIBRARY_PATH=/home/agresor:/opt/libs

Oprócz zmiennej LD_LIBRARY_PATH istnieje jeszcze LIBRARY_PATH – ale jest ona używana przez kompilator gcc przed procesem kompilacji do wyszukiwania katalogów zawierających biblioteki statyczne i dynamiczne, które muszą być połączone z naszym programem.

Więcej informacji: Libraries: Static or shared?, All you need to know about C Static libraries, Solving The “Cannot Open Shared Object File: No Such File Or Directory” Error In Linux, LD_LIBRARY_PATH vs LIBRARY_PATH

Idź do oryginalnego materiału