JavaScript lezen die niet gelezen mag worden

· Praktijk · 6 min

JavaScript lezen die niet gelezen mag worden

Open de broncode van een phishingpagina, een kwaadaardige advertentie of een skimmer, en je vindt vaak een muur JavaScript die eruitziet alsof iemand op het toetsenbord in slaap is gevallen. Variabelen als _0x3c1e. Strings die nergens op slaan. Eén regel waar je een minuut langs scrolt.

Dat is obfuscation. De maker wil dat de code draait, maar niet dat je hem leest.

Je hoeft niet elke regel te begrijpen. Je moet twee vragen beantwoorden: wat doet dit script, en met wie praat het? De meeste obfuscation is een stapel simpele trucs die je laag voor laag kunt afpellen.

Voordat je iets aanraakt

Draai onbekende JavaScript niet op je dagelijkse machine. Niet in je gewone browser, niet terwijl je bent ingelogd.

  • Gebruik een wegwerp-VM met een snapshot, zonder persoonlijke data en zonder opgeslagen logins.
  • Eerst lezen, dan pas draaien. Het grootste deel van deze post is statisch: je kijkt alleen naar tekst en decodeert die.
  • Ook je tools kunnen bugs hebben. Met de deobfuscator webcrack kon een kwaadaardige bundle ooit overal op Windows bestanden wegschrijven. Goede tool, maar draai hem ook in de VM.
  • Laat het originele bestand ongemoeid. Werk op kopieën.

Vier begrippen die door elkaar lopen

Minification haalt witruimte weg en kort namen in om bestanden kleiner te maken. Het is niet bedoeld om iets te verbergen, maar maakt code wel lastig leesbaar.

Obfuscation is juist bedoeld om te verbergen wat de code doet. Het hernoemt dingen, verstopt strings en verpakt code in lagen.

Encoding (base64, hex, rot13) verandert hoe data is opgeschreven. Iedereen kan het terugdraaien. Er is geen sleutel.

Encryptie heeft wel een sleutel nodig. Staat die sleutel in het script, zoals meestal op een webpagina, dan is het gewoon encoding met extra stappen.

Daarom werken geheimen aan de clientkant nooit: wat de browser kan uitvoeren, kun jij lezen.

Het sample

Om het veilig te houden heb ik een klein script geschreven dat zich gedraagt als een echt script, maar niets schadelijks doet. Het verstopt een webadres en “maakt contact”, alleen print het in werkelijkheid één regel. Zo zou je het aantreffen:

var _0x3c1e=['dmFyIHBhcnRzPVsnZXhhbScsJ3BsZS5pbicsJ3ZhbGlkJ107dmFyIGhvc3Q9cGFydHMuam9pbignJyk7Y29uc29sZS5sb2coJ3dvdWxkIGNvbnRhY3QgaHR0cHM6Ly8nK2hvc3QrJy9jb2xsZWN0Jyk7'];(function(_0xa){var _0xd=globalThis['\x61\x74\x6f\x62'](_0xa);eval(_0xd);})(_0x3c1e[0]);

Wil je meedoen, sla het dan op als sample.js in een VM. Niets erin maakt verbinding met het netwerk. Het sample en scripts om het te decoderen en te verifiëren staan op GitHub.

Stap 1: maak het leesbaar

Haal het door een beautifier: de playground van Prettier, beautifier.io, js-beautify of de pretty print-knop in de developer tools van je browser. Dat verandert de opmaak, niet het gedrag.

var _0x3c1e = ['dmFyIHBhcnRzPVsnZXhhbScsJ3BsZS5pbicsJ3ZhbGlkJ107dmFyIGhvc3Q9...'];
(function (_0xa) {
  var _0xd = globalThis['\x61\x74\x6f\x62'](_0xa);
  eval(_0xd);
})(_0x3c1e[0]);

Nu zie je de structuur. Een array bevat één lange string. Een functie haalt die string door iets heen en geeft het resultaat aan eval, dat elke tekst uitvoert die het krijgt. Onthoud dit: de echte code wordt tijdens het draaien opgebouwd en aan eval gegeven.

Stap 2: herken wat je ziet

Voor de meeste encodings heb je geen tool nodig. Je moet alleen weten hoe ze eruitzien.

