Robimy analizę prostego (i trochę starego) malware w JavaScript, jego części odpowiadającej za wykrywanie rodzaju User-Agenta (przeglądarki). Lekcja ciekawa, do dzieła zatem.
Analiza takich rzeczy jak malware, to coś, co powinniśmy robić. Zwłaszcza jeżeli myślimy, że wszystko już umiemy i pewność siebie uderza nam do głowy. Analizowanie takich rzeczy pokazuje, jak wiele jeszcze nie wiemy i jacy eksperci są gdzieś i pracują i jakie kody tworzą.
Programowanie powoli staje się branżą, gdzie pewien poziom bycia ekspertem jest już na wejściu wymagany. Kiedyś było inaczej i choć nie jest to taka branża jak, nie wiem, ubezpieczeniowa czy jakaś, to jednak programiści mieli ogromny komfort.
Jeżeli tylko dostali się do jakiejś pracy, to już dalej byli szkoleni i powoli rozwijali swoje umiejętności, firmy inwestowały w to. Taki ktoś potem stawał się midem/regularem w swojej dziedzinie, ale i on z czasem robił się pewny siebie, niekoniecznie wiedząc jak wiele jeszcze nie wie.
To się powoli zmienia i ta branża programistyczna zaczyna bardziej przypominać medycynę niż to, co było, choć obowiązują inne regulacje. Teoretycznie uczyć się możesz ile chcesz, ale w praktyce rekruterzy odrzucają całą masę dobrych CV, wszystkie niejasności rozstrzygają na niekorzyść rekrutowanego (fajny projekt? pewnie skopiował? dobre CV? pewnie mu chat napisał…) i tak, jak kiedyś byli przesadnie pobłażliwi, teraz stali się przesadnie czepialscy i paranoiczni.
Tym niemniej jest tylko jeden sposób, aby się rozwijać, czyli mierzyć się z wyzwaniami trudniejszymi niż to, co już umiemy. I proszę mi wierzyć – to, co będziemy analizować teraz jest bardzo, bardzo proste.
Jest to prosty mechanizm wykrywający rodzaj przeglądarki internetowej. Widziałem już takie mechanizmy, które wykrywały nie tylko rodzaj ale i zgadywały wersję przeglądarki i były częścią większego malware. A i to nie jest przesadnie trudnym zagadnieniem.
Więc tak, nie jest to przesadnie trudne, tym bardziej musimy się mierzyć z wyzwaniami. Wyzwania to taki kod, w którym dziękujesz, że autor zostawił krótki komentarz, zamiast myśleć „co za idiota komentarze pisze, pewnie po 3 dniowym kursie online”.
Pierwszy fragment:
/**
* Some bots will define outerWidth & outerHeight as 0. I believe this has to do with some bots' windows being minimized or the window being very small.
**/
function checkScreen() {
var w = window;
return w.outerWidth == 0 && w.outerHeight == 0
}
Ok, sprawdzamy outerWidth i outerHeight. Ale przecież jest Selenium i inne takie i nie wszystkie będą miały zminimalizowane okno, prawda?
Ok, możemy sprawdzić, czy jakichś zmiennych nam ktoś nie wstrzykuje do obiektów, po których możemy rozpoznać, co się dzieje:
function checkVars() {
var w = window,
d = document,
n = navigator,
e = d.documentElement,
windowBotVars = ['webdriver', '_Selenium_IDE_Recorder', 'callSelenium', '_selenium', 'callPhantom', '_phantom', 'phantom', '__nightmare'],
navigatorBotVars = ['webdriver', '__webdriver_script_fn', '__driver_evaluate', '__webdriver_evaluate', '__selenium_evaluate', '__fxdriver_evaluate', '__driver_unwrapped', '__webdriver_unwrapped', '__selenium_evaluate', '__fxdriver_evaluate', '__driver_unwrapped', '__webdriver_unwrapped', '__selenium_unwrapped', '__fxdriver_unwrapped', '__webdriver_script_func'],
documentBotAttributes = ['webdriver', 'selenium', 'driver'];
for (var i = 0; i < windowBotVars.length; i++)
if (windowBotVars[i] in w) return true;
for (var i = 0; i < navigatorBotVars.length; i++)
if (navigatorBotVars[i] in n) return true;
for (var i = 0; i < documentBotAttributes.length; i++)
if (e.getAttribute(documentBotAttributes[i]) !== null) return true;
return false;
}
Ok, bazujące na Chromium (czyli de facto wszystkie, Chromium !== Chrome…) przeglądarki mają w navigator.connection RTT, czyli round time trip (ile czasu zajmuje request i response).
Dokładnie, jest to czas wysłania sygnału i odebrania ACK. Ok, wiele botów ma RTT 0:
/**
* Round Time Trip of the connection is obtainable in Chromium-based browsers. The RTT equals zero in many bots, but may result in a few false positives.
**/
function checkRTT() {
var connection = navigator.connection;
var connectionRtt = connection ? connection.rtt : -1;
return connectionRtt === 0;
}
Ok, a jak sprawdzić, czy mamy urządzenie mobilne? Cóż, otwórzmy konsolę przeglądarki (możemy nawet ustawić tryb mobilny, to nie ma znaczenia, nie jesteśmy w android studio, nie mamy emulatora) i wpiszmy takie coś:
document.createEvent("MouseEvent");
Fajnie, fajnie, a teraz touch event:
document.createEvent("TouchEvent");
I mamy error. Można to wykorzystać:
function touchSupport() {
var ts = "ontouchstart" in window, d = document;
if (ts) return ts;
try {
d.createEvent("TouchEvent");
ts = true;
} catch (r) {
ts = false;
}
return ts;
}
Co to jest to pierwsze? Cóż, wpiszmy w konsolę:
window.ontouchstart
Nie mamy tego. A teraz wpiszmy to:
window.ontouchstart = function(){console.log("hello world")};
I jeszcze raz. Teraz „mamy to”, ale to fake, ale pomylić się można. Dlatego autor nie ufa, że coś jest w obiekcie window, tylko dalej drąży temat, próbuje utworzyć TouchEvent i na NotSupportedError uznaje, że to jednak nie jest urządzenie mobilne.
Autor ma kod z 2021 roku i wiele jest tam rzeczy, które już nie istnieją. Bo bardzo szybko to się zmienia, warto to mieć na uwadze.
Ok, weźmy odpalmy w konsoli Chrome taki kod:
eval.toString()
//'function eval() { [native code] }'
eval.toString().length
//33
A teraz FireFox:
eval.toString()
//"function eval() {
// [native code]
//}"
eval.toString().length
//37
Autor wyczuł pismo nosem, że ten toString w różnych przeglądarkach różną długość ma i to różną na tyle, że można na jej podstawie określać rodzaj przeglądarki i użył tego w swoim kodzie:
function toStringLength(a) {
return a.toString().length;
}
Tam jako a jest wrzucony eval potem.
Autor dobrze zna obiekty typu document, window, także navigator i wnioskuje różne rzeczy dzięki tej znajomości:
/**
* navigator.language & navigator.languages are always defined & non-empty, unless the browser is MSIE.
**/
function checkLanguages() {
if (isIE()) return false;
var n = navigator;
try {
var language = n.language;
var languagesLength = n.languages.length;
if (!language || languagesLength === 0)
return true;
} catch (e) {}
return false;
}
Kolejny przykład:
/**
* Unless on MSIE or Firefox, navigator.plugins will always have at least one element defined.
* Note: This does not support mobile browsers, in which case navigator.plugins is always an empty array.
**/
function checkPlugins() {
if (isIE() || isFirefox()) return false;
var n = navigator;
if (!n.plugins) return false;
return n.plugins.length === 0
}
Garść funkcji od autora, choć mówię, rok 2021, znaczy już przestarzałe w większości:
/**
* These are some ways to detect browsers/engines based on feature detection. I got this code from somewhere but forgot where (sorry!).
**/
function reduce(e) {
return e.reduce((function(e, t) {
return e + (t ? 1 : 0)
}), 0)
}
function isIE() {
var w = window,
n = navigator;
return reduce(["MSCSSMatrix" in w, "msSetImmediate" in w, "msIndexedDB" in w, "msMaxTouchPoints" in n, "msPointerEnabled" in n]) >= 4
}
function isChrome() {
var w = window,
n = navigator;
return reduce(["webkitPersistentStorage" in n, "webkitTemporaryStorage" in n, 0 === n.vendor.indexOf("Google"), "webkitResolveLocalFileSystemURL" in w, "BatteryManager" in w, "webkitMediaStream" in w, "webkitSpeechGrammar" in w]) >= 5
}
function isSafari() {
var w = window,
n = navigator;
return reduce(["ApplePayError" in w, "CSSPrimitiveValue" in w, "Counter" in w, "WebKitMediaKeys" in w, 0 === n.vendor.indexOf("Apple"), "getStorageUpdates" in n]) >= 4
}
function isMobileSafari() {
var w = window,
n = navigator;
return reduce(["safari" in w, !("DeviceMotionEvent" in w), !("ongestureend" in w), !("standalone" in n)]) >= 3
}
function isEdgeHTML() {
var w = window,
n = navigator;
return reduce(["msWriteProfilerMark" in w, "MSStream" in w, "msLaunchUri" in n, "msSaveBlob" in n]) >= 3 && !isIE()
}
function isFirefox() {
return !isIE() && !isChrome() && !isSafari() && !isMobileSafari() && !isEdgeHTML();
}
Oczywiście nie wszystkie. Bierzemy Chrome i Firefoxa i wpisujemy:
navigator.webkitPersistentStorage
Chrome ma undefined, Firefox ma informację, że deprecated w nazwie obiektu, który zwraca, ale ma tam coś. Bardziej zaawansowane malware by na tej podstawie, i w oparciu o inne przesłanki, zgadywało wersję przeglądarki.
Autor nie boi się także zaglądać do navigator.userAget, bo niby dlaczego ma zakładać, że na pewno jest fałszywe i nadpisane? Albo nadpisane, ale niezgodnie z prawdą?
function checkChrome() {
/**
* The following should be true of all Chromium-based browsers:
* - eval.toString().length should equal 33, otherwise it's lying about its user agent.
* - If the RTT equals zero, it's probably a bot.
* - If it's not a mobile browser and navigator.plugins is empty, it's probably a bot.
**/
if (toStringLength(eva) != 33) return false;
if (checkRTT()) return false;
if (navigator.userAgent.match(new RegExp(['Mobile', 'Tablet', 'Android'].join('|')) == false && checkPlugins())) return false;
return true;
}
Jeszcze jeden przykład czegoś podobnego:
/**
* A bunch of tests based on the user agent.
**/
function checkUA() {
var n = navigator;
var u = n.userAgent;
/**
* Modern browsers start their user agent strings with "Mozilla/5.0". Bots often do not start their user agent strings with this.
* Note: This excludes very old browsers such as MSIE 7 and will thus be marked as malicious.
**/
if (u.substr(0, 11) !== "Mozilla/5.0") return false;
/**
* Literally the easiest way to detect a bot: if they just tell you they're a bot via their user agent string.
**/
if (u.match(new RegExp(['headless', 'bot', 'crawl', 'index', 'archive', 'spider', 'http', 'google', 'bing', 'yahoo', 'msn', 'yandex', 'facebook'].join('|'), 'i'))) return false;
/**
* Check mobile devices to ensure they have touch support.
* Note: I don't think that navigator.maxTouchPoints is supported on older versions of iOS Safari, and may come up as a false positive.
**/
if (u.match(new RegExp(['Mobile', 'Tablet', 'Android', 'iPhone', 'iPad', 'iPod'].join('|')))) {
if (n.maxTouchPoints < 1 || !touchSupport()) return false;
}
if (u.match(new RegExp('Safari'))) {
if (u.match(new RegExp('Chrome'))) {
if (u.match(new RegExp('Edge'))) {
/**
* Original EdgeHTML-based Microsoft Edge. Obsolete.
**/
return !isEdgeHTML() || !checkEdgeHTML()
} else {
/**
* Chromium-based Browsers (Chrome, Edge, Opera, Brave)
**/
return !isChrome() || !checkChrome()
}
} else if (u.match(new RegExp('Mobile Safari'))) {
/**
* iOS Devices
**/
return !isSafari() || !isMobileSafari() || !checkSafari()
} else {
/**
* macOS Devices
**/
return !isSafari() || !checkSafari()
}
} else if (u.match(new RegExp('Firefox'))) {
/**
* Mozilla Firefox
**/
return !isFirefox() || !checkFirefox()
} else if (u.match(new RegExp(['Trident', 'MSIE'].join('|')))) {
/**
* Internet Explorer
**/
return !isIE() || !checkMSIE()
}
return true;
}
Ok i tak wygląda nasze proste malware. Gościa i jego projekt można odnaleźć na githubie, malidetect/malidetect.js jakby kto pytał.
Dużo jeszcze nie wiemy i napisanie własnego frameworka MVC a nawet napisanie własnego reaktywnego frameworka frontendowego, z vdomem, diffingiem, to jeszcze nie jest wielka sztuka.
Możemy wziąć kurs pisania malware i go zrobić. W Pythonie albo innym Golangu, a potem się dowiemy, że połowa z tych rzeczy w prawdziwym świecie jest pisana w shellcode, a druga połowa, cóż, fajna, ale latając po githubie i analizując też się tego nauczyć możemy.
Możemy wziąć ulubioną, starą grę (np. Shadows of the Empire, polecam), odpalić na najwyższym poziomie trudności i spróbować sobie fejkowe savy stworzyć (prędzej przejdziemy na Jedi, niż to zrobimy, bo to jednak wymaga zachodu z kodem maszynowym i złamania hasła).
Nie chodzi o to, abyśmy już to i tamto lecieli robić, ale chodzi o podejście i nieustanne wyszukiwanie sobie wyzwań i mierzenie się z nimi. Kiedyś oznaka najlepszych, ale powoli staje się koniecznością, aby wejść do branży i jeszcze z minimalnym komfortem, że się coś umie i nie jest balastem przebywać.
Także tak, do roboty! Programowanie trudne nie jest, ale wymaga czasu, poświęcenia, a osiągnięcie mistrzostwa, na jakimkolwiek poziomie, cóż wymaga od nas nieustannego szlifowania umiejętności i wyszukiwania sobie wyzwań.