Decoding Hancitor Malware with Suricata and Lua

Many types of malware send and receive data via HTTP. They may either be sending updates back to their command and control (CnC) centers or they may receive updates. Typically these won't be sent in plain text but rather with some type of encoding to avoid detection. Much like in my other blog post "Advanced Malware Detection with Suricata Lua Scripting", we will use Suricata's Lua scripting engine to decode some payload on the fly. Unlike the last one though, we'll also create a log file to record the decoded data.

For this example we'll use a Hancitor malware traffic sample. Hancitor is a malware downloader first seen in the wild in 2014. When Hancitor initially infects a system it send a POST request to its CnC server with information about the system it infected. This information is in clear text and can be detected with normal signatures. So we don't need any additional detection for this and what is sent back from the server could have valuable information stored inside.

Picture1
Figure 1: Hancitor POST


Suricata Signature:

alert tcp $HOME_NET any -> $EXTERNAL_NET $HTTP_PORTS (msg:"Hancitor Infection - POST System Info to PHP"; 
flow:established,to_server; content:"POST"; http_method; content:".php"; http_uri; content:"GUID="; http_client_body; 
content:"&BUILD="; http_client_body; content:"&IP="; http_client_body; sid:123456; rev:1;)

Since Hancitor has been around for a while, it's pretty well known that the payload from the server is XOR'd with a key of 0x7A and then base64 encoded. 4 bytes are also added to the beginning of the payload before it's sent. To extract the data we can use something like CyberChef to quickly verify that the payload is encoded like other samples were. Of course, we don't want to do this manually every time. It would be much more efficient to have it done automatically and logged for review.

Picture2
Figure 2: Hancitor POST decode in CyberChef

Lua output scripts need four functions defined. The first of those four is the init() function, which registers where the script will hook into the Suricata output engine. In this case, we'll need "packet" and "alerts".

function init (args)
    local needs = {}
    needs["type"] = "packet"
    needs["filter"] = "alerts"
    return needs
end

Setup() and deinit() functions will also be needed. Setup() sets the log files and deinit() cleans up afterwards.

function setup (args)
    filename = SCLogPath() .. "/" .. "hancitor.log"
    file = assert(io.open(filename, "a"))
    SCLogInfo("HTTP Log Filename " .. filename)
    http = 0
end

function deinit (args)
    SCLogInfo ("HTTP transactions logged: " .. http);
    file:close(file)
end

The real work goes in the log() function. This is where we will see if the above signatures has generated an alert and then extract and decode the payload. This is also where we'll format out output to the log.

function log(args)
    local decoded = {}
    local output = ""
    --local test = ""
    --local key = 0x7a
    sid, rev, gid = SCRuleIds()
    a, o, e = HttpGetResponseBody();
    --print("offset " .. o .. " end " .. e)

    --Make sure we only alert when the correct signatures triggers   
    if sid == 123456 then
        for n, v in ipairs(a) do
            b64_str = string.sub(v,5) --Strip 4 bytes
            b64_decoded = base64.dec(b64_str)
        end
            key, output = CheckXoR(b64_decoded)

        --Gather other traffic info 
        http_host = HttpGetRequestHost()
        if http_host == nil then
            http_host = ""
        end
        http_host = string.gsub(http_host, "%c", ".")

        http_uri = HttpGetRequestUriRaw()
        if http_uri == nil then
            http_uri = ""
        end
        http_uri = string.gsub(http_uri, "%c", ".")

        timestring = SCPacketTimeString()
        ip_version, src_ip, dst_ip, protocol, src_port, dst_port = SCFlowTuple()
        --Setup log file for json
        local json_data = "{"
        ..'"'.."p_timestamp"..'"'..":"..'"'..timestring..'"'..","
        ..'"'.."ipv"..'"'..":"..'"'..ip_version..'"'..","
        ..'"'.."sip"..'"'..":"..'"'..src_ip..'"'..","
        ..'"'.."dip"..'"'..":"..'"'..dst_ip..'"'..","
        ..'"'.."sp"..'"'..":"..'"'..src_port..'"'..","
        ..'"'.."dp"..'"'..":"..'"'..dst_port..'"'..","
        ..'"'.."host"..'"'..":"..'"'..http_host..'"'..","
        ..'"'.."raw_uri"..'"'..":"..'"'..http_uri..'"'..","
        ..'"'.."xor_key"..'"'..":"..'"'.."0x"..bit.tohex(key, 2)..'"'..","
        ..'"'.."data"..'"'..":"..'"'..output..'"'..
        "}"

        file:write(json_data)
        file:flush()
    else
        http = http + 1
        return
    end
http = http + 1
end

The code used is fairly simple, it checks to make sure that the correct sid number has generated an alert. If an alert hasn't been generated by this signature then it will exit the script. This also reduces the number of response payloads we'll have to go through in order to find the correct one. After we are sure we're looking at the correct payload, we can strip the first 4 bytes, store it in variable "b64_str" and then pass that string to a base64 decoder function.

sid, rev, gid = SCRuleIds()
a, o, e = HttpGetResponseBody();
...
if sid == 123456 then
    for n, v in ipairs(a) do
        b64_str = string.sub(v,5) --Strip 4 bytes
        b64_decoded = base64.dec(b64_str)
    end
