Czy dobrym pomysłem jest wywoływanie poleceń powłoki z poziomu C?

50

Istnieje polecenie powłoki unix ( udevadm info -q path -n /dev/ttyUSB2), które chcę wywołać z programu C. Mając zapewne około tydzień walki, mógłbym sam go wdrożyć, ale nie chcę tego robić.

Czy ogólnie przyjętą dobrą praktyką jest po prostu dzwonienie popen("my_command", "r");, czy też spowoduje to niedopuszczalne problemy z bezpieczeństwem i przekaże problemy ze zgodnością? Robienie czegoś takiego jest dla mnie złe, ale nie mogę wskazać, dlaczego byłoby to złe.

Johnny Boy
źródło
8
Jedną rzeczą do rozważenia są ataki iniekcyjne.
MetaFight
8
Głównie myślałem o przypadku, w którym podałeś dane wejściowe użytkownika jako argumenty poleceń powłoki.
MetaFight
8
Czy ktoś może umieścić złośliwy udevadmplik wykonywalny w tym samym katalogu co twój program i użyć go do eskalacji uprawnień? Nie wiem dużo o bezpieczeństwie unixowym, ale czy twój program będzie działał setuid root?
Andrew Piliser,
7
@AndrewPiliser Jeśli bieżący katalog nie znajduje się w PATH, to nie - trzeba go uruchomić ./udevadm. IIRC Windows będzie jednak uruchamiał pliki wykonywalne tego samego katalogu.
Izkata
4
O ile to możliwe, należy wywołać żądany program bezpośrednio, bez przechodzenia przez powłokę.
CodesInChaos

Odpowiedzi:

58

Nie jest to szczególnie złe, ale są pewne zastrzeżenia.

  1. jak przenośne będzie twoje rozwiązanie? Czy wybrany plik binarny będzie wszędzie działał tak samo, generując wyniki w tym samym formacie itp.? Czy wyjdzie inaczej w ustawieniach LANGitp.?
  2. ile dodatkowego obciążenia to dodaje do twojego procesu? Rozwidlenie pliku binarnego powoduje o wiele większe obciążenie i wymaga więcej zasobów niż wykonywanie wywołań bibliotecznych (ogólnie). Czy jest to dopuszczalne w twoim scenariuszu?
  3. Czy są problemy z bezpieczeństwem? Czy ktoś może zastąpić twój wybrany plik binarny innym, a potem dokonać niegodziwych czynów? Czy używasz argumentów dostarczonych przez użytkownika do pliku binarnego i czy mogą one dostarczać ;rm -rf /(na przykład) (zwróć uwagę, że niektóre interfejsy API pozwalają na bezpieczniejsze określanie argumentów niż podawanie ich w wierszu poleceń)

Generalnie z przyjemnością wykonuję pliki binarne, gdy jestem w znanym środowisku, które mogę przewidzieć, kiedy dane binarne można łatwo przeanalizować (w razie potrzeby - może być potrzebny kod zakończenia) i nie muszę tego też robić często.

Jak już zauważyłeś, innym problemem jest to, ile pracy wymaga odtworzenie tego, co robi plik binarny? Czy korzysta z biblioteki, którą możesz wykorzystać?

Brian Agnew
źródło
13
Forking a binary results in a lot more load and requires more resources than executing library calls (generally speaking). Gdyby tak było w systemie Windows, zgodziłbym się. Jednak OP określa Unix, gdzie rozwidlenie jest na tyle niskie, że trudno powiedzieć, czy dynamiczne ładowanie biblioteki jest gorsze niż rozwidlenie.
wallyk
1
Normalnie oczekiwałbym, że biblioteka zostanie załadowana raz , podczas gdy plik binarny może być wykonywany wiele razy. Jak zawsze, zależy to od scenariuszy użytkowania, ale należy wziąć to pod uwagę (jeśli zostanie następnie odrzucone)
Brian Agnew,
3
W systemie Windows nie ma „rozwidlenia”, więc cytat musi być specyficzny dla Uniksa.
Cody Gray
5
Jądro NT obsługuje rozwidlanie, chociaż nie jest ujawniane przez Win32. W każdym razie uważam, że koszt uruchomienia zewnętrznego programu byłby większy execniż z fork. Ładowanie sekcji, łączenie w udostępnionych obiektach, uruchamianie kodu inicjującego dla każdego. A następnie trzeba przekonwertować wszystkie dane na tekst, który zostanie przeanalizowany po drugiej stronie, zarówno dla danych wejściowych, jak i wyjściowych.
spectras
8
Szczerze udevadm info -q path -n /dev/ttyUSB2mówiąc , i tak nie będzie działać na Windowsie, więc cała rozwidlona dyskusja jest nieco teoretyczna.
MSalters
37

