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 ziet | Waarschijnlijk | Toelichting |
|---|---|---|
Letters en cijfers, soms + en /, vaak eindigend op = | Base64 | Heel gebruikelijk. De =-padding verraadt het |
Alleen 0-9 en a-f | Hex | Twee tekens per byte |
\x61\x74\x6f\x62 tussen aanhalingstekens | Hex escapes | De browser zet ze bij het uitvoeren om naar gewone tekst |
| Normaal ogende tekst die niet klopt, met dezelfde vorm als het origineel | ROT13 of Caesar | De structuur blijft, de letters schuiven op |
eval(function(p,a,c,k,e,d) | Een packer | Klassieke manier om code in te pakken. Een unpacker lost het op |
| Enorme arrays met strings plus een functie die items op nummer ophaalt | String array obfuscation | Typisch 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.
| Situatie | Probeer |
|---|---|
| Geminificeerd, verder gewoon leesbaar | Een beautifier: Prettier, beautifier.io, js-beautify |
| Gepackte code | Een unpacker, of js-beautify, die gangbare packers aankan |
| Output van tools in de stijl van obfuscator.io, webpack-bundles | webcrack, dat deobfusceert, unminifiet en uitpakt |
| Oudere encoders zoals JSFuck of JJEncode | de4js. Gearchiveerd sinds 2021, dus verwacht niet dat het nieuwere obfuscators aankan |
| Onbekend of zelfgebouwd | Handwerk, 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.
- Bewaar het origineel. Bewerk het nooit.
- Werk in een wegwerp-VM.
- Beautify. Lees de structuur.
- Zoek de uitgang:
eval,Function, timers met strings,document.write. - Herken de encoding aan hoe hij eruitziet.
- Decodeer één laag. Herhaal.
- Print in plaats van uitvoeren: vervang de uitgang door
console.log. - Noteer domeinen, URL’s en bestandsnamen. Defang ze.
- Controleer: klopt jouw lezing met wat het script in de VM doet?
- 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