key, output = CheckXoR(b64_decoded)
…
end

I needed to create a XoR function that would accept the byte array, iterate through the array while XoR'ing it with the key of 0x7A, and finally return the decoded string.

function CheckXoR(data)
    local key = 0x7a
    local decoded_data = {}
    local test = ""
    local output = ""
    local xkey = 0x00

    for i=1, #data, 1 do
        decoded_data[i] = bit.bxor(string.byte(data, i), key)
        --io.write(bit.tohex(decoded_data[i], 2))
        if i <= 11 then
            test = test .. string.char(decoded_data[i])
        end
        if i == 11 then
            if not string.find(test, "http") then
                xkey, output = BruteXoR(data)
                return xkey, output
            end
        end
    end

    for i=1, #decoded_data, 1 do
        output = output .. string.char(decoded_data[i])
    end
    return key,output
end

After writing the function to XoR the payload with a static key, it came to mind that this key could change at any time. We'd then have to update the code to use the new keys or, perhaps, we can simply brute force it.

In the CheckXoR() function and the BruteXoR() function I only decode the first 10bytes and check to see if the decoding is correct by searching for "https". If CheckXoR() finds this string it will continue on using 0x7A as they key, otherwise it will stop and call BruteXoR(). BruteXoR() will then iterate from 0x00 to 0xff trying to decode the first 10bytes of the payload, again check for "https". Once it's found it will continue with the decoding process and return the key and output string.

function BruteXoR(data)
    local test = ""
    local decoded_data = {}
    local found = 0
    local output = ""
    local xkey = 0x00

    for key=0x01, 0xff, 1 do
        if found == 0 then
        for i=1, #data, 1 do
            decoded_data[i] = bit.bxor(string.byte(data, i), key)
            if i <= 11 then
                test = test .. string.char(decoded_data[i])
            end
            if i == 11 then
                if string.find(test, "http") then
                    xkey = key
                    found = 1
                else
                    test = ""
                    break
                end
            end

        end
        else
            break
        end
    end
    for i=1, #decoded_data, 1 do
        output = output .. string.char(decoded_data[i])
    end
return xkey, output
end

From here we can gather other pertinent information, like the HOST, URI, packet timestamp, as well as source and destination IP information. Once gathered you can format the data as needed. I choose to JSON format as shown below.

local json_data = "{"
        ..'"'.."p_timestamp"..'"'..":"..'"'..timestring..'"'..","
        ..'"'.."ipv"..'"'..":"..'"'..ip_version..'"'..","
        ..'"'.."sip"..'"'..":"..'"'..src_ip..'"'..","
        ..'"'.."dip"..'"'..":"..'"'..dst_ip..'"'..","
        ..'"'.."sp"..'"'..":"..'"'..src_port..'"'..","
        ..'"'.."dp"..'"'..":"..'"'..dst_port..'"'..","
        ..'"'.."host"..'"'..":"..'"'..http_host..'"'..","
        ..'"'.."raw_uri"..'"'..":"..'"'..http_uri..'"'..","
        ..'"'.."xor_key"..'"'..":"..'"'.."0x"..bit.tohex(key, 2)..'"'..","
        ..'"'.."data"..'"'..":"..'"'..output..'"'..
        "}"

With the Lua script in place our log output will look something like this:

Log File Output:

{"p_timestamp":"10/17/2018-14:50:24.165203","ipv":"4","sip":"192.168.56.103","dip":"81.177.165.226","sp":"49867","dp":"80","host":"repkehanhar.com","raw_uri":"/4/forum.php","xor_key":"0x7a","data":"{l:https://guz-nmgb.ru/wp-content/plugins/contact-form-7/1|https://brouwershuys.nl/wp-content/plugins/92938dc3b901/1}{b:https://guz-nmgb.ru/wp-content/plugins/contact-form-7/2|https://brouwershuys.nl/wp-content/plugins/92938dc3b901/2}{r:https://guz-nmgb.ru/wp-content/plugins/contact-form-7/3|https://brouwershuys.nl/wp-content/plugins/92938dc3b901/3}"}

Formatted Output:

{
"p_timestamp": "10/17/2018-14:50:24.165203",
"ipv": "4",
"sip": "192.168.56.103",
"dip": "81.177.165.226",
"sp": "49867",
"dp": "80",
"host": "repkehanhar.com",
"raw_uri": "/4/forum.php",
"xor_key": "0x7a",
"data": "{l:https://guz-nmgb.ru/wp-content/plugins/contact-form-7/1|https://brouwershuys.nl/wp-content/plugins/92938dc3b901/1}{b:https://guz-nmgb.ru/wp-content/plugins/contact-form-7/2|https://brouwershuys.nl/wp-content/plugins/92938dc3b901/2}{r:https://guz-nmgb.ru/wp-content/plugins/contact-form-7/3|https://brouwershuys.nl/wp-content/plugins/92938dc3b901/3}"
}


This is of course just one example of the possibilities for using lua output scripts. This can not only extract IoCs from malicious network traffic while it's happening but because it's automated the logs can be extracted and used to further prove an infection and/or distributed to other devices for investigation or blocking.

Trustwave reserves the right to review all comments in the discussion below. Please note that for security and other reasons, we may not approve comments containing links.