Kiedy wprowadzasz potencjalny wektor, należy zachować szczególną ostrożność, aby uchronić się przed podatnością na wstrzyknięcia. To jest teraz w twojej głowie, ale później możesz potrzebować możliwości wyboru ttyUSB0-3, wtedy ta lista będzie używana w innych miejscach, więc zostanie uwzględniona zgodnie z zasadą jednej odpowiedzialności, a następnie klient będzie musiał wprowadzić dowolne urządzenie na liście, a programista dokonujący tej zmiany nie będzie miał pojęcia, że ​​lista ostatecznie zostanie użyta w niebezpieczny sposób.

Innymi słowy, kod jakby najbardziej nieostrożny programista, którego znasz, wprowadza niebezpieczną zmianę w pozornie niezwiązanej części kodu.

Po drugie, dane wyjściowe narzędzi CLI nie są ogólnie uważane za stabilne interfejsy, chyba że dokumentacja je specjalnie oznaczy. Być może możesz na nich liczyć, że uruchomisz skrypt, który możesz rozwiązać samodzielnie, ale nie na coś, co wdrożysz u klienta.

Po trzecie, jeśli chcesz w łatwy sposób wyodrębnić wartość ze swojego oprogramowania, istnieje szansa, że ​​ktoś też tego chce. Poszukaj biblioteki, która już robi to, co chcesz. libudevzostał już zainstalowany w moim systemie:

#include <libudev.h>
#include <sys/stat.h>
#include <stdio.h>

int main(int argc, char* argv[]) {
    struct stat statbuf;

    if (stat("dev/ttyUSB2", &statbuf) < 0)
        return -1;
    struct udev* udev = udev_new();
    struct udev_device *dev = udev_device_new_from_devnum(udev, 'c', statbuf.st_rdev);

    printf("%s\n", udev_device_get_devpath(dev));

    udev_device_unref(dev);
    udev_unref(udev);
    return 0;
}

W tej bibliotece znajdują się inne przydatne funkcje. Domyślam się, że jeśli potrzebujesz tego, niektóre pokrewne funkcje również mogą się przydać.

Karl Bielefeldt
źródło
2
DZIĘKUJĘ CI. Tak długo szukałem libudev. Po prostu nie mogłem tego znaleźć!
johnny_boy
2
Nie rozumiem, w jaki sposób „luki w zabezpieczeniach” są tutaj problemem, chyba że program OP jest wywoływany, gdy inny użytkownik ma kontrolę nad swoim środowiskiem, co ma miejsce przede wszystkim w przypadku suid (również prawdopodobnie, ale w mniejszym stopniu, takich jak cgi i ssh wymuszone polecenie). Nie ma powodu, dla którego normalne programy muszą być zabezpieczone przed użytkownikiem, na którym działają.
R ..
2
@R ..: „Luka w zabezpieczeniach związana z wstrzykiwaniem” występuje podczas generalizowania ttyUSB2i używania sprintf(buf, "udevadm info -q path -n /dev/%s", argv[1]). Teraz, gdy pojawia się dość bezpieczny, ale co jeśli argv[1]jest "nul || echo OOPS"?
MSalters
3
@R ..: potrzebujesz tylko wyobraźni. Najpierw jest to ustalony ciąg, potem argument dostarczony przez użytkownika, następnie argument dostarczony przez inny program uruchamiany przez użytkownika, ale którego nie rozumieją, to argument, który ich przeglądarka wyodrębniła ze strony internetowej. Jeśli wszystko idzie w dół, to w końcu ktoś musi wziąć odpowiedzialność za zdezynfekowanie danych wejściowych, a Karl mówi, że niezwykle dokładna jest identyfikacja tego punktu, gdziekolwiek by on nie przyszedł.
Steve Jessop,
6
Chociaż jeśli zasadą ostrożności naprawdę było „założenie, że najbardziej nieostrożny programista, o którym wiesz, dokonuje niebezpiecznej zmiany”, a ja musiałem teraz napisać kod, aby zagwarantować, że nie może to spowodować exploita, to osobiście nie mogłem wstać z łóżka. Najbardziej nieostrożni programiści, których znam, sprawiają, że Machievelli wygląda, jakby nawet nie próbował .
Steve Jessop,
16

W konkretnym przypadku, w którym chcesz się wywołać udevadm, podejrzewam, że możesz pobrać się udevjako biblioteka i wykonać odpowiednie wywołania funkcji jako alternatywę?

np. możesz rzucić okiem na to, co robi sam udevadm podczas wywoływania w trybie „informacji”: https://github.com/gentoo/eudev/blob/master/src/udev/udevadm-info.c i wykonywać połączenia equiv co do tych, które robi udevadm.

Pozwoliłoby to uniknąć wielu kompromisów wymienionych w doskonałej odpowiedzi Briana Agnewa - np. Nie polegając na pliku binarnym istniejącym na określonej ścieżce, unikając kosztów rozwidlenia itp.

