20.11.2019 | 07:26

Mostko

Jak działają carderzy: analiza techniczna Antidetecta

Dzisiaj zapraszamy na trzeci już odcinek z podróży po świecie carderów. Tym razem przyjrzymy się dokładnie jednemu z ich ulubionych narzędzi, tzw. Antidetectowi. Przewodnikiem po tym ciekawym świecie będzie Marcin Mostek.

Skoro mamy już przedstawioną panoramę najpopularniejszych rozwiązań używanych do oszukiwania systemów antyfraudowych wraz z możliwościami, jakie oferują, należałoby lepiej przeanalizować sposób, w jaki działają one „pod maską”. Jako że autor posiada skończone zasoby wolnego czasu i chęci, w poniższej części artykułu skupimy się na najdłużej istniejącym rozwiązaniu o nazwie Antidetect, a konkretnie na wersji 7.3.

W analizowanej wersji Antidetect zawiera następujące pliki / foldery:

  • Antidetect Browser
    Plik wykonywalny będący unpackerem całego programu.
  • antidetect.cc
    Plik binarny zawierający zaszyfrowane zmodyfikowane wersje Firefoksa portable, będącego podstawą działania całego narzędzia.
  • folder fla
    Folder zawierający spatchowane wersje wtyczki Flash przeznaczone do Firefoksa, umożliwiające modyfikacje danych zwracanych przez API wtyczki. Wtyczki są skonfigurowane w ten sposób, by nie aktywował się mechanizm auto-update.
  • folder configs
    Folder zawierający wiele profili ustawień służących do spoofowania przeglądarek z różnych systemów operacyjnych i urządzeń. Folder może zawierać następujące pliki:
    • config.ini wypełniony tylko w wypadku, w którym dany profil powinien mieć możliwość uruchomienia wtyczki Flash (czyli najczęściej Windows); zawiera informacje o czcionkach jakie mają zostać zwrócone poprzez API Flasha,
    • jsoverider.js zawierający zwykle kod mający na celu oszukać canvas fingerprint (zwykle ten plik nie jest wykorzystywany),
    • jsoverider.json – plik konfiguracyjny zawierający wszystkie ustawienia, jakie mają zostać podmienione podczas spoofingu przeglądarki i urządzenia (DOM API),
    • modifyheaders.conf – plik zawierający instrukcje mające na celu poinformowanie przeglądarki, w jaki sposób ma spoofować nagłówki HTTP,
    • useragent.txt – plik zawierający informacje o urządzeniu, na podstawie którego powstał profil przeglądarki (system operacyjny, przeglądarka, rozdzielczość ekranu, język, obecność wtyczki Flash),
    • usercontent.css – plik zawierający informacje, jakie czcionki mają zostać spoofowane.
  • plik overrider.xpi
    Rozszerzenie do Firefoksa faktycznie realizujące część funkcji narzędzia.
Przykładowa zawartość pliku jsoverrider

Uruchomienie pliku Antidetect browser powoduje skopiowanie folderów zawierających pliki konfiguracyjne oraz zmodyfikowane wersje wtyczki Flash do folderu tymczasowego (%AppData%/local/Temp). Wraz z tymi plikami kopiowany jest wcześniej wymieniony dodatek overrrider.xpi . W tej samej lokacji wypakowywany jest również właściwy plik wykonywalny narzędzia, który jest następnie uruchamiany.

Po uruchomieniu pliku oczom użytkownika ukazuje się ekran rejestracyjny wraz z wygenerowanym identyfikatorem sprzętowym. Identyfikator ten służy do wygenerowania przez twórcę oprogramowania klucza umożliwiającego odblokowanie pełnej wersji narzędzia. Istnieje też możliwość uruchomienia ograniczonej czasowo wersji testowej.

Ekran rejestracji

Po przejściu rejestracji należy wybrać miejsce, w którym przechowywany jest plik binarny antidetect.cc, który zostanie rozpakowany do pamięci i uruchomi właściwe okno programu. Plik ten jest zabezpieczony hasłem.

Otwieranie zabezpieczonego hasłem pliku binarnego antidetect.cc

Po wybraniu przez użytkownika odpowiednich opcji konfiguracyjnych (wersji Firefoksa i Flasha oraz urządzenia wraz z przeglądarką, która ma zostać zespoofowana) następuje wypakowanie na dysk przeglądarki w wersji portable (ponownie %AppData%/local/Temp). Wypakowana przeglądarka jest oczywiście odpowiednio zmodyfikowana:

  • wyłączone są automatyczne aktualizacje przeglądarki,
  • wyłączona jest usługa healthreport odpowiedzialna za przesyłanie informacji na temat wydajności i stabilności przeglądarki,
  • wyłączone są API związane ze statusem baterii czy webworkerami,
  • wyłączone są pluginy,
  • wyłączona jest usługa odpowiedzialna za synchronizowanie ustawień pomiędzy komputerami,
  • wyłączone jest przesyłanie raportów dotyczących crashy,
  • rejestrowany jest wcześniej wspomniane rozszerzenie overrider.xpi (umieszczane jest ono w folderze /data/profile/extensions w miejscu, gdzie wypakowana została przeglądarka),
  • w folderze /App/Firefox/browser zostają umieszczone dwa dodatkowe pliki – jsoverider.js i jsoverider.json,
  • w folderze /Data/profile/chrome pojawia się plik userContent.css,
  • w folderze /Data/plugins pojawia się spatchowana wtyczka Flash (tylko w przypadku odpowiedniego systemu).

