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.
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.
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.
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.
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(){}”
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ą.
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 /files/FileIO.js
- 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);
});
};
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.
__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
Komentarze
Marzy mi sie jeszcze artykuł o tym nowszym narzędziu Linken Sphere. Ten zgrabnie unaocznił mi kilka wyrazistych luk antidetecta.
na które konkretnie najbardziej zwróciłeś uwagę?
Na te najciekawsze
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ą