Skip to content

Commit 3bc38a4

Browse files
authored
Merge pull request #1115 from stratosphereips/alya/fix_detecting_the_gw_in_pcaps
Detect GW ip and mac when analysing files by using the dst mac of outgoing traffic
2 parents 1a0b311 + 5ae34f0 commit 3bc38a4

File tree

8 files changed

+272
-22
lines changed

8 files changed

+272
-22
lines changed

docs/contributing.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,14 @@ Evidence Handler is the only process that stops but keeps waiting in memory for
161161
Once all modules are done processing, EvidenceHandler is killed by the Process manager.
162162

163163

164-
### How the tests work?
164+
### How does the tests work?
165165

166166
- Running the tests locally should be done using ./tests/run_all_tests.sh
167167
- It runs the unit tests first, then the integration tests.
168168
- Please get familiar with pytest first https://docs.pytest.org/en/stable/how-to/output.html
169+
170+
### Where and how do we get the GW info?
171+
172+
Using one of these 3 ways
173+
174+
<img src="https://raw.githubusercontent.com/stratosphereips/StratosphereLinuxIPS/develop/docs/images/gw_info.jpg"

modules/ip_info/ip_info.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import platform
2-
from typing import Union
2+
from typing import (
3+
Union,
4+
Optional,
5+
)
36
from uuid import uuid4
47
import datetime
58
import maxminddb
@@ -58,7 +61,6 @@ def init(self):
5861
"new_dns": self.c3,
5962
"check_jarm_hash": self.c4,
6063
}
61-
self.is_gw_mac_set = False
6264
self.whitelist = Whitelist(self.logger, self.db)
6365
self.is_running_non_stop: bool = self.db.is_running_non_stop()
6466
self.valid_tlds = whois.validTlds()
@@ -307,7 +309,7 @@ async def shutdown_gracefully(self):
307309
await self.reading_mac_db_task
308310

309311
# GW
310-
def get_gateway_ip(self):
312+
def get_gateway_ip_if_interface(self):
311313
"""
312314
Slips tries different ways to get the ip of the default gateway
313315
this method tries to get the default gateway IP address using ip route
@@ -338,7 +340,7 @@ def get_gateway_ip(self):
338340
gw_ip = route_default_result[2]
339341
return gw_ip
340342

341-
def get_gateway_mac(self, gw_ip: str):
343+
def get_gateway_mac(self, gw_ip: str) -> Optional[str]:
342344
"""
343345
Given the gw_ip, this function tries to get the MAC
344346
from arp.log or from arp tables
@@ -352,6 +354,7 @@ def get_gateway_mac(self, gw_ip: str):
352354
return gw_mac
353355

354356
if not self.is_running_non_stop:
357+
# running on pcap or a given zeek file/dir
355358
# no MAC in arp.log (in the db) and can't use arp tables,
356359
# so it's up to the db.is_gw_mac() function to determine the gw mac
357360
# if it's seen associated with a public IP
@@ -492,9 +495,14 @@ def pre_main(self):
492495
utils.drop_root_privs()
493496
self.wait_for_dbs()
494497
# the following method only works when running on an interface
495-
if ip := self.get_gateway_ip():
498+
if ip := self.get_gateway_ip_if_interface():
496499
self.db.set_default_gateway("IP", ip)
497500

501+
# whether we found the gw ip using dhcp in profiler
502+
# or using ip route using self.get_gateway_ip()
503+
# now that it's found, get and store the mac addr of it
504+
self.get_gateway_mac(ip)
505+
498506
def handle_new_ip(self, ip: str):
499507
try:
500508
# make sure its a valid ip
@@ -531,16 +539,6 @@ async def main(self):
531539

532540
self.get_vendor(mac_addr, profileid)
533541
self.check_if_we_have_pending_offline_mac_queries()
534-
# set the gw mac and ip if they're not set yet
535-
if not self.is_gw_mac_set:
536-
# whether we found the gw ip using dhcp in profiler
537-
# or using ip route using self.get_gateway_ip()
538-
# now that it's found, get and store the mac addr of it
539-
if ip := self.db.get_gateway_ip():
540-
# now that we know the GW IP address,
541-
# try to get the MAC of this IP (of the gw)
542-
self.get_gateway_mac(ip)
543-
self.is_gw_mac_set = True
544542