Po uruchomieniu przeglądarki wgrywane jest rozszerzenie wytworzone przez twórców Antidetecta, które realizuje przeważającą część funkcji programu.

Instalacja rozszerzenia w nowo rozpakowanej wersji portable Firefoksa

Plik jsoverrider.js jest generowany na podstawie wcześniej wybranych opcji (np. czy został włączony spoofing webRTC) i może zawierać następujący kod:

function () {

    // adres ip, który użytkownik chce spoofować, wybrany w oknie programu (jak widać, nie ma walidacji :) )
    var ipAddressRemote = '192.2';

    // losowy adres lokalny, który ma zostać "wyleakowany" przez WebRTC
    var ipAddressLocal = '192.168.1.157';

    // losowy parametr używany przy canvas fingerprintingu
    var CanvasWebglRandomParameter = '192.230.96';

    // losowe liczby mające urealnić spoofing WebRTC
    var randomInt0 = '326905068';
    var randomInt1 = '-1917327006';
    var randomInt2 = '1288248044';

    // backup hookowanej funkcji służącej do tworzenia elementu na stronie
    document.b_createElement = document.createElement;

    // właściwa zhookowana funkcja hookująca tworzenie canvasu używanego w canvas fingerprinting / WebGL          
    // fingerprintingu
    document.createElement = function createElement(elementType) {

        // hook ma zadziałać tylko w wypadku canvasu
        if (elementType == 'canvas') {

            // stworzenie realnego canvasu na podstawie backupu
            element = document.b_createElement('canvas');

            // kolejny backup - tym razem funkcji getContext zwracającej właściwy obiekt canvas, po którym  
            //można rysować
            // w argumencie funkcji podaje się, jaki ma być to typ canvasu - 2d czy 3d (właściwie WebGL)
            element.b_getContext = element.getContext;

            // kolejny hook - tym razem obsługujący zwracanie canvasu w aplikacji
            element.getContext = function (contextType) {

                // stworzenie realnego kontekstu za pomocą backupu
                var context = element.b_getContext(contextType);

                // obsługa hooka dla WebGL-a; w tablicy znajdują się słowa kluczowe, którymi w
                // różnych wersjach przeglądarek aktywowało się WebGL
                if (["webgl", "experimental-webgl", "moz-webgl", "webkit-3d"].indexOf(contextType) != -1)  
                {

                    // stworzenie backupu oryginalnej funkcji getParameter służącej do pobierania 
                    // właściwości WebGL
                    context.b_getParameter = context.getParameter;

                    // następny hook - funkcji getPatamer
                    context.getParameter = function (parameterType) {

                        // pobranie realnego parametru
                        var parameter = context.b_getParameter(parameterType);

                        // wzbogacenie wersji WebGL randomowym parametrem
                        // ma to zrandomizować fingerprint tworzony na podstawie WebGL
                        if (parameterType == context.VERSION) {
                            return parameter + '.' + CanvasWebglRandomParameter;

                        // zwrócenie prawidłowej oryginalnej wartości żądanego parametru
                        } else if (parameterType == context.SHADING_LANGUAGE_VERSION) {
                            return parameter;

                        } else if (parameterType == context.VENDOR) {
                            return parameter;

                        } else if (parameterType == context.RENDERER) {
                            return parameter;

                        } else {
                            return parameter;
                        }
                    };
                }

                // obsługa canvas fingerprintingu
                if (contextType == '2d' && !('b_fillText' in context)) {

                    // backup oryginalnej funkcji fillText
                    context.b_fillText = context.fillText;

                    // hook mający na celu zwrócić za każdym razem losowy fingerprint (przez dodanie   
                    // losowego parametru)
                    context.fillText = function () {
                        return context.b_fillText(CanvasWebglRandomParameter, 2, 17);
                    };
                }
                return context;
            };

            return element;
        } else {

            // jeśli element nie jest canvasem, zwróć go za pomocą oryginalnej funkcji
            return document.b_createElement(elementType);
        }
    };

    // hook funkcji służącej do zwracania WebRTC
    window.mozRTCPeerConnection = function () {
        return {

            //hook funkcji tworzącej kanał, którym mogą być przesyłane dane
            createDataChannel: function () {
            },

            // hook handlera do eventu wywoływanego przy komunikacji z signaling server
            onicecandidate: function () {
            },

            localDescription: {
                type: 'offer',
                sdp: "v=0"
            },

            // hook funkcji zwracającej leakujące ip
            createOffer: function () {

                // zwrócenie po 0,2 sekundy "leaku" adresu zewnętrznego ip ustawionego w opcjach programu
                // randomInt0 ma zwiększyć podobieństwo do prawdziwej odpowiedzi
                setTimeout(
                    this.onicecandidate({
                        candidate: {
                            candidate: 'candidate:0 1 UDP ' + randomInt0 + ' ' + ipAddressRemote + ' 54795  typ host'
                        }
                    }), 200
                );

                // zwrócenie po 0,5 sekundy "leaku" lokalnego adresu ip (wylosowany)
                setTimeout(
                    this.onicecandidate({
                        candidate: {
                            candidate: 'candidate:2 1 UDP ' + randomInt2 + ' ' + ipAddressLocal + ' 56097 typ host'
                        }
                    }), 500
                );


                // zwrócenie po 0,5 sekundy "leaku" adresu zewnętrznego ip ustawionego w opcjach programu (tym razem trochę inna struktura)
                setTimeout(
                    this.onicecandidate({
                        candidate: {
                            candidate: 'candidate:1 1 UDP ' + randomInt1 + ' 192.168.1.3 19005 typ srflx raddr ' + ipAddressRemote + ' rport 54795'
                        }
                    }), 350
                );
            }
        }
    };

    // nadpisanie zhookowaną opcją API
    window.RTCPeerConnection = window.mozRTCPeerConnection;
}

