Trustwave's 2024 Financial Services Threat Reports Highlight Alarming Trends in Insider Threats & Phishing-as-a-Service. Learn More

Trustwave's 2024 Financial Services Threat Reports Highlight Alarming Trends in Insider Threats & Phishing-as-a-Service. Learn More

Services
Managed Detection & Response

Eliminate active threats with 24/7 threat detection, investigation, and response.

Co-Managed SOC (SIEM)

Maximize your SIEM investment, stop alert fatigue, and enhance your team with hybrid security operations support.

Advisory & Diagnostics

Advance your cybersecurity program and get expert guidance where you need it most.

Penetration Testing

Test your physical locations and IT infrastructure to shore up weaknesses before exploitation.

Database Security

Prevent unauthorized access and exceed compliance requirements.

Email Security

Stop email threats others miss and secure your organization against the #1 ransomware attack vector.

Digital Forensics & Incident Response

Prepare for the inevitable with 24/7 global breach response in-region and available on-site.

Firewall & Technology Management

Mitigate risk of a cyberattack with 24/7 incident and health monitoring and the latest threat intelligence.

Solutions
BY TOPIC
Microsoft Security
Unlock the full power of Microsoft Security
Offensive Security
Solutions to maximize your security ROI
Rapidly Secure New Environments
Security for rapid response situations
Securing the Cloud
Safely navigate and stay protected
Securing the IoT Landscape
Test, monitor and secure network objects
Why Trustwave
About Us
Awards and Accolades
Trustwave SpiderLabs Team
Trustwave Fusion Security Operations Platform
Trustwave Security Colony
Partners
Technology Alliance Partners
Key alliances who align and support our ecosystem of security offerings
Trustwave PartnerOne Program
Join forces with Trustwave to protect against the most advance cybersecurity threats

Advanced Malware Detection with Suricata Lua Scripting

Normal IDPS signatures using either Snort or Suricata have quite a few options and, if regex is added in, can be very effective and flexible for matching network traffic. However, there are some instances where those options just don't quite get the job done and more complex detection is needed.

Take for instance when some malware is using encoding to talk to its "Command and Control" server or exfiltrating credit card numbers. It can be difficult to create a signature to not only detect this encoding but also do it in a way that reduces false positives. Suricata has the ability to invoke Lua scripts which, in turn, gives us the ability to decode this type of malware traffic and peer into what is being sent.

The example I'll be using in this post is traffic from Alina Point of Sale (PoS) malware. Alina PoS has been well documented, but if you aren't familiar with it then I suggest you read some older Trustwave SpiderLabs research on the family:

Alina--Casting-a-Shadow-on-POS
Alina--Following-The-Shadow-Part-1
Alina--Following-The-Shadow-Part-2
Alina-POS-malware--sparks--off-a-new-variant

Below a snippet of a HTTP POST command to one of the CnC addresses. As you can see the header is completely visible and we could even use a few elements from it to create a normal IDPS signature. For instance we can develop a signature that keys off of data from the HTTP method, URI, and the User-Agent. Depending on what version of Alina you are inspecting, the URI and User-Agent will likely be different and isn't 100% dependable. That's why we'll need to look into the payload to see what's really going on.

POST /adobe/version_check.php HTTP/1.1
Accept: application/octet-stream
Content-Type: application/octet-stream
Connection: Close
User-Agent: Alina v5.3
Host: 172.20.30.40
Content-Length: 2980
Cache-Control: no-cache

....................................................................y.....h..QP_...S...UWVQV2%W...Z.....DUW.QPd%W...[..S..DUWPQV6%W .
]..U...UQ.QW1%V
.. ......US.QRb%PZ.

We know from the previous Alina analysis that there are two keys we need to use to decode the payload. For the variants we are focusing on, we will need to extract bytes 18-35, as this is the running key that will be used to decode bytes 76 on. We then need to XOR the entire payload with 0xAA. To do this with Suricata, we will use Lua.

The beginning of the Lua script will have to initiate what buffer we'll be using. In this example, we need the "http.request_body". We also need a function called match(). This is where most the code will be and where we'll be decoding the payload. Both init() and match() are required but other functions can be added.

function init (args)
local needs = {}
needs["http.request_body"] = tostring(true)
return needs
end