Adam Krouskop
źródło
Dzięki, znalazłem to dokładne repo i skończyłem przeglądać je przez chwilę.
johnny_boy
1
… A libudev nie jest szczególnie trudny w użyciu. Trochę brakuje jego obsługi błędów, ale wyliczanie, zapytania i monitorowanie interfejsów API są dość proste. Walka, którą strach może okazać się krótszy niż 2 godziny, jeśli spróbujesz.
spectras
7

Twoje pytanie wymagało leśnej odpowiedzi, a odpowiedzi tutaj wydają się podobne do drzew, więc pomyślałem, że dam ci leśną odpowiedź.

Bardzo rzadko pisane są programy C. Zawsze tak pisane są skrypty powłoki, a czasem pisane są programy Python, Perl lub Ruby.

Ludzie zwykle piszą w języku C, aby ułatwić korzystanie z bibliotek systemowych i bezpośredni dostęp do wywołań systemowych na niskim poziomie, a także szybkość. Język C jest trudny do napisania, więc jeśli ludzie nie potrzebują tych rzeczy, wówczas nie używają języka C. Oczekuje się również, że programy w języku C będą zależały tylko od bibliotek współdzielonych i plików konfiguracyjnych.

Ucieczka do podprocesu nie jest szczególnie szybka i nie wymaga drobnoziarnistego i kontrolowanego dostępu do obiektów systemowych niskiego poziomu, i wprowadza potencjalnie zaskakującą zależność od zewnętrznego pliku wykonywalnego, więc nie jest rzadkością w programach C.

Istnieją pewne dodatkowe obawy. Bezpieczeństwo i przenośność dotyczą osób, które wspominają, są całkowicie ważne. Oczywiście są one równie ważne dla skryptów powłoki, ale ludzie oczekują tego rodzaju problemów w skryptach powłoki. Jednak zwykle nie oczekuje się, że programy typu C będą miały tę klasę bezpieczeństwa, co czyni je bardziej niebezpiecznymi.

Ale moim zdaniem największe obawy dotyczą sposobu popeninterakcji z resztą twojego programu. popenmusi utworzyć proces potomny, odczytać jego dane wyjściowe i zebrać status wyjścia. W międzyczasie stderr tego procesu zostanie podłączony do tego samego stderr co twój program, co może powodować mylące wyjście, a jego stdin będzie taki sam jak twój program, co może powodować inne interesujące problemy. Możesz rozwiązać ten problem, dołączając </dev/null 2>/dev/nullciąg, który przekazujesz, popenponieważ jest on interpretowany przez powłokę.

I popentworzy proces potomny. Jeśli zrobisz coś z obsługą sygnałów lub rozwidlaniem procesów, możesz otrzymać dziwne SIGCHLDsygnały. Twoje wezwania do waitmogą dziwnie współdziałać popeni stwarzać dziwne warunki wyścigowe.

Istnieją oczywiście obawy dotyczące bezpieczeństwa i przenośności. Tak jak w przypadku skryptów powłoki lub czegokolwiek, co uruchamia inne pliki wykonywalne w systemie. I musisz uważać, aby ludzie używający twojego programu nie byli w stanie wprowadzić meta-znaków w powłoce do łańcucha, który przekazujesz, popenponieważ ten łańcuch jest przekazywany bezpośrednio za shpomocą sh -c <string from popen as a single argument>.

Ale nie sądzę, że są, dlaczego to jest dziwne, aby zobaczyć program C przy użyciu popen. To dziwne, ponieważ C jest zwykle językiem niskiego poziomu, a popennie niskim poziomem. A ponieważ użycie popenograniczeń projektowych ogranicza Twój program, ponieważ dziwnie współdziała ze standardowym wejściem i wyjściem Twojego programu i utrudnia zarządzanie procesami lub obsługę sygnałów. A ponieważ zwykle nie oczekuje się, że programy C będą miały zależności od zewnętrznych plików wykonywalnych.

Wszelaki
źródło
0

Twój program może podlegać atakom hakerskim itp. Jednym ze sposobów ochrony przed tego rodzaju działaniami jest utworzenie kopii bieżącego środowiska komputera i uruchomienie programu przy użyciu systemu o nazwie chroot.

Z punktu widzenia twojego programu jego działanie odbywa się w normalnym środowisku, z punktu widzenia bezpieczeństwa, jeśli ktoś złamie twój program, ma on dostęp tylko do elementów, które podałeś podczas tworzenia kopii.

Taka konfiguracja nazywa się więzieniem chroot, aby uzyskać więcej informacji, zobacz więzienie chroot .

Zwykle służy do konfigurowania publicznie dostępnych serwerów itp.

Dave
źródło
3
OP pisze program do uruchomienia dla innych ludzi, nie bawiąc się niezaufanymi plikami binarnymi lub źródłowymi we własnym systemie.
Peter Cordes,