TL;DR dla leniwych: Funkcja hookuje API odpowiedzialne za canvas i WebGL fingerprinting zwraca podstawione wartości. Dodatkowo spoofowane są wartości odpowiedzi z API WebRTC, z których wydobywa się zwykle adresy IP. Przy każdym ponownym uruchumieniu przeglądarki za pomocą Antidetecta (nawet w dokładnie tej samej konfiguracji) wypakowywana jest zupełnie nowa instancja przeglądarki i losowane są nowe wartości mające zrandomizować fingerprint czy zmienić odpowiedź z WebRTC. Podejście tego typu zapewnia również czyszczenie wszelkiego rodzaju cookies.

Plik jsoverider.json zawiera zestaw dyrektyw dla rozszerzenia Antidetect dotyczących tego, jakie informacje mają zostać podmienione i jaką mają zawierać treść. Przykładowo mogą to być:

  • API powiązane z rozdzielczością,
  • informacje o User-Agencie i przeglądarce,
  • informacje o zainstalowanych pluginach,
  • informacje o obsługiwanych typach plików (mimeTypes).

Wszystkie wartości w tym pliku pochodzą z wybranego wcześniej przez użytkownika profilu spoofowania przeglądarki. Profile te mają pochodzić od realnych urządzeń. Co ciekawe, w pliku tym można znaleźć takie fragmenty kodu:

"mozUnlockOrientation": {toString: function () {return "function mozUnlockOrientation() {[native code]}";}}

Służą one ukryciu faktu podmienienia funkcji. W przypadku gdy dana funkcja zapewniana przez przeglądarkę zostanie podmieniona, np. tak jak to miało miejsce w przypadku window.mozRTCPeerConnection z pliku jsoverrider.js, to wywołanie na niej metody toString() zwróci ciało funkcji zamiast [native code].

Przykładowo:

window.alert.toString()

zwróci:

„function  alert(){
   [native code]
}”

zaś po nadpisaniu funkcji za pomocą:

window.alert = function(){};

wywołanie:

window.alert.toString()

zwróci:

„function(){}”
Rozszerzenie umożliwia przejście na najpopularniejsze serwisy weryfikujące skuteczność fingerprintingu

Kolejnym plikiem wartym przeanalizowania jest userContent.css. Według wiki Mozilli plik ten nie pojawia się domyślnie przy tworzeniu nowego profilu przeglądarki i musi zostać dodany manualnie. Służy on do personalizacji wyglądu stron WWW (w przypadku Firefoksa) lub templatów mailowych (w przypadku Thunderbirda). Powyższe zmiany widoczne są tylko lokalnie. Można zadać sobie pytanie – po co twórcy Antidetecta włożyli dodatkową pracę w tworzenie pliku modyfikującego wygląd stron? Czy są aż takimi estetami? Okazuje się, że nie, przynajmniej nie w tym wypadku – plik ten jest wykorzystywany do spoofowania czcionek. Jak to? – spyta zaintrygowany czytelnik. Bardzo prosto:

@font-face {
font-family: 'AlNile';
src: local('Arial');
font-style: normal;
}

Powyższą regułę CSS w uproszczeniu należy rozmieć w następujący sposób:
w przypadku żądania użycia czcionki o nazwie „AlNile” wykorzystaj lokalną czcionkę o nazwie „Arial”. Jako że czcionka „Arial” występuje w praktycznie każdym systemie, „AlNile” zostanie na pewno wyrenderowany. Metoda ta jest w stanie z powodzeniem oszukać algorytm fingerpintowania czcionek opisany w poprzedniej części artykułu. Plik userContent.css zawiera naturalnie więcej niż jedną taką dyrektywę i generowany jest na podstawie wybranego przez użytkownika profilu spoofowanej przeglądarki analogicznie jak jsoverrider.json (patrz też opis folderu configs).

