Network Monitor as a Programmatic Intrusion Detection System

Detect Threats, Passively Identify Devices and Selectively Capture Packets

Network Monitor release 2.7.1 implements the ability to add custom scripting rules that can run on every packet or flow, allowing automatic analysis of network metadata. This capability allows for advanced intrusion detection – not just checking for certain byte sequences, but direct access to metadata while coding in a programming language.

For example, a rule can be created to match instances where a sender of an email uses an email address that has a different domain from where it was actually sent. Another could save PCAP for a certain IP address or when a certain protocol is used or only during off-hours. Rules could determine if the traffic includes a series of suspicious behaviors before sending alarms to both the Network Monitor interface and SIEM for further correlations.

Programmatic handing of network traffic is fairly complex, and this post will give a quick overview of how to create new analytics. The examples above only touch the surface, but the ability to perform these tasks is built based on only a few major features: selecting metadata, setting custom metadata, alarming and syslog and saving PCAP.

These scripts are written in the Lua programming language, and these features are basically functions that allow the user to plug into Network Monitor. These will be described in the following sections.

Selecting Metadata

Network Monitor’s core feature is recognizing and parsing thousands of metadata fields from hundreds of network applications. With that metadata, users can search key/value pairs using Lucene Query Syntax to find pretty much anything happening on their network (Network Monitor Quick Tips). Combining this power with a scripting language opens many possibilities.

Metadata is divided into categories, and it’s important to know the category when trying to select specific fields. There are two major sections: general metadata and extended metadata. General metadata is information that should be found in most flows. It will have its own function to retrieve it.

Name Function Datatype Use
Session ID/Unique ID GetUuid(dpiMsg) int Find specific flow
Application GetLatestApplication(dpiMsg) string Protocol/known application (ie, http, dns, gmail)
Source IP GetSrcIP4String(dpiMsg) string Filter on IP
Destination IP GetDstIP4String(dpiMsg) string Filter on IP
Start Time GetStartTime(dpiMsg) int Beginning of flow
End Time GetEndTime(dpiMsg) int End of flow
Source MAC GetSrcMacString(dpiMsg) string
Destination MAC GetDstMacString(dpiMsg) string

Extended metadata falls into three subcategories based on datatype: strings, integers, and longs. To determine which datatype a field is, the Network Monitor Help tab has a document under Help/Content/Managing Network Monitor/Deep Packet Analytics/Network Monitor Metadata fields. Or use, this link with your Network Monitor’s hostname.

To get a specific value for a field, the following functions will make sure you are selecting the correct datatype:

GetString(dpiMsg, protocol, field)
GetInt(dpiMsg, protocol, field)
GetLong(dpiMsg, protocol, field)

For example, local port_dst = GetInt(dpiMsg, 'internal', 'destport') will return the destination port for a flow.

To get all parsed field names available to a flow (those fields that have been observed and parsed), use the following functions (and note the curly brackets):

{GetAllStringAttributes(dpiMsg)}
{GetAllIntAttributes(dpiMsg)}
{GetAllLongAttributes(dpiMsg)}

Setting and Getting Custom Fields

Although Network Monitor can identify thousands of fields, it certainly doesn’t parse everything. With Lua rules, a user can set their own search-able fields.

This might be useful for parsing headers for obscure protocols, identifying devices, and including that information in the flow or adding suspicious tags to flows that violate certain behaviors.

The function for this is very simple: SetCustomField(dpiMsg, key, value), where the key is the field name, and the value is the data being set. Note that when searching for the field name, Network Monitor appends “_NM” to the field name in order to avoid naming conflicts.

To retrieve a custom field in another flow, use: {GetCustomField(dpiMessage, key)}. Note the brackets, because the return type will be a Lua table (ie, an array).

Alarming, Syslog and Packet Capture

Being able to see flows where the rule’s conditions are met is the ultimate goal. There are three main methods for doing so: sending an alarm to the Network Monitor Alarms tab, sending syslog to another device (probably a SIEM), and saving the raw packets as PCAP.

To trigger an alarm and send syslog to the pre-configured source (again, probably a SIEM), use the TriggerUserAlarm function:

TriggerUserAlarm(dpiMsg, ruleEngine, rule_severity)

The alarm will then be visible in the Alarms tab. Use the Session ID to then find the specific flow that triggered the alarm. Likewise, alarms will be searchable in the SIEM.

Alarming from Lua rules

Saving PCAP is done by invoking the VoteForPacketCapture() function in a packet-scoped script. This will set the selected packet for capture. To download the PCAP, use the Capture or Analyze tabs and click on the Captured icon (for the Analyze tab, first make sure to set “Captured” as a field).

