Zdjęcie, które ilustruje ten wpis wymaga słowa komentarza. Przedstawia właz do ulicznego kanału, po angielsku – manhole.
manhole – a covered opening in a road that a worker can enter in order to reach underground pipes, wires, or drains that need to be examined or repaired – Cambridge Dictionary
Jak to się ma zaglądania w bebechy działającej aplikacji przekonasz się czytając kolejne akapity.
Podejrzenie zawartości zmiennych w działającym programie bywa nieocenioną pomocą, gdy debuggujemy błędy – czytaj niepożądane feature’y. W środowisku developerskim są w zasadzie trzy możliwe podejścia. Po pierwsze tak zwany dupa debugging, gdzie wypisujemy nazwę miejsca, w którym plecy tracą swoją szlachetną nazwę razem z danymi, które chcemy poznać. Dupa, najczęściej z dołączonym numerkiem pozwala łatwo namierzyć miejsce w programie, które badamy – szczególnie, gdy robi się ich kilkanaście.
O wiele czystszą metodą będą testy, jednakże warunkiem koniecznym jest posiadanie ładnego, testowalnego kodu (wszyscy taki mamy, prawda? 😀 ). Ostatnią z technik jest skorzystanie z programistycznej zabawki, debuggera. Wymaga odpowiednich narzędzi, ale bez problemu znajdą się darmowe odpowiedniki. W skrócie – oznaczamy miejsce w kodzie, które nas interesuje zastawiając tak zwaną pułapkę. W chwili gdy egzekucja kodu przechodzi przez oznaczoną linię program zostaje wstrzymany, a my możemy podglądać i ingerować w zawartość zmiennych.
To już coś. Swoją drogą, prezentowane zrzuty pochodzą z Pycharm’a, a podstawy podstaw obsługi debuggera można opanować dzięki temu filmikowi.
Debugger to niezwykle potężne narzędzie, które świetnie się sprawdza w środowisku developerskim. W ŚRODOWISKU DEVELOPERSKIM. Natomiast wszystkie fajne problemy dzieją się w wiadomym miejscu – na produkcji!
Można wprawdzie salwować się zdalnym debuggerem (PyCharm też to ma!), ale takie podejście ma dwie poważne wady:
- aplikacja pod debuggerem działa wolniej
- wejście w pułapkę (ang. breakpoint) spowoduje zatrzymanie programu. Najczęściej nie można na to pozwolić.
Nie wspominając o tym, że trzeba przecież aplikację zrestartować i liczyć, że znowu wpadnie w niepożądany stan. Z tej perspektywy dupa debugging zaczyna wyglądać jak niezłe rozwiązanie…
Właz konserwacyjny dla programistów
Wracając do samego początku artykułu – istnieje jeszcze inne rozwiązanie znane pod nazwą ‚manhole’ i dosłownie oznacza właz konserwacyjny – tylko że w tym przypadku dla programisty. Odpowiednia paczka pod tą samą nazwą jest dostępna na PyPI, zainstalować można ją standardowo pip’em:
1 |
pip install manhole |
Nie działa na Windowsie.
Ten właz prowadzi do interaktywnej konsoli Pythona działającej w kontekście naszego programu i umożliwia nam podglądanie i manipulację udostępnionych mu danych. Manhole ‚wciska się’ w program jako osobny wątek. Aby zademonstrować działanie, posłużę się przykładem składającym się z prostego serwera HTTP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
import http.server import socketserver import manhole PORT = 8000 class Counter: value = 0 @classmethod def next_value(cls): old_value = cls.value cls.value += 1 return old_value class CountingHTTPServerRequestHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() raw_data = 'Counter: {}'.format(Counter.next_value()).encode() self.wfile.write(raw_data) httpd = socketserver.TCPServer(("", PORT), CountingHTTPServerRequestHandler) print("serving at port", PORT) manhole.install(activate_on='USR1', locals={'Counter': Counter}) httpd.serve_forever() |
Serwer HTTP nasłuchuje na porcie 8000 [linia 6.]. Kolejne odświeżenia strony powodują zwiększenie o 1 licznika odwiedzin, przechowywanego jako pole klasy Counter [9] o nazwie value [10] poprzez metodę klasy – next_value [13].
Wykorzystanie manhole jest w linii 32 – tutaj instruujemy bibliotekę, że ma się aktywować po otrzymaniu przez program sygnału SIGUSR1. Jeżeli pominiemy ten argument, to nasz właz zostanie otwarty razem z programem (w miejscu wywołania manhole.install()). Po otwarciu dostępu, możemy dobić się do konsoli poprzez unix socket – na przykład programem netcat:
1 |
nc -U /tmp/manhole-62596 |
Domyślnie unix socket jest umieszczany w katalogu /tmp z nazwą manhole-<pid procesu>. Jest to oczywiście konfigurowalne zachowanie (jeden z argumentów manhole.install() – socket_path odpowiada za nazwę). Potrzebny jest jeszcze pid procesu z odpalonym programem i można już wysłać rozkaz otwarcia włazu:
1 2 3 |
bash-3.2$ ps aux | grep count_up.py | grep -v grep spb 62596 0,0 0,1 2465220 15244 ?? S 10:06 0:00.49 /usr/local/bin/python3.5 /opt/projects/count_up.py bash-3.2$ kill -s USR1 62596 |
Drugi z wykorzystywanych w przykładzie nazwanych argumentów – locals określa słownik z nazwami, które będą dostępne w otwartej konsoli. W tym przypadku udostępniamy tylko klasę Counter pod tą samą nazwą. To wystarczy, by podglądać i wprowadzać modyfikacje do działającego kodu tego prostego serwerka.
1 2 3 4 5 6 7 8 9 10 |
Python 3.5.1 (v3.5.1:37a07cee5969, Dec 5 2015, 21:12:44) [GCC 4.2.1] Type "help", "copyright", "credits" or "license" for more information. (ManholeConsole) >>> Counter.value 0 >>> Counter.value = 25 >>> Counter.value 25 >>> |
Oczywiście podejrzenie wartości pola w klasie to pikuś, podobnie z podmianą. Korzystając z dynamicznej natury Pythona, można w ten sam sposób modyfikować zachowanie programu:
1 2 3 4 5 6 |
>>> def decrease(cls): ... old = cls.value ... cls.value -= 1 ... return old ... >>> Counter.next_value = classmethod(decrease) |
Od tej chwili kolejne odświeżenia strony będą zmniejszać licznik, zamiast go zwiększać. Z kreatywnych zastosowań do głowy przychodzi mi jeszcze wymuszenie przeładowania modułu poprzez usunięcie konkretnego wpisu z sys.modules, zresztą dokumentacja wspomina o takim zastosowaniu:
sys.modules: This is a dictionary that maps module names to modules which have already been loaded. This can be manipulated to force reloading of modules and other tricks.
A bezpieczeństwo?
Artykuł byłby niekompletny, gdyby nie poruszał kwestii związanych z bezpieczeństwem takiej zabawy. Po pierwsze, to dostęp do unix socket’u utworzonego przez manhole umożliwia egzekucję w zasadzie dowolnego kodu. Jeżeli proces działa z uprawnieniami użytkownika uprzywilejowanego, oznacza to potencjalnie poważne kłopoty.
Na szczęście systemy operacyjne umożliwiają wysyłanie sygnałów tylko do procesów, które należą do nas (działają z uprawnieniami naszego użytkownika) lub gdy to my działamy z roota. Tak więc bez uzyskania specjalnych uprawnień atakujący nie może otworzyć włazu. Widzę tu jednak potencjalny problem, gdy manhole zostanie otwarty i uprawnienia do unix socketu zostaną zmienione na bardziej liberalne, jak 0777. Wtedy, o zgrozo, każdy atakujący będzie mógł wykorzystać nasz program do wykonania kodu z uprawnieniami, jakich nie powinien dostać.
Pewnym zabezpieczeniem jest też filozofia dostępu przez unix socket – czyli możliwe tylko z tego samego serwera.
Ostatnie kwestie dotyczą samego programu. Po pierwsze, nie każda aplikacja może strawić to, że w jej ramach uruchomiliśmy dodatkowy wątek. Są rozwiązania, które używając monkey patchingu robią brzydkie rzeczy z modułem wątków w bibliotece standardowej. Tej samej techniki używa zresztą manhole.
Operacje modyfikujące dane z interaktywnej konsoli mogą być potencjalnie niebezpieczne i dawać nieoczekiwane rezultaty, szczególnie gdy dotykane zasoby będą zmieniane przez inne jednostki egzekucji (tj wątki). Jedyną w miarę bezpieczną operacją jest podglądanie zawartości zmiennych.
Zakończenie
Powyższy artykuł to tylko wierzchołek góry lodowej – najwięcej informacji można znaleźć na stronie pypi manhole, polecam lekturę szczególnie ze względu na listę podobnych rozwiązań oraz omówienie opcji konfiguracyjnych.
photo credit: visually_conscious Manhole in Dumbo via photopin (license)
Z tym debuggerem jest jeszcze taki problem że fakt iż on spowalnia działanie aplikacji uniemożliwia powtórzenie pewnych sytuacji typu race-condition. Co do bezpieczeństwa – nikt przy zdrowych zmysłach nie da 0777 na taki socket 🙂 chyba że lubi adrenalinę 😉 Tak czy siak – fajny wpis ;DDD