Jednak najważniejszą częścią całego programu jest niezaprzeczalnie rozszerzenie Firefoksa – to ono spaja wszystkie opisywane wcześniej części, a także realizuje dodatkowe funkcje. By lepiej zrozumieć działanie narzędzia, należy wpierw zrozumieć przynajmniej pobieżnie architekturę i możliwości rozszerzeń tworzonych do Firefoksa. Tworząc przykładowe rozszerzenie, można:

  • modyfikować wygląd i działanie wszystkich stron WWW, np. poprzez wstrzykiwanie dodatkowego kodu JS, usuwanie elementów ze strony czy modyfikowanie już istniejących,
  • modyfikować działanie samej przeglądarki, np. poprzez dodawanie nowych funkcji, części interfejsu lub modyfikowanie API JS,
  • przechwytywanie zapytań HTTP i HTTPS oraz ich modyfikacji.

Twórcy przeglądarek nie chcieli zbytnio komplikować procesu tworzenia takich rozszerzeń, toteż wykorzystali oni w tym celu dobrze znane technologie HTML i CSS do tworzenia interfejsu użytkownika i JavaScript do logiki biznesowej. Poza standardowymi API znanymi z klasycznego JavaScriptu dodano też dodatkowe API umożliwiające dużo większe możliwości interakcji z przeglądarką i odwiedzanymi stronami.  Rozszerzenia mogą również cieszyć się innymi (bardziej liberalnymi) zasadami  przesyłania informacji między domenami.

Z racji wykorzystywania przez Antidetect starszych wersji Firefoksa do napisania rozszerzenia zostały użyte API oznaczone już jako porzucone (bootstraped extensions). W zamierzchłych czasach, których nie pamiętają już najstarsi górale, rozszerzenia Firefoksa oparte były o tak zwane overlays i windows. Rozszerzenia tego typu pozwalały na wczytanie z plików XUL (XML User Interface Language) informacji na temat interfejsu i  stworzenie tytułowego overlay, które rozszerzało window o dodatkowe elementy. Przykładowo window reprezentujące przeglądarkowy interfejs pobierania plików było rozszerzane przez overlay dodający do niego dodatkowe przyciski definiowane przez rozszerzenie. Rozwiązanie to miało jednak jedną wadę – przy operacjach takich jak instalacje nowych rozszerzeń czy wyłączanie starych należało restartować przeglądarkę. Postanowiono temu zaradzić poprzez wprowadzenie bootstraped extensions. Główną zaletą tego typu rozszerzeń było wymaganie od nich rozpoczęcia / zatrzymania działania w dowolnym momencie działania przeglądarki. Odbywało się to przez zdefiniowane funkcje startup() i shutdown(). Podczas wywołania startup() musiała ona ręcznie wstrzyknąć tworzony przez rozszerzenie interfejs użytkownika wraz z jego logiką do przeglądarki, podczas gdy funkcja shutdown() musiała „posprzątać” wstrzyknięte informacje.

Kolejnym ważnym założeniem architektonicznym w przypadku rozszerzeń Firefoksa jest podział na Content Script i Add-on Script.

Add-on Script, jak sama nazwa wskazuje, jest kodem związanym z logiką rozszerzenia (zwykle plik main.js i zawartość folderu lib). Ma on dostęp do całego API SDK przewidzianego dla rozszerzeń, aczkolwiek nie ma bezpośredniej możliwości modyfikowania stron w przeglądarce. Content Script natomiast może bezpośrednio modyfikować strony, ale nie może odwoływać się do API SDK. Zwykle do Content Script zaliczają się wszelkie pliki znajdujące się w folderze data. Umieszcza się tam pliki powiązane z interfejsem rozszerzenia (jest on definiowany za pomocą HTML, JS i CSS). Należy pamiętać, że interfejs dodawany przez rozszerzenie jest wyświetlany jako normalna strona WWW wraz z własnym obiektem window. Nie jest ona jednak widoczna z poziomu użytkownika.

Ciekawostka mostka

Naturalnie autor częściowo (jak zwykle) mija się z prawdą.  Jakiś czas temu był możliwy atak za pomocą pseudoprotokołu chrome:// umożliwiającego enumeracje zasobów Content Script i tym samy określenie jakie rozszerzenia dany użytkownik ma zainstalowane. Dokładniejszy opis można przeczytać tu. Blog ten jest bardzo dobrym źródłem uciech, które są fundowane researcherom przez twórców przeglądarek i rozszerzeń do nich.

Bardzo często zachodzi potrzeba przekazywania informacji pomiędzy tymi dwiema warstwami, np. gdy główny skrypt dokonał jakiejś zmiany za pomocą SDK API i należy ją wyświetlić użytkownikowi. Do takowej komunikacji mógł być używany obiekt port. Metoda port.emit() pozwalała na wysłanie informacji, zaś metoda port.on() na jej odebranie i reakcję na nią.

Komunikacja pomiędzy Content Script i Add-on Script

