vivarium 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +54 -5
- data/examples/execve_demo.rb +49 -0
- data/examples/file_operation_demo.rb +68 -0
- data/examples/privilege_event_demo.rb +38 -0
- data/examples/signal_kill_demo.rb +38 -0
- data/lib/vivarium/logger.rb +14 -2
- data/lib/vivarium/version.rb +1 -1
- data/lib/vivarium.rb +687 -2
- data/sig/vivarium.rbs +17 -0
- metadata +19 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 973df86f00b8e2ca9d5e963958562e2f58822e13c0e4d33af53e85a8d38f12be
|
|
4
|
+
data.tar.gz: a5651655ef69c10e6b6eab89baf9f98e5d99a158a8e89a6fc940fe2d844ab636
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3606b1f6df38a813e2b0cdf48aef48312be1a7c0bd3f20f88ca8a4c527c724733184c1c0d52f39fc9242282ab6d41ca18f6bcc977f4cafad0c5d4faa17a27d78
|
|
7
|
+
data.tar.gz: 4171eefdd7052d8634cd5a49267c8fe4f925e87f5b18691cb0721dd3f014efac68475196762e9ecc800627bcc529d9077e088b7d0366d91a6252867d3a0fc5f1
|
data/README.md
CHANGED
|
@@ -18,9 +18,20 @@ The goal is to visualize which Ruby method context triggered low-level events.
|
|
|
18
18
|
Implemented in this repository:
|
|
19
19
|
|
|
20
20
|
- BPF LSM hook on `file_open`
|
|
21
|
+
- BPF LSM hooks on `inode_symlink`, `inode_link`, `inode_rename`, `path_chmod`
|
|
22
|
+
- BPF tracepoint on `sys_enter_getdents64`
|
|
23
|
+
- BPF tracepoint on `sys_enter_execve` (captures executable path and first few argv entries as `proc_exec`)
|
|
24
|
+
- BPF LSM hooks for suspicious behavior checks:
|
|
25
|
+
- `ptrace_access_check` (emits `ptrace_check`)
|
|
26
|
+
- `sb_mount` (emits `sb_mount`)
|
|
27
|
+
- `kernel_read_file` (emits `kernel_read_file`)
|
|
28
|
+
- `task_kill` (emits `task_kill`)
|
|
29
|
+
- `task_fix_setuid` (emits `setid_change`)
|
|
30
|
+
- `capable` for high-risk capabilities only (emits `capable_check`)
|
|
31
|
+
- `bprm_creds_from_file` (emits `bprm_creds`)
|
|
21
32
|
- BPF LSM hook on `socket_create` (flags unusual socket creation as `odd_socket`)
|
|
22
33
|
- BPF LSM hook on `socket_connect` (captures destination family/address/port as `sock_connect`)
|
|
23
|
-
- BPF
|
|
34
|
+
- BPF tracepoints on `sys_enter_sendmsg`, `sys_enter_sendto`, `sys_enter_sendmmsg` (capture UDP/53 DNS QNAME raw bytes as `dns_req`)
|
|
24
35
|
- Shared pinned maps on bpffs
|
|
25
36
|
- `config_root_targets` (root PID -> 0/1)
|
|
26
37
|
- `config_spawned_targets` (spawned TID -> 0/1)
|
|
@@ -38,6 +49,7 @@ Implemented in this repository:
|
|
|
38
49
|
|
|
39
50
|
```c
|
|
40
51
|
struct event_t {
|
|
52
|
+
u64 ktime_ns;
|
|
41
53
|
u32 pid;
|
|
42
54
|
char event_name[16];
|
|
43
55
|
char payload[256];
|
|
@@ -48,7 +60,7 @@ struct event_t {
|
|
|
48
60
|
|
|
49
61
|
- Linux kernel/environment supporting BPF LSM
|
|
50
62
|
- `libbcc` installed
|
|
51
|
-
- `bpftool` installed (used to resolve `struct file::f_path`
|
|
63
|
+
- `bpftool` installed (used to resolve `struct file::f_path` and `struct dentry::d_name` offsets from BTF)
|
|
52
64
|
- root privileges for `vivariumd`
|
|
53
65
|
- bpffs mounted (typically `/sys/fs/bpf`)
|
|
54
66
|
|
|
@@ -92,6 +104,38 @@ bundle exec ruby examples/network_client_demo.rb
|
|
|
92
104
|
|
|
93
105
|
This demo intentionally triggers `sock_connect`, `dns_req`, and `odd_socket` events.
|
|
94
106
|
|
|
107
|
+
4) File operation demo client (only touches `/tmp`):
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
bundle exec ruby examples/file_operation_demo.rb
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
This demo intentionally triggers `path_open`, `file_symlink`, `file_hardlink`, `file_rename`, `file_chmod`, and `file_getdents` events under `/tmp`.
|
|
114
|
+
|
|
115
|
+
5) Execve demo client:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
bundle exec ruby examples/execve_demo.rb
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
This demo intentionally triggers `proc_exec` with several argument patterns using direct `execve`-style process launches.
|
|
122
|
+
|
|
123
|
+
6) Signal demo client:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
bundle exec ruby examples/signal_kill_demo.rb
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
This demo forks a child process and sends `TERM` with `Process.kill`, which is useful for triggering `task_kill`.
|
|
130
|
+
|
|
131
|
+
7) Privilege-related event demo client:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
bundle exec ruby examples/privilege_event_demo.rb
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This demo attempts setuid/setgid changes, sensitive file access, and `sudo` exec to trigger privilege-related events such as `setid_change`, `capable_check`, and `bprm_creds`.
|
|
138
|
+
|
|
95
139
|
You can also start top-level observation without a block (it keeps observing until process exit):
|
|
96
140
|
|
|
97
141
|
```ruby
|
|
@@ -135,11 +179,16 @@ bundle exec vivariumd --pin-dir /sys/fs/bpf/vivarium
|
|
|
135
179
|
## Notes
|
|
136
180
|
|
|
137
181
|
- Thread/Ractor-awareness is not yet implemented.
|
|
138
|
-
- `event_invoked` uses fixed
|
|
139
|
-
-
|
|
182
|
+
- `event_invoked` uses fixed 1024 slots and wraps around when full.
|
|
183
|
+
- `payload` is 256 bytes in `event_t`; some event types intentionally use smaller structured slices inside that buffer.
|
|
184
|
+
- `proc_exec` currently stores the executable path plus up to 3 argv entries in 4 fixed 64-byte slots to keep the BPF verifier happy.
|
|
185
|
+
- Each event is tagged with severity metadata: `high` for `setid_change`, `capable_check`, `bprm_creds`, `task_kill`, `ptrace_check`, `sb_mount`, and `kernel_read_file`; others are `medium` by default.
|
|
186
|
+
- In `human` format output, `high` severity events are rendered in red.
|
|
187
|
+
- `capable_check` is intentionally filtered to high-risk capabilities to reduce noise from extremely frequent `capable` hook calls.
|
|
140
188
|
- Current output format is textual and intended for iteration.
|
|
141
189
|
- `vivariumd` resolves `struct file::f_path` offset from `/sys/kernel/btf/vmlinux` at startup.
|
|
142
|
-
-
|
|
190
|
+
- `vivariumd` also resolves `struct dentry::d_name` offset from `/sys/kernel/btf/vmlinux` at startup.
|
|
191
|
+
- You can override offsets manually with `VIVARIUM_FILE_F_PATH_OFFSET` and `VIVARIUM_DENTRY_D_NAME_OFFSET` if auto-detection fails.
|
|
143
192
|
- `vivariumd` also prints `bpf_trace_printk` lines (`vivarium: pid=... path=...`) to its own logs.
|
|
144
193
|
|
|
145
194
|
## Contributing
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "vivarium"
|
|
6
|
+
|
|
7
|
+
# Usage:
|
|
8
|
+
# 1) In another shell (root): sudo bundle exec vivariumd
|
|
9
|
+
# 2) Run this script: bundle exec ruby examples/execve_demo.rb
|
|
10
|
+
|
|
11
|
+
TMP_PREFIX = "vivarium-exec-demo"
|
|
12
|
+
|
|
13
|
+
def try_step(title)
|
|
14
|
+
puts "[exec-demo] #{title}"
|
|
15
|
+
yield
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
puts "[exec-demo] #{title} failed: #{e.class}: #{e.message}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Dir.mktmpdir(TMP_PREFIX, "/tmp") do |dir|
|
|
21
|
+
output_path = File.join(dir, "execve-demo.out")
|
|
22
|
+
|
|
23
|
+
Vivarium.observe do
|
|
24
|
+
try_step("system echo with multiple args") do
|
|
25
|
+
system("/bin/echo", "hello", "from", "vivarium", out: File::NULL)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
try_step("spawn env with explicit argv") do
|
|
29
|
+
pid = Process.spawn(
|
|
30
|
+
"/usr/bin/env",
|
|
31
|
+
"env",
|
|
32
|
+
"printf",
|
|
33
|
+
"execve-demo\n",
|
|
34
|
+
out: output_path,
|
|
35
|
+
err: File::NULL
|
|
36
|
+
)
|
|
37
|
+
Process.wait(pid)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
try_step("spawn sleep with flag") do
|
|
41
|
+
pid = Process.spawn("/bin/sleep", "0")
|
|
42
|
+
Process.wait(pid)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
puts "[exec-demo] output file: #{output_path}" if File.exist?(output_path)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
puts "[exec-demo] done"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
require "vivarium"
|
|
7
|
+
|
|
8
|
+
# Usage:
|
|
9
|
+
# 1) In another shell (root): sudo bundle exec vivariumd
|
|
10
|
+
# 2) Run this script: bundle exec ruby examples/file_operation_demo.rb
|
|
11
|
+
|
|
12
|
+
TMP_PREFIX = "vivarium-file-demo"
|
|
13
|
+
|
|
14
|
+
def try_step(title)
|
|
15
|
+
puts "[file-demo] #{title}"
|
|
16
|
+
yield
|
|
17
|
+
rescue StandardError => e
|
|
18
|
+
puts "[file-demo] #{title} failed: #{e.class}: #{e.message}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Dir.mktmpdir(TMP_PREFIX, "/tmp") do |dir|
|
|
22
|
+
source_path = File.join(dir, "source.txt")
|
|
23
|
+
renamed_path = File.join(dir, "renamed.txt")
|
|
24
|
+
hardlink_path = File.join(dir, "hardlink.txt")
|
|
25
|
+
symlink_path = File.join(dir, "symlink.txt")
|
|
26
|
+
|
|
27
|
+
Vivarium.observe do
|
|
28
|
+
try_step("create source file") do
|
|
29
|
+
File.write(source_path, "vivarium sample\n")
|
|
30
|
+
File.read(source_path)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
try_step("directory listing") do
|
|
34
|
+
Dir.children(dir)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
try_step("rename file") do
|
|
38
|
+
File.rename(source_path, renamed_path)
|
|
39
|
+
File.read(renamed_path)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
try_step("create hardlink") do
|
|
43
|
+
File.link(renamed_path, hardlink_path)
|
|
44
|
+
File.read(hardlink_path)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
try_step("create symlink") do
|
|
48
|
+
File.symlink(renamed_path, symlink_path)
|
|
49
|
+
File.read(symlink_path)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
try_step("chmod file") do
|
|
53
|
+
File.chmod(0o640, renamed_path)
|
|
54
|
+
File.stat(renamed_path)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
try_step("list directory again") do
|
|
58
|
+
Dir.children(dir)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
FileUtils.rm_f(symlink_path)
|
|
63
|
+
FileUtils.rm_f(hardlink_path)
|
|
64
|
+
FileUtils.rm_f(renamed_path)
|
|
65
|
+
FileUtils.rm_f(source_path)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
puts "[file-demo] done"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "vivarium"
|
|
5
|
+
|
|
6
|
+
# Usage:
|
|
7
|
+
# 1) In another shell (root): sudo bundle exec vivariumd
|
|
8
|
+
# 2) Run this script: bundle exec ruby examples/privilege_event_demo.rb
|
|
9
|
+
|
|
10
|
+
def try_step(title)
|
|
11
|
+
puts "[priv-demo] #{title}"
|
|
12
|
+
yield
|
|
13
|
+
rescue StandardError => e
|
|
14
|
+
puts "[priv-demo] #{title} failed: #{e.class}: #{e.message}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
Vivarium.observe do
|
|
18
|
+
try_step("attempt setuid(0)") do
|
|
19
|
+
Process::UID.change_privilege(0)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
try_step("attempt setgid(0)") do
|
|
23
|
+
Process::GID.change_privilege(0)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
try_step("attempt opening /etc/shadow") do
|
|
27
|
+
File.read("/etc/shadow")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
try_step("exec setuid-related binary") do
|
|
31
|
+
pid = Process.spawn("/usr/bin/sudo", "-n", "true", out: File::NULL, err: File::NULL)
|
|
32
|
+
Process.wait(pid)
|
|
33
|
+
rescue Errno::ENOENT
|
|
34
|
+
puts "[priv-demo] sudo not found; skipped"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
puts "[priv-demo] done"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "vivarium"
|
|
5
|
+
|
|
6
|
+
# Usage:
|
|
7
|
+
# 1) In another shell (root): sudo bundle exec vivariumd
|
|
8
|
+
# 2) Run this script: bundle exec ruby examples/signal_kill_demo.rb
|
|
9
|
+
|
|
10
|
+
def try_step(title)
|
|
11
|
+
puts "[signal-demo] #{title}"
|
|
12
|
+
yield
|
|
13
|
+
rescue StandardError => e
|
|
14
|
+
puts "[signal-demo] #{title} failed: #{e.class}: #{e.message}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
child_pid = nil
|
|
18
|
+
|
|
19
|
+
Vivarium.observe do
|
|
20
|
+
try_step("fork child process") do
|
|
21
|
+
child_pid = fork do
|
|
22
|
+
trap("TERM") { exit!(0) }
|
|
23
|
+
loop { sleep 1 }
|
|
24
|
+
end
|
|
25
|
+
puts "[signal-demo] child pid=#{child_pid}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
try_step("send TERM signal to child") do
|
|
29
|
+
sleep 0.1
|
|
30
|
+
Process.kill("TERM", child_pid)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
try_step("wait child process") do
|
|
34
|
+
Process.wait(child_pid)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
puts "[signal-demo] done"
|
data/lib/vivarium/logger.rb
CHANGED
|
@@ -5,6 +5,8 @@ require "json"
|
|
|
5
5
|
module Vivarium
|
|
6
6
|
class Logger
|
|
7
7
|
FORMATS = %i[human json].freeze
|
|
8
|
+
ANSI_RED = "\e[31m"
|
|
9
|
+
ANSI_RESET = "\e[0m"
|
|
8
10
|
|
|
9
11
|
# dest: IO object or file path string
|
|
10
12
|
# format: :human or :json
|
|
@@ -45,7 +47,9 @@ module Vivarium
|
|
|
45
47
|
@io.puts "[vivarium] #{events.size} event(s) at #{tp.defined_class}##{tp.method_id} (#{tp.event})"
|
|
46
48
|
@io.puts " location: #{tp.path}:#{tp.lineno}"
|
|
47
49
|
events.each do |event|
|
|
48
|
-
|
|
50
|
+
severity = event.respond_to?(:severity) ? event.severity : Vivarium.event_severity(event.event_name)
|
|
51
|
+
line = " ktime_ns=#{event.ktime_ns} pid=#{event.pid} severity=#{severity} #{event.event_name} payload=#{Vivarium.render_event_payload(event)}"
|
|
52
|
+
@io.puts(severity == "high" ? "#{ANSI_RED}#{line}#{ANSI_RESET}" : line)
|
|
49
53
|
end
|
|
50
54
|
@io.puts " stack:"
|
|
51
55
|
stack.each do |loc|
|
|
@@ -59,7 +63,15 @@ module Vivarium
|
|
|
59
63
|
event: tp.event.to_s,
|
|
60
64
|
path: tp.path,
|
|
61
65
|
lineno: tp.lineno,
|
|
62
|
-
events: events.map
|
|
66
|
+
events: events.map do |e|
|
|
67
|
+
{
|
|
68
|
+
ktime_ns: e.ktime_ns,
|
|
69
|
+
pid: e.pid,
|
|
70
|
+
severity: (e.respond_to?(:severity) ? e.severity : Vivarium.event_severity(e.event_name)),
|
|
71
|
+
event_name: e.event_name,
|
|
72
|
+
payload: Vivarium.render_event_payload(e)
|
|
73
|
+
}
|
|
74
|
+
end,
|
|
63
75
|
stack: stack.map { |loc| "#{loc.path}:#{loc.lineno}:in #{loc.base_label}" }
|
|
64
76
|
}
|
|
65
77
|
@io.puts JSON.generate(entry)
|
data/lib/vivarium/version.rb
CHANGED
data/lib/vivarium.rb
CHANGED
|
@@ -22,12 +22,56 @@ module Vivarium
|
|
|
22
22
|
EVENT_NAME_SIZE = 16
|
|
23
23
|
EVENT_PAYLOAD_SIZE = 256
|
|
24
24
|
EVENT_TS_SIZE = 8
|
|
25
|
+
PROC_EXEC_SLOT_SIZE = 64
|
|
26
|
+
PROC_EXEC_SLOT_COUNT = 4
|
|
25
27
|
EVENT_STRUCT_SIZE = 288
|
|
26
28
|
EVENT_TS_OFFSET = 0
|
|
27
29
|
EVENT_PID_OFFSET = 8
|
|
28
30
|
EVENT_NAME_OFFSET = 12
|
|
29
31
|
EVENT_PAYLOAD_OFFSET = 28
|
|
30
32
|
EVENT_CAPACITY = 1024
|
|
33
|
+
EVENT_SEVERITY_HIGH = %w[
|
|
34
|
+
capable_check bprm_creds setid_change task_kill
|
|
35
|
+
ptrace_check sb_mount kernel_read_file
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
CAPABILITY_NAMES = {
|
|
39
|
+
0 => "CAP_CHOWN",
|
|
40
|
+
1 => "CAP_DAC_OVERRIDE",
|
|
41
|
+
2 => "CAP_DAC_READ_SEARCH",
|
|
42
|
+
3 => "CAP_FOWNER",
|
|
43
|
+
4 => "CAP_FSETID",
|
|
44
|
+
5 => "CAP_KILL",
|
|
45
|
+
6 => "CAP_SETGID",
|
|
46
|
+
7 => "CAP_SETUID",
|
|
47
|
+
8 => "CAP_SETPCAP",
|
|
48
|
+
9 => "CAP_LINUX_IMMUTABLE",
|
|
49
|
+
10 => "CAP_NET_BIND_SERVICE",
|
|
50
|
+
12 => "CAP_NET_ADMIN",
|
|
51
|
+
13 => "CAP_NET_RAW",
|
|
52
|
+
16 => "CAP_SYS_MODULE",
|
|
53
|
+
17 => "CAP_SYS_RAWIO",
|
|
54
|
+
18 => "CAP_SYS_CHROOT",
|
|
55
|
+
19 => "CAP_SYS_PTRACE",
|
|
56
|
+
21 => "CAP_SYS_ADMIN",
|
|
57
|
+
22 => "CAP_SYS_BOOT",
|
|
58
|
+
23 => "CAP_SYS_NICE",
|
|
59
|
+
24 => "CAP_SYS_RESOURCE",
|
|
60
|
+
25 => "CAP_SYS_TIME",
|
|
61
|
+
27 => "CAP_MKNOD",
|
|
62
|
+
29 => "CAP_AUDIT_WRITE",
|
|
63
|
+
37 => "CAP_AUDIT_READ",
|
|
64
|
+
38 => "CAP_PERFMON",
|
|
65
|
+
39 => "CAP_BPF",
|
|
66
|
+
40 => "CAP_CHECKPOINT_RESTORE"
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
SETID_FLAG_NAMES = {
|
|
70
|
+
0x01 => "LSM_SETID_ID",
|
|
71
|
+
0x02 => "LSM_SETID_RE",
|
|
72
|
+
0x04 => "LSM_SETID_RES",
|
|
73
|
+
0x08 => "LSM_SETID_FS"
|
|
74
|
+
}.freeze
|
|
31
75
|
|
|
32
76
|
@bpf_pin_dir = PIN_DIR
|
|
33
77
|
|
|
@@ -44,6 +88,10 @@ module Vivarium
|
|
|
44
88
|
ktime_ns.to_i.zero? && pid.to_i.zero? && event_name.to_s.empty? && payload.to_s.empty?
|
|
45
89
|
end
|
|
46
90
|
|
|
91
|
+
def severity
|
|
92
|
+
Vivarium.event_severity(event_name)
|
|
93
|
+
end
|
|
94
|
+
|
|
47
95
|
def self.from_binary(raw)
|
|
48
96
|
bytes = raw.to_s.b
|
|
49
97
|
bytes = bytes.ljust(EVENT_STRUCT_SIZE, "\x00")
|
|
@@ -52,7 +100,13 @@ module Vivarium
|
|
|
52
100
|
pid = bytes[EVENT_PID_OFFSET, 4].unpack1("L<")
|
|
53
101
|
event_name = c_string(bytes[EVENT_NAME_OFFSET, EVENT_NAME_SIZE])
|
|
54
102
|
raw_payload = bytes[EVENT_PAYLOAD_OFFSET, EVENT_PAYLOAD_SIZE]
|
|
55
|
-
|
|
103
|
+
raw_payload_events = %w[
|
|
104
|
+
dns_req sock_connect odd_socket proc_exec
|
|
105
|
+
file_symlink file_hardlink file_rename file_chmod file_getdents
|
|
106
|
+
ptrace_check sb_mount kernel_read_file task_kill
|
|
107
|
+
setid_change capable_check bprm_creds
|
|
108
|
+
]
|
|
109
|
+
payload = if raw_payload_events.include?(event_name)
|
|
56
110
|
raw_payload
|
|
57
111
|
else
|
|
58
112
|
c_string(raw_payload)
|
|
@@ -70,6 +124,14 @@ module Vivarium
|
|
|
70
124
|
end
|
|
71
125
|
end
|
|
72
126
|
|
|
127
|
+
def self.c_string(bytes)
|
|
128
|
+
Event.c_string(bytes)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def self.event_severity(event_name)
|
|
132
|
+
EVENT_SEVERITY_HIGH.include?(event_name.to_s) ? "high" : "medium"
|
|
133
|
+
end
|
|
134
|
+
|
|
73
135
|
def self.decode_dns_qname(raw_payload)
|
|
74
136
|
bytes = raw_payload.to_s.b.bytes
|
|
75
137
|
labels = []
|
|
@@ -143,6 +205,131 @@ module Vivarium
|
|
|
143
205
|
decode_odd_socket_payload(raw_payload)
|
|
144
206
|
end
|
|
145
207
|
|
|
208
|
+
def self.decode_file_symlink_payload(raw_payload)
|
|
209
|
+
bytes = raw_payload.to_s.b
|
|
210
|
+
target = c_string(bytes[0, 128])
|
|
211
|
+
link_name = c_string(bytes[128, 128])
|
|
212
|
+
"target=#{target.inspect} link_name=#{link_name.inspect}"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def self.decode_file_hardlink_payload(raw_payload)
|
|
216
|
+
bytes = raw_payload.to_s.b
|
|
217
|
+
old_path = c_string(bytes[0, 128])
|
|
218
|
+
new_name = c_string(bytes[128, 128])
|
|
219
|
+
"old_path=#{old_path.inspect} new_name=#{new_name.inspect}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def self.decode_file_rename_payload(raw_payload)
|
|
223
|
+
bytes = raw_payload.to_s.b
|
|
224
|
+
old_name = c_string(bytes[0, 128])
|
|
225
|
+
new_name = c_string(bytes[128, 128])
|
|
226
|
+
"old_name=#{old_name.inspect} new_name=#{new_name.inspect}"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def self.decode_file_chmod_payload(raw_payload)
|
|
230
|
+
bytes = raw_payload.to_s.b
|
|
231
|
+
return "" if bytes.bytesize < 2
|
|
232
|
+
|
|
233
|
+
mode = bytes[0, 2].unpack1("S<")
|
|
234
|
+
path = c_string(bytes[2, 254])
|
|
235
|
+
"mode=#{format('0o%o', mode)} path=#{path.inspect}"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def self.decode_file_getdents_payload(raw_payload)
|
|
239
|
+
bytes = raw_payload.to_s.b
|
|
240
|
+
return "" if bytes.bytesize < 8
|
|
241
|
+
|
|
242
|
+
fd = bytes[0, 4].unpack1("L<")
|
|
243
|
+
count = bytes[4, 4].unpack1("L<")
|
|
244
|
+
"fd=#{fd} count=#{count}"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def self.decode_proc_exec_payload(raw_payload)
|
|
248
|
+
bytes = raw_payload.to_s.b
|
|
249
|
+
slots = PROC_EXEC_SLOT_COUNT.times.map do |index|
|
|
250
|
+
offset = index * PROC_EXEC_SLOT_SIZE
|
|
251
|
+
c_string(bytes[offset, PROC_EXEC_SLOT_SIZE])
|
|
252
|
+
end
|
|
253
|
+
slots.reject!(&:empty?)
|
|
254
|
+
return "" if slots.empty?
|
|
255
|
+
|
|
256
|
+
filename = slots.shift
|
|
257
|
+
argv = slots
|
|
258
|
+
"filename=#{filename.inspect} argv=[#{argv.map(&:inspect).join(', ')}]"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def self.decode_ptrace_check_payload(raw_payload)
|
|
262
|
+
bytes = raw_payload.to_s.b
|
|
263
|
+
return "" if bytes.bytesize < 4
|
|
264
|
+
|
|
265
|
+
mode = bytes[0, 4].unpack1("L<")
|
|
266
|
+
"mode=0x#{mode.to_s(16)}"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def self.decode_sb_mount_payload(raw_payload)
|
|
270
|
+
bytes = raw_payload.to_s.b
|
|
271
|
+
return "" if bytes.bytesize < 248
|
|
272
|
+
|
|
273
|
+
flags = bytes[0, 8].unpack1("Q<")
|
|
274
|
+
dev_name = c_string(bytes[8, 120])
|
|
275
|
+
fs_type = c_string(bytes[128, 120])
|
|
276
|
+
"flags=0x#{flags.to_s(16)} dev_name=#{dev_name.inspect} fs_type=#{fs_type.inspect}"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def self.decode_kernel_read_file_payload(raw_payload)
|
|
280
|
+
bytes = raw_payload.to_s.b
|
|
281
|
+
return "" if bytes.bytesize < 8
|
|
282
|
+
|
|
283
|
+
id = bytes[0, 4].unpack1("L<")
|
|
284
|
+
contents = bytes[4, 4].unpack1("L<")
|
|
285
|
+
"id=#{id} contents=#{contents}"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def self.decode_task_kill_payload(raw_payload)
|
|
289
|
+
bytes = raw_payload.to_s.b
|
|
290
|
+
return "" if bytes.bytesize < 4
|
|
291
|
+
|
|
292
|
+
sig = bytes[0, 4].unpack1("l<")
|
|
293
|
+
signame = begin
|
|
294
|
+
Signal.signame(sig)
|
|
295
|
+
rescue ArgumentError
|
|
296
|
+
nil
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
signame ? "sig=#{sig} signame=#{signame}" : "sig=#{sig}"
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def self.decode_setid_change_payload(raw_payload)
|
|
303
|
+
bytes = raw_payload.to_s.b
|
|
304
|
+
return "" if bytes.bytesize < 4
|
|
305
|
+
|
|
306
|
+
flags = bytes[0, 4].unpack1("L<")
|
|
307
|
+
names = SETID_FLAG_NAMES.each_with_object([]) do |(bit, name), acc|
|
|
308
|
+
acc << name if (flags & bit) != 0
|
|
309
|
+
end
|
|
310
|
+
names << "UNKNOWN" if names.empty?
|
|
311
|
+
"flags=0x#{flags.to_s(16)} kinds=[#{names.join(', ')}]"
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def self.decode_capable_check_payload(raw_payload)
|
|
315
|
+
bytes = raw_payload.to_s.b
|
|
316
|
+
return "" if bytes.bytesize < 8
|
|
317
|
+
|
|
318
|
+
cap = bytes[0, 4].unpack1("L<")
|
|
319
|
+
opts = bytes[4, 4].unpack1("L<")
|
|
320
|
+
cap_name = CAPABILITY_NAMES.fetch(cap, "UNKNOWN")
|
|
321
|
+
"cap=#{cap}(#{cap_name}) opts=0x#{opts.to_s(16)}"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def self.decode_bprm_creds_payload(raw_payload)
|
|
325
|
+
bytes = raw_payload.to_s.b
|
|
326
|
+
return "" if bytes.bytesize < 2
|
|
327
|
+
|
|
328
|
+
has_file = bytes.getbyte(0).to_i
|
|
329
|
+
path = c_string(bytes[1, EVENT_PAYLOAD_SIZE - 1])
|
|
330
|
+
"has_file=#{has_file} file=#{path.inspect}"
|
|
331
|
+
end
|
|
332
|
+
|
|
146
333
|
def self.render_event_payload(event)
|
|
147
334
|
case event.event_name
|
|
148
335
|
when "dns_req"
|
|
@@ -154,6 +341,45 @@ module Vivarium
|
|
|
154
341
|
when "odd_socket"
|
|
155
342
|
decoded = decode_odd_socket_payload(event.payload)
|
|
156
343
|
decoded.empty? ? event.payload.inspect : decoded
|
|
344
|
+
when "proc_exec"
|
|
345
|
+
decoded = decode_proc_exec_payload(event.payload)
|
|
346
|
+
decoded.empty? ? event.payload.inspect : decoded
|
|
347
|
+
when "ptrace_check"
|
|
348
|
+
decoded = decode_ptrace_check_payload(event.payload)
|
|
349
|
+
decoded.empty? ? event.payload.inspect : decoded
|
|
350
|
+
when "sb_mount"
|
|
351
|
+
decoded = decode_sb_mount_payload(event.payload)
|
|
352
|
+
decoded.empty? ? event.payload.inspect : decoded
|
|
353
|
+
when "kernel_read_file"
|
|
354
|
+
decoded = decode_kernel_read_file_payload(event.payload)
|
|
355
|
+
decoded.empty? ? event.payload.inspect : decoded
|
|
356
|
+
when "task_kill"
|
|
357
|
+
decoded = decode_task_kill_payload(event.payload)
|
|
358
|
+
decoded.empty? ? event.payload.inspect : decoded
|
|
359
|
+
when "setid_change"
|
|
360
|
+
decoded = decode_setid_change_payload(event.payload)
|
|
361
|
+
decoded.empty? ? event.payload.inspect : decoded
|
|
362
|
+
when "capable_check"
|
|
363
|
+
decoded = decode_capable_check_payload(event.payload)
|
|
364
|
+
decoded.empty? ? event.payload.inspect : decoded
|
|
365
|
+
when "bprm_creds"
|
|
366
|
+
decoded = decode_bprm_creds_payload(event.payload)
|
|
367
|
+
decoded.empty? ? event.payload.inspect : decoded
|
|
368
|
+
when "file_symlink"
|
|
369
|
+
decoded = decode_file_symlink_payload(event.payload)
|
|
370
|
+
decoded.empty? ? event.payload.inspect : decoded
|
|
371
|
+
when "file_hardlink"
|
|
372
|
+
decoded = decode_file_hardlink_payload(event.payload)
|
|
373
|
+
decoded.empty? ? event.payload.inspect : decoded
|
|
374
|
+
when "file_rename"
|
|
375
|
+
decoded = decode_file_rename_payload(event.payload)
|
|
376
|
+
decoded.empty? ? event.payload.inspect : decoded
|
|
377
|
+
when "file_chmod"
|
|
378
|
+
decoded = decode_file_chmod_payload(event.payload)
|
|
379
|
+
decoded.empty? ? event.payload.inspect : decoded
|
|
380
|
+
when "file_getdents"
|
|
381
|
+
decoded = decode_file_getdents_payload(event.payload)
|
|
382
|
+
decoded.empty? ? event.payload.inspect : decoded
|
|
157
383
|
else
|
|
158
384
|
event.payload.inspect
|
|
159
385
|
end
|
|
@@ -249,6 +475,11 @@ module Vivarium
|
|
|
249
475
|
struct net;
|
|
250
476
|
struct sock;
|
|
251
477
|
struct sk_buff;
|
|
478
|
+
struct task_struct;
|
|
479
|
+
struct kernel_siginfo;
|
|
480
|
+
struct cred;
|
|
481
|
+
struct user_namespace;
|
|
482
|
+
struct linux_binprm;
|
|
252
483
|
|
|
253
484
|
struct path {
|
|
254
485
|
void *mnt;
|
|
@@ -259,6 +490,24 @@ module Vivarium
|
|
|
259
490
|
struct path f_path;
|
|
260
491
|
};
|
|
261
492
|
|
|
493
|
+
struct qstr {
|
|
494
|
+
union {
|
|
495
|
+
struct {
|
|
496
|
+
u64 hash_len;
|
|
497
|
+
};
|
|
498
|
+
struct {
|
|
499
|
+
u32 hash;
|
|
500
|
+
u32 len;
|
|
501
|
+
};
|
|
502
|
+
};
|
|
503
|
+
const unsigned char *name;
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
struct dentry_base {
|
|
507
|
+
char __pad[__VIVARIUM_DENTRY_D_NAME_OFFSET__];
|
|
508
|
+
struct qstr d_name;
|
|
509
|
+
};
|
|
510
|
+
|
|
262
511
|
struct sockaddr_t {
|
|
263
512
|
u16 sa_family;
|
|
264
513
|
unsigned char sa_data[14];
|
|
@@ -341,6 +590,29 @@ module Vivarium
|
|
|
341
590
|
return 0;
|
|
342
591
|
}
|
|
343
592
|
|
|
593
|
+
static __always_inline int monitored_capability(int cap)
|
|
594
|
+
{
|
|
595
|
+
switch (cap) {
|
|
596
|
+
case 1: /* CAP_DAC_OVERRIDE */
|
|
597
|
+
case 2: /* CAP_DAC_READ_SEARCH */
|
|
598
|
+
case 6: /* CAP_SETGID */
|
|
599
|
+
case 7: /* CAP_SETUID */
|
|
600
|
+
case 12: /* CAP_NET_ADMIN */
|
|
601
|
+
case 16: /* CAP_SYS_MODULE */
|
|
602
|
+
case 17: /* CAP_SYS_RAWIO */
|
|
603
|
+
case 19: /* CAP_SYS_PTRACE */
|
|
604
|
+
case 21: /* CAP_SYS_ADMIN */
|
|
605
|
+
case 22: /* CAP_SYS_BOOT */
|
|
606
|
+
case 25: /* CAP_SYS_TIME */
|
|
607
|
+
case 38: /* CAP_PERFMON */
|
|
608
|
+
case 39: /* CAP_BPF */
|
|
609
|
+
case 40: /* CAP_CHECKPOINT_RESTORE */
|
|
610
|
+
return 1;
|
|
611
|
+
default:
|
|
612
|
+
return 0;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
344
616
|
static __always_inline void submit_event(struct event_t *ev)
|
|
345
617
|
{
|
|
346
618
|
u32 zero = 0;
|
|
@@ -396,6 +668,29 @@ module Vivarium
|
|
|
396
668
|
submit_event(&ev);
|
|
397
669
|
}
|
|
398
670
|
|
|
671
|
+
static __always_inline int read_dentry_name(struct dentry *dentry, char *buffer, size_t max)
|
|
672
|
+
{
|
|
673
|
+
struct dentry_base d = {};
|
|
674
|
+
struct qstr qname = {};
|
|
675
|
+
|
|
676
|
+
if (!dentry || !buffer) {
|
|
677
|
+
return -1;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
bpf_probe_read_kernel(&d, sizeof(d), (void *)dentry);
|
|
681
|
+
if (!d.d_name.name) {
|
|
682
|
+
return -1;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
unsigned int len = d.d_name.len;
|
|
686
|
+
if (len > max) {
|
|
687
|
+
len = max;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
bpf_probe_read_kernel_str(buffer, len + 1, (void *)d.d_name.name);
|
|
691
|
+
return len;
|
|
692
|
+
}
|
|
693
|
+
|
|
399
694
|
TRACEPOINT_PROBE(sched, sched_process_fork)
|
|
400
695
|
{
|
|
401
696
|
u32 parent = args->parent_pid;
|
|
@@ -630,6 +925,343 @@ module Vivarium
|
|
|
630
925
|
|
|
631
926
|
return 0;
|
|
632
927
|
}
|
|
928
|
+
|
|
929
|
+
TRACEPOINT_PROBE(syscalls, sys_enter_execve)
|
|
930
|
+
{
|
|
931
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
932
|
+
u32 pid = pid_tgid >> 32;
|
|
933
|
+
u32 tid = (u32)pid_tgid;
|
|
934
|
+
const char *argv0 = 0;
|
|
935
|
+
const char *argv1 = 0;
|
|
936
|
+
const char *argv2 = 0;
|
|
937
|
+
|
|
938
|
+
if (!target_enabled(pid, tid)) {
|
|
939
|
+
return 0;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (!args->filename) {
|
|
943
|
+
return 0;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
struct event_t ev = {};
|
|
947
|
+
ev.pid = pid;
|
|
948
|
+
__builtin_memcpy(ev.event_name, "proc_exec", 10);
|
|
949
|
+
bpf_probe_read_user_str(&ev.payload[0], #{PROC_EXEC_SLOT_SIZE}, args->filename);
|
|
950
|
+
|
|
951
|
+
if (args->argv) {
|
|
952
|
+
bpf_probe_read_user(&argv0, sizeof(argv0), &args->argv[0]);
|
|
953
|
+
bpf_probe_read_user(&argv1, sizeof(argv1), &args->argv[1]);
|
|
954
|
+
bpf_probe_read_user(&argv2, sizeof(argv2), &args->argv[2]);
|
|
955
|
+
|
|
956
|
+
if (argv0) {
|
|
957
|
+
bpf_probe_read_user_str(&ev.payload[#{PROC_EXEC_SLOT_SIZE}], #{PROC_EXEC_SLOT_SIZE}, argv0);
|
|
958
|
+
}
|
|
959
|
+
if (argv1) {
|
|
960
|
+
bpf_probe_read_user_str(&ev.payload[#{PROC_EXEC_SLOT_SIZE * 2}], #{PROC_EXEC_SLOT_SIZE}, argv1);
|
|
961
|
+
}
|
|
962
|
+
if (argv2) {
|
|
963
|
+
bpf_probe_read_user_str(&ev.payload[#{PROC_EXEC_SLOT_SIZE * 3}], #{PROC_EXEC_SLOT_SIZE}, argv2);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
submit_event(&ev);
|
|
968
|
+
return 0;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
LSM_PROBE(ptrace_access_check, struct task_struct *child, unsigned int mode)
|
|
972
|
+
{
|
|
973
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
974
|
+
u32 pid = pid_tgid >> 32;
|
|
975
|
+
u32 tid = (u32)pid_tgid;
|
|
976
|
+
|
|
977
|
+
if (!target_enabled(pid, tid)) {
|
|
978
|
+
return 0;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
struct event_t ev = {};
|
|
982
|
+
u32 mode32 = mode;
|
|
983
|
+
|
|
984
|
+
ev.pid = pid;
|
|
985
|
+
__builtin_memcpy(ev.event_name, "ptrace_check", 13);
|
|
986
|
+
__builtin_memcpy(&ev.payload[0], &mode32, sizeof(mode32));
|
|
987
|
+
submit_event(&ev);
|
|
988
|
+
|
|
989
|
+
return 0;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
LSM_PROBE(sb_mount, const char *dev_name, const struct path *path, const char *type, unsigned long flags, void *data)
|
|
993
|
+
{
|
|
994
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
995
|
+
u32 pid = pid_tgid >> 32;
|
|
996
|
+
u32 tid = (u32)pid_tgid;
|
|
997
|
+
|
|
998
|
+
if (!target_enabled(pid, tid)) {
|
|
999
|
+
return 0;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
struct event_t ev = {};
|
|
1003
|
+
u64 flags64 = flags;
|
|
1004
|
+
|
|
1005
|
+
ev.pid = pid;
|
|
1006
|
+
__builtin_memcpy(ev.event_name, "sb_mount", 9);
|
|
1007
|
+
__builtin_memcpy(&ev.payload[0], &flags64, sizeof(flags64));
|
|
1008
|
+
|
|
1009
|
+
if (dev_name) {
|
|
1010
|
+
bpf_probe_read_kernel_str(&ev.payload[8], 120, dev_name);
|
|
1011
|
+
}
|
|
1012
|
+
if (type) {
|
|
1013
|
+
bpf_probe_read_kernel_str(&ev.payload[128], 120, type);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
submit_event(&ev);
|
|
1017
|
+
|
|
1018
|
+
return 0;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
LSM_PROBE(kernel_read_file, struct file *file, int id, int contents)
|
|
1022
|
+
{
|
|
1023
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
1024
|
+
u32 pid = pid_tgid >> 32;
|
|
1025
|
+
u32 tid = (u32)pid_tgid;
|
|
1026
|
+
|
|
1027
|
+
if (!target_enabled(pid, tid)) {
|
|
1028
|
+
return 0;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
struct event_t ev = {};
|
|
1032
|
+
u32 id32 = id;
|
|
1033
|
+
u32 contents32 = contents;
|
|
1034
|
+
|
|
1035
|
+
ev.pid = pid;
|
|
1036
|
+
__builtin_memcpy(ev.event_name, "kernel_read_file", 16);
|
|
1037
|
+
__builtin_memcpy(&ev.payload[0], &id32, sizeof(id32));
|
|
1038
|
+
__builtin_memcpy(&ev.payload[4], &contents32, sizeof(contents32));
|
|
1039
|
+
submit_event(&ev);
|
|
1040
|
+
|
|
1041
|
+
return 0;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
LSM_PROBE(task_kill, struct task_struct *p, struct kernel_siginfo *info, int sig, const struct cred *cred)
|
|
1045
|
+
{
|
|
1046
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
1047
|
+
u32 pid = pid_tgid >> 32;
|
|
1048
|
+
u32 tid = (u32)pid_tgid;
|
|
1049
|
+
|
|
1050
|
+
if (!target_enabled(pid, tid)) {
|
|
1051
|
+
return 0;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
struct event_t ev = {};
|
|
1055
|
+
|
|
1056
|
+
ev.pid = pid;
|
|
1057
|
+
__builtin_memcpy(ev.event_name, "task_kill", 10);
|
|
1058
|
+
__builtin_memcpy(&ev.payload[0], &sig, sizeof(sig));
|
|
1059
|
+
submit_event(&ev);
|
|
1060
|
+
|
|
1061
|
+
return 0;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
LSM_PROBE(task_fix_setuid, struct cred *new, const struct cred *old, int flags)
|
|
1065
|
+
{
|
|
1066
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
1067
|
+
u32 pid = pid_tgid >> 32;
|
|
1068
|
+
u32 tid = (u32)pid_tgid;
|
|
1069
|
+
|
|
1070
|
+
if (!target_enabled(pid, tid)) {
|
|
1071
|
+
return 0;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
struct event_t ev = {};
|
|
1075
|
+
u32 flags32 = flags;
|
|
1076
|
+
|
|
1077
|
+
ev.pid = pid;
|
|
1078
|
+
__builtin_memcpy(ev.event_name, "setid_change", 13);
|
|
1079
|
+
__builtin_memcpy(&ev.payload[0], &flags32, sizeof(flags32));
|
|
1080
|
+
submit_event(&ev);
|
|
1081
|
+
|
|
1082
|
+
return 0;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
LSM_PROBE(capable, const struct cred *cred, struct user_namespace *targ_ns, int cap, unsigned int opts)
|
|
1086
|
+
{
|
|
1087
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
1088
|
+
u32 pid = pid_tgid >> 32;
|
|
1089
|
+
u32 tid = (u32)pid_tgid;
|
|
1090
|
+
|
|
1091
|
+
if (!target_enabled(pid, tid)) {
|
|
1092
|
+
return 0;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
if (!monitored_capability(cap)) {
|
|
1096
|
+
return 0;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
struct event_t ev = {};
|
|
1100
|
+
u32 cap32 = cap;
|
|
1101
|
+
u32 opts32 = opts;
|
|
1102
|
+
|
|
1103
|
+
ev.pid = pid;
|
|
1104
|
+
__builtin_memcpy(ev.event_name, "capable_check", 14);
|
|
1105
|
+
__builtin_memcpy(&ev.payload[0], &cap32, sizeof(cap32));
|
|
1106
|
+
__builtin_memcpy(&ev.payload[4], &opts32, sizeof(opts32));
|
|
1107
|
+
submit_event(&ev);
|
|
1108
|
+
|
|
1109
|
+
return 0;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
LSM_PROBE(bprm_creds_from_file, struct linux_binprm *bprm, struct file *file)
|
|
1113
|
+
{
|
|
1114
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
1115
|
+
u32 pid = pid_tgid >> 32;
|
|
1116
|
+
u32 tid = (u32)pid_tgid;
|
|
1117
|
+
|
|
1118
|
+
if (!target_enabled(pid, tid)) {
|
|
1119
|
+
return 0;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
struct event_t ev = {};
|
|
1123
|
+
u8 has_file = 0;
|
|
1124
|
+
|
|
1125
|
+
ev.pid = pid;
|
|
1126
|
+
__builtin_memcpy(ev.event_name, "bprm_creds", 11);
|
|
1127
|
+
|
|
1128
|
+
if (file) {
|
|
1129
|
+
has_file = 1;
|
|
1130
|
+
bpf_d_path(&file->f_path, &ev.payload[1], sizeof(ev.payload) - 1);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
__builtin_memcpy(&ev.payload[0], &has_file, sizeof(has_file));
|
|
1134
|
+
submit_event(&ev);
|
|
1135
|
+
|
|
1136
|
+
return 0;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
LSM_PROBE(inode_symlink, struct inode *dir, struct dentry *dentry, const char *oldname)
|
|
1140
|
+
{
|
|
1141
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
1142
|
+
u32 pid = pid_tgid >> 32;
|
|
1143
|
+
u32 tid = (u32)pid_tgid;
|
|
1144
|
+
|
|
1145
|
+
if (!target_enabled(pid, tid)) {
|
|
1146
|
+
return 0;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
struct event_t ev = {};
|
|
1150
|
+
ev.pid = pid;
|
|
1151
|
+
__builtin_memcpy(ev.event_name, "file_symlink", 13);
|
|
1152
|
+
|
|
1153
|
+
if (oldname) {
|
|
1154
|
+
bpf_probe_read_user_str(&ev.payload[0], 128, oldname);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
if (dentry) {
|
|
1158
|
+
read_dentry_name(dentry, &ev.payload[128], 128);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
submit_event(&ev);
|
|
1162
|
+
return 0;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
LSM_PROBE(inode_link, struct dentry *old_dentry, struct inode *dir, struct dentry *new_dentry)
|
|
1166
|
+
{
|
|
1167
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
1168
|
+
u32 pid = pid_tgid >> 32;
|
|
1169
|
+
u32 tid = (u32)pid_tgid;
|
|
1170
|
+
|
|
1171
|
+
if (!target_enabled(pid, tid)) {
|
|
1172
|
+
return 0;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
struct event_t ev = {};
|
|
1176
|
+
ev.pid = pid;
|
|
1177
|
+
__builtin_memcpy(ev.event_name, "file_hardlink", 14);
|
|
1178
|
+
|
|
1179
|
+
if (old_dentry) {
|
|
1180
|
+
read_dentry_name(old_dentry, &ev.payload[0], 128);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
if (new_dentry) {
|
|
1184
|
+
read_dentry_name(new_dentry, &ev.payload[128], 128);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
submit_event(&ev);
|
|
1188
|
+
return 0;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
LSM_PROBE(inode_rename, struct inode *old_dir, struct dentry *old_dentry,
|
|
1192
|
+
struct inode *new_dir, struct dentry *new_dentry, unsigned int flags)
|
|
1193
|
+
{
|
|
1194
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
1195
|
+
u32 pid = pid_tgid >> 32;
|
|
1196
|
+
u32 tid = (u32)pid_tgid;
|
|
1197
|
+
|
|
1198
|
+
if (!target_enabled(pid, tid)) {
|
|
1199
|
+
return 0;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
struct event_t ev = {};
|
|
1203
|
+
ev.pid = pid;
|
|
1204
|
+
__builtin_memcpy(ev.event_name, "file_rename", 12);
|
|
1205
|
+
|
|
1206
|
+
if (old_dentry) {
|
|
1207
|
+
read_dentry_name(old_dentry, &ev.payload[0], 128);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
if (new_dentry) {
|
|
1211
|
+
read_dentry_name(new_dentry, &ev.payload[128], 128);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
submit_event(&ev);
|
|
1215
|
+
return 0;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
LSM_PROBE(path_chmod, struct path *path, umode_t mode)
|
|
1219
|
+
{
|
|
1220
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
1221
|
+
u32 pid = pid_tgid >> 32;
|
|
1222
|
+
u32 tid = (u32)pid_tgid;
|
|
1223
|
+
|
|
1224
|
+
if (!target_enabled(pid, tid)) {
|
|
1225
|
+
return 0;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if (!path) {
|
|
1229
|
+
return 0;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
struct event_t ev = {};
|
|
1233
|
+
u16 mode_short = mode & 0xFFFF;
|
|
1234
|
+
ev.pid = pid;
|
|
1235
|
+
__builtin_memcpy(ev.event_name, "file_chmod", 11);
|
|
1236
|
+
__builtin_memcpy(&ev.payload[0], &mode_short, sizeof(mode_short));
|
|
1237
|
+
|
|
1238
|
+
bpf_d_path(path, &ev.payload[2], sizeof(ev.payload) - 2);
|
|
1239
|
+
submit_event(&ev);
|
|
1240
|
+
return 0;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
TRACEPOINT_PROBE(syscalls, sys_enter_getdents64)
|
|
1244
|
+
{
|
|
1245
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
1246
|
+
u32 pid = pid_tgid >> 32;
|
|
1247
|
+
u32 tid = (u32)pid_tgid;
|
|
1248
|
+
|
|
1249
|
+
if (!target_enabled(pid, tid)) {
|
|
1250
|
+
return 0;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
struct event_t ev = {};
|
|
1254
|
+
u32 fd = args->fd;
|
|
1255
|
+
u32 count = args->count;
|
|
1256
|
+
|
|
1257
|
+
ev.pid = pid;
|
|
1258
|
+
__builtin_memcpy(ev.event_name, "file_getdents", 14);
|
|
1259
|
+
__builtin_memcpy(&ev.payload[0], &fd, sizeof(fd));
|
|
1260
|
+
__builtin_memcpy(&ev.payload[4], &count, sizeof(count));
|
|
1261
|
+
|
|
1262
|
+
submit_event(&ev);
|
|
1263
|
+
return 0;
|
|
1264
|
+
}
|
|
633
1265
|
CLANG
|
|
634
1266
|
|
|
635
1267
|
def initialize(pin_dir: Vivarium.bpf_pin_dir)
|
|
@@ -641,7 +1273,10 @@ module Vivarium
|
|
|
641
1273
|
FileUtils.mkdir_p(@pin_dir)
|
|
642
1274
|
|
|
643
1275
|
f_path_offset = detect_f_path_offset
|
|
644
|
-
|
|
1276
|
+
d_name_offset = detect_dentry_d_name_offset
|
|
1277
|
+
program = BPF_PROGRAM_TEMPLATE
|
|
1278
|
+
.gsub("__VIVARIUM_F_PATH_OFFSET__", f_path_offset.to_s)
|
|
1279
|
+
.gsub("__VIVARIUM_DENTRY_D_NAME_OFFSET__", d_name_offset.to_s)
|
|
645
1280
|
|
|
646
1281
|
bpf = RbBCC::BCC.new(text: program)
|
|
647
1282
|
kprint_thread = start_kprint_logger(bpf)
|
|
@@ -777,6 +1412,56 @@ module Vivarium
|
|
|
777
1412
|
rescue Errno::ENOENT
|
|
778
1413
|
raise Error, "bpftool is required to resolve struct file::f_path offset"
|
|
779
1414
|
end
|
|
1415
|
+
|
|
1416
|
+
def detect_dentry_d_name_offset
|
|
1417
|
+
env_offset = ENV["VIVARIUM_DENTRY_D_NAME_OFFSET"]
|
|
1418
|
+
return Integer(env_offset, 10) if env_offset
|
|
1419
|
+
|
|
1420
|
+
raw = IO.popen(
|
|
1421
|
+
%w[bpftool btf dump file /sys/kernel/btf/vmlinux format raw],
|
|
1422
|
+
err: IO::NULL,
|
|
1423
|
+
&:read
|
|
1424
|
+
)
|
|
1425
|
+
|
|
1426
|
+
in_dentry_struct = false
|
|
1427
|
+
d_name_bits_offset = nil
|
|
1428
|
+
|
|
1429
|
+
raw.each_line do |line|
|
|
1430
|
+
if line =~ /^\[\d+\] STRUCT 'dentry' /
|
|
1431
|
+
in_dentry_struct = true
|
|
1432
|
+
next
|
|
1433
|
+
end
|
|
1434
|
+
|
|
1435
|
+
if in_dentry_struct && line.start_with?("[")
|
|
1436
|
+
break
|
|
1437
|
+
end
|
|
1438
|
+
|
|
1439
|
+
next unless in_dentry_struct
|
|
1440
|
+
|
|
1441
|
+
if (match = line.match(/'d_name'.*bits_offset=(\d+)/))
|
|
1442
|
+
d_name_bits_offset = Integer(match[1], 10)
|
|
1443
|
+
break
|
|
1444
|
+
end
|
|
1445
|
+
end
|
|
1446
|
+
|
|
1447
|
+
if d_name_bits_offset
|
|
1448
|
+
if (d_name_bits_offset % 8).positive?
|
|
1449
|
+
raise Error, "unsupported d_name bits offset=#{d_name_bits_offset}"
|
|
1450
|
+
end
|
|
1451
|
+
|
|
1452
|
+
if d_name_bits_offset >= 1024
|
|
1453
|
+
warn "[vivariumd] suspicious d_name offset=#{d_name_bits_offset / 8}, fallback to offset=32"
|
|
1454
|
+
return 32
|
|
1455
|
+
end
|
|
1456
|
+
|
|
1457
|
+
return d_name_bits_offset / 8
|
|
1458
|
+
end
|
|
1459
|
+
|
|
1460
|
+
warn "[vivariumd] could not find struct dentry::d_name in BTF, fallback to offset=32"
|
|
1461
|
+
32
|
|
1462
|
+
rescue Errno::ENOENT
|
|
1463
|
+
raise Error, "bpftool is required to resolve struct dentry::d_name offset"
|
|
1464
|
+
end
|
|
780
1465
|
end
|
|
781
1466
|
|
|
782
1467
|
class ObservationSession
|
data/sig/vivarium.rbs
CHANGED
|
@@ -11,6 +11,7 @@ module Vivarium
|
|
|
11
11
|
attr_accessor payload: String?
|
|
12
12
|
|
|
13
13
|
def empty?: bool
|
|
14
|
+
def severity: () -> String
|
|
14
15
|
def self.from_binary: (String raw) -> Event
|
|
15
16
|
end
|
|
16
17
|
|
|
@@ -41,6 +42,8 @@ module Vivarium
|
|
|
41
42
|
EVENT_NAME_SIZE: Integer
|
|
42
43
|
EVENT_PAYLOAD_SIZE: Integer
|
|
43
44
|
EVENT_TS_SIZE: Integer
|
|
45
|
+
PROC_EXEC_SLOT_SIZE: Integer
|
|
46
|
+
PROC_EXEC_SLOT_COUNT: Integer
|
|
44
47
|
EVENT_STRUCT_SIZE: Integer
|
|
45
48
|
EVENT_TS_OFFSET: Integer
|
|
46
49
|
EVENT_PID_OFFSET: Integer
|
|
@@ -50,10 +53,24 @@ module Vivarium
|
|
|
50
53
|
|
|
51
54
|
def self.bpf_pin_dir: () -> String
|
|
52
55
|
def self.bpf_pin_dir=: (String dir) -> String
|
|
56
|
+
def self.event_severity: (String event_name) -> String
|
|
53
57
|
def self.decode_dns_qname: (String raw_payload) -> String
|
|
54
58
|
def self.decode_sock_connect_payload: (String raw_payload) -> String
|
|
55
59
|
def self.decode_odd_socket_payload: (String raw_payload) -> String
|
|
56
60
|
def self.decode_bad_socket_payload: (String raw_payload) -> String
|
|
61
|
+
def self.decode_proc_exec_payload: (String raw_payload) -> String
|
|
62
|
+
def self.decode_ptrace_check_payload: (String raw_payload) -> String
|
|
63
|
+
def self.decode_sb_mount_payload: (String raw_payload) -> String
|
|
64
|
+
def self.decode_kernel_read_file_payload: (String raw_payload) -> String
|
|
65
|
+
def self.decode_task_kill_payload: (String raw_payload) -> String
|
|
66
|
+
def self.decode_setid_change_payload: (String raw_payload) -> String
|
|
67
|
+
def self.decode_capable_check_payload: (String raw_payload) -> String
|
|
68
|
+
def self.decode_bprm_creds_payload: (String raw_payload) -> String
|
|
69
|
+
def self.decode_file_symlink_payload: (String raw_payload) -> String
|
|
70
|
+
def self.decode_file_hardlink_payload: (String raw_payload) -> String
|
|
71
|
+
def self.decode_file_rename_payload: (String raw_payload) -> String
|
|
72
|
+
def self.decode_file_chmod_payload: (String raw_payload) -> String
|
|
73
|
+
def self.decode_file_getdents_payload: (String raw_payload) -> String
|
|
57
74
|
def self.render_event_payload: (Event event) -> String
|
|
58
75
|
def self.observe: (?pin_dir: String pin_dir, ?logger: untyped logger, ?dest: untyped dest, ?format: Symbol format) { () -> untyped } -> untyped
|
|
59
76
|
def self.top_observe: (?pin_dir: String pin_dir, ?logger: untyped logger, ?dest: untyped dest, ?format: Symbol format) -> ObservationSession
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vivarium
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Uchio Kondo
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: 0.11.4
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ostruct
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
26
40
|
description: Vivarium visualizes low-level events such as file open paths and relates
|
|
27
41
|
them to Ruby method boundaries by combining RbBCC (eBPF LSM) and TracePoint.
|
|
28
42
|
email:
|
|
@@ -34,7 +48,11 @@ extra_rdoc_files: []
|
|
|
34
48
|
files:
|
|
35
49
|
- README.md
|
|
36
50
|
- Rakefile
|
|
51
|
+
- examples/execve_demo.rb
|
|
52
|
+
- examples/file_operation_demo.rb
|
|
37
53
|
- examples/network_client_demo.rb
|
|
54
|
+
- examples/privilege_event_demo.rb
|
|
55
|
+
- examples/signal_kill_demo.rb
|
|
38
56
|
- exe/vivariumd
|
|
39
57
|
- lib/vivarium.rb
|
|
40
58
|
- lib/vivarium/logger.rb
|