Welcome to the first edition of Zeek Roulette, where I pick a random Zeek topic and try to solve it!
For this article I picked njRAT malware from Any.Run and tried to write a detector for it. There is a copy of the njRAT malware, PCAP, and its analysis available here:
https://app.any.run/tasks/72f74893-b9dc-4b1d-9d55-39e0eae86bda/#
If you download the PCAP file and run it through Zeek, you will see the following line in conn.log:
1681921215.277831 CF6WFfOHjuxFz6wtc 192.168.100.204 49228 3.68.56.232 15145 tcp - 41.031513 1268 10585 S1 - - 0 ShADaTdt 12 1999 10 5280 -
This is where the malware’s C2 happens.
First, we need to understand njRAT’s C2 protocol. After a little Googling I found this article and Suricata rule set:
njRAT
Discovered almost a decade ago, njRAT, also known as Bladabindi, is the most active and prevalent remote access trojan. It allows attackers to do surveillance and control the victim’s computer. Its features include remote desktop, logging keystrokes, stealing credentials, capturing microphone and webcam, and many more. njRAT is mostly found to be delivered via phishing email campaigns containing malicious Word document attachments. It is also found to be delivered by masquerading as a legitimate application installer uploaded to file-sharing services and luring victims via drive-by download campaigns.
Since the leak of source code 2013, njRAT has become widely adopted by cybercriminals and APT actors including Gorgon Group and APT41. Numerous variants have been detected over the years. Some variants have been found to be communicating over standard HTTP protocol and others were found to be communicating over custom protocols over TCP. The packet begins with data length in a decimal format null-terminated string followed by command and then delimiter followed by exfiltrated data.
…
alert tcp $HOME_NET any -> $EXTERNAL_NET any (msg:”Zscaler Win32.Backdoor.NjRat – Data Exfil activity”; flow:to_server,established; content:”|00|inf”; offset:3; depth:4; pcre:”/\d{1,3}\x00\w{1,3}/”; pcre:”/(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?/”; flowbits:isset,ZS.njrat; flowbits:unset,ZS.njrat; classtype:trojan-activity; reference:url,https://research.zscaler.com;)
alert tcp $HOME_NET any -> $EXTERNAL_NET any (msg:”Zscaler Win32.Backdoor.NjRat – Data Exfil activity”; flow:to_server,established; content:”|00|ll”; offset:3; depth:3; pcre:”/^\d{1,3}\x00/”; pcre:”/(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?/”; flowbits:set,ZS.njrat; flowbits:noalert; classtype:trojan-activity; reference:url,https://research.zscaler.com;)
https://securityboulevard.com/2021/05/catching-rats-over-custom-protocols/
We can use the general information from the article quoted above to write Zeek detection logic too.
First, we know that each message fits a known format of message length (in ASCII, only counting characters coming after the NULL), a NULL character, a command (in ASCII), and then a delimiter. After the delimiter comes the remaining data, also delimited.
In the PCAP for this malware sample, the first message example is (<NULL> is literally 0x00 in this case):
156<NULL>ll|'|'|SGFjS2VkX0M0QkEzNjQ3|'|'|USER-PC|'|'|admin|'|'|23-04-19|'|'||'|'|Win 7 Professional SP1 x86|'|'|No|'|'|im523|'|'|..|'|'|UHJvZ3JhbSBNYW5hZ2VyAA==|'|'|152.inf|'|'|SGFjS2VkDQo3LnRjcC5ldS5uZ3Jvay5pbzoxNTE0NQ0KQWxsVXNlcnNQcm9maWxlDQpTeXN0ZW0uZXhlDQpUcnVlDQpUcnVlDQpUcnVlDQpUcnVlDQpUcnVlDQpUcnVlDQpUcnVlDQpUcnVl32.act|'|'|UHJvZ3JhbSBNYW5hZ2VyAA==
Here, the delimiter is:
|'|'|
This delimiter can change, so we will design our detector to be delimiter indifferent!
We can write a Spicy protocol analyzer to detect this type of C2. First, we use the following dynamic protocol detection (DPD) signature to trigger our Spicy njRAT C2 protocol analyzer with the message format specified above:
signature dpd_njrat {
ip-proto == tcp
payload /^[0-9]+\x00[a-zA-Z]+\|/
enable "spicy_NJRAT"
}
The Spicy code for this analyzer is pretty simple and follows:
module NJRAT;
function bytes2uint(input: bytes) : uint64 {
local exp: uint64 = |input|;
local sum: uint64 = 0;
local val: uint64;
local shift: uint64;
for (c in input)
{
exp--;
val = c-48;
shift = 10**exp;
sum = sum + ( val * shift );
}
return sum;
}
public type njRATMessages = unit {
messages: njRATMessage[];
};
public type njRATMessage = unit {
len: /[0-9]+/ &convert=bytes2uint($$);
: /\x00/;
payload: bytes &size=self.len;
};
Any time a njRATMessage is parsed, the Spicy EVT file specifies that the following event fires:
event NJRAT::message(c: connection, is_orig: bool, payload: string)
This event is handled in main.zeek to create an njrat.log entry where each line represents a command.
After installing this package, the connection log will now look like the following (note the service of spicy_njrat!):
1681921215.277831 CqlVyW1YwZ15RhTBc4 192.168.100.204 49228 3.68.56.232 15145 tcp spicy_njrat 40.956665 1268 10585 S1 - - 0 ShADaTdt 10 1895 8 2788 -
And the njrat.log will look like the following:
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path njrat
#open 2023-04-20-12-16-33
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p is_orig payload
#types time string addr port addr port bool string
1681921215.595297 CqlVyW1YwZ15RhTBc4 192.168.100.204 49228 3.68.56.232 15145 T ll|'|'|SGFjS2VkX0M0QkEzNjQ3|'|'|USER-PC|'|'|admin|'|'|23-04-19|'|'||'|'|Win 7 Professional SP1 x86|'|'|No|'|'|im523|'|'|..|'|'|UHJvZ3JhbSBNYW5hZ2VyAA==|'|'|
1681921215.674016 CqlVyW1YwZ15RhTBc4 192.168.100.204 49228 3.68.56.232 15145 T inf|'|'|SGFjS2VkDQo3LnRjcC5ldS5uZ3Jvay5pbzoxNTE0NQ0KQWxsVXNlcnNQcm9maWxlDQpTeXN0ZW0uZXhlDQpUcnVlDQpUcnVlDQpUcnVlDQpUcnVlDQpUcnVlDQpUcnVlDQpUcnVlDQpUcnVl
1681921221.103591 CqlVyW1YwZ15RhTBc4 192.168.100.204 49228 3.68.56.232 15145 T act|'|'|UHJvZ3JhbSBNYW5hZ2VyAA==
1681921256.102097 CqlVyW1YwZ15RhTBc4 192.168.100.204 49228 3.68.56.232 15145 F CAP|'|'|35|'|'|23
1681921256.170255 CqlVyW1YwZ15RhTBc4 192.168.100.204 49228 3.68.56.232 15145 T CAP|'|'|\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00\xff\xdb\x00C\x00\x08\x06\x06\x07\x06\x05\x08\x07\x07\x07\x09\x09\x08\x0a\x0c\x14\x0d\x0c\x0b\x0b\x0c\x19\x12\x13\x0f\x14\x1d\x1a\x1f\x1e\x1d\x1a\x1c\x1c $.' ",#\x1c\x1c(7),01444\x1f'9=82<.342\xff\xdb\x00C\x01\x09\x09\x09\x0c\x0b\x0c\x18\x0d\x0d\x182!\x1c!22222222222222222222222222222222222222222222222222\xff\xc0\x00\x11\x08\x00\x17\x00#\x03\x01"\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x1f\x00\x00\x01\x05\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\xff\xc4\x00\xb5\x10\x00\x02\x01\x03\x03\x02\x04\x03\x05\x05\x04\x04\x00\x00\x01}\x01\x02\x03\x00\x04\x11\x05\x12!1A\x06\x13Qa\x07"q\x142\x81\x91\xa1\x08#B\xb1\xc1\x15R\xd1\xf0$3br\x82\x09\x0a\x16\x17\x18\x19\x1a%&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\x83\x84\x85\x86\x87\x88\x89\x8a\x92\x93\x94\x95\x96\x97\x98\x99\x9a\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xff\xc4\x00\x1f\x01\x00\x03\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\xff\xc4\x00\xb5\x11\x00\x02\x01\x02\x04\x04\x03\x04\x07\x05\x04\x04\x00\x01\x02w\x00\x01\x02\x03\x11\x04\x05!1\x06\x12AQ\x07aq\x13"2\x81\x08\x14B\x91\xa1\xb1\xc1\x09#3R\xf0\x15br\xd1\x0a\x16$4\xe1%\xf1\x17\x18\x19\x1a&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x92\x93\x94\x95\x96\x97\x98\x99\x9a\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00?\x00\xe6\xff\x00\xb2\xa6\x92(\x8b\x82T\x96\x00\x95\xc7Lf\xae&\x84\xdb3\xb0\xfeU\xd9hZ\x02\xdc\x04|\x82=3\x9cV\xbe\xa5j\x96\x0d\x06\xd8\xf7\x01"\xee\xda\xbb\xb0;\x9c}\x05}\\\xb1<\xab\x92:\xc8\xf8\xceyN\xa3\x93\xd27\xe9\xfa\x1eYu\xa5\x98\xf2\x0a\xe0\xd6\x1d\xcd\xb1BH\x15\xec\xde2[W\xd2\xed\xe6\x8d 2\xce\xc5\xfc\xc5\xe1\xca\xf6\x0c1\xc7\x18\xaf/\xbe\x88s\xc5V\x06\xb3\xc5\xd0\xf6\x8d%\xabZ;\xad7\xfcM\xaa?c[\x91;\xaf\xf39\xfd\xc4q\x9a*f\x8cn4Q\xec\x8e\x9es\xdf|>\xb3Z\xda\x85\x9a\x12\x1d\xba`\x8cc\xb53Q\x82\xfavm\x90n\xcf\xfbc\xfch\xa2\xbe~8\xd9\xa9\xf3Y~?\xe6vK+\xa4\xe0\xa3\xcc\xff\x00\x0f\xf29\xeb\xad\x03U\x9f\xa5\xb8\x1e\x99\x91\x7f\xc6\xb0.\xfc%\xae8$X\xff\x00\xe4T\xff\x00\xe2\xa8\xa2\xbbVs]+Y~?\xe6D2z\x09\xde\xef\xf0\xff\x00#4\xf8'Y\xcf6\xea\x0fq\xe6/\x1f\xad\x14QS\xfd\xa7Y\xf4_\xd7\xcc\xdf\xea4\xbb\xb3\xff\xd9
#close 2023-04-20-12-16-33
At this point the commands in the payloads could be split with the following function:
https://docs.zeek.org/en/master/scripts/base/bif/strings.bif.zeek.html#id-split_string
But, since we don’t know what delimiter the attackers will use I chose to leave the full string in the log.
Fixing The PCAP
Hah hah, not so fast! Now you would think this code would work with the PCAP referenced throughout this article, wouldn’t you? Well the PCAP, as downloaded from Any.Run, cuts the C2 connection short and therefore the Spicy analyzer will not detect it as njRAT. I fixed the PCAP by removing the last fragment of the C2 communications and saved it here:
https://github.com/keithjjones/zeek-njrat-detector/tree/master/testing/Traces
njRAT IOCs And Zeek’s Intelligence Framework
We also see that Any.Run has some IOCs listed for this sample:
SHA256: 3f1a2a27304c02ea6e56bfd81b0bfc4cf8db5040c23f854d09b6728b1803a8b9
Domain: 7.tcp.eu.ngrok.io
IP: 3.68.56.232
While not as robust as our Spicy njRAT C2 analyzer, we can add IOCs to Zeek’s intelligence framework with the following function:
https://docs.zeek.org/en/master/scripts/base/frameworks/intel/main.zeek.html#id-Intel::insert
The code I used was:
local intel_item = [$indicator="7.tcp.eu.ngrok.io", $indicator_type=Intel::DOMAIN, $meta=[$source="njRAT", $url="https://app.any.run/tasks/72f74893-b9dc-4b1d-9d55-39e0eae86bda/#"]];
Intel::insert(intel_item);
intel_item = [$indicator="3.68.56.232", $indicator_type=Intel::ADDR, $meta=[$source="njRAT", $url="https://app.any.run/tasks/72f74893-b9dc-4b1d-9d55-39e0eae86bda/#"]];
Intel::insert(intel_item);
intel_item = [$indicator="3f1a2a27304c02ea6e56bfd81b0bfc4cf8db5040c23f854d09b6728b1803a8b9", $indicator_type=Intel::FILE_HASH, $meta=[$source="njRAT", $url="https://app.any.run/tasks/72f74893-b9dc-4b1d-9d55-39e0eae86bda/#"]];
Intel::insert(intel_item);
And we load the intelligence framework with the following load commands:
@load frameworks/intel/seen
@load base/frameworks/intel/files.zeek
Now, the intel.log for this PCAP looks like the following:
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path intel
#open 2023-04-20-12-44-15
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p seen.indicator seen.indicator_type seen.where seen.node matched sources fuid file_mime_type file_desc
#types time string addr port addr port string enum enum string set[enum] set[string] string string string
1681921215.261590 C1Xkzz2MaGtLrc1Tla 192.168.100.204 52145 192.168.100.2 53 7.tcp.eu.ngrok.io Intel::DOMAIN DNS::IN_REQUEST zeek Intel::DOMAIN njRAT - - -
1681921215.341329 CqlVyW1YwZ15RhTBc4 192.168.100.204 49228 3.68.56.232 15145 3.68.56.232 Intel::ADDR Conn::IN_RESP zeek Intel::ADDR njRAT - - -
#close 2023-04-20-12-44-15
KilerRAT
Note this will work for RAT variants, like KilerRAT too:
KilerRAT uses the following delimiter instead:
|kiler|
Since the delimiter was not hard coded into our detector, we will still detect this variant.
The Source Code
You can install or see the full source code of this package from:
https://github.com/keithjjones/zeek-njrat-detector
You can install this package with:
zkg install https://github.com/keithjjones/zeek-njrat-detector
More Info
Here is a good link with more info: https://hidocohen.medium.com/njrat-malware-analysis-198188d6339a
You can read an update here: njRAT/Bladabindi Zeek Detector Update – Zeek Roulette #1 Part 2
If none of this made sense to you, check out my Zeek videos over at YouTube to learn more of the technology in this article.
Leave a Reply