Note that alarms can only be triggered from flows, and PCAP can only be saved for packets. The distinction between these will be described in the next section.

Putting It All Together: Simple Examples

First, let’s quickly cover how to upload a rule. The scripts can be uploaded and managed under the Configuration/Deep Packet Analytics tab.

Configure and add Lua rules here.

Add a rule by clicking the “Choose File…” button. Notice the dropdown menu under “Scope.”

Window that will pop-up when adding a new rule.

Until now, we haven’t addressed the topic of when the scripts will be run. This is the scope, and Network Monitor has two: packets and flows.

Packets

Packets are individual units of combined network layer data. Rules set to run on packets will run very, very frequently. For example, a medium-sized organization might generate 10-30,0000 packets per second during business hours. A bad rule at this level can easily take down a Network Monitor appliance, and so any rules running at this level should be very streamlined and minimal.

Rules at packet level do have one major, exclusive feature not available to flows, however, so they are still useful. This is access to the raw data. With access to the data, a packet level rule can save that data in PCAP format before that data is discarded.

This example will save PCAP when a certain IP (defined as my_ip) is seen as the source or destination. Note the two functions to return general metadata and the lua_packet_object being used to save the packet.

function packet_capture_ip (msg, packet)
  require('IpMatch')
  --- this IP is purely an example (google dns server)
  if (myIpMatch == nil) then
    myIpMatch = IpMatch:new()
    myIpMatch:SetIP4Src("8.8.8.8") -- using example IP: google dns server
    myIpMatch:SetIP4Dst("8.8.8.8")
  end
  if (myIpMatch:MatchIP4SrcOrDst(msg)) then
    VoteForPacketCapture(msg)
  end
end

Flows

Flows are a series of packets that make up transport layer communication. For example, the conversation between a client and a server to download a webpage. Rules that trigger on flows will occur much less frequently than packets—a medium-sized organization might expect 75–150 flows per second—and so to reduce the chance of negatively affecting Network Monitor performance, most Lua rules should be classified as flow rules.

For this example, we’ll use some simple scripting logic to find non-DNS traffic on port 53. This may be a sign of a covert channel (e.g., malware or a malicious actor attempting to hide among legitimate traffic).

53 is the destination port, so we’ll start by getting that value and stopping the script if the flow isn’t using it. Then we check if the application is identified as DNS. If not, we have a protocol mismatch.

To notify a user that the mismatch was detected, two actions are taken: First, a custom metadata value is set to make the session easy to find in the Analyze tab (again, remember that “_NM” is appended to custom field names. So the Lucene query would be “proto_mismatch_NM:53”); Second, an alarm is triggered that will be visible in both Network Monitor and the SIEM.

function flow_proto_mismatch_53 (dpiMsg, ruleEngine)
  local port_dst = GetInt(dpiMsg, 'internal', 'destport')
  if port_dst ~= 53 then
    return
  end
  local apps = {dns=true, krb5=true}
  local my_application = GetLatestApplication(dpiMsg)

  if not apps[my_application] then
    TriggerUserAlarm(dpiMsg, ruleEngine, 'medium')
    SetCustomField(dpiMsg, "proto_mismatch", '53')
  end
end

Flow States

Network Monitor can classify a flow as being in one of three stages: Final, Intermediate or Intermediate Final.

The important element here is the intermediate cutoff time. This can be set in Configure/Engine/Report on Long Running Sessions. Shorter lengths will increase the update rate, but may affect performance. Most installations will work well with values between 60-300s. Any Flow that last longer than this value will become “Intermediate.” This prevents a long-running flow from evading detection by never ending.

Using these three functions to determine the flow’s stage:

Function Stage Description
IsFinalShortFlow(dpiMsg) Final Shorter Flows that last for less time than the intermediate cutoff rate.
IsIntermediateFlow(dpiMsg) Intermediate Flows that are not finished but have lasted longer than the intermediate cutoff time.
IsFinalLongFlow(dpiMsg) Intermediate Final Long flows that have ended.

For many rules, Flow state can be ignored. But to filter the script on Flow states, use a typical Lua pattern of returning false if the Flow isn’t in the desired state.

if not (IsFinalShortFlow(dpi_msg)) then
  return false
end

Long Example

This example shows an effective rule for finding potential phishing emails: when the email address used has a domain different from the actual domain sending the SMTP message. Malicious actors can claim to send an email from anyone, like from someone inside your organization, but they won’t be able to fake the sender domain.

-- domain in the email address of the sender domain does not match the domain sending the email.
-- possible indicator of a phishing attack, although additional indicators are needed to confirm
-- eg:
-- SenderEmail: mrX@corporateXYZ-email.com
-- SenderDomain: openSMTPserver.com

function flow_smtp_sender_domain_mismatch (dpiMsg, ruleEngine)
   require('LOG')

  -- get/verify current application
  local app = GetLatestApplication(dpiMsg)
  if app ~= "smtp"  then
    return
  end

  -- get/verify sender domain
  local sender_domain = GetString(dpiMsg, "smtp", "sender_domain")
  if (sender_domain == nil or sender_domain == '') then
    return
  end
  sender_domain = string.lower(sender_domain)

  -- get/verify sender email
  local sender_email  = GetString(dpiMsg, "smtp", "sender_email")
  if (sender_email == nil or sender_email == '') then
    return
  end

  -- parse/verify/save the domain from sender email
  local sender_email_domain = string.sub(sender_email, string.find(sender_email, '@')+1, string.len(sender_email))
  if (sender_email_domain == nil or sender_email_domain == '') then
    return
  end
  sender_email_domain = string.lower(sender_email_domain)
  SetCustomField(dpiMsg, "sender_email_domain", sender_email_domain)

  -- check if sender's real domain matches their claimed domain (exclude gmail)
  -- alarm on mismatch
  if not string.find(sender_domain, sender_email_domain, 1, true) then
    if (string.find(sender_domain, 'gmail') or string.find(sender_domain, 'google')) then
      return
    end
    SetCustomField(dpiMsg, "sender_domain_mismatch", 'true')
    TriggerUserAlarm(dpiMsg, ruleEngine, 'medium')
    INFO ( debug.getinfo(1, "S"), 'domain mismatch, sender domain: ' .. sender_domain .. ', email domain: ' .. sender_email_domain)
    return true
  end
end

Programming Notes

Function Name and Initial Argument

Primary function names must be unique – two scripts with the same function name cannot be uploaded to the same Network Monitor. Because the main function could either work on a flow or packet, system functions provided by LogRhythm will start their name with either “packet” or “flow,” but user-added functions can be named with any Lua-compliant naming scheme. Also, there must be a space between the function name and the parenthesis that contain the arguments.

The names for the initial arguments are somewhat arbitrary, but it’s a good idea so standardize them. For packets, the first argument is the packet message (‘msg’); the second are the packet functions (‘packet’). For flows, the first argument is the message, although the Deep Packet Inspection message (‘dpiMsg’) rather than just a packet. The ‘ruleEngine’ is the set of functions that can be used.

Debugging

Getting rules to work can be tricky. Fortunately, debug messages can be printed to log files at any stage of a script.

To do so, include require 'LOG' in your code before places where you wish to print. Then use INFO(debug.getinfo(1, "S"), "Test: " .. variable) to write out strings, where “Test” is a static string and “variable” is the dynamic value you wish to print.

For packet rules, this will print to the ProbeReader log (/var/log/probe/ProbeReader.log); for flow rules, to ProbeLogger (/var/log/probe/ProbeLogger.log). These two log files are also where errors will be found.

Use standard CentOS/Linux commands for reading the files (like “tail -F /var/log/probe/ProbeReader.log” to see it live).

Optimization

When processing network traffic, speed and volume are critical considerations. It would not be a good idea to perform complex actions on every packet. Even in flows, make sure that complex operations occur on heavily filtered traffic. This can be done by eliminating broad swaths of traffic early on using if statements.

For example, start by filtering down on values that will exclude the most traffic right off the bat, like application. Or, in the SMTP example above, note how the script ends (ie, returns false) whenever something makes the next step pointless.

Future Features

The Lua capabilities in 2.7.1 are the first release where scripting functionality is being made public to customers. Features will be added in future versions to allow for even greater capabilities.

This includes thread-safe writing to save script output to files or databases, setting custom alarm field names to have multiple alarms in one script, and setting the organization’s internal IP space for filtering.

Getting Help

The Help tab includes a large section on the Lua scripting features. Click on the Help tab, Managing Network Monitor in the Contents list, and then Deep Packet Analytics. [Or use this link}(https://your_network_monitor_hostname/userDocs/NetworkMonitorHelp.htm#4Configuration/LuaRules/LuaRules.htm%3FTocPath%3DManaging%2520Network%2520Monitor%7CDeep%2520Packet%2520Analytics%7C____0), substituting in your Network Monitor hostname.