545543
if msg := self.get_msg("new_dns"):
546544
msg = json.loads(msg["data"])

slips_files/core/database/redis_db/database.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1539,7 +1539,10 @@ def increment_processed_flows(self):
15391539
return self.r.incr(self.constants.PROCESSED_FLOWS, 1)
15401540

15411541
def get_processed_flows_so_far(self) -> int:
1542-
return int(self.r.get(self.constants.PROCESSED_FLOWS))
1542+
processed_flows = self.r.get(self.constants.PROCESSED_FLOWS)
1543+
if not processed_flows:
1544+
return 0
1545+
return int(processed_flows)
15431546

15441547
def store_std_file(self, **kwargs):
15451548
"""

slips_files/core/helpers/flow_handler.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ def new_software(self, profileid, flow):
6060

6161

6262
class FlowHandler:
63+
"""
64+
Each flow seen by slips will be a different instance of this class
65+
"""
66+
6367
def __init__(self, db, symbol_handler, flow):
6468
self.db = db
6569
self.publisher = Publisher(self.db)
@@ -161,9 +165,18 @@ def handle_notice(self):
161165
self.db.add_out_notice(self.profileid, self.twid, self.flow)
162166

163167
if "Gateway_addr_identified" in self.flow.note:
164-
# get the gw addr form the msg
165-
gw_addr = self.flow.msg.split(": ")[-1].strip()
166-
self.db.set_default_gateway("IP", gw_addr)
168+
# foirst check if the gw ip and mac are set by
169+
# profiler.get_gateway_info() or ip_info module
170+
gw_ip = False
171+
if not self.db.get_gateway_ip():
172+
# get the gw addr from the msg
173+
gw_ip = self.flow.msg.split(": ")[-1].strip()
174+
self.db.set_default_gateway("IP", gw_ip)
175+
176+
if not self.db.get_gateway_mac() and gw_ip:
177+
gw_mac = self.db.get_mac_addr_from_profile(f"profile_{gw_ip}")
178+
if gw_mac:
179+
self.db.set_default_gateway("MAC", gw_mac)
167180

168181
self.db.add_altflow(self.flow, self.profileid, self.twid, "benign")
169182

slips_files/core/profiler.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# modify it under the terms of the GNU General Public License
55
# as published by the Free Software Foundation; either version 2
66
# of the License, or (at your option) any later version.
7+
import json
78

89
# This program is distributed in the hope that it will be useful,
910
# but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -22,6 +23,7 @@
2223
import multiprocessing
2324
from typing import (
2425
List,
26+
Optional,
2527
)
2628

2729
import validators
@@ -95,6 +97,8 @@ def init(
9597
# is set by this proc to tell input proc that we are done
9698
# processing and it can exit no issue
9799
self.is_profiler_done_event = is_profiler_done_event
100+
self.gw_mac = None
101+
self.gw_ip = None
98102

99103
def read_configuration(self):
100104
conf = ConfigParser()
@@ -140,6 +144,87 @@ def get_rev_profile(self):
140144
)
141145
return rev_profileid, rev_twid
142146

147+
def get_gw_ip_using_gw_mac(self) -> Optional[str]:
148+
"""
149+
gets the ip of the given mac from the db
150+
prioritizes returning the ipv4. if not found, the function returns
151+
the ipv6. or none if both are not found.
152+
"""
153+
# the db returns a serialized list of IPs belonging to this mac
154+
gw_ips: str = self.db.get_ip_of_mac(self.gw_mac)
155+
156+
if not gw_ips:
157+
return
158+
159+
gw_ips: List[str] = json.loads(gw_ips)
160+
# try to get the ipv4 if found in that list
161+
for ip in gw_ips:
162+
try:
163+
ipaddress.IPv4Address(ip)
164+
return ip
165+
except ipaddress.AddressValueError:
166+
continue
167+
168+
# all of them are ipv6, return the first
169+
return gw_ips[0]
170+
171+
def is_gw_info_detected(self, info_type: str) -> bool:
172+
"""
173+
checks own attributes and the db for the gw mac/ip
174+
:param info_type: can be 'mac' or 'ip'
175+
"""
176+
info_mapping = {
177+
"mac": ("gw_mac", self.db.get_gateway_mac),
178+
"ip": ("gw_ip", self.db.get_gateway_ip),
179+
}
180+
181+
if info_type not in info_mapping:
182+
raise ValueError(f"Unsupported info_type: {info_type}")
183+
184+
attr, check_db_method = info_mapping[info_type]
185+
186+
if getattr(self, attr):
187+
# the reason we don't just check the db is we don't want a db
188+
# call per each flow
189+
return True
190+
191+
# did some other module manage to get it?
192+
if info := check_db_method():
193+
setattr(self, attr, info)
194+
return True
195+
196+
return False
197+
198+
def get_gateway_info(self):
199+
"""
200+
Gets the IP and MAC of the gateway and stores them in the db
201+
202+
usually the mac of the flow going from a private ip -> a
203+
public ip is the mac of the GW
204+
"""
205+
gw_mac_found: bool = self.is_gw_info_detected("mac")
206+
if not gw_mac_found:
207+
if utils.is_private_ip(
208+
self.flow.saddr
209+
) and not utils.is_ignored_ip(self.flow.daddr):
210+
self.gw_mac: str = self.flow.dmac
211+
self.db.set_default_gateway("MAC", self.gw_mac)
212+
self.print(
213+
f"MAC address of the gateway detected: "
214+
f"{green(self.gw_mac)}"
215+
)
216+
gw_mac_found = True
217+
218+
# we need the mac to be set to be able to find the ip using it
219+
if not self.is_gw_info_detected("ip") and gw_mac_found:
220+
self.gw_ip: Optional[str] = self.get_gw_ip_using_gw_mac()
221+
if self.gw_ip:
222+
self.db.set_default_gateway("IP", self.gw_ip)
223+
self.print(
224+
f"IP address of the gateway detected: "
225+
f"{green(self.gw_ip)}"
226+
)
227+
143228
def add_flow_to_profile(self):
144229
"""
145230
This is the main function that takes the columns of a flow
@@ -170,6 +255,8 @@ def add_flow_to_profile(self):
170255
# software and weird.log flows are allowed to not have a daddr
171256
return False
172257