Struktura rozszerzenia wygląda następująco:

  • folder data
    zawierający  wszelkie elementy dotyczące interfejsu rozszerzenia – pliki css, html, czcionki, biblioteki pomocnicze (bootstrap, jquery), a także JavaScript odpowiedzialny za logikę działania GUI.
  • folder lib
    zawierający wszelką logikę biznesową:
    • plik /files/FileIO.js
      odpowiedzialny za operacje na plikach (wczytywanie, zapisywanie itp.),
    • plik /files/InjectScriptHandler.js
      służący do wczytywania pliku jsoverrider.js zawierającego kod, który ma zostać wstrzyknięty w przeglądarkę, zwykle są to funkcje służące do spoofowania WebRTC, WebGL czy canvas fingerprintu,
    • plik /files/NavigatorConfigHandler.js
      służący do wczytania pliku jsoverrider.json zawierającego wytyczne, jak spoofować poszczególne API przeglądarkowe, np. dotyczące rozdzielczości. Dodatkowo parsuje dane takie jak User-Agent, języki czy oscpu w celu wyświetlenia ich w GUI rozszerzenia,
    • plik HTTPRequestOverrider.js
      serce całego narzędzia, wbrew nazwie nie zajmuje się tylko nadpisywaniem właściwości żądań HTTP, ale też wstrzykiwaniem skryptów czy podmianą danych zwracanych przez API przeglądarki,
    • plik IPAdressMonitor.js
      służący do cyklicznego sprawdzania zewnętrznego adresu IP poprzez odpytania na adres „https://api.ipfy.org”. W przypadku gdy nastąpi jakakolwiek zmiana adresu IP, użytkownik jest o niej natychmiast powiadamiany.
  • plik bootstrap.js
    Wymagany przez wcześniej opisany mechanizm bootraped extension.
  • plik install.rdf
    Manifest instalacji rozszerzenia zawierający metadane o dodatku.
  • plik main.js
    Entry point rozszerzenia Antidetect, zawiera wywołania całej logiki.
  • plik package.json
    Zawiera metadane dotyczące rozszerzenia, np. jakie wersje są obsługiwane, kto jest autorem (w tym wypadku „honest_and_sweet_guy” :) ) i jaki jest główny entry point pluginu (main.js).

Po tym wstępnym zapoznaniu można przyjrzeć się plikowi main.js. Na samym początku skrypt tworzy „obiekty” mające odczytać wcześniej opisane pliki jsoverrider.json i jsoverrider.js:

var navigatorConfigFile = require('./lib/files/NavigatorConfigHandler.js').NavigatorConfigHandler(
    FileUtils.getFile('CurProcD', ['jsoverrider.json'])
);

var injectScriptFile = require('./lib/files/InjectScriptHandler.js').InjectScriptHandler(
    FileUtils.getFile('CurProcD', ['jsoverrider.js'])
);

Następnie definiowana jest funkcja mająca na celu wyświetlenie informacji o zawartości poszczególnych plików w interfejsie graficznym rozszerzenia:

const updatePanelContent = function() {
    var pObjects = [
        new Promise((resolve) => {
            navigatorConfigFile.fetch((_, data) => { resolve(data); });
        }),

        new Promise((resolve) => {
            injectScriptFile.fetch((_, data) => { resolve(data) });
        })
    ];

    Promise.all(pObjects).then(values => {
        panel.port.emit('editor.load', values);
    });
};
Możliwość zmiany spoofowanych wartości w locie oferowana przez rozszerzenie

Jako że API jest asynchroniczne, do obsługi wczytania plików konfiguracyjnych używana jest promisa (więcej na temat tego mechanizmu można przeczytać pod tym adresem). W przypadku pomyślnego wczytania obu plików panel jest powiadamiany o tym fakcie za pomocą wiadomości editor.load oraz payloadu zawierającego wczytane dane.

Metoda fetch NavigatorConfigHandlera zwraca prócz zawartości pliku konfiguracyjnego również sparsowane dane:

var parseDataFromContent = function (content) {
    const regexList = {
        oscpu:      /"oscpu"\s*:\s*"(.*?)"/i,
        appVersion: /"appVersion"\s*:\s*"(.*?)"/i,
        userAgent:  /"userAgent"\s*:\s*"(.*?)"/i,
        language:   /"language"\s*:\s*"(.*?)"/i,
        languages:  /"languages"\s*:.*?toString.*?"(.*?)"/i
    };

    var result = {};
    for (var key in regexList) {
        var matches = regexList[key].exec(content);
        if (matches) {
            result[key] = matches[1];
        }
    }

    return result;
}

Natomiast analogiczna metoda w przypadku obiektu injectScriptFile dodatkowo stara się wyekstraktować lokalny i zewnętrzny adres IP:

const stringRegex = '(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')';
const dataRegexList = [
    new RegExp("var\\s*(ipAddressRemote)\\s*=\\s*" + stringRegex + ";", 'i'),
    new RegExp("var\\s*(ipAddressLocal)\\s*=\\s*" + stringRegex + ";", 'i')
];

var parseDataFromContent = function (content) {

    var result = {};
    dataRegexList.forEach(function (regex) {
        var match = regex.exec(content);
        if (match) {
            result[match[1]] = match[2] || match[3];
        }
    });

    return result;
}

Kolejnym krokiem jest zainicjalizowanie dodatkowego przycisku powiązanego z rozszerzeniem:

var toggleButton = ToggleButton({
    id: 'my-popup-button',
    label: 'Addon Plugin: Settings',
    badge_color: '#00AAAA',
    icon: {
        16: './assets/images/icon-16.png',
        32: './assets/images/icon-32.png',
        64: './assets/images/icon-64.png'
    },
    onChange: function(state) {
        if (state.checked) {
            panel.show({
                position: toggleButton
            });
        }
    }
});

