vivarium 0.4.0 → 0.4.2
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 +24 -0
- data/examples/box_demo.rb +59 -0
- data/examples/env_access_external_demo.rb +52 -0
- data/examples/env_access_ruby_demo.rb +52 -0
- data/lib/vivarium/box.rb +136 -0
- data/lib/vivarium/version.rb +1 -1
- data/lib/vivarium.rb +174 -11
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0a0f8b6dfc29af71d39ff5fe1ebceca9040cf62a2c6147dba5c2a074b392cdc1
|
|
4
|
+
data.tar.gz: c9f10129e5b42fd51653dcda0a02ed0e7be8b4c2ad7daef4f48a9fa5c03dcf41
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9530187d826e63976bd7e1c86872345f5cad281608b2ee06f0dbe20e6211455d8d050a18013822f6baecf4c0af18e9e01b58fec7fe13174d9631181df8660569
|
|
7
|
+
data.tar.gz: c3ee802cf168d5ca917c6d9de5cce25c9441ba46a35995bc5e5f78ce6c78055c1a4d8141bff7dd4e7763205d65c1a41344a789185dbea8fc65f4589ef8e90586
|
data/README.md
CHANGED
|
@@ -139,6 +139,30 @@ bundle exec ruby examples/privilege_event_demo.rb
|
|
|
139
139
|
|
|
140
140
|
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`.
|
|
141
141
|
|
|
142
|
+
8) Ruby internal ENV access demo client:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
bundle exec ruby examples/env_access_ruby_demo.rb
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
This demo triggers Ruby-side ENV methods (`[]`, `fetch`, `key?`, `[]=`, `store`, `delete`, `clear`, `replace`) and is intended to produce SPAN events.
|
|
149
|
+
|
|
150
|
+
9) External command ENV libc access demo client:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
bundle exec ruby examples/env_access_external_demo.rb
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
This demo spawns an external process that directly calls libc `getenv`, `setenv`, `unsetenv`, `putenv`, and `clearenv`, intended to trigger `env_caccess` eBPF events.
|
|
157
|
+
|
|
158
|
+
10) Box context demo:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
bundle exec ruby examples/box_demo.rb
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
This demo shows how to use `Vivarium::Box` to isolate Ruby code evaluation and trace method calls within that isolated context.
|
|
165
|
+
|
|
142
166
|
You can also start top-level observation without a block (it keeps observing until process exit):
|
|
143
167
|
|
|
144
168
|
```ruby
|
|
@@ -0,0 +1,59 @@
|
|
|
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/box_demo.rb
|
|
9
|
+
#
|
|
10
|
+
# This demo demonstrates Vivarium::Box usage for automatically tracing
|
|
11
|
+
# method calls within an isolated eval context.
|
|
12
|
+
|
|
13
|
+
FILTER = {
|
|
14
|
+
include_events: %w[span_start span_stop]
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
# Create a box and define a class within it
|
|
18
|
+
box = Vivarium::Box.new
|
|
19
|
+
|
|
20
|
+
box.eval(<<~RUBY)
|
|
21
|
+
require "net/http"
|
|
22
|
+
class Calculator
|
|
23
|
+
def add(a, b)
|
|
24
|
+
a + b
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def multiply(a, b)
|
|
28
|
+
a * b
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Greeter
|
|
33
|
+
def greet(name)
|
|
34
|
+
system "echo Hello \#{name}!"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
system "ping -c 1 example.com"
|
|
39
|
+
RUBY
|
|
40
|
+
|
|
41
|
+
box.done_load!
|
|
42
|
+
|
|
43
|
+
# Enable observation - all Box method calls will be automatically traced
|
|
44
|
+
puts "[box-demo] calling box methods with automatic tracing"
|
|
45
|
+
|
|
46
|
+
# Access classes defined in the box through const_missing
|
|
47
|
+
calc = box::Calculator.new
|
|
48
|
+
result1 = calc.add(10, 20)
|
|
49
|
+
puts "[box-demo] calc.add(10, 20) = #{result1}"
|
|
50
|
+
|
|
51
|
+
result2 = calc.multiply(3, 4)
|
|
52
|
+
puts "[box-demo] calc.multiply(3, 4) = #{result2}"
|
|
53
|
+
|
|
54
|
+
greeter = box::Greeter.new
|
|
55
|
+
greeting = greeter.greet("World")
|
|
56
|
+
puts "[box-demo] greeter.greet('World') = #{greeting}"
|
|
57
|
+
|
|
58
|
+
puts "[box-demo] done"
|
|
59
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "rbconfig"
|
|
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/env_access_external_demo.rb
|
|
10
|
+
#
|
|
11
|
+
# This demo launches an external Ruby process and forces direct libc calls to
|
|
12
|
+
# getenv/setenv/unsetenv/putenv/clearenv through Fiddle.
|
|
13
|
+
# These should appear as eBPF events with event_name=env_caccess.
|
|
14
|
+
|
|
15
|
+
FILTER = {
|
|
16
|
+
include_events: %w[env_caccess proc_fork proc_exec]
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
CHILD_CODE = <<~RUBY
|
|
20
|
+
require "fiddle"
|
|
21
|
+
|
|
22
|
+
libc = begin
|
|
23
|
+
Fiddle.dlopen("libc.so.6")
|
|
24
|
+
rescue Fiddle::DLError
|
|
25
|
+
Fiddle.dlopen(nil)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
getenv = Fiddle::Function.new(libc["getenv"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOIDP)
|
|
29
|
+
setenv = Fiddle::Function.new(libc["setenv"], [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT], Fiddle::TYPE_INT)
|
|
30
|
+
unsetenv = Fiddle::Function.new(libc["unsetenv"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT)
|
|
31
|
+
putenv = Fiddle::Function.new(libc["putenv"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT)
|
|
32
|
+
clearenv = Fiddle::Function.new(libc["clearenv"], [], Fiddle::TYPE_INT)
|
|
33
|
+
|
|
34
|
+
key = "VIVARIUM_ENV_EXT_DEMO"
|
|
35
|
+
putenv_buf = "VIVARIUM_ENV_EXT_PUT=from_putenv"
|
|
36
|
+
|
|
37
|
+
getenv.call("HOME")
|
|
38
|
+
setenv.call(key, "from_setenv", 1)
|
|
39
|
+
getenv.call(key)
|
|
40
|
+
putenv.call(putenv_buf)
|
|
41
|
+
unsetenv.call(key)
|
|
42
|
+
clearenv.call
|
|
43
|
+
RUBY
|
|
44
|
+
|
|
45
|
+
Vivarium.observe(filter: FILTER) do
|
|
46
|
+
puts "[env-external-demo] spawning external child"
|
|
47
|
+
pid = Process.spawn(RbConfig.ruby, "-e", CHILD_CODE)
|
|
48
|
+
Process.wait(pid)
|
|
49
|
+
puts "[env-external-demo] child exit status=#{Process.last_status.exitstatus}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
puts "[env-external-demo] done"
|
|
@@ -0,0 +1,52 @@
|
|
|
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/env_access_ruby_demo.rb
|
|
9
|
+
#
|
|
10
|
+
# This demo intentionally triggers Ruby-side ENV access methods so they are
|
|
11
|
+
# observed through TracePoint -> SPAN (USDT) path.
|
|
12
|
+
|
|
13
|
+
FILTER = {
|
|
14
|
+
include_events: %w[span_start span_stop env_caccess]
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def safe_fetch(key)
|
|
18
|
+
ENV.fetch(key)
|
|
19
|
+
rescue KeyError
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def demo_env_reads
|
|
24
|
+
ENV["HOME"]
|
|
25
|
+
safe_fetch("PATH")
|
|
26
|
+
ENV.key?("SHELL")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def demo_env_writes
|
|
30
|
+
ENV["VIVARIUM_ENV_DEMO_A"] = "1"
|
|
31
|
+
ENV.store("VIVARIUM_ENV_DEMO_B", "2")
|
|
32
|
+
ENV.delete("VIVARIUM_ENV_DEMO_A")
|
|
33
|
+
ENV.replace(ENV.to_h.merge("VIVARIUM_ENV_DEMO_C" => "3"))
|
|
34
|
+
ENV.delete("VIVARIUM_ENV_DEMO_B")
|
|
35
|
+
ENV.delete("VIVARIUM_ENV_DEMO_C")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Vivarium.observe(filter: FILTER) do
|
|
39
|
+
original_env = ENV.to_h
|
|
40
|
+
|
|
41
|
+
puts "[env-ruby-demo] read methods"
|
|
42
|
+
demo_env_reads
|
|
43
|
+
|
|
44
|
+
puts "[env-ruby-demo] write methods"
|
|
45
|
+
demo_env_writes
|
|
46
|
+
|
|
47
|
+
puts "[env-ruby-demo] clear"
|
|
48
|
+
ENV.clear
|
|
49
|
+
ENV.replace(original_env)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
puts "[env-ruby-demo] done"
|
data/lib/vivarium/box.rb
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Vivarium
|
|
6
|
+
# Box provides an isolated execution context where method calls are automatically traced
|
|
7
|
+
# through Vivarium's observation system.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# box = Vivarium::Box.new
|
|
11
|
+
# box.eval('class MyClass; def foo; "result"; end; end')
|
|
12
|
+
# result = box::MyClass.new.foo # automatically traced if Vivarium.observe is active
|
|
13
|
+
#
|
|
14
|
+
class Box < Module
|
|
15
|
+
DEFAULT_FILTER = {
|
|
16
|
+
include_events: %w[
|
|
17
|
+
proc_fork proc_exec span_start span_stop
|
|
18
|
+
sock_connect dns_req odd_socket
|
|
19
|
+
ssl_write
|
|
20
|
+
dlopen mmap_exec
|
|
21
|
+
task_kill
|
|
22
|
+
setid_change capable_check bprm_creds
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def initialize(pin_dir: Vivarium.bpf_pin_dir, dest: $stdout, filter: DEFAULT_FILTER)
|
|
27
|
+
super()
|
|
28
|
+
@inner_box = Ruby::Box.new
|
|
29
|
+
@pin_dir = pin_dir
|
|
30
|
+
@dest = dest
|
|
31
|
+
@filter = filter
|
|
32
|
+
@session = nil
|
|
33
|
+
|
|
34
|
+
@tracing_level = [0]
|
|
35
|
+
# Set up TracePoint to automatically trace method calls within this box
|
|
36
|
+
@tracer = TracePoint.new(:call, :return) do |tp|
|
|
37
|
+
handle_trace_event(tp, @tracing_level, @inner_box)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
attr_reader :inner_box, :tracer
|
|
41
|
+
|
|
42
|
+
# Evaluate code within the box context
|
|
43
|
+
def eval(code)
|
|
44
|
+
result = nil
|
|
45
|
+
Vivarium.observe(filter: @filter) do
|
|
46
|
+
result = @inner_box.eval(code)
|
|
47
|
+
end
|
|
48
|
+
result
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Require a file within the box context
|
|
52
|
+
# Automatically traced if Vivarium.observe is active
|
|
53
|
+
def require(path)
|
|
54
|
+
result = nil
|
|
55
|
+
Vivarium.observe(filter: @filter) do
|
|
56
|
+
result = @inner_box.require(path)
|
|
57
|
+
end
|
|
58
|
+
result
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Require a file relative to the current file within the box context
|
|
62
|
+
# Automatically traced if Vivarium.observe is active
|
|
63
|
+
def require_relative(path)
|
|
64
|
+
result = nil
|
|
65
|
+
Vivarium.observe(filter: @filter) do
|
|
66
|
+
result = @inner_box.require_relative(path)
|
|
67
|
+
end
|
|
68
|
+
result
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Load a file within the box context (executed every time, unlike require)
|
|
72
|
+
# Automatically traced if Vivarium.observe is active
|
|
73
|
+
def load(path, wrap = false)
|
|
74
|
+
result = nil
|
|
75
|
+
Vivarium.observe(filter: @filter) do
|
|
76
|
+
result = @inner_box.load(path, wrap)
|
|
77
|
+
end
|
|
78
|
+
result
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Intercept constant access to resolve from the box's evaluated context
|
|
82
|
+
def const_missing(name)
|
|
83
|
+
@inner_box.const_get(name)
|
|
84
|
+
rescue NameError => e
|
|
85
|
+
raise NameError, "#{name} not found in box: #{e.message}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def done_load!
|
|
89
|
+
@tracer.enable
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def handle_trace_event(tp, tracing_level, target_box)
|
|
95
|
+
begin
|
|
96
|
+
if should_trace_call?(tp, target_box)
|
|
97
|
+
case tp.event
|
|
98
|
+
when :call
|
|
99
|
+
if tracing_level[0].zero?
|
|
100
|
+
start_vivarium_observation
|
|
101
|
+
end
|
|
102
|
+
tracing_level[0] += 1
|
|
103
|
+
file_arg = Vivarium.tail_fit_string(tp.path, Vivarium::SPAN_FILE_ARG_MAX)
|
|
104
|
+
root = Ruby::Box.root
|
|
105
|
+
root::Vivarium::Usdt.start("#{tp.defined_class}", tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
|
|
106
|
+
when :return
|
|
107
|
+
tracing_level[0] -= 1
|
|
108
|
+
file_arg = Vivarium.tail_fit_string(tp.path, Vivarium::SPAN_FILE_ARG_MAX)
|
|
109
|
+
root = Ruby::Box.root
|
|
110
|
+
root::Vivarium::Usdt.stop("#{tp.defined_class}", tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
|
|
111
|
+
if tracing_level[0].zero?
|
|
112
|
+
stop_vivarium_observation
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
rescue StandardError
|
|
117
|
+
# Silently ignore tracing errors to avoid breaking user code
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def should_trace_call?(tp, target_box)
|
|
122
|
+
tp.binding.eval("Ruby::Box.current") == target_box
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def start_vivarium_observation
|
|
126
|
+
puts "[debug] Starting Vivarium observation for Box method calls"
|
|
127
|
+
@session = Vivarium.top_observe(pin_dir: @pin_dir, dest: @dest, filter: @filter)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def stop_vivarium_observation
|
|
131
|
+
puts "[debug] Stopping Vivarium observation for Box method calls"
|
|
132
|
+
@session&.stop
|
|
133
|
+
@session = nil
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
data/lib/vivarium/version.rb
CHANGED
data/lib/vivarium.rb
CHANGED
|
@@ -7,6 +7,12 @@ require "optparse"
|
|
|
7
7
|
require "pathname"
|
|
8
8
|
require "rbbcc"
|
|
9
9
|
require "socket"
|
|
10
|
+
if defined?(Ruby) && defined?(Ruby::Box) && Ruby::Box.enabled?
|
|
11
|
+
Ruby::Box.root.require "vivarium_usdt"
|
|
12
|
+
else
|
|
13
|
+
require "vivarium_usdt"
|
|
14
|
+
end
|
|
15
|
+
|
|
10
16
|
require_relative "vivarium/version"
|
|
11
17
|
require_relative "vivarium/cli"
|
|
12
18
|
|
|
@@ -95,7 +101,20 @@ module Vivarium
|
|
|
95
101
|
"Kernel#eval",
|
|
96
102
|
"Object#instance_eval",
|
|
97
103
|
"Object#instance_exec",
|
|
104
|
+
"ENV#[]",
|
|
105
|
+
"ENV#fetch",
|
|
106
|
+
"ENV#key?",
|
|
107
|
+
"ENV#[]=",
|
|
108
|
+
"ENV#store",
|
|
109
|
+
"ENV#delete",
|
|
110
|
+
"ENV#clear",
|
|
111
|
+
"ENV#replace",
|
|
98
112
|
].freeze
|
|
113
|
+
|
|
114
|
+
ENV_PAYLOAD_OP_SIZE = 16
|
|
115
|
+
ENV_PAYLOAD_KEY_OFFSET = ENV_PAYLOAD_OP_SIZE
|
|
116
|
+
ENV_PAYLOAD_KEY_SIZE = EVENT_PAYLOAD_SIZE - ENV_PAYLOAD_KEY_OFFSET
|
|
117
|
+
|
|
99
118
|
EVENT_SEVERITY_HIGH = %w[
|
|
100
119
|
capable_check bprm_creds setid_change task_kill
|
|
101
120
|
ptrace_check sb_mount kernel_read_file
|
|
@@ -407,6 +426,20 @@ module Vivarium
|
|
|
407
426
|
{ data_len: data_len, cap_len: cap_len, data: data }
|
|
408
427
|
end
|
|
409
428
|
|
|
429
|
+
def self.decode_env_payload(raw_payload)
|
|
430
|
+
bytes = raw_payload.to_s.b
|
|
431
|
+
return "" if bytes.bytesize < ENV_PAYLOAD_OP_SIZE
|
|
432
|
+
|
|
433
|
+
op = c_string(bytes[0, ENV_PAYLOAD_OP_SIZE])
|
|
434
|
+
key = c_string(bytes[ENV_PAYLOAD_KEY_OFFSET, ENV_PAYLOAD_KEY_SIZE])
|
|
435
|
+
|
|
436
|
+
return "" if op.empty?
|
|
437
|
+
return "op=#{op}" if key.empty?
|
|
438
|
+
|
|
439
|
+
key = key.split("=", 2).first if op == "putenv"
|
|
440
|
+
"op=#{op} key=#{key.inspect}"
|
|
441
|
+
end
|
|
442
|
+
|
|
410
443
|
def self.decode_span_raise_payload(raw_payload)
|
|
411
444
|
bytes = raw_payload.to_s.b
|
|
412
445
|
return "" if bytes.bytesize < 8
|
|
@@ -494,6 +527,9 @@ module Vivarium
|
|
|
494
527
|
when "ssl_write"
|
|
495
528
|
decoded = decode_ssl_write_payload(event.payload)
|
|
496
529
|
"data_len=#{decoded[:data_len]} cap_len=#{decoded[:cap_len]}"
|
|
530
|
+
when "env_caccess"
|
|
531
|
+
decoded = decode_env_payload(event.payload)
|
|
532
|
+
decoded.empty? ? event.payload.inspect : decoded
|
|
497
533
|
when "dlopen", "mmap_exec"
|
|
498
534
|
strip_to_first_null(event.payload).inspect
|
|
499
535
|
else
|
|
@@ -726,6 +762,26 @@ module Vivarium
|
|
|
726
762
|
events.ringbuf_submit(ev, 0);
|
|
727
763
|
}
|
|
728
764
|
|
|
765
|
+
static __always_inline void submit_env_event(u32 pid, const char *op, u32 op_len, const char *name_ptr)
|
|
766
|
+
{
|
|
767
|
+
struct event_t ev = {};
|
|
768
|
+
ev.pid = pid;
|
|
769
|
+
__builtin_memcpy(ev.event_name, "env_caccess", 12);
|
|
770
|
+
|
|
771
|
+
if (op && op_len > 0) {
|
|
772
|
+
if (op_len > #{ENV_PAYLOAD_OP_SIZE} - 1) {
|
|
773
|
+
op_len = #{ENV_PAYLOAD_OP_SIZE} - 1;
|
|
774
|
+
}
|
|
775
|
+
__builtin_memcpy(&ev.payload[0], op, op_len);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (name_ptr) {
|
|
779
|
+
bpf_probe_read_user_str(&ev.payload[#{ENV_PAYLOAD_KEY_OFFSET}], #{ENV_PAYLOAD_KEY_SIZE}, name_ptr);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
submit_event(&ev);
|
|
783
|
+
}
|
|
784
|
+
|
|
729
785
|
static __always_inline int is_dns_destination(void *addr)
|
|
730
786
|
{
|
|
731
787
|
u16 family = 0;
|
|
@@ -1519,6 +1575,80 @@ module Vivarium
|
|
|
1519
1575
|
return 0;
|
|
1520
1576
|
}
|
|
1521
1577
|
|
|
1578
|
+
int on_getenv(struct pt_regs *ctx)
|
|
1579
|
+
{
|
|
1580
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
1581
|
+
u32 pid = pid_tgid >> 32;
|
|
1582
|
+
u32 tid = (u32)pid_tgid;
|
|
1583
|
+
const char *name = (const char *)PT_REGS_PARM1(ctx);
|
|
1584
|
+
|
|
1585
|
+
if (!target_enabled(pid, tid) || !name) {
|
|
1586
|
+
return 0;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
submit_env_event(pid, "getenv", 6, name);
|
|
1590
|
+
return 0;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
int on_setenv(struct pt_regs *ctx)
|
|
1594
|
+
{
|
|
1595
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
1596
|
+
u32 pid = pid_tgid >> 32;
|
|
1597
|
+
u32 tid = (u32)pid_tgid;
|
|
1598
|
+
const char *name = (const char *)PT_REGS_PARM1(ctx);
|
|
1599
|
+
|
|
1600
|
+
if (!target_enabled(pid, tid) || !name) {
|
|
1601
|
+
return 0;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
submit_env_event(pid, "setenv", 6, name);
|
|
1605
|
+
return 0;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
int on_unsetenv(struct pt_regs *ctx)
|
|
1609
|
+
{
|
|
1610
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
1611
|
+
u32 pid = pid_tgid >> 32;
|
|
1612
|
+
u32 tid = (u32)pid_tgid;
|
|
1613
|
+
const char *name = (const char *)PT_REGS_PARM1(ctx);
|
|
1614
|
+
|
|
1615
|
+
if (!target_enabled(pid, tid) || !name) {
|
|
1616
|
+
return 0;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
submit_env_event(pid, "unsetenv", 8, name);
|
|
1620
|
+
return 0;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
int on_putenv(struct pt_regs *ctx)
|
|
1624
|
+
{
|
|
1625
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
1626
|
+
u32 pid = pid_tgid >> 32;
|
|
1627
|
+
u32 tid = (u32)pid_tgid;
|
|
1628
|
+
const char *string = (const char *)PT_REGS_PARM1(ctx);
|
|
1629
|
+
|
|
1630
|
+
if (!target_enabled(pid, tid) || !string) {
|
|
1631
|
+
return 0;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
submit_env_event(pid, "putenv", 6, string);
|
|
1635
|
+
return 0;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
int on_clearenv(struct pt_regs *ctx)
|
|
1639
|
+
{
|
|
1640
|
+
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
1641
|
+
u32 pid = pid_tgid >> 32;
|
|
1642
|
+
u32 tid = (u32)pid_tgid;
|
|
1643
|
+
|
|
1644
|
+
if (!target_enabled(pid, tid)) {
|
|
1645
|
+
return 0;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
submit_env_event(pid, "clearenv", 8, 0);
|
|
1649
|
+
return 0;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1522
1652
|
int on_span_raise(struct pt_regs *ctx)
|
|
1523
1653
|
{
|
|
1524
1654
|
u64 pid_tgid = bpf_get_current_pid_tgid();
|
|
@@ -1551,11 +1681,12 @@ module Vivarium
|
|
|
1551
1681
|
CLANG
|
|
1552
1682
|
|
|
1553
1683
|
def initialize(pin_dir: Vivarium.bpf_pin_dir, ssl_trace: true, libssl_path: nil,
|
|
1554
|
-
dlopen_trace: true, libc_path: nil)
|
|
1684
|
+
dlopen_trace: true, env_trace: true, libc_path: nil)
|
|
1555
1685
|
@pin_dir = pin_dir
|
|
1556
1686
|
@ssl_trace = ssl_trace
|
|
1557
1687
|
@libssl_path = libssl_path
|
|
1558
1688
|
@dlopen_trace = dlopen_trace
|
|
1689
|
+
@env_trace = env_trace
|
|
1559
1690
|
@libc_path = libc_path
|
|
1560
1691
|
end
|
|
1561
1692
|
|
|
@@ -1569,7 +1700,6 @@ module Vivarium
|
|
|
1569
1700
|
.gsub("__VIVARIUM_F_PATH_OFFSET__", f_path_offset.to_s)
|
|
1570
1701
|
.gsub("__VIVARIUM_DENTRY_D_NAME_OFFSET__", d_name_offset.to_s)
|
|
1571
1702
|
|
|
1572
|
-
require "vivarium_usdt"
|
|
1573
1703
|
usdt_so_path = ENV.fetch("VIVARIUM_USDT_SO_PATH") { Vivarium.locate_vivarium_usdt_so }
|
|
1574
1704
|
usdt = RbBCC::USDT.new(path: usdt_so_path)
|
|
1575
1705
|
usdt.enable_probe(probe: "start_probe", fn_name: "on_span_start")
|
|
@@ -1580,6 +1710,7 @@ module Vivarium
|
|
|
1580
1710
|
|
|
1581
1711
|
attach_ssl_write_uprobe(bpf) if @ssl_trace
|
|
1582
1712
|
attach_dlopen_uprobe(bpf) if @dlopen_trace
|
|
1713
|
+
attach_env_uprobes(bpf) if @env_trace
|
|
1583
1714
|
|
|
1584
1715
|
config_root_targets = bpf["config_root_targets"]
|
|
1585
1716
|
config_spawned_targets = bpf["config_spawned_targets"]
|
|
@@ -1652,6 +1783,30 @@ module Vivarium
|
|
|
1652
1783
|
warn "[vivariumd] dlopen uprobe attach failed: #{e.class}: #{e.message}"
|
|
1653
1784
|
end
|
|
1654
1785
|
|
|
1786
|
+
def attach_env_uprobes(bpf)
|
|
1787
|
+
path = resolve_libc_path
|
|
1788
|
+
unless path
|
|
1789
|
+
warn "[vivariumd] libc not found; ENV uprobes disabled " \
|
|
1790
|
+
"(set --libc PATH or VIVARIUM_LIBC_PATH to override)"
|
|
1791
|
+
return
|
|
1792
|
+
end
|
|
1793
|
+
|
|
1794
|
+
{
|
|
1795
|
+
"getenv" => "on_getenv",
|
|
1796
|
+
"setenv" => "on_setenv",
|
|
1797
|
+
"unsetenv" => "on_unsetenv",
|
|
1798
|
+
"putenv" => "on_putenv",
|
|
1799
|
+
"clearenv" => "on_clearenv"
|
|
1800
|
+
}.each do |sym, fn_name|
|
|
1801
|
+
begin
|
|
1802
|
+
bpf.attach_uprobe(name: path, sym: sym, fn_name: fn_name)
|
|
1803
|
+
puts "[vivariumd] #{sym} uprobe attached via #{path}"
|
|
1804
|
+
rescue StandardError => e
|
|
1805
|
+
warn "[vivariumd] #{sym} uprobe attach failed: #{e.class}: #{e.message}"
|
|
1806
|
+
end
|
|
1807
|
+
end
|
|
1808
|
+
end
|
|
1809
|
+
|
|
1655
1810
|
def resolve_libc_path
|
|
1656
1811
|
if @libc_path
|
|
1657
1812
|
return @libc_path if File.exist?(@libc_path)
|
|
@@ -1819,8 +1974,6 @@ module Vivarium
|
|
|
1819
1974
|
end
|
|
1820
1975
|
|
|
1821
1976
|
def self.top_observe(pin_dir: bpf_pin_dir, dest: $stdout, filter: nil)
|
|
1822
|
-
require "vivarium_usdt"
|
|
1823
|
-
|
|
1824
1977
|
store = MapStore.new(pin_dir: pin_dir)
|
|
1825
1978
|
pid = Process.pid
|
|
1826
1979
|
store.register_pid(pid)
|
|
@@ -1847,8 +2000,6 @@ module Vivarium
|
|
|
1847
2000
|
end
|
|
1848
2001
|
|
|
1849
2002
|
def self.scoped_observe(pin_dir:, dest:, filter: nil)
|
|
1850
|
-
require "vivarium_usdt"
|
|
1851
|
-
|
|
1852
2003
|
store = MapStore.new(pin_dir: pin_dir)
|
|
1853
2004
|
pid = Process.pid
|
|
1854
2005
|
store.register_pid(pid)
|
|
@@ -1892,18 +2043,23 @@ module Vivarium
|
|
|
1892
2043
|
next
|
|
1893
2044
|
end
|
|
1894
2045
|
|
|
1895
|
-
signature =
|
|
2046
|
+
signature = if tp.self.equal?(ENV)
|
|
2047
|
+
"ENV##{tp.method_id}"
|
|
2048
|
+
else
|
|
2049
|
+
"#{tp.defined_class}##{tp.method_id}"
|
|
2050
|
+
end
|
|
1896
2051
|
is_target = allowlist.include?(signature) || \
|
|
1897
2052
|
allow_classes.any? { |klass| tp.defined_class == klass } || \
|
|
1898
2053
|
allow_classes.any? { |klass| tp.defined_class == klass.singleton_class }
|
|
1899
2054
|
next unless is_target
|
|
1900
2055
|
|
|
1901
2056
|
file_arg = tail_fit_string(tp.path, SPAN_FILE_ARG_MAX)
|
|
2057
|
+
span_class_name = tp.self.equal?(ENV) ? "ENV" : tp.defined_class.to_s
|
|
1902
2058
|
case tp.event
|
|
1903
2059
|
when :call, :c_call
|
|
1904
|
-
Vivarium::Usdt.start(
|
|
2060
|
+
Vivarium::Usdt.start(span_class_name, tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
|
|
1905
2061
|
when :return, :c_return
|
|
1906
|
-
Vivarium::Usdt.stop(
|
|
2062
|
+
Vivarium::Usdt.stop(span_class_name, tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
|
|
1907
2063
|
end
|
|
1908
2064
|
end
|
|
1909
2065
|
end
|
|
@@ -1924,7 +2080,6 @@ module Vivarium
|
|
|
1924
2080
|
end
|
|
1925
2081
|
|
|
1926
2082
|
def self.locate_vivarium_usdt_so
|
|
1927
|
-
require "vivarium_usdt/vivarium_usdt"
|
|
1928
2083
|
so = $LOADED_FEATURES.find { |p| p =~ %r{vivarium_usdt/vivarium_usdt\.(so|bundle|dylib)\z} }
|
|
1929
2084
|
raise Error, "vivarium_usdt native extension not found in $LOADED_FEATURES" unless so
|
|
1930
2085
|
|
|
@@ -1935,10 +2090,11 @@ module Vivarium
|
|
|
1935
2090
|
|
|
1936
2091
|
def self.run_daemon!(argv = ARGV)
|
|
1937
2092
|
options = { pin_dir: bpf_pin_dir, ssl_trace: true, libssl_path: nil,
|
|
2093
|
+
env_trace: true,
|
|
1938
2094
|
dlopen_trace: true, libc_path: nil }
|
|
1939
2095
|
OptionParser.new do |opts|
|
|
1940
2096
|
opts.banner = "Usage: vivariumd [--pin-dir PATH] [--no-ssl-trace] [--libssl PATH] " \
|
|
1941
|
-
"[--no-dlopen-trace] [--libc PATH]"
|
|
2097
|
+
"[--no-dlopen-trace] [--no-env-trace] [--libc PATH]"
|
|
1942
2098
|
opts.on("--pin-dir PATH", "Pinned map directory") { |v| options[:pin_dir] = v }
|
|
1943
2099
|
opts.on("--[no-]ssl-trace", "Attach OpenSSL SSL_write uprobe (default: enabled)") do |v|
|
|
1944
2100
|
options[:ssl_trace] = v
|
|
@@ -1949,6 +2105,9 @@ module Vivarium
|
|
|
1949
2105
|
opts.on("--[no-]dlopen-trace", "Attach libc dlopen uprobe (default: enabled)") do |v|
|
|
1950
2106
|
options[:dlopen_trace] = v
|
|
1951
2107
|
end
|
|
2108
|
+
opts.on("--[no-]env-trace", "Attach libc getenv/setenv uprobes (default: enabled)") do |v|
|
|
2109
|
+
options[:env_trace] = v
|
|
2110
|
+
end
|
|
1952
2111
|
opts.on("--libc PATH", "Path to libc.so for dlopen uprobe") do |v|
|
|
1953
2112
|
options[:libc_path] = v
|
|
1954
2113
|
end
|
|
@@ -1959,6 +2118,7 @@ module Vivarium
|
|
|
1959
2118
|
ssl_trace: options[:ssl_trace],
|
|
1960
2119
|
libssl_path: options[:libssl_path],
|
|
1961
2120
|
dlopen_trace: options[:dlopen_trace],
|
|
2121
|
+
env_trace: options[:env_trace],
|
|
1962
2122
|
libc_path: options[:libc_path]
|
|
1963
2123
|
).run
|
|
1964
2124
|
end
|
|
@@ -1967,3 +2127,6 @@ end
|
|
|
1967
2127
|
require_relative "vivarium/correlator"
|
|
1968
2128
|
require_relative "vivarium/display_filter"
|
|
1969
2129
|
require_relative "vivarium/tree_renderer"
|
|
2130
|
+
if defined?(Ruby) && defined?(Ruby::Box) && Ruby::Box.enabled?
|
|
2131
|
+
require_relative "vivarium/box"
|
|
2132
|
+
end
|
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.
|
|
4
|
+
version: 0.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Uchio Kondo
|
|
@@ -64,8 +64,11 @@ files:
|
|
|
64
64
|
- CONTEXT.md
|
|
65
65
|
- README.md
|
|
66
66
|
- Rakefile
|
|
67
|
+
- examples/box_demo.rb
|
|
67
68
|
- examples/dlopen_demo.rb
|
|
68
69
|
- examples/drop_demo.rb
|
|
70
|
+
- examples/env_access_external_demo.rb
|
|
71
|
+
- examples/env_access_ruby_demo.rb
|
|
69
72
|
- examples/execve_demo.rb
|
|
70
73
|
- examples/file_operation_demo.rb
|
|
71
74
|
- examples/network_client_demo.rb
|
|
@@ -78,6 +81,7 @@ files:
|
|
|
78
81
|
- exe/vivariumd
|
|
79
82
|
- image.png
|
|
80
83
|
- lib/vivarium.rb
|
|
84
|
+
- lib/vivarium/box.rb
|
|
81
85
|
- lib/vivarium/cli.rb
|
|
82
86
|
- lib/vivarium/correlator.rb
|
|
83
87
|
- lib/vivarium/display_filter.rb
|