Wat je zietWaarschijnlijkToelichting
Letters en cijfers, soms + en /, vaak eindigend op =Base64Heel gebruikelijk. De =-padding verraadt het
Alleen 0-9 en a-fHexTwee tekens per byte
\x61\x74\x6f\x62 tussen aanhalingstekensHex escapesDe browser zet ze bij het uitvoeren om naar gewone tekst
Normaal ogende tekst die niet klopt, met dezelfde vorm als het origineelROT13 of CaesarDe structuur blijft, de letters schuiven op
eval(function(p,a,c,k,e,d)Een packerKlassieke manier om code in te pakken. Een unpacker lost het op
Enorme arrays met strings plus een functie die items op nummer ophaaltString array obfuscationTypisch voor door tools gegenereerde code

In ons sample vallen twee dingen op: de functienaam staat in hex escapes, en de lange string lijkt op base64.

Stap 3: decodeer met de hand

Begin met de hex escapes. Je terminal kan ze lezen:

printf '\x61\x74\x6f\x62\n'
atob

atob is de ingebouwde base64-decoder van de browser. Het script roept hem aan om zijn eigen string te decoderen, en verstopt de naam zodat een snelle zoekactie op atob niets oplevert. Decodeer nu de string:

echo 'dmFyIHBhcnRzPVsnZXhhbScsJ3BsZS5pbicsJ3ZhbGlkJ107dmFyIGhvc3Q9cGFydHMuam9pbignJyk7Y29uc29sZS5sb2coJ3dvdWxkIGNvbnRhY3QgaHR0cHM6Ly8nK2hvc3QrJy9jb2xsZWN0Jyk7' | base64 -d
var parts=['exam','ple.in','valid'];var host=parts.join('');console.log('would contact https://'+host+'/collect');

Dat is de volgende laag. Echte samples stapelen er vaak meerdere, dus herhaal: kijken, herkennen, decoderen.

Stap 4: laat eval het je vertellen

Bij veel lagen wordt decoderen met de hand traag. De shortcut: het script geeft aan het eind tekst aan eval, dus vervang eval door iets dat die tekst alleen print.

In de wegwerp-VM, met Node.js:

node -e "globalThis.eval = console.log; require('./sample.js')"
var parts=['exam','ple.in','valid'];var host=parts.join('');console.log('would contact https://'+host+'/collect');

Zelfde resultaat, maar de verborgen code werd geprint in plaats van uitgevoerd. In een browserconsole werkt het net zo: overschrijf eval voordat het script draait.

eval is niet de enige uitgang. Let ook op new Function(...), setTimeout of setInterval met een string, en document.write. Sommige obfuscators voegen ook self-defending code toe die het script breekt zodra het opnieuw is opgemaakt. Nog een reden om je origineel te bewaren.

Stap 5: lees wat eruit komt

De verborgen code is nu leesbaar. Hij knipt een domein in drie stukken, zodat een zoekactie op de volledige naam hem mist, plakt ze weer aan elkaar en bouwt de URL:

exam + ple.in + valid  ->  example.invalid

Dat beantwoordt vraag twee. In een echt onderzoek is dit de indicator die je noteert, gedefanged als example[.]invalid zodat niemand er per ongeluk op klikt. Check hem daarna tegen je logs en deel hem met je team.

(.invalid is een gereserveerde naam die nooit resolvet, daarom heb ik hem gebruikt.)

Wanneer je een tool pakt

Met de hand decoderen leert je de patronen. Tools besparen tijd zodra je weet waar je naar kijkt.

SituatieProbeer
Geminificeerd, verder gewoon leesbaarEen beautifier: Prettier, beautifier.io, js-beautify
Gepackte codeEen unpacker, of js-beautify, die gangbare packers aankan
Output van tools in de stijl van obfuscator.io, webpack-bundleswebcrack, dat deobfusceert, unminifiet en uitpakt
Oudere encoders zoals JSFuck of JJEncodede4js. Gearchiveerd sinds 2021, dus verwacht niet dat het nieuwere obfuscators aankan
Onbekend of zelfgebouwdHandwerk, of een eigen script bovenop een parser zoals Babel

Een praktische volgorde: beautifyen, een automatische tool proberen, daarna met de hand verder met wat overblijft. Bewaar elke tussenversie zodat je kunt vergelijken.

Spiekbriefje

Bewaar deze.

  1. Bewaar het origineel. Bewerk het nooit.
  2. Werk in een wegwerp-VM.
  3. Beautify. Lees de structuur.
  4. Zoek de uitgang: eval, Function, timers met strings, document.write.
  5. Herken de encoding aan hoe hij eruitziet.
  6. Decodeer één laag. Herhaal.
  7. Print in plaats van uitvoeren: vervang de uitgang door console.log.
  8. Noteer domeinen, URL’s en bestandsnamen. Defang ze.
  9. Controleer: klopt jouw lezing met wat het script in de VM doet?
  10. Documenteer wat je deed, zodat iemand anders het kan herhalen.

Handige commando’s:

printf 'text' | base64          # encode
echo 'dGV4dA==' | base64 -d     # decode
printf 'text' | xxd -p          # hex encode
echo '74657874' | xxd -p -r     # hex decode
echo 'text' | tr 'A-Za-z' 'N-ZA-Mn-za-m'   # rot13, zelfde commando beide kanten op

Tot slot

Automatische tools doen het meeste werk, maar ze begrijpen geen intentie. Lukt het ze niet, dan brengt kennis van waarom elke laag er zit je alsnog tot het einde.

Zie je de volgende keer een muur van _0x-namen, ga hem dan niet lezen. Maak hem leesbaar, zoek de uitgang en laat het script zelf zien wat het verbergt.

Bronnen

De basis heb ik geleerd in de Hack The Box Academy-module over JavaScript deobfuscation. Het sample en de tekst in deze post zijn van mijzelf.

Sample en scripts bij deze post: https://github.com/sbroekhoven/javascript-blogpost

webcrack, projectpagina: https://github.com/j4k0xb/webcrack

webcrack, advisory GHSA-ccqh-278p-xq6w (arbitrary file write op Windows): https://github.com/j4k0xb/webcrack/security/advisories/GHSA-ccqh-278p-xq6w

de4js, projectpagina, door de eigenaar gearchiveerd op 7 december 2021: https://github.com/lelinhtinh/de4js

Vond je dit nuttig? Geef kudos: