Skip to content

Commit 109d7ec

Browse files
lockboxlacraig2
authored andcommitted
feat: KmodTracker has allow/deny list
1 parent bee0ec2 commit 109d7ec

File tree

1 file changed

+238
-27
lines changed

1 file changed

+238
-27
lines changed

pyplugins/interventions/kmods.py

Lines changed: 238 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,272 @@
1+
"""
2+
# Kernel Module Tracker
3+
4+
This plugin tracks and controls kernel module loading attempts in the guest system.
5+
By default, it blocks all kernel module loading except for igloo.ko (the internal
6+
framework module). Optionally, an allowlist can be configured to allow specific
7+
kernel modules to load, or a denylist to explicitly block specific modules.
8+
9+
## Features
10+
11+
- Intercepts `init_module` and `finit_module` syscalls
12+
- Tracks all kernel module loading attempts to `modules.log`
13+
- Blocks module loading (except igloo.ko) by default
14+
- Supports allowlist for specific modules to allow them to load
15+
- Supports denylist for explicit blocking of specific modules
16+
17+
## Configuration
18+
19+
To enable the plugin with default behavior (block all except igloo.ko):
20+
21+
```yaml
22+
plugins:
23+
kmods: {}
24+
```
25+
26+
To allow specific modules to load, provide an allowlist:
27+
28+
```yaml
29+
plugins:
30+
kmods:
31+
allowlist:
32+
- wireguard
33+
- nf_conntrack
34+
- xt_TCPMSS
35+
```
36+
37+
To explicitly block specific modules, provide a denylist:
38+
39+
```yaml
40+
plugins:
41+
kmods:
42+
denylist:
43+
- suspicious_module
44+
- untrusted_driver
45+
```
46+
47+
To reduce logging verbosity, enable quiet mode:
48+
49+
```yaml
50+
plugins:
51+
kmods:
52+
quiet: true
53+
```
54+
55+
Both lists can be used together. Denylist takes precedence over allowlist.
56+
Module names should not include the `.ko` extension or path.
57+
When `quiet` is set to `true`, only errors are logged; otherwise, info-level logs are shown (default).
58+
59+
## Outputs
60+
61+
- `modules.log`: List of all kernel modules that were attempted to be loaded
62+
"""
63+
64+
import logging
65+
from typing import Optional
166
from penguin import plugins, Plugin
267

368

469
class KmodTracker(Plugin):
70+
"""
71+
Tracks and controls kernel module loading in the guest system.
72+
73+
This plugin intercepts kernel module loading syscalls and can either block
74+
them (default behavior), allow specific modules via allowlist, or explicitly
75+
block specific modules via denylist.
76+
77+
Attributes:
78+
allowlist (list): List of kernel module names allowed to load
79+
denylist (list): List of kernel module names to explicitly block
80+
quiet (bool): If True, set log level to error; if False, use info level
81+
"""
82+
83+
def __init__(self):
84+
"""Initialize the KmodTracker plugin and load configuration."""
85+
# Get allowlist of kernel modules that are allowed to be loaded
86+
self.allowlist = self.get_arg("allowlist") or []
87+
# Get denylist of kernel modules that are explicitly blocked
88+
self.denylist = self.get_arg("denylist") or []
89+
# Get quiet mode setting (defaults to False)
90+
self.quiet = self.get_arg("quiet") or False
91+
92+
# Set log level based on quiet mode
93+
if self.quiet:
94+
self.logger.setLevel(logging.ERROR)
95+
else:
96+
self.logger.setLevel(logging.INFO)
97+
98+
def _extract_module_name(self, kmod_path: str) -> Optional[str]:
99+
"""
100+
Extract the module name from the full path.
101+
102+
Args:
103+
kmod_path (str): Full path to the kernel module
104+
105+
Returns:
106+
Optional[str]: Module name without path or .ko extension,
107+
or None if kmod_path is empty
108+
"""
109+
if not kmod_path:
110+
return None
111+
112+
module_name = kmod_path.split('/')[-1]
113+
if module_name.endswith('.ko'):
114+
module_name = module_name[:-3]
115+
116+
return module_name
117+
118+
def is_allowed(self, kmod_path: str) -> bool:
119+
"""
120+
Check if a kernel module is in the allowlist.
121+
Extracts the module name from the path and checks against allowlist.
122+
123+
Args:
124+
kmod_path: Path to the kernel module (e.g., "/lib/modules/foo.ko")
125+
126+
Returns:
127+
True if the module is in the allowlist, False otherwise
128+
"""
129+
if not kmod_path:
130+
return False
131+
132+
# Extract module name from path (remove directory and .ko extension)
133+
module_name = self._extract_module_name(kmod_path)
134+
135+
return module_name in self.allowlist
136+
137+
def is_denied(self, kmod_path: str) -> bool:
138+
"""
139+
Check if a kernel module is in the denylist.
140+
Extracts the module name from the path and checks against denylist.
141+
142+
Args:
143+
kmod_path: Path to the kernel module (e.g., "/lib/modules/foo.ko")
5144
6-
def track_kmod(self, kmod_path):
145+
Returns:
146+
True if the module is in the denylist, False otherwise
7147
"""
8-
Track a kernel module by its path.
9-
This method can be used to monitor the loading and unloading of kernel modules.
148+
if not kmod_path:
149+
return False
150+
151+
# Extract module name from path (remove directory and .ko extension)
152+
module_name = self._extract_module_name(kmod_path)
153+
154+
return module_name in self.denylist
155+
156+
def track_kmod(self, kmod_path: str):
157+
"""
158+
Track a kernel module loading attempt by recording it to modules.log.
159+
160+
Args:
161+
kmod_path (str): Path to the kernel module being loaded
10162
"""
11163
self.logger.info(f"Tracking kernel module: {kmod_path}")
12164
with open(self.get_arg("outdir") + "/modules.log", "a") as f:
13165
f.write(f"{kmod_path}\n")
14166

