Imagine that you are battling a known threat actor. You have gathered indicators of compromise (IOCs) from reversing malware as well as helpful contributions from the rest of the security community. But how could those IOCs be tasked across your existing data quickly in order to track attacker movement in real-time? Here is one possible solution:
- Use a lookup file
- Clever Splunk search
- Even more clever dashboard
Figure 1: Known IOC Dashboard provided at the end of the article |
Lookup File
We used the following process to create a lookup file and definition. Create a file in excel and save it as a CSV called known_iocs.csv (similar to the file below).Figure 2: CSV that we initially populated with our IOCs |
Then within Splunk, navigate to the following to create the lookup and the definition:
Settings > Lookups > Lookup table files > Add new
- Destination app: Select the app
- Upload a lookup file: known_iocs.csv
- Destination filename: known_iocs.csv
Settings > Lookups > Lookup definitions > Add new
- Destination app: Select the app
- Name: known_iocs.csv
- Type: File-based
- Lookup file: known_iocs.csv
Now, here is the problem. How do you scale this solution to a group effort to update a lookup table with IOCs? It does not work well to pass the CSV around and then constantly upload. Enter another graceful solution from Luke Murphey -- The Lookup File Editor Splunk App (https://splunkbase.splunk.com/app/1724/).
Figure 3: Lookup File Editor App from Luke Murphey |
Once the Lookup File Editor Splunk App is installed, navigate to it, search for your known_ioc.csv file. Open it and right click on the bottom line and "Insert a new row". You can edit the lookup file right in Splunk. Once it is saved, the correlation searches will automatically run with the new IOC data.
Figure 4: Inserting a new line to our known_ioc.csv file |
Clever Search
Now that we have a lookup table that has our IOCs in it and a convenient way to edit it, we just need a search that will apply the IOCs to our data. The example below applies the IOCs to the cylance_protect index, but feel free to change the index name as needed. Additionally, we show how to search just one column of the IOC data as well as multiple columns.
One type of IOC (Hash):
index=cylance_protect [|inputlookup known_iocs.csv | rename Hash as query | table query] | stats count
Two types of IOCs (Hash & FileName)
index=cylance_protect [|inputlookup known_iocs.csv | rename Hash as query | table query] OR [|inputlookup known_iocs.csv | rename FileName as query | table query] | stats count
Note the OR statement between the two inputlookups -- needed when querying multiple columns.
One type of IOC (Hash):
index=cylance_protect [|inputlookup known_iocs.csv | rename Hash as query | table query] | stats count
Two types of IOCs (Hash & FileName)
index=cylance_protect [|inputlookup known_iocs.csv | rename Hash as query | table query] OR [|inputlookup known_iocs.csv | rename FileName as query | table query] | stats count
Note the OR statement between the two inputlookups -- needed when querying multiple columns.
Figure 5: What will be our top panels showing a count of the hits |
Even More Clever Dashboard
Now that we have functional searches, we need a dashboard to monitor our different data feeds such as:
You can see in the screenshot below that we use Single Value panels on the top row. Each of these panels contains a dynamic drilldown to populate the panel below it with the contents of the Single Value panel when clicked.
The drilldown for each Single Value panel sets a token which is essentially the search, but without the stats count (feel free to table the data as needed):
<drilldown>
<set token="alert">index=proxy $wild$ [|inputlookup known_iocs.csv | rename Domain as query | table query] | table _raw</set>
</drilldown>
Then the bottom panel is just a search of the token set in the drilldown above.
<search>
<query>| search $alert$</query>
<earliest>$time.earliest$</earliest>
<latest>$time.latest$</latest>
</search>
- Proxy
- Firewalls
- DNS
- Antivirus Hits
- Email Protection
- Windows Event Logs
You can see in the screenshot below that we use Single Value panels on the top row. Each of these panels contains a dynamic drilldown to populate the panel below it with the contents of the Single Value panel when clicked.
Figure 6: Dashboard displayed at the start of the article and in the Sample Dashboard section below |
The drilldown for each Single Value panel sets a token which is essentially the search, but without the stats count (feel free to table the data as needed):
<drilldown>
<set token="alert">index=proxy $wild$ [|inputlookup known_iocs.csv | rename Domain as query | table query] | table _raw</set>
</drilldown>
Then the bottom panel is just a search of the token set in the drilldown above.
<search>
<query>| search $alert$</query>
<earliest>$time.earliest$</earliest>
<latest>$time.latest$</latest>
</search>
Conclusion
Using a clever combination of features that already exist within Splunk (for the most part), we were able to create a quick method to update an IOC list and apply it against existing data within Splunk. Simply monitor these dashboards and use it to track the attacker's activities in real-time.
Sample Dashboard
The sample dashboard below uses a number of indexes to search over different data feeds. Just change these indexes to the ones you are interested in monitoring.
<form>
<label>Known IOC Hits</label>
<description>Threat Actor</description>
<fieldset submitButton="true">
<input type="time" searchWhenChanged="true" token="time">
<label>Time Range</label>
<default>
<earliest>-24h@h</earliest>
<latest>now</latest>
</default>
</input>
<input type="text" searchWhenChanged="true" token="wild">
<label>Wildcard Search</label>
<default>*</default>
</input>
</fieldset>
<row>
<panel>
<single>
<title>Proxy</title>
<search>
<query>index=proxy $wild$ [|inputlookup known_iocs.csv | rename Domain as query | table query] | stats count</query>
<earliest>$time.earliest$</earliest>
<latest>$time.latest$</latest>
</search>
<option name="colorMode">none</option>
<option name="drilldown">all</option>
<option name="rangeColors">["0x65a637","0xd93f3c"]</option>
<option name="rangeValues">[0]</option>
<option name="useColors">1</option>
<drilldown>
<set token="alert">index=proxy $wild$ [|inputlookup known_iocs.csv | rename Domain as query | table query] | table _raw</set>
</drilldown>
</single>
</panel>
<panel>
<single>
<title>Firewalls</title>
<search>
<query>index=firewalls $wild$ [|inputlookup known_iocs.csv | rename IP as query | table query] | stats count</query>
<earliest>$time.earliest$</earliest>
<latest>$time.latest$</latest>
</search>
<option name="drilldown">all</option>
<option name="rangeColors">["0x65a637","0xd93f3c"]</option>
<option name="rangeValues">[0]</option>
<option name="refresh.display">progressbar</option>
<option name="useColors">1</option>
<drilldown>
<set token="alert">index=firewalls $wild$ [|inputlookup known_iocs.csv | rename IP as query | table query] | table _raw</set>
</drilldown>
</single>
</panel>
<panel>
<single>
<title>DNS</title>
<search>
<query>index=dns $wild$ [|inputlookup known_iocs.csv | rename Domain as query | table query] | stats count</query>
<earliest>$time.earliest$</earliest>
<latest>$time.latest$</latest>
</search>
<option name="drilldown">all</option>
<option name="rangeColors">["0x65a637","0xd93f3c"]</option>
<option name="rangeValues">[0]</option>
<option name="useColors">1</option>
<drilldown>
<set token="alert">index=dns $wild$ [|inputlookup known_iocs.csv | rename Domain as query | table query] | table _raw</set>
</drilldown>
</single>
</panel>
<panel>
<single>
<title>Antivirus Hits</title>
<search>
<query>index=av $wild$ [|inputlookup known_iocs.csv | rename Hash as query | table query] | stats count</query>
<earliest>$time.earliest$</earliest>
<latest>$time.latest$</latest>
</search>
<option name="drilldown">all</option>
<option name="rangeColors">["0x65a637","0xd93f3c"]</option>
<option name="rangeValues">[0]</option>
<option name="useColors">1</option>
<drilldown>
<set token="alert">index=av $wild$ [|inputlookup known_iocs.csv | rename Hash as query | table query] | table _raw</set>
</drilldown>
</single>
</panel>
<panel>
<single>
<title>Email Protection</title>
<search>
<query>index=mail_protection $wild$ [|inputlookup known_iocs.csv | rename Domain as query | table query] | stats count</query>
<earliest>$time.earliest$</earliest>
<latest>$time.latest$</latest>
</search>
<option name="drilldown">all</option>
<option name="rangeColors">["0x65a637","0xd93f3c"]</option>
<option name="rangeValues">[0]</option>
<option name="useColors">1</option>
<drilldown>
<set token="alert">index=mail_protection $wild$ [|inputlookup known_iocs.csv | rename Domain as query | table query] | table _raw</set>
</drilldown>
</single>
</panel>
</row>
<row>
<panel>
<title>Information Table (Click one of the numbers above to populate this table with Details)</title>
<table>
<search>
<query>| search $alert$</query>
<earliest>$time.earliest$</earliest>
<latest>$time.latest$</latest>
</search>
<option name="drilldown">cell</option>
</table>
</panel>
</row>
</form>
<form>
<label>Known IOC Hits</label>
<description>Threat Actor</description>
<fieldset submitButton="true">
<input type="time" searchWhenChanged="true" token="time">
<label>Time Range</label>
<default>
<earliest>-24h@h</earliest>
<latest>now</latest>
</default>
</input>
<input type="text" searchWhenChanged="true" token="wild">
<label>Wildcard Search</label>
<default>*</default>
</input>
</fieldset>
<row>
<panel>
<single>
<title>Proxy</title>
<search>
<query>index=proxy $wild$ [|inputlookup known_iocs.csv | rename Domain as query | table query] | stats count</query>
<earliest>$time.earliest$</earliest>
<latest>$time.latest$</latest>
</search>
<option name="colorMode">none</option>
<option name="drilldown">all</option>
<option name="rangeColors">["0x65a637","0xd93f3c"]</option>
<option name="rangeValues">[0]</option>
<option name="useColors">1</option>
<drilldown>
<set token="alert">index=proxy $wild$ [|inputlookup known_iocs.csv | rename Domain as query | table query] | table _raw</set>
</drilldown>
</single>
</panel>
<panel>
<single>
<title>Firewalls</title>
<search>
<query>index=firewalls $wild$ [|inputlookup known_iocs.csv | rename IP as query | table query] | stats count</query>
<earliest>$time.earliest$</earliest>
<latest>$time.latest$</latest>
</search>
<option name="drilldown">all</option>
<option name="rangeColors">["0x65a637","0xd93f3c"]</option>
<option name="rangeValues">[0]</option>
<option name="refresh.display">progressbar</option>
<option name="useColors">1</option>
<drilldown>
<set token="alert">index=firewalls $wild$ [|inputlookup known_iocs.csv | rename IP as query | table query] | table _raw</set>
</drilldown>
</single>
</panel>
<panel>
<single>
<title>DNS</title>
<search>
<query>index=dns $wild$ [|inputlookup known_iocs.csv | rename Domain as query | table query] | stats count</query>
<earliest>$time.earliest$</earliest>
<latest>$time.latest$</latest>
</search>
<option name="drilldown">all</option>
<option name="rangeColors">["0x65a637","0xd93f3c"]</option>
<option name="rangeValues">[0]</option>
<option name="useColors">1</option>
<drilldown>
<set token="alert">index=dns $wild$ [|inputlookup known_iocs.csv | rename Domain as query | table query] | table _raw</set>
</drilldown>
</single>
</panel>
<panel>
<single>
<title>Antivirus Hits</title>
<search>
<query>index=av $wild$ [|inputlookup known_iocs.csv | rename Hash as query | table query] | stats count</query>
<earliest>$time.earliest$</earliest>
<latest>$time.latest$</latest>
</search>
<option name="drilldown">all</option>
<option name="rangeColors">["0x65a637","0xd93f3c"]</option>
<option name="rangeValues">[0]</option>
<option name="useColors">1</option>
<drilldown>
<set token="alert">index=av $wild$ [|inputlookup known_iocs.csv | rename Hash as query | table query] | table _raw</set>
</drilldown>
</single>
</panel>
<panel>
<single>
<title>Email Protection</title>
<search>
<query>index=mail_protection $wild$ [|inputlookup known_iocs.csv | rename Domain as query | table query] | stats count</query>
<earliest>$time.earliest$</earliest>
<latest>$time.latest$</latest>
</search>
<option name="drilldown">all</option>
<option name="rangeColors">["0x65a637","0xd93f3c"]</option>
<option name="rangeValues">[0]</option>
<option name="useColors">1</option>
<drilldown>
<set token="alert">index=mail_protection $wild$ [|inputlookup known_iocs.csv | rename Domain as query | table query] | table _raw</set>
</drilldown>
</single>
</panel>
</row>
<row>
<panel>
<title>Information Table (Click one of the numbers above to populate this table with Details)</title>
<table>
<search>
<query>| search $alert$</query>
<earliest>$time.earliest$</earliest>
<latest>$time.latest$</latest>
</search>
<option name="drilldown">cell</option>
</table>
</panel>
</row>
</form>
Great stuff! Thank you!
ReplyDeleteThanks for the feedback!
DeleteThanks for this post, it helped me a lot. I am just wondering, If I am getting the IOCS from the Threat Intelligence, in this case , I am talking about IP types. Sometimes we have a lot of positives, right? because some ips could be webhosting service providers. So, How Could we remove these false positives and avoid those IPs come back when we do the append with the threat intelligence again?
ReplyDeleteGreat question. The longevity of an IP address is much less than a domain name. If the IP address is rather unique and hardcoded within the RAT/c2, then it might be worth adding. However, if it maps back to a CDN or cloud provider it might result in FPs. If this is the case, simply remove it from the list. Consider using domain names over IPs if possible.
Deleteindex=protect
ReplyDelete[| inputlookup your_ioc.csv
| table File_Hash]
| dedup File_Hash | table _time ,File_Hash,file_path,DeviceName,UserName,sourcetype