…no i samego interfejsu użytkownika wraz z dodatkowymi bibliotekami i logiką jego działania:

var panel = require('sdk/panel').Panel({
    width: 500,
    height: 470,
    contentURL: data.url('panel.html'),
    contentScriptFile: [
        data.url('assets/js/jquery-3.1.0.min.js'),
        data.url('assets/js/bootstrap.min.js'),
        data.url('panel.js')
    ],

    onShow: function() {
        this.port.emit('show');

        updatePanelContent();
    },

    onHide: function() {
        toggleButton.state('window', {checked: false});
    }
});

Po obsłużeniu interfejsu użytkownika inicjalizowany jest obiekt odpowiedzialny za monitorowanie zmian zewnętrznego adresu IP. W ramach „konstruktora” obiektu definiowane są dwie funkcje: onInit obsługująca pierwsze sprawdzenie adresu i onChange definiująca, co się ma stać w przypadku zmiany adresu w trakcie działania rozszerzenia:

var monitor = require('./lib/IPAddressMonitor.js').IPAddressMonitor({
    onInit: function (data) {
        var message = null;

	// podmiana wiadomości wysłanej do GUI na adres ip w przypadku połączenia z internetem
        if (data.status.online) {
            message = { text: 'Your current IP address: ' + data.ip , type: 'success' };
	// podmiana wiadomości wysyłanej do użytkownika na informacje o byciu offline
        } else {
            message = { text: 'Oops, looks like you are offline!' , type: 'danger' };
        }

	// powiadomienie dla GUI
        panel.port.emit('monitor.notification', message);
        toggleButton.badge = message.type == 'danger' ? 1 : null;
    },

    onChange: function (data) {
        var message = null;
	
	// powiadomienie użytkownika o zmianie adresu ip
        if (data.status.online) {
            if (!data.status.retain) {
                message = { text: 'Your current IP address: ' + data.ip_new + ' (changed from '+ data.ip_old +' )', type: 'danger' };
            } else {
                message = { text: 'Your current IP address: ' + data.ip_new , type: 'success' };
            }
	// obsłużenie przypadku gdy użytkownik jest offline
        } else {
            message = { text: 'Oops, looks like you are offline!' , type: 'danger' };
        }

	// powiadomienie dla GUI
        panel.port.emit('monitor.notification', message);
        toggleButton.badge = message.type == 'danger' ? 1 : null;
    }
});

Po wykonaniu tego należy tylko uruchomić monitoring po odpowiedniej informacji z panelu:

panel.port.on('monitor.start', function() {
    monitor.start();
});

Metoda start() sprawia, że wywoływana jest funkcja fetchRemoteAddress, która odpytuje adres https://api.ipify.org/?format=json zwracający zewnętrzne IP. W przypadku gdy API zwróci prawidłową odpowiedź, wywoływany jest callback, który następnie wywoła (yo dawg!) wcześniej przekazaną do konstruktora funkcję onInit wraz z odpowiednimi argumentami. Następnie za pomocą funkcji setInterval cyklicznie wywoływana będzie funkcja performChangeCheck, która w przypadku zmiany adresu IP wywoła wcześniej przekazaną funkcji onChange. Jak widać, kto pisał w JavaScripcie, ten w cyrku się nie śmieje.

Przypadek? Nie sondzem!
__public.start = function () {
    if (fetchIntervalHandler) {
        return;     // zapobiega kilkukrotnemu uruchomieniu
    }

    // jako argument do funkcji  fetchRemoteAddress podawana jest jako callback 
    //anonimowa funkcja wywołująca w przypadku zwrócenia 200 z requestu HTTP
   // przekazaną wcześniej w konstruktorze onInit
    fetchRemoteAddress(function (currentRemoteAddress, httpStatusCode) {
        monitoredRemoteAddress = latestRemoteAddress = currentRemoteAddress;

        if (options.onInit) {
            options.onInit({
                ip: monitoredRemoteAddress,
                status: {
                    online: httpStatusCode == 200
                }
            })
        }

        // wywołanie cyklicznego sprawdzenia zmiany adresu IP
	// w swoim ciele funkcja performChangeCheck w razie potrzeby wywołuje wcześniej
	// przekazaną do konstruktora funkcję onChange
        fetchIntervalHandler = setInterval(performChangeCheck, options.interval || 10  * 1000);
    });
}

Następnie ustawiany jest handler mający na celu obsługę przypadku, w którym użytkownik chciałby zmienić spoofowany adres IP bądź też uruchomić którąś z proponowanych przez rozszerzenie stron do sprawdzania stopnia swojej podatności na fingerprintowanie / wyciek informacji:

// obsługa zmiany adresu IP podmieniająca go we wczytanym do pamięci pliku konfiguracyjnym
panel.port.on('editor.update', function(data) {
    injectScriptFile.update(data);
});

// obsługa przypadku, gdy użytkownik chce uruchomić stronę proponowaną przez twórców antidetecta do sprawdzania stopnia swojej prywatności
var tabs = require("sdk/tabs");
panel.port.on('webpage.open', function(data) {
    tabs.open(data.url);
});

Na koniec pozostaje przeanalizować kluczowy element rozszerzenia  realizujący większość jego funkcji:

// inicjalizacja obiektu mającego wstrzykiwać dodatkowy kod JavaScript lub podmieniać
// wartości zwracane przez API
var httpRequestOverrider = require('./lib/HTTPRequestOverrider.js').HTTPRequestOverrider(
    navigatorConfigFile,
    injectScriptFile
);

// funkcja ta jest uruchamiana po załadowaniu rozszerzenia
exports.main = function() {
    
    // tworzenie interfejsu użytkownika
    updatePanelContent();
    // uruchamianie kodu odpowiedzialnego za wstrzyknięcia
    httpRequestOverrider.register();
}

// funkcja uruchamiana przy zamknięciu rozszerzenia
exports.onUnload = function() {
    // wyłączenie kodu odpowiedzialnego za wstrzyknięcia
    httpRequestOverrider.unregister();
}

Co się dzieje w funkcji register?

    __public.register = function () {
        Services.obs.addObserver(observerHandler, 'content-document-global-created', false);
        Services.obs.addObserver(observerHandler, 'http-on-opening-request', false);
        Services.obs.addObserver(observerHandler, 'http-on-modify-request', false);
    }

Jak widać w powyższym kodzie rejestrowane są usługi (Services.obs.addObserver) obserwujące pewien typ zdarzeń występujących  w przeglądarce. Jeśli jakiekolwiek zdarzenie danego typu będzie miało miejsce podczas działania rozszerzenia, spowoduje to wywołanie metody observe zdefiniowanej w przekazywanym obiekcie observerHandler. Jako drugi argument do funkcji rejestrującej obserwatora podawany jest typ zdarzenia. Dla powyższego przykładu są to odpowiednio:

  • content-document-global-created
    Wysyłany bezpośrednio po tym, jak utworzony zostanie obiekt DOM Window, ale przed wywołaniem na nich jakichkolwiek skryptów. W praktyce zdarzenie to jest wykorzystywane do wstrzyknięcia własnego API do DOM Window. Przez właśnie ten obiekt odwołuje się do API wykorzystywanych do fingerprintigu, np. User-Agenta.
  • http-on-modify-request
    Wysyłany w momencie wykonywania żadania HTTP. Może być używany do podmiany nagłówków żądania.
  • http-on-opening-request
    Bardzo podobny do poprzedniego zdarzenia, wysyłany troszeczkę wcześniej niż w poprzednim wypadku. Trzeci argument (boolean) mówi o tym, czy referencja do funkcji observerHandler jest mocna czy słaba.

Zatem teraz przyjrzyjmy się obiektowi observerHandler:

var observerHandler = {

    observeDocument: function (subject) {
	//wybranie obiektu window danej strony
        var window = subject.QueryInterface(Ci.nsIDOMWindow);

	// tworzenie sandboxowego kontekstu wykonania
        const sandbox = new Cu.Sandbox(window, {sandboxPrototype: window, wantXrays: false});
        if (!sandbox) {
            return false;
        }

	// pierwsze wywołanie eval
	// podmieniające właściwości DOM API takie jak np. Screen
	// na podstawie jsoverrider.json
        navigatorConfigFile.fetch(function (content) {
            Cu.evalInSandbox(override.toSource() + '(' + content + ');', sandbox);
        });

	// drugie wywołanie eval
	// wstrzykujące skrypty z jsoverrider.js
        injectScriptFile.fetch(function (content) {
            Cu.evalInSandbox(eval('(' + content + ')').toSource() + '();', sandbox);
        });
    },

    observeRequest: function (subject) {

	// pobranie najnowszego jsoverrider.json
        navigatorConfigFile.fetch(function (_, data) {

	     //  Wybranie interfejsu umożliwiającego zmianę nagłówków
            var httpChannel = subject.QueryInterface(Ci.nsIHttpChannel);

		// ustawienie nagłówków HTTP 
            httpChannel.setRequestHeader('User-Agent', data.userAgent, false);

		// funkcja applyAcceptLanguageRules dokleja prawidłowy współczynnik „q=” na podstawie typu spoofowanej przeglądarki
		// i wybranych przez użytkownika języków
            httpChannel.setRequestHeader('Accept-Language', applyAcceptLanguageRules(data), false);
        });
    },

    observe: function (subject, topic, data) {
        switch (topic) {
            case 'content-document-global-created':
                this.observeDocument(subject, topic, data);
                break;

            case 'http-on-opening-request':
            case 'http-on-modify-request':
                this.observeRequest(subject, topic, data);
                break;
        }
    }
};

Analizę tego obiektu należy zacząć od metody observe. Jako argumenty przyjmuje ona:

  • subject
    czyli obiekt, którego dotyczyła dana akcja lub obserwacja, np. obiekt reprezentujący nagłówki HTTP,
  • topic
    typ zaobserwowanej zmiany lub akcji, np. wysłanie żądania HTTP,
  • data
    dodatkowe dane powiązane ze zmianą lub akcją.

W naszym przypadku funkcja observe pełni rolę prostego routera rozdysponowującego zdarzenia dotyczące wysłania żądania HTTP do funkcji observeRequest, natomiast zdarzenia dotyczące załadowania strony do observeDocument.

Funkcja observeRequest na podstawie danych wczytanych z wcześniej analizowanego pliku jsoverrider.json nadpisuje nagłówki HTTP dotyczące User-Agenta i akceptowanych języków. Wykonuje się to za metody setRequestHeader interfejsu nsIHttpChannel.

Funkcja observeDocument jest nieco bardziej skomplikowana. W pierwszym kroku pobiera na podstawie parametru subject obiekt window prawie załadowanej strony (skrypty jeszcze nie zdążyły się uruchomić). Następnie tworzy sandbox – obiekt reprezentujący środowisko wykonania JS odizolowany od oryginalnej strony. Sandbox ten tworzony jest na podstawie wcześniej uzyskanego obiektu window pozyskanego z oryginalnej strony (zawiera pełną kopię obiektu window – parametr sandboxPrototype: window). Sandbox działa też z wyłączonym „Xray vision”.

Ciekawostka mostka

„Xray vision” to mechanizm mający na celu, w bardzo dużym uproszczeniu, zwrócenie oryginalnych wartości zawartości DOM API w przypadku utworzonego przez użytkownika kontekstu wykonania. Mówiąc po ludzku:

– domyślnie Window.alert(„Nie lubie javascriptu”) zwróci okno z komunikatem „Nie lubie javascriptu”,
– w JavaScripcie można nadpisać dużo rzeczy, w tym funkcje alert, np: window.alert = function(text) { console.log(„Pisanie w tym języku przyprawia mnie o ból głowy – https://github.com/miguelmota/javascript-idiosyncrasies”) };
– po tej operacji Window.alert(„Nie lubie javascript”) wypisze nam „Pisanie w tym języku przyprawia mnie o ból głowy – https://github.com/miguelmota/javascript-idiosyncrasies” w konsoli,
– włączając mechanizm „Xray vision” przy tworzeniu sandboxu, jak w przypadku rozszerzenia, uzyskamy jednak oryginalne zachowanie window.alert().

Więcej na temat tego mechanizmu można znaleźć tu.

W przypadku gdy operacja tworzenia sandboxu powiodła się, zostaje dwukrotnie wywołana funkcja evalInSandbox, dowolny kod JavaScript w podanym sanboxie. W pierwszym przypadku wykonywany jest kod, który jest wywołaniem funkcji override, do której jako parametr json przekazywana jest zawartość wcześniej opisanego pliku jsoverrider.json:

const override = function (json) {
    Object.keys(json).forEach(function (owner) {
        /* jshint evil:true */
        const target = eval(owner);
        if (!target) {
            window.alert('window.' + owner + ' is not defined!');
            return false;
        }

	// podmiana funkcji DOM API
        Object.keys(json[owner]).forEach(function (property) {
            Object.defineProperty(target, property, {
                value: json[owner][property]
            });
        });
    });
}

Drugie wywołanie evalInSandbox de facto wstrzykuje do kontekstu otworzonej strony WWW kod znajdujący się w pliku jsoverrider.js. Ot, cała tajemnica działania Antidetecta!

Podsumowanie

Po przeczytaniu powyższego tekstu odbiorca artykułu powinien posiadać w mniemaniu autora przynajmniej mgliste pojęcie, w jaki sposób działa najpopularniejsze narzędzie do omijania fingerpintowania (Antidetect). Czytelnika zapewne zacznie nurtować w tym miejscu następująca zagwozdka: „Skoro już wiem, jak wyglądają programy do antyfingerpintingu, to jak wygląda ekosystem biznesowy, jaki wokół nich się wykształcił?” By odpowiedzieć na to pytanie, niezwykle proaktywny autor zaprasza do następnej, ostatniej (uf!) już części artykułu.

Marcin Mostek
Nethone

Powrót

Komentarze

  • 2019.11.20 15:33 Gość

    Marzy mi sie jeszcze artykuł o tym nowszym narzędziu Linken Sphere. Ten zgrabnie unaocznił mi kilka wyrazistych luk antidetecta.

    Odpowiedz
    • 2019.11.20 18:40 hmm

      na które konkretnie najbardziej zwróciłeś uwagę?

      Odpowiedz
      • 2019.11.22 10:47 Gość

        Na te najciekawsze

        Odpowiedz
      • 2019.11.22 10:52 Gość

        O lukach urządzeń pomagających w oszustwach lepiej w szczegółach nie mówić, bo jeszcze zostaną załatane. Każdy z branży przeciwdziałającej oszustwom pewnie znajdzie tutaj coś dla siebie – jakieś świeże spojrzenie. Ten artykuł właśnie w idealny sposób pokazał wady w podejściu oszustów nie wskazując jednocześnie na to na co patrzą Ci, którzy im przeciwdziałają

        Odpowiedz

Zostaw odpowiedź

Jeśli chcesz zwrócić uwagę na literówkę lub inny błąd techniczny, zapraszamy do formularza kontaktowego. Reagujemy równie szybko.

Jak działają carderzy: analiza techniczna Antidetecta

Komentarze