The init() function is one of the required functions in the script. It load the buffers that are needed into a table. This buffer will be used in the match() function. There are multiple buffers that can be called upon in a script depending on what is needed. For instance, if we needed to inspect the URI, we can change http.request_body to http.uri or http.uri_raw.

function decodeSTR(s)
if(s) then
s = string.gsub(s, '%%(%x%x)',
function (hex) return string.char(tonumber(hex,16)) end )
end
return s
end

The decodeSTR() function removes % symbols and returns the ASCII equivalent of the hex string.

function match(args)
a = tostring(args["http.request_body"])
local bit = require("bit")
local bxor, tohex = bit.bxor, bit.tohex
local decoded1 = {}
local key1 = 0xAA
local key2 = {}
local decoded2 = {}
local decoded_str = ""
local counter = 1

if #a > 0 then
for index=19,36,1 do
key2[counter] = string.byte(a, index)
counter = counter + 1
end

for i=1, #a, 1 do
decoded1[i] = bxor(string.byte(a, i), key1)
end

counter = 1
for index=77,#decoded1,1 do
if ((counter % 18) ~= 0) then
decoded2[counter] = bxor(decoded1[index], key2[counter % 18])
decoded_str = decoded_str .. string.char(decoded2[counter])
counter = counter + 1
else
decoded2[counter] = bxor(decoded1[index], key2[18])
decoded_str = decoded_str .. string.char(decoded2[counter])
counter = counter + 1
end
end

if string.find(decodeSTR(decoded_str), ".exe") then
return 1
end
end
return 0
end
return 0

If we break down the match() function above, the first step is to extract the second key from bytes 18-35 then decode the entire payload using XOR and 0xAA.

for index=19,36,1 do
key2[counter] = string.byte(a, index)
counter = counter + 1
end

for i=1, #a, 1 do
decoded1[i] = bxor(string.byte(a, i), key1)
end
Key2 = cf 92 9b 92 93 9c 98 9b 14 2e df da ce cb de cf aa aa

Payload XOR 0xAA = 03 05 41 6c 69 6e 61 20 76 35 2e 33 00 00
00 00 00 00 65 38 31 38 39 36 32 31 29 23 75 70 64 61 74 65
00 00 44 45 4c 4c 58 54 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 d3 0b 00 00 6c ab
c2 26 ab fb fa f5 ae b9 ad f9 a6 ba be ff fd fc fb fc 98 8f
fd a2 be a1 f0 b9 ae ae a6 bf ee ff fd ae fb fa ce 8f fd a2
be a5 f1 b9 ad f9 a6 bb ee ff fd fa fb...

After we have the second key extracted and XOR'd the payload with the initial key of 0xAA, we need to extract the main part of the payload that's been XOR'd with that second key. The second key is a running key, which means that when you reach the end of the key you wrap around back to the beginning of the key and continue XOR'ing the payload. The below code will iterate over bytes 77 -> end while also iterating the second key so the XOR will be in sync. If the below script looks a bit odd it's because Lua starts its table count at 1 instead of 0. Normally, this script wouldn't need the else statement. But when counter reaches 18 it throws an error since 18 % 18 = 0 and there is nothing available for key2[0].

The for loop takes the number retrieved from the XOR data and converts it to a string character.

counter = 1
for index=77,#decoded1,1 do
if ((counter % 18) ~= 0) then
decoded2[counter] = bxor(decoded1[index], key2[counter % 18])
decoded_str = decoded_str .. string.char(decoded2[counter])
counter = counter + 1
else
decoded2[counter] = bxor(decoded1[index], key2[18])
decoded_str = decoded_str .. string.char(decoded2[counter])
counter = counter + 1
end
end

After the second XOR and the string conversion is complete we can see that some addition obfuscation was used but at this point we can see that the XOR'ing worked. This is where decodeSTR() function is going to be used. What this function will do is it will remove the "%" symbols, take the string, convert it to hex, and finally back to its ASCII string equivalent.

