Snort3, Snort2lua, and the Emerging Threats Snort 2.9 ruleset

Summary

Thanks to some teamwork, the Emerging Threats Snort 2.9 ruleset is 99% compatible with Snort3. ETOPEN consumers, and/or ETPRO customers who do not use the scada or scada_special ruleset should not experience any problems. The notable exceptions are rules from the following categories/files:

  • deleted.rules
  • scada.rules
  • scada_special.rules

Problems in these rulesets should not affect the majority of customers.

By default, the problematic rules in the scada.rules and scada_special.rules files are not enabled. Additionally, the rules that are causing some issues from both the scada and scada_special rulesets are ETPRO only. Per Emerging Threats policy, any rules in deleted.rules are no longer supported by Proofpoint or the Emerging Threats team, and are always marked as disabled when moved to the deleted rules category. They are moved for a number of reasons mostly pertaining to relevance, and are kept for historical purposes.

What this Achievement means for consumers of the Emerging Threats Snort 2.9 Ruleset:

Currently the Snort2.9 ruleset passes the following checks successfully:

  • when passed to snort2lua via the -c and -r options, snort2lua will successfully create a .rules file, plus a corresponding snort.lua file that contains a list of thresholds that were present in any rules processed, since inline thresholding (e.g. limits/thresholds defined within the rule body itself) are no longer a feature of snort3. In a round about way, there is detection_filter, but it’s not exactly the same.
  • Emerging Threats rules processed by snort2lua and included in the user’s lua configuration files (usually snort.lua) or command line arguments (--rule-path /path/to/rules/dir/ or -R /path/to/your.rules will successfully pass Snort -T (configuration testing) with zero warnings or errors.

Compatibility does not Imply Optimization

Be aware that compatibility with Snort3 does not imply that these rules are optimized or that they will not require further analysis to make use of Snort3’s features and/or enhance their performance. Let me provide an example of what I’m talking about. Let’s look at a recent infostealer rule to detect the Pennywise Infostealer. Here is the snort2.9 version of the rule:

alert tcp $HOME_NET any -> $EXTERNAL_NET 5001 (msg:"ET MALWARE PennyWise Stealer Exfil"; flow:established,to_server; content:"|2a|PennyWise|20|v1|2e|"; fast_pattern; content:"Worker|3a 20|"; content:"IP|3a 20|"; content:"Username|3a 20|"; content:"PC|3a 20|"; content:"System|3a 20|"; content:"|2a|Browsers|3a 2a|"; reference:url,twitter.com/crep1x/status/1638596449226170370; classtype:trojan-activity; sid:2044748; rev:2; metadata:affected_product Windows_XP_Vista_7_8_10_Server_32_64_Bit, attack_target Client_Endpoint, created_at 2023_03_22, deployment Perimeter, former_category MALWARE, malware_family PennyWise, performance_impact Low, confidence High, signature_severity Major, updated_at 2023_03_30;)

and here is the Suricata 5 version:

alert http $HOME_NET any -> $EXTERNAL_NET 5001 (msg:"ET MALWARE PennyWise Stealer Exfil"; flow:established,to_server; http.request_line; content:"POST|20|/uploadfile|20|"; http.request_body; content:"|2a|PennyWise|20|v1|2e|"; fast_pattern; content:"Worker|3a 20|"; content:"IP|3a 20|"; content:"Username|3a 20|"; content:"PC|3a 20|"; content:"System|3a 20|"; reference:url,twitter.com/crep1x/status/1638596449226170370; classtype:trojan-activity; sid:2044748; rev:2; metadata:affected_product Windows_XP_Vista_7_8_10_Server_32_64_Bit, attack_target Client_Endpoint, created_at 2023_03_22, deployment Perimeter, former_category MALWARE, malware_family PennyWise, performance_impact Low, confidence High, signature_severity Major, updated_at 2023_03_30;)

If you compare the two of them, you’ll notice that the Suricata rule is protocol aware, and is able to use HTTP sticky buffers. Suricata doesn’t care what port http traffic is on. If it detects it as HTTP traffic, you can use the http application layer protocol rule header, and you can use http sticky buffers and modifiers. Snort on the other hand…

The use of application layer rule headers, via automatic service inspection (e.g. The “Wizard” system) is a relatively new feature for Snort3. Snort3 is able to automatically detect Application layer protocol traffic based on a set of criteria now, instead of using a portvar, such as HTTP_PORTS. Traditionally, Snort relied on customers knowing what ports to expect what traffic from - through the use of portvar variables in snort.conf.

Seeing as how TCP/IP ports are entirely arbitrary, that means bad guys could choose any TCP port they desire to host a web server. Snort 2.x wouldn’t do a terribly great job of analyzing this traffic if it wasn’t defined in the HTTP_PORTS port variable, in the TCP stream reassembly ports of the stream5 preprocessor, and in the inspection ports of the http_inspect preprocessor. This would cause a cascading chain of failures, making network detection more difficult.

If a snort rule used $HTTP_PORTS in the rule header, the malicious traffic would never be compared to that rule. Because the port isn’t in the stream5 preprocessor, The traffic wouldn’t have its TCP stream reassembled. Then as a direct result the http_inspect preprocessor, which requires stream reassembly to work, snort would not recognize the malicious traffic as HTTP traffic.

That means modifiers like http_method, http_uri, urilen, http_header, etc. would not work. So we would be required to write rules for this traffic without HTTP content modifiers or http_inspect features. That also means that if threat actors were especially cheeky and decided to throw in random HTTP Expect: 100-continue headers to divide http header traffic, and http client body traffic across multiple TCP packets, that we would have to somehow account for this, since we have zero stream reassembly for this malicious traffic. This is why the Pennywise infostealer rule above for snort 2.9 is written the way it is.

So any rules we wrote in the past for snort 2.9 that accounted for these gaps, will look roughly the same for snort3 – a syntactically correct rule, but entirely un-optimized.

How do I use snort2lua?

Snort2lua is included with the snort3.x source files, and is automatically compiled while compiling snort3. To my knowledge, there is no standalone snort2lua project or binary available at this time. Also to my knowledge, most Linux distributions do not provide packages for snort3, so manual compilation seems to be the only way to acquire it at this time. For those looking for guidance on how to install snort3 on their distro of choice, I would recommend visiting snort.org’s documentation page and review the Snort3 Setup Guides section. Alternatively, in the not-so-distant past, I used a (now hidden) Setup guide for snort3 on Ubuntu 18 and 20, and created a script to automatically install snort3. I can confirm that this script works on Ubuntu 18.04, 20.04, and 22.04. Use whatever method you prefer, but from here on out I’m going to assume that you have both snort3, and snort2lua installed. This can be confirmed by running:

which snort2lua snort
snort -V

by default they get installed to:

/usr/local/bin/snort2lua
/usr/local/bin/snort

If snort3 was installed and ran with the -V option, your output should look something like this:

   ,,_     -*> Snort++ <*-
  o"  )~   Version 3.1.57.0
   ''''    By Martin Roesch & The Snort Team
           http://snort.org/contact#team
           Copyright (C) 2014-2023 Cisco and/or its affiliates. All rights reserved.
           Copyright (C) 1998-2013 Sourcefire, Inc., et al.
           Using DAQ version 3.0.11
           Using LuaJIT version 2.1.0-beta3
           Using OpenSSL 3.0.2 15 Mar 2022
           Using libpcap version 1.10.1 (with TPACKET_V3)
           Using PCRE version 8.45 2021-06-15
           Using ZLIB version 1.2.11
           Using Hyperscan version 5.4.0 2021-01-26
           Using LZMA version 5.2.5

Before we get started with snort2lua, you’ll need a copy of the ETOPEN or ETPRO ruleset for snort2.9. You may download the ruleset using wget or whatever rule manage you use to process snort2.9 rules. However, I highly recommend that your rule manager be configured to dump all of your active snort rules into a single unified rules file. Snort2lua does not have an option to batch process all rules file in a directory, but can only process one rule file at a time. For those of you who do not use a rule management tool, here is a quick an dirty list of commands I recommend running to download the ET/ETPRO rules to the ~/snort2lua directory, moving deleted.rules, scada.rules, and scada_special.rules to ~/snort2lua/rules/disabled, concatenating the remaining rule files into a single rule file located in ~/snort2lua/conversion, then running snort2lua on the concatenated file:

mkdir ~/snort2lua
cd ~/snort2lua

ETOPEN users will run:

wget http://rules.emergingthreats.net/open/snort-2.9.0/emerging.rules.tar.gz
tar -xzvf emerging.rules.tar.gz

ETPRO customers will run:

wget https://rules.emergingthreatspro.com/<your-etpro-pro-oinkcode-here>/snort-2.9.0/etpro.rules.tar.gz
tar -xzvf etpro.rules.tar.gz

From here, both etpro and etopen users will run the following commands:

mkdir ~/snort2lua/rules/disabled
mkdir ~/snort2lua/conversion

ETPRO customers will need to run:

mv ~/snort2lua/rules/*scada*.rules ~/snort2lua/rules/disabled

Then both ETPRO and ETOPEN users will run:

mv ~/snort2lua/rules/*deleted.rules ~/snort2lua/rules/disabled
cat ~/snort2lua/rules/*.rules >> ~/snort2lua/conversion/et_all.rules
cd ~/snort2lua/conversion
snort2lua -c et_all.rules -r et_snort3_all.rules

Snort2lua has a variety of different configuration options and switches, but the two options we are concerned with are -c and -r. These options take the snort 2.9 rules (-c) and convert them to snort3 (-r). After running snort2lua, the conversions directory should have the files et_snort3_all.rules file, and a snort.lua file. This file contains a list of thresholds (suppressions, limits, etc.) that were defined inline in a snort2.9 rule body. Unfortunately, snort3 no longer supports the threshold rule option. There is detection_filter, but its not exactly the same.

Turning snort2lua’s snort.lua file into et_thresholds.lua

In addition to inline thresholds, this snort.lua file will dump every single line comment (this includes all commented out/disabled rules) and place them into a lua-style comment block at the end of the file, causing the size of this file to be somewhat large for whats just a config file. I’ve devised a sed command that will remove the massive comment block and save the changes to a new file titled et_thresholds.tmp.lua:

sed '/^--\[\[/,/\]\]/d;s/--\[\[.*$//' snort.lua >> et_thresholds.tmp.lua
du -h *.lua
336K    et_thresholds.tmp.lua
5.4M    snort.lua

that looks a lot better now, doesn’t it?

The next thing we need to do, is remove these lines from the start of the file:

---------------------------------------------------------------------------
-- Snort++ prototype configuration
---------------------------------------------------------------------------

---------------------------------------------------------------------------
-- setup environment
---------------------------------------------------------------------------
-- given:
-- export DIR=/install/path
-- configure --prefix=$DIR
-- make install
--
-- then:
-- export SNORT_LUA_PATH=$DIR/conf/
---------------------------------------------------------------------------

dir = os.getenv('SNORT_LUA_PATH')

if ( not dir ) then
    dir = '.'
end

dofile(dir .. '/snort_defaults.lua')

wizard = default_wizard

and these lines from the new end of the file:


binder =
{
    { use = { type = 'wizard', }, },
}

If you have a preferred text editor, use whatever you are most comfortable with to remove these lines manually, or you can use these commands to do it for you:

sed -i 1,26d et_thresholds.tmp.lua
head -n -6 et_thresholds.tmp.lua >> et_thresholds.lua

Implementing and Testing Your Newly Converted Rules

With all these tasks completed, all that remains is to:

  • place the et_snort3_all.rules file into your snort3 rule directory either defined in your snort.lua, or via the --rule-directory or the -R option.
  • place the et_thresholds.lua into the directory with your other snort3 lua configuration files, and append an include statement telling snort3 to read your new lua file on startup.

In my case, my snort3 rule directory is:
/usr/local/etc/rules

and my snort3 lua config file directory is:
/usr/local/etc/snort

So here is a quick and dirty set of commands to achieve our objectives. These commands will copy our rules file and thresholds file to the proper directories, make a backup of /usr/local/etc/snort/snort.lua (just in case something goes wrong), then append our include statements to /usr/local/etc/snort/snort.lua. Change the directory paths and names as necessary for your snort3 deployment:

cp ~/snort2lua/conversion/et_snort3_all.rules /usr/local/etc/rules
cp ~/snort2lua/conversion/et_thresholds.lua /usr/local/etc/snort
cp  /usr/local/etc/snort/snort.lua /usr/local/etc/snort/snort_lua.bak
echo "-- this include statement is necessary for emerging threats rule thresholds to operate correctly" >> /usr/local/etc/snort/snort.lua
echo "include 'et_thresholds.lua'" >> /usr/local/etc/snort/snort.lua

The quickest way to make sure you’ve done all of this correctly is to run snort3 with the -T flag to confirm there are no warnings or errors. Try this, replacing the directory path for snort, config files, rules, and plugins as necessary for your deployment:

/usr/local/bin/snort -c /usr/local/etc/snort/snort.lua -u snort -g snort --plugin-path=/usr/local/lib/snort --rule-path /usr/local/etc/rules -T

If everything ran successfully, the last lines should read:

Snort successfully validated the configuration (with 0 warnings).
o")~   Snort exiting

Congratulations! You’re good to go. We have a lot of work to do in better supporting Snort3, but we hope you’ll agree that this is a great first step. Stay tuned for more information.

1 Like

Summary: Several IP and Port Variables need to be defined to use certain rules not enabled by default.

For users of the scada.rules, scada_special.rules, and some rules that are disabled by default in the ruleset, several IP variables and port variables need to be manually defined, because snort3 does define them in the default configuration files. These IP and port variables are required for certain rules in the ETPRO scada.rules and scada_special.rules file, as well as a small collection of rules that still utilize the SHELLCODE_PORTS port variable.

The IP variables in question are:

ENIP_CLIENT
ENIP_SERVER
DNP3_CLIENT
DNP3_SERVER
MODBUS_SERVER
MODBUS_CLIENT

While the port variables are:

DNP3_PORTS
SHELLCODE_PORTS

How do I configure snort3 to define these variables?

Some of the rules in these rulesets were made with the cooperation and collaboration of DigitalBond (with proper referencing, of course). That being said, a lot of the rules in these rulesets use custom IP and Port variables. By default, snort3 doesn’t define any of these variables. For the scada.rules file, this is a big reason why the rules won’t work (in spite of snort2lua transforming them properly).

Here are the ipvars some of the rules use that aren’t defined by default:

ENIP_CLIENT
ENIP_SERVER
DNP3_CLIENT
DNP3_SERVER
MODBUS_SERVER
MODBUS_CLIENT

And custom portvars:

DNP3_PORTS
SHELLCODE_PORTS

In a stock snort3 installation, IP and Port variables are defined in snort_defaults.lua, and included into the main snort.lua file – just like the thresholds.lua file we made in the previous post. We’re going to make the necessary edits to snort_defaults.lua to define these IP and Port variables. First, before we get started, you will need to know:

  • What networks contain your PLCs
  • What networks contain the SCADA software/servers responsible for managing those PLCs
  • What industrial control system network protocol(s) your organization uses for communicating between management systems and PLCs – MODBUS, DNP3, or CIP/ENIP

In my case, I don’t actually have any PLCs or SCADA servers, so I’m cheating, and I’m going to define the new IP variables to HOME_NET. But you will need to define IP addresses and network ranges as suitable for your network. Fortunately, snort3’s IP variables are formatted almost identically to snort 2.x:

IP Variables can be defined as:

A single IP address:

  • DNP3_SERVERS = [[10.0.0.1]]

A CIDR Range:

  • MODBUS_CLIENT = [[172.16.0.0/12]]

Another IP variable:

  • 'ENIP_CLIENT = HOME_NET
  • Notice how there is no $ in front of the variable if you’re calling another ip variable, and no need for the double brackets ([[ ]])

Or a combination:

  • DNP3_CLIENT = [[$HOME_NET,10.0.0.1,172.16.2.0/24]]
  • Note in this case that if you are combining an IP variable with other IP addresses, you’ll need the ‘$’ in front of the variable for snort3 to recognize it.
  • Notice that there are no spaces between any of the values, only commas

As a final note on this section, please be aware that snort3 does support using IPv6 and IPv4 IP addresses in IP variables.

With that out of the way, the next thing to do would be to make a backup copy of your snort_defaults.lua file. Just like did for the snort.lua file in the previous post. In this example, I’ll be using /usr/local/etc/snort as the directory used to hold snort3’s lua config files, Adjust as necessary for your snort3 deployment.

cp /usr/local/etc/snort/snort_defaults.lua /usr/local/etc/snort/snort_defaults_lua.bak

Next, use your favorite text editor and open up snort_defaults.lua for editing. Navigate to this section:

---------------------------------------------------------------------------
-- default networks - used in Talos rules
---------------------------------------------------------------------------
-- define servers on your network you want to protect

DNS_SERVERS = HOME_NET
FTP_SERVERS = HOME_NET
HTTP_SERVERS = HOME_NET
SIP_SERVERS = HOME_NET
SMTP_SERVERS = HOME_NET
SQL_SERVERS = HOME_NET
SSH_SERVERS = HOME_NET
TELNET_SERVERS = HOME_NET

Immediately below the TELNET_SERVERS = HOME_NET line, add:

ENIP_CLIENT = HOME_NET
ENIP_SERVER = HOME_NET
DNP3_CLIENT = HOME_NET
DNP3_SERVER = HOME_NET
MODBUS_SERVER = HOME_NET
MODBUS_CLIENT = HOME_NET

Again, adjust these IP variables as necessary to meet the needs of your deployment. If there are ICS network protocols that you do not utilize for your sensor deployment, then use the setting of HOME_NET above as a default.

Next, move to the following section of the snort_defaults.lua file:

---------------------------------------------------------------------------
-- default ports - used in Talos rules
---------------------------------------------------------------------------
-- define ports on your network you want to protect
-- where possible, use the wizard for inspection instead of explicit port
-- bindings. this gives you some port independence and allows you find c&c
-- channels hard port bindings would miss. Talos rules will still use these
-- ports if there is no match on service.

FTP_PORTS = ' 21 2100 3535'

HTTP_PORTS =
[[
    80 81 311 383 591 593 901 1220 1414 1741 1830 2301 2381 2809 3037 3128
    3702 4343 4848 5250 6988 7000 7001 7144 7145 7510 7777 7779 8000 8008
    8014 8028 8080 8085 8088 8090 8118 8123 8180 8181 8243 8280 8300 8800
    8888 8899 9000 9060 9080 9090 9091 9443 9999 11371 34443 34444 41080
    50002 55555
]]

MAIL_PORTS = ' 110 143'

ORACLE_PORTS = ' 1024:'

SIP_PORTS = ' 5060 5061 5600'

SSH_PORTS = ' 22'

FILE_DATA_PORTS = HTTP_PORTS .. MAIL_PORTS

Once again, we’ll be appending two lines. This time, directly below FILE_DATA_PORTS = HTTP_PORTS .. MAIL_PORTS:

DNP3_PORTS = ' 20000'

SHELLCODE_PORTS = ' !80'

Usually the default TCP port for DNP3 traffic is 20000, while the default setting for SHELLCODE_PORTS was set to !80. However if you want to modify SHELLCODE_PORTS and/or DNP3_PORTS, that can be done here and now. The syntax to define a port variable for snort3 are pretty similar to those for snort2.9.

Rather than list every example out, take a look at the other port variables that snort defines by default. If you ever have need to define a custom port variable, or change the port(s) for the DNP3_PORTS and/or SHELLCODE_PORTS variables.

With those two edits made, we have two more areas of the snort_defaults.lua file to edit. Navigate to this section:

default_variables =
{
    nets =
    {
        HOME_NET = HOME_NET,
        EXTERNAL_NET = EXTERNAL_NET,
        DNS_SERVERS = DNS_SERVERS,
        FTP_SERVERS = FTP_SERVERS,
        HTTP_SERVERS = HTTP_SERVERS,
        SIP_SERVERS = SIP_SERVERS,
        SMTP_SERVERS = SMTP_SERVERS,
        SQL_SERVERS = SQL_SERVERS,
        SSH_SERVERS = SSH_SERVERS,
        TELNET_SERVERS = TELNET_SERVERS,
    },

Under TELNET_SERVERS = TELNET_SERVERS, Within the brackets, add:

        ENIP_CLIENT = ENIP_CLIENT,
        ENIP_SERVER = ENIP_SERVER,
        DNP3_CLIENT = DNP3_CLIENT,
        DNP3_SERVER = DNP3_SERVER,
        MODBUS_SERVER = MODBUS_SERVER,
        MODBUS_CLIENT = MODBUS_CLIENT,

Please be aware that the spacing and comma and the end of each line here is important. Next, navigate to this section:

    ports =
    {
        FTP_PORTS = FTP_PORTS,
        HTTP_PORTS = HTTP_PORTS,
        MAIL_PORTS = MAIL_PORTS,
        ORACLE_PORTS = ORACLE_PORTS,
        SIP_PORTS = SIP_PORTS,
        SSH_PORTS = SSH_PORTS,
        FILE_DATA_PORTS = FILE_DATA_PORTS,
    }
}

And immediately under FILE_DATA_PORTS = FILE_DATA_PORTS, add the lines:

        DNP3_PORTS = DNP3_PORTS,
        SHELLCODE_PORTS = SHELLCODE_PORTS,

Again, ensure that the spacing is correct, there is a comma after both the lines added, and that they are placed WITHIN the brackets correctly.

Save your modified copy of snort_defaults.lua, and try running snort3 again with the -T flag, like we did in the first post of this thread, and confirm that snort3 exits with zero errors and zero warnings.

At this point, you’ll need to enable/uncomment the rules in the scada.rules category, and follow the steps listed in the first post to either convert the scada.rules file separately via snort2lua, have it concatenated with the other rules from the rule category and re-convert the whole ruleset, or modify your rule management tool of choice, to provide a single unified snort rules file with the rules in the scada.rules file enabled.

All of this ground covered, and I still have a little bit more to cover with the scada_special rules, and changes to the DNP3 and ENIP/CIP preprocessors. Stay tuned.

Summary

The final post on this subject will cover some of the differences in the ICS preprocessors/inspectors supported by snort2.x, vs. those supported by Snort3, as well as difficulties and differences in rule options and their syntax that I encountered while trying to convert various snort 2.x ICS rules for use with Snort3.

About scada_special.rules…

As part of this procress of ensuring that our rules were able to be “translated” via snort2lua, we went through our ruleset, and identified rules that were not able to be processed correctly through an output file that snort2lua generates called “snort.rej”. At first we had somewhere in the neighborhood of 200 rules that required manual review.

Initially, the thought never crossed my mine to look at rules that are still in the ET ruleset, but are disabled by default. One sed -i s/^#alert/alert/g et_all.rules later, and we had all of the rules enabled, and about another 100 or so rules that needed tending to.

Whenever we disable rules in the ETOPEN/ETPRO rulesets that are not in the disabled category, we usually do so for rules that we believe are extremely useful, but are computationally expensive, or may be prone to false positives, etc… So we leave it as an exercise to the user community and their rule management tools to decide what rules they want to enable. But don’t take my word for it, check out our FAQ.

For an example of this, in the ETPRO HUNTING category, we have a series of rules to identify java deserialization/RCE gadgets (Common Java RCE Gadgets Observed), but we leave them disabled by default because currently, there is no way to use content matches that are decoded using base64_decode and base64_data in a fast_pattern match, so we don’t have a strong fast_pattern candidate for these rules, and so they end up being “checked” in traffic frequently, which eats CPU time, and causes performance problems if they are enabled. But if you host java web applications on your network, (which, who doesn’t these days) you might see the value of enabling them.

Back to the topic at hand, a significant number of these disabled by default rules were from the ETPRO scada_special rule category, as well as some ETPRO rules in the scada category. Aside from the IP address and Port variable problems that we solved in the previous post, some of the disabled rules also used specific preprocessor options and modifiers for their rules. Additionally, these rules didn’t really have any strong fast_pattern candidates, either. Which is likely a contributing factor towards most of them being disabled by default.

Snort2lua didn’t like a vast portion of these rules, because the syntax for these options have changed a bit, and in some cases, some of the options have disappeared entirely, or have been renamed to something else.

I’ve gone through the process of analyzing the rules manually updating them for use with the new syntax and options Snort3 uses for its ICS preprocessors, however:

  • They are all ETPRO rules
  • As of right now, we do not have an official pipeline for distributing Snort3 rules. But we are addressing this problem, a pipeline for this process is being built, and gears are turning behind the scenes.

So instead, if ICS networks are your concern, and you need scada_special rules, or need to know how to write your own ICS rules for Snort3, here is a little primer on the changes I encountered, and problems to be aware of for writing (or updating) your own ICS rules.

Snort2.x, Snort3, and their ICS Preprocessors and features

Snort 2.x has both a modbus and DNP3 preprocessor. By default, the modbus preprocessor analyzes traffic on port 502/tcp, while DNP3 defaults to port 20000.

Modbus

Snort 2.x provides a read me file for their preprocessors, and the rule options they enable. In this case, README.modbus.

For the most part, we had no problems processing MODBUS rules in the ET/ETPRO ruleset. Our rules don’t really make use of the MODBUS rule options Which are:

  • modbus_func: accepts a modbus function code between 0 and 255, or accepts a string that respresents the function of a modbus function code. For example:

modbus_func:read_coils;
modbus_func:1;

Mean the same thing, we’re looking at modbus traffic over port 502/tcp, for the modbus function code that shows a read coils request.

  • modbus_unit : a number between 0 and 255 that represents the modbus unit field in the modbus protocol header.

modbus_unit:137;

  • modbus_data: according to the documentation, this option sets the detection pointer to the modbus data portion of a modbus request/response. According to the syntax, this looks like it may be a sticky buffer.

modbus_data; content:"ayylmao";

would look for the string ayylmao in the data portion of a modbus request/response.

For snort3, it looks like this preprocessor hasn’t changed much if at all. Something I will make note of is that Cisco has two different pages for information regarding their preprocessors/“inspectors”.

One reference is in the Snort3 rule writing guide. While the other is a Cisco webpage titled “Snort3 Inspector Reference”. Whats interesting is that the Snort3 Rule writing guide informs users that the modbus_func keyword accepts strings in place of some of the function codes (just like with snort2.x), but then tells readers “go look in the source code if you want to know what they are”, while the Inspector Reference page gives readers a nicely formatted table that shows what string maps to what modbus function code. This won’t be the last time you see me cite both of these resources. My personal advice is to take advantage of both of these documentation sources.

As another aside, as previously mentioned, Snort3 has automatic application layer protocol detection now. Based on their Wizard/Binder system. By default, Snort3 is configured to detect modbus traffic. What this means is that users can write rules using the modbus rule header option:

alert modbus any any -> any any (msg:"ayy"; sid:1000000; rev:1;)

DNP3

Just like with modbus, snort 2.x has a README.dnp3 file for specific DNP3 preprocessor features. The readme defines 4 DNP3 specific rule options:

  • dnp3_func : not unlike modbus, this option allows users to define either a string or a number between 0 and 255 that corresponds to a DNP3 function.

dnp3_func:read;
dnp3_func:1;

refer to the same function.

  • dnp3_ind: This option allows users to specify Internal Indicator flags present in a DNP3 Application Response Header. This option takes strings only.

dnp3_ind:need_time;

  • dnp3_obj: Refers to DNP3 object headers in requests/responses. This option appears to take two numbers between 0 and 255, separated by a comma. The first number is a group id number, while the second is a var id number.

dnp3_obj:50,1;

The example above (in the README.dnp3) refers to a DNP3 Date and Time object.

  • dnp3_data: The DNP3 protocol’s application data includes CRCs every 16 bytes. this rule content option (like modbus_data) allows users to write rules looking in the DNP3 application data portion of network traffic, without having to be cognizant of the CRC calculations every 16 bytes. It appears to be a sticky buffer.

dnp3_data; content:"ayy";

It appears in the distant past, when the DNP3 rules in the ET ruleset were written, some of these rule options were different, and some no longer exist. For example, instead of dnp3_func there was a similar rule option called dnp3_cmd_fc which was similar in nature. I also spotted a few rules with the options dnp3_cmd_ot and dnp3_resp_ot. These rule options no longer exist, and they do not map to any of the supported rule options for the Snort3 DNP3 inspector.

Aside from those problems, Just like with the modbus preprocessor, the Snort3 rule writing guide has some information about the various DNP3 preprocessor options, but then tells readers to go root around in the snort3 source files to see what strings are supported for the dnp3_func, and dnp3_ind rule options, while the Snort 3 Inspector Reference spells out all of the supported strings up front.

The most important difference between Snort 2.x and Snort3 is that the dnp3_obj rule option syntax has changed. Snort3 expects the following syntax:

dnp3_obj:group 50, var 1;

Snort 3 expects the keywords group and var to be placed before the obj group and var numbers respectively. The syntax presented in the Snort3 Inspector Reference is NOT correct.

ENIP/CIP

This is the last preprocessor I’m gonna be covering in this post, as it was the last one to give me trouble, while also being the most interesting. I’m relatively new member to the Emerging Threats team, so sometime looking at parts of the ruleset is like doing archeology. This was one of those times. In the not-so-distant past, Digital Bond led an effort to write an ENIP/CIP preprocessor for CIP (Common Industrial Protocol). Over time, updates to snort broke the preprocessor. For a while the preprocessor was continually maintained, but was eventually scrapped. Community members ported ENIP/CIP rules over to Suricata some time in 2017.

For some period of time, these ENIP/CIP rules that used the ENIP/CIP preprocessor options remained in the Emerging Threats ruleset, disabled by default, long after the preprocessor was gone. However with Snort3, the CIP Inspector/Preprocessor exists. Interestingly, the Snort 3 Rule writing guide states that the CIP inspector is disabled by default, but the stock snort.lua that ships with the snort3 source has both the correct service inspector and binder configuration options present to where it is enabled.

Once again, there are some discrepancies between the Snort 3 Inspector Reference, and the Snort 3 Rule writing guide regarding the features of the CIP Inspector. The Inspector Reference lists 8 rule options for the CIP Inspector, while the rule writing guide states that there are 11 – the 8 in the Inspector reference plus three more rule options specifically for enip – enip_command, enip_req and enip_resp.

The one option that gave me the most trouble, seeing as how it was present in the most snort 2.x rules I converted was the cip_service option.

In the old snort 2.x CIP preprocessor, the cip_service option expected a single numeric value between 0 and 127:

cip_service:78;

However with snort3, they’ve enabled the ability to use comparison logic with service numbers. Snort2lua failed to validate rules looking for a single cip_service number unless there was an = next to it like so:

cip_service: =78;
“We’re only looking for values where the CIP service is set to 78.”

However, using calling cip_service with no comparison symbols next to it will validate perfectly fine with snort3’s -T option, assuming the rest of the rule is syntactically correct. With the rules being able to use comparison logic, you can do things like this, if there are multiple service codes that mean roughly the same thing:

cip_service: 77<=>78
“We’re only looking for values where the CIP service is less than or equal to 78, and greater than or equal to 77.”

The Snort 3 rule writing guide entry for the CIP Inspector provides a whole list of range/comparison operations that this rule option supports.

A lot of the other CIP Inspector rule options, such as cip_attribute, cip_class, cip_conn_path_class, cip_status, and enip_command. The only major difference between these various options are the maximum number ranges they support:

cip_attribute: 0-65535
cip_class: 0-65535
cip_conn_path_class: 0-65535
cip_instance: 0-4294967295
cip_service: 0-127
cip_status: 0-255
enip_command: 0-65535

The remaining options for the inspector are:
cip_req: match on CIP request packets.
cip_rsp: match on CIP response packets.
enip_req: match on ENIP request packets.
enip_rsp: match on ENIP response packets.

None of the options above take any arguments.

Conclusion

That about covers it so far. Bear in mind, this wasn’t meant to be a comprehensive guide to Snort2.x’s or Snort3’s ICS protocol preprocessors, but to cover some of the issues I ran into specifically with ICS rules during the process of making our snort 2.9.x ruleset compatible with snort3 via snort2lua.

You might be interesting in reviewing some of the different Inspector options and/or preprocessor alerts, as well as taking a look at the IEC 104 and S7comm Inspectors as well. Until next time, happy hunting!

1 Like