Jak oswoić Bota? – Requester

Powracamy do oswajania botów.

Hej, w tym wpisie chciałbym Wam zaproponować drugie rozwiązanie dotyczące pisania botów – wymagające odrobinę większej finezji przy projektowaniu niż przy poprzednim podejściu (jeżeli nie czytaliście mojego poprzedniego wpisu o botach to zapraszam [tutaj]).

Jednocześnie nie ukrywam, że pewną inspiracją do napisania tego bota są liczne pytania na forum pasji-informatyki dotyczące pewnej bazy danych pytań z pewnej strony w celach szkoleniowych (sytuacja jest nieco podobna do tej z naszej „fabuły”, patrz rozdział: Praktyka).

Disclaimer niniejszy wpis służy jedynie w celach edukacyjnych i autor nie ponosi odpowiedzialności za użycie zdobytej wiedzy w „niecnych” (ani żadnych innych) celach. Zdobytą tutaj wiedzę należy używać jedynie na aplikacjach, które są naszą własnością (lub mamy stosowne uprawnienia).

Trochę teorii


Zanim przejdziemy do praktyki to omówmy w dwóch zdaniach czym się charakteryzuje ten typ bota:

Otóż komunikuje się on ze stroną / aplikacją, już nie za pomocą emulacji tego co robi użytkownik (tak jak robił Clicker), lecz schodzi „warstwę niżej” i używa do tego zapytań webowych (ang. requests – stąd nazwa bota).

Aby móc używać requestów (tak aby rozumiała to aplikacja), to należy najpierw dowiedzieć się jak to robi aplikacja. Tutaj można albo przeczytać dokumentację, albo zapytać autora (jeżeli nim nie jesteśmy), albo jeżeli autor nie pracuje już w naszej pracy, nie zrobił dokumentacji, a źródła aplikacji są długie i zawiłe – to możemy wykonać samodzielną analizę.

Praktyka


Wstęp fabularny (różniący się nieco od inspirowanej aplikacji):

Jesteśmy uczniami jakiejś bliżej nieokreślonej szkoły, uczymy się do egzaminu do którego pytania (z zeszłych lat) udostępnia sama szkoła. Niefortunnie udostępnia je jedynie w formie formularza losującego jedno pytanie.

Co prawda można klikać dużo razy w przycisk „losuj”, lecz wydaje się to mało praktyczne (bo baza jest duża), szczególnie że chcemy mieć możliwość wydrukowania całego zestawu pytań w celu wielokrotnych powtórek na odpowiedzi do tych samych pytań.

Skoro pytania w bazie i tak są dostępne publicznie to postanawiamy napisać bota, który zrzuci wszystkie pytania za nas…

Przykładowe pytanie z aplikacji

Rekonesans

UWAGA Ta faza jest do pominięcia w przypadku gdy już wiemy jak działa aplikacja webowa (bo np. mamy dostęp do jej dokumentacji / jesteśmy jej autorem). W naszym przypadku zakładamy, że musimy się sami tego dowiedzieć i właśnie ten proces pokazuję w tym paragrafie.

Szybki rekonesans pozwala nam stwierdzić, że:

  • znamy dokładną ilość pytań w bazie (twórcy aplikacji chwalą się tym na stronie głównej quizu),
  • pytania są stałego formatu:

  • (prawdopodobnie) znamy ID pytania w bazie (wynika to z treści widocznej przy pytaniu).

Aby dowiedzieć się czegoś więcej, to należy gdzieś się „zaczaić” i podejrzeć co się dzieje gdy:

  1. losujemy pytanie,
  2. wybierzemy odpowiedź.

Losowanie pytania

W naszym przypadku w zupełności wystarczają narzędzia dostarczone przez same przeglądarki, a więc „Narzędzia deweloperskie”, które dostarczają informacji o wysyłanych zapytaniach, w niektórych sytuacjach lepszym / wygodniejszym rozwiązaniem może się okazać lokalne proxy.

Wysłane zapytanie przez przeglądarkę w celu uzyskania mojego bloga

Zobaczmy co się dzieje na kliknięcie przycisku „LOSUJ” w testowanej przez nas aplikacji:

Na podstawie powyższego requestu jesteśmy w stanie stwierdzić parę rzeczy:

  • skrypt losujący znajduje się w src/loadquestion.php oraz posiada jeden parametr wejściowy,
  • komunikacja odbywa się przez POST’y,
  • aplikacja posiada dwie warstwy „zabezpieczeń”: PHPSESSID oraz token.

Uruchomienie powyższego requestu parę razy (przepuszczając je dalej) upewnia nas, że token i id sesji PHP są stałe dla jednej sesji, co nas cieszy bo nie musimy się martwić o pobieranie nowego tokenu co nowe losowanie pytania.

Ponowne uruchomienie powyższego żądania, ale bez tokenu pokazuje że autor skryptu chciał się zabezpieczyć przed czymś (na pewno nie przed botem), bo token jest wymagany.

Pobieranie odpowiedzi

Przejdźmy do pobrania odpowiedzi:

Widzimy, że:

  • skrypt do pobierania odpowiedzi znajduje się w src/loadanswer.php,
  • wysyłamy do niego swój token sesji PHP,
  • przyjmuje 3 argumenty:
    • token – tak samo jak wyżej, ważna uwaga: jest on tożsamy z tym który posiadamy także przy pobieraniu pytania (podobnie jak PHPSESSID),
    • idp – zgadujemy, że to ID pytania (co sugeruje ten sam numer co występuje w zdaniu: „Wylosowałeś pytanie z bazy nr: [nr pytania]”),
    • odp – są to wartości od 1-4 odpowiadające literom odpowiednio A-D.

Sam szkielet odpowiedzi nie różni się zbytnio od tego z ramki zawierającej format pytania, otóż posiada ona gotowy kod HTML, który jest wstrzykiwany w zawartość strony. Jedyną nowością jest string informujący o sukcesie:

Wnioski

Podsumujmy powyższe informacje, wiemy że:

  1. Skrypty loadquestion.phploadanswer.php służą do ładowania pytania i odpowiedzi.
    1. Całkiem możliwe, że do enumeracji całej bazy pytań wystarczy nam jedynie drugi ze skryptów (bo po co je losować, skoro można je pobrać po kolei?).
  2. Występuje dość naiwne zabezpieczenie przed botami / osobami (w postaci tokenu), które manualnie wchodzą bezpośrednie na powyższe skrypty.
  3. Tokeny żyją długo, więc nie musimy ich pobierać co uruchomienie skryptu, ułatwia to całkiem sporo,
  4. Komunikacja (ważna dla nas) odbywa się tylko i wyłącznie przy pomocy requestu POST.

Proof of Concept

Zebraliśmy kilka wniosków, czas na weryfikację naszego podejrzenia z punktu 1.1.

Do tego z powodzeniem może nam posłużyć program curl wywołany następująco:

Jako zmienną idp proponuję podstawić dowolną wartość różną od ostatnio wylosowanego pytania (np jeżeli ostatnio wylosowaliśmy pytanie nr 3, to spróbujmy pobrać odpowiedź na pytanie nr 7).

Niestety, napotykamy tutaj mały problem:

Wniosek nasuwa się sam: autorzy porównują id ostatnio wylosowanego pytania z tym co my podajemy jako argument. Jeżeli są różne, to wyskakuje powyższy błąd.

Ta ochrona ma zapobiegać uruchamiania strony na wielu kartach z jaśniej niezrozumiałych powodów.

Powyższy kod co prawda pokazuje, że będzie trudniej, ale nas jeszcze nie eliminuje bo wciąż możemy wyciągnąć bazę chociaż nieco większym nakładem sił.

Oswajamy bota


Do tej pory przedstawiłem jak można poznać sposób działania aplikacji, zobaczyliśmy też że nie nie pobierzemy wszystkich pytań po kolei, musimy dostosować się do tego jak działają powyższe skrypty.

Algorytm

Ogólny algorytm może wyglądać następująco:

  1. (opcjonalnie) Wejdź na stronę w celu pobrania PHPSESSID i tokena (można też zrobić to ręcznie, po czym wkleić te wartości).
  2. Niech M – hash-mapa postaci, M[idp] – informacje o pytaniu (treść, odpowiedzi, poprawna odpowiedź) o wskazanym ID pytania.
  3. Niech |Q| – ilość wszystkich pytań w bazie, |M| – ilość pobranych pytań do mapy
  4. Uruchom skrypt loadquestion:
    1. Pobierz ID pobranego pytania, jeżeli istnieje już w mapie to przejdź do kroku (3),
    2. w p.p. stwórz nowy wpis do M i dodaj do niego: treść pytania, odpowiedzi.
  5. Uruchom skrypt loadanswer podając ID pytania oraz odpowiedź ‚1’ jako poprawną:
    1. Jeżeli odpowiedź zawiera ciąg znaków „jest: poprawna”, to oznacz w M[idp] odpowiedź ‚A’ jako poprawną,
    2. w p.p. pobierz ciąg znaków po stringu „poprawna to ” i oznacz ją jako poprawną.
  6. Jeżeli |M| < |Q|, to wróć do kroku (2).
  7. Wyświetl pobrane pytania.

Całość może wydawać się skomplikowana, jednakże bot jest prosty – ba! nawet dużo prostszy od Clikera, wymaga jedynie nieco większej wiedzy o działaniu aplikacji.

Implementacja

Poniższy kod implementuje nasz algorytm przy użyciu biblioteki requests (do wysyłania zapytań) oraz re (do wyrażeń regularnych). Zachęcam do samodzielnej analizy całości, zaraz omówię krótko wszystkie części.

Parametrów wejściowe

Podświetlone linie zawierają dane które przekazujemy na wejściu do zapytania, tzn. pola które musimy uzupełnić aby wyglądało jak zapytanie od aplikacji webowej, a nie bota. Chyba nie muszę mówić, że pola wewnątrz REPLACE ME są do zmiany oraz przydałoby się podmienić nagłówek  User-Agent na coś bardziej „legalnego” niż zmyślona przeglądarka? ;)

Pobieranie unikalnego pytania

Następna jest funkcja, której zadaniem  jest pobranie unikalnego pytania, tzn takiego którego jeszcze nie ma w naszej mapie / słowniku. Z racji, że zdajemy się na losowość, to została dostarczona razem z dodatkowym parametrem  max_tries który służy do przerwania działania bota w razie gdyby szczęście przestało nam sprzyjać i nie moglibyśmy trafić na pytanie, któ©ego jeszcze nie mamy.

Podświetlone linie pokazują odpowiednio: wysłanie zapytania do serwera w celu uzyskania pytania, wyrażenie regularne do odnalezienia pytania i wyciągnięcia z niego odpowiednich danych oraz zapisanie go do słownika.

Pobieranie odpowiedzi

W tej funkcji pobieramy odpowiedź do pytania. Warto zwrócić uwagę, że domyślnie wysyłamy odpowiedź ‚A’ jako wybraną, następnie na podstawie odpowiedzi decydujemy jaka jest poprawna.

W przypadku gdyby wygasł token, to nasze wyrażenie regularne nie zadziała i po prostu zwróci błąd.

Główna pętla

Zadaniem głównej pętli jest wyciągnięcie wszystkich błędów lub w przypadku błędu zatrzymania bota. Na koniec drukuje na standardowe wyjście wszystkie znalezione pytania.

Podsumowanie


W tym wpisie zobaczyliśmy jak można napisać bota, który potrafi komunikować się z aplikacją przy użyciu zapytań. Niewątpliwie wymaga nieco większego nakłądu pracy na etapie przygotowawczym, jednakże w wielu sytuacjach może okazać się znacznie wydajniejszy niż Clicker.

Jako zadanie domowe polecam udoskonalić powyższą implementację o działanie na wielu wątkach (zobaczycie że jest niewiele do modyfikacji) oraz automatyczne pobieranie tokenów.

Jeżeli macie jakieś tematy, które chcelibyście zobaczyć, to oczywiście piszcie śmiało w komentarzach, na koniec tradycyjnie zapraszam do śledzenia bloga przez social-media.

C0de On!