diag=%5b%3a%37%32%20%3c%65%61%3e%5d%20%7b%5b%21%31%36%21%5d%7d%7b%5b
%21%32%30%21%5d%7d%7b%5b%21%32%36%21%5d%7d%43%3a%5c%43%72%61%73%68
%70%6c%61%6e%30%30%31%5c%34%43%37%35%34%31%35%30%36%33%39%41%41%33
%41%38%36%43%41%34%44%36%42%36%33%34%32%38%32%30%42%45%2e%65%78%65
%0a%5b%3a%31%31%32%20%3c%32%3e%5d%20%7b%5b%21%31%36%21%5d%7d%7b%5b
%21%34%36%21%5d%7d%76%6d%61%63%74%68%6c%70%2e%65%78%65...
function decodeSTR(s)
if(s) then
s = string.gsub(s, '%%(%x%x)',
function (hex) return string.char(tonumber(hex,16)) end )
end
return s
end

Below is the final output after the decodeSTR() function. This function may not be necessary if you know to what you are looking for. For instance, if you know you want to find "exe" you could look for "%65%78%65", otherwise using this statement would identify the ASCII characters "exe". From this point you could also print the keys, and decoded payload to a file for later use.

if string.find(decodeSTR(decoded_str), ".exe") then
return 1
end
diag=[:72 ] {[!16!]}{[!20!]}{[!26!]}C:\Crashplan001\4C754150639AA3A86CA4D6B6342820BE.exe
[:112 ] {[!16!]}{[!46!]}vmacthlp.exe (904)
[:112 ] {[!16!]}{[!46!]}AcrylicService.exe (1632)
[:112 ] {[!16!]}{[!46!]}ekrn.exe (1724)
[:112 ] {[!16!]}{[!46!]}jqs.exe (1752)
[:112 ] {[!16!]}{[!46!]}mdm.exe (1772)
[:112 ] {[!16!]}{[!46!]}vmtoolsd.exe (1988)
[:112 ] {[!16!]}{[!46!]}TPAutoConnSvc.exe (548)
[:112 ] {[!16!]}{[!46!]}TPAutoConnect.exe (1936)
[:112 ] {[!16!]}{[!46!]}rundll32.exe (596)
[:112 ] {[!16!]}{[!46!]}vmtoolsd.exe (656)
[:112 ] {[!16!]}{[!46!]}ctfmon.exe (764)
[:112 ] {[!16!]}{[!46!]}langpack.exe (460)
[:112 ] {[!16!]}{[!46!]}mspaint.exe (812)
[:112 ] {[!16!]}{[!46!]}cmd.exe (1316)
[:112 ] {[!16!]}{[!46!]}cmd.exe (3320)
[:112 ] {[!16!]}{[!46!]}translator.exe (1808)
[:112 ] {[!16!]}{[!46!]}notepad.exe (3136)
[:112 ] {[!16!]}{[!46!]}wireshark.exe (1184)
[:112 ] {[!16!]}{[!46!]}wireshark.exe (3184)
&

Here is an example of what the signature could look like. In order for the script to be called, it would still need to match the criteria of a signature. This keeps the resource demand low and multiple signatures can be created to adjust for changes in the traffic for variants of the malware.

alert http $HOME_NET any -> $EXTERNAL_NET $HTTP_PORTS (msg:"SLR Alert - Alina PoS"; 
content:"POST"; http_method; content:"version_check.php"; http_uri; content:"User-Agent: Alina v5.3";
http_header; luajit:scripts/alina_pos.lua; sid:11223344; rev:1;)

Adding Lua scripts as an extension of an IDPS detection system can greatly increase its ability to detect data coming in and going out of your network. This is just one example of what these scripts can do. Of course they will take more resources, especially for the inline devices. However, if they are combined with proper signature creation, both the effectiveness and the confidence of the signature goes up. With inline devices, a higher confidence in the signatures will result in more of them being set to "DROP" traffic and be an active protection rather than just an "ALERT".

ABOUT TRUSTWAVE

Trustwave is a globally recognized cybersecurity leader that reduces cyber risk and fortifies organizations against disruptive and damaging cyber threats. Our comprehensive offensive and defensive cybersecurity portfolio detects what others cannot, responds with greater speed and effectiveness, optimizes client investment, and improves security resilience. Learn more about us.

Latest Intelligence

Discover how our specialists can tailor a security program to fit the needs of
your organization.

Request a Demo