258+
self.get_gateway_info()
259+
173260
# Check if the flow is whitelisted and we should not process it
174261
if self.whitelist.is_whitelisted_flow(self.flow):
175262
return True

tests/test_flow_handler.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,12 +280,17 @@ def test_handle_notice(flow):
280280
flow.note = "Gateway_addr_identified: 192.168.1.1"
281281
flow.msg = "Gateway_addr_identified: 192.168.1.1"
282282

283+
flow_handler.db.get_gateway_ip.return_value = False
284+
flow_handler.db.get_gateway_mac.return_value = False
285+
flow_handler.db.get_mac_addr_from_profile.return_value = "xyz"
286+
283287
flow_handler.handle_notice()
284288

285289
flow_handler.db.add_out_notice.assert_called_with(
286290
flow_handler.profileid, flow_handler.twid, flow
287291
)
288-
flow_handler.db.set_default_gateway.assert_called_with("IP", "192.168.1.1")
292+
flow_handler.db.set_default_gateway.assert_any_call("IP", "192.168.1.1")
293+
flow_handler.db.set_default_gateway.assert_any_call("MAC", "xyz")
289294
flow_handler.db.add_altflow.assert_called_with(
290295
flow, flow_handler.profileid, flow_handler.twid, "benign"
291296
)

tests/test_ip_info.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ def test_get_gateway_ip(
421421
mocker.patch("platform.system", return_value=platform_system)
422422
mocker.patch("subprocess.check_output", return_value=subprocess_output)
423423
mocker.patch("sys.argv", ["-i", "eth0"])
424-
result = ip_info.get_gateway_ip()
424+
result = ip_info.get_gateway_ip_if_interface()
425425
assert result == expected_ip
426426

427427

0 commit comments

Comments
 (0)