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ń.