15167
@plugins.syscalls.syscall("on_sys_init_module_enter")
16168
def init_module(self, regs, proto, syscall, module_image, size, param_values):
169+
"""
170+
Handle the init_module syscall to track and optionally block module loading.
171+
172+
This method intercepts attempts to load kernel modules via the init_module
173+
syscall. It always allows igloo.ko to load, tracks all other module loading
174+
attempts, and blocks modules unless they are allow-listed.
175+
176+
Args:
177+
regs: CPU register state
178+
proto: Syscall prototype
179+
syscall: Syscall object with retval and skip_syscall attributes
180+
module_image: Pointer to module image in memory
181+
size: Size of the module image
182+
param_values: Module parameters
183+
184+
Yields:
185+
Results from plugins.osi calls for process and file descriptor information
186+
"""
17187
# Determine if this is our module!
18188
args = yield from plugins.osi.get_args()
19189
igloo_mod_args = ['/igloo/utils/busybox', 'insmod', '/igloo/boot/igloo.ko']
20190
if args == igloo_mod_args:
21191
return
22192

23-
# We never allow actual module loading other than igloo.ko
24-
# So we just fake success here.
25-
syscall.retval = 0
26-
syscall.skip_syscall = True
27-
28-
# Analyze information to determine the module being loaded
193+
# Determine the module path
194+
kmod_path = None
29195

30196
# Check args for .ko file
31197
matching_ko = [arg for arg in args if arg.endswith('.ko')]
32198
if any(matching_ko):
33-
self.track_kmod(matching_ko[-1])
34-
return
35-
if any(arg for arg in args if arg.endswith('modprobe')):
199+
kmod_path = matching_ko[-1]
200+
elif any(arg for arg in args if arg.endswith('modprobe')):
36201
self.logger.info(f"modprobe detected, cannot determine module path from args: {args}")
202+
else:
203+
# Check open fds for .ko file
204+
fds = yield from plugins.osi.get_fds()
205+
for i in range(len(fds)):
206+
fdname = fds[i].name
207+
if fdname.endswith('.ko'):
208+
kmod_path = fdname
209+
break
210+
211+
if not kmod_path:
212+
self.logger.info(f"Could not determine kernel module path from args: {args} or fds: {fds}")
213+
214+
# Track the module
215+
if kmod_path:
216+
self.track_kmod(kmod_path)
217+
218+
# Check if module is explicitly denied (denylist takes precedence)
219+
if kmod_path and self.is_denied(kmod_path):
220+
self.logger.info(f"Blocking denied module: {kmod_path}")
221+
syscall.retval = 0
222+
syscall.skip_syscall = True
37223
return
38224

39-
# Check open fds for .ko file
40-
fds = yield from plugins.osi.get_fds()
41-
for i in range(len(fds)):
42-
fdname = fds[i].name
43-
if fdname.endswith('.ko'):
44-
self.track_kmod(fdname)
45-
return
225+
# Check if module is in allowlist
226+
if kmod_path and self.is_allowed(kmod_path):
227+
self.logger.info(f"Allowing module from allowlist to load: {kmod_path}")
228+
return
46229

47-
# We can give up here or try to read the module image from memory
48-
# for now we give up
49-
# module = yield from plugins.mem.read_bytes(module_image, size)
50-
self.logger.info(f"Could not determine kernel module path from args: {args} or fds: {fds}")
230+
# Block module loading by default (fake success)
231+
syscall.retval = 0
232+
syscall.skip_syscall = True
51233

52234
@plugins.syscalls.syscall("on_sys_finit_module_enter")
53235
def finit_module(self, regs, proto, syscall, fd, param_values, flags):
54-
# We never allow actual module loading other than igloo.ko
55-
# So we just fake success here.
56-
syscall.skip_syscall = True
57-
syscall.retval = 0
236+
"""
237+
Handle the finit_module syscall to track and optionally block module loading.
238+
239+
This method intercepts attempts to load kernel modules via the finit_module
240+
syscall (which loads modules from a file descriptor). It tracks all module
241+
loading attempts and blocks modules unless they are allow-listed.
242+
243+
Args:
244+
regs: CPU register state
245+
proto: Syscall prototype
246+
syscall: Syscall object with retval and skip_syscall attributes
247+
fd: File descriptor of the kernel module file
248+
param_values: Module parameters
249+
flags: Module loading flags
58250
251+
Yields:
252+
Results from plugins.osi.get_fd_name to retrieve the module path
253+
"""
59254
# Analyze information to determine the module path
60255
fdname = yield from plugins.osi.get_fd_name(fd)
61256
self.track_kmod(fdname)
257+
258+
# Check if module is explicitly denied (denylist takes precedence)
259+
if self.is_denied(fdname):
260+
self.logger.info(f"Blocking denied module: {fdname}")
261+
syscall.skip_syscall = True
262+
syscall.retval = 0
263+
return
264+
265+
# Check if module is in allowlist
266+
if self.is_allowed(fdname):
267+
self.logger.info(f"Allowing module from allowlist to load: {fdname}")
268+
return
269+
270+
# Block module loading by default (fake success)
271+
syscall.skip_syscall = True
272+
syscall.retval = 0

0 commit comments

Comments
 (0)