vivarium 0.4.0 → 0.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6549be32807adc904ffe18fbefc055600cf14c85c68b106a3abfd7dce354e8b1
4
- data.tar.gz: 1f9c961424d4712b2c43ad3df8d81ee679ea2190acaa8ea24e78a4f88b92f452
3
+ metadata.gz: be07034ab56d73e0aaa4fe9e124d67888ae2006cc5f6cb632cf4b93b20855422
4
+ data.tar.gz: 5f95cdaa111a56415f4fed08da0c638a87f5c08f01e51db2b5b818c04c256bc4
5
5
  SHA512:
6
- metadata.gz: ebe88587a6328a37703899da2f0c8275f9321d7ea34e1af9806dccfaf929a6ee45bce68378356aeb2171708936b74ebb33545ada8a7eace2774aa60783b31b24
7
- data.tar.gz: f707b190642345628ddf613038942a87cd95585877b45a264e13ffb1c243da0b7576712d6b5e5eb65e336dab06ff95a01f329b383bdb0007aec879629c30d2eb
6
+ metadata.gz: 49e166adbaca6231263d10255779ce4ff16a98ba9905a4afc48da012382d21fe1635b4f3bfff3990125bcb9718f7e1e5f3c5168a3a7f39b7fd8981be312bc3a4
7
+ data.tar.gz: a85783ba20a6eb866bd8215f55a4edb6890bef1b0b3e3e7af6d11bfcc0fda77adbfa2e7ea465e201c424d6fc9f883bcb20d7846e3c051c294560151ac0078d30
data/README.md CHANGED
@@ -139,6 +139,22 @@ 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
+
142
158
  You can also start top-level observation without a block (it keeps observing until process exit):
143
159
 
144
160
  ```ruby
@@ -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"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vivarium
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.1"
5
5
  end
data/lib/vivarium.rb CHANGED
@@ -95,7 +95,20 @@ module Vivarium
95
95
  "Kernel#eval",
96
96
  "Object#instance_eval",
97
97
  "Object#instance_exec",
98
+ "ENV#[]",
99
+ "ENV#fetch",
100
+ "ENV#key?",
101
+ "ENV#[]=",
102
+ "ENV#store",
103
+ "ENV#delete",
104
+ "ENV#clear",
105
+ "ENV#replace",
98
106
  ].freeze
107
+
108
+ ENV_PAYLOAD_OP_SIZE = 16
109
+ ENV_PAYLOAD_KEY_OFFSET = ENV_PAYLOAD_OP_SIZE
110
+ ENV_PAYLOAD_KEY_SIZE = EVENT_PAYLOAD_SIZE - ENV_PAYLOAD_KEY_OFFSET
111
+
99
112
  EVENT_SEVERITY_HIGH = %w[
100
113
  capable_check bprm_creds setid_change task_kill
101
114
  ptrace_check sb_mount kernel_read_file
@@ -407,6 +420,20 @@ module Vivarium
407
420
  { data_len: data_len, cap_len: cap_len, data: data }
408
421
  end
409
422
 
423
+ def self.decode_env_payload(raw_payload)
424
+ bytes = raw_payload.to_s.b
425
+ return "" if bytes.bytesize < ENV_PAYLOAD_OP_SIZE
426
+
427
+ op = c_string(bytes[0, ENV_PAYLOAD_OP_SIZE])
428
+ key = c_string(bytes[ENV_PAYLOAD_KEY_OFFSET, ENV_PAYLOAD_KEY_SIZE])
429
+
430
+ return "" if op.empty?
431
+ return "op=#{op}" if key.empty?
432
+
433
+ key = key.split("=", 2).first if op == "putenv"
434
+ "op=#{op} key=#{key.inspect}"
435
+ end
436
+
410
437
  def self.decode_span_raise_payload(raw_payload)
411
438
  bytes = raw_payload.to_s.b
412
439
  return "" if bytes.bytesize < 8
@@ -494,6 +521,9 @@ module Vivarium
494
521
  when "ssl_write"
495
522
  decoded = decode_ssl_write_payload(event.payload)
496
523
  "data_len=#{decoded[:data_len]} cap_len=#{decoded[:cap_len]}"
524
+ when "env_caccess"
525
+ decoded = decode_env_payload(event.payload)
526
+ decoded.empty? ? event.payload.inspect : decoded
497
527
  when "dlopen", "mmap_exec"
498
528
  strip_to_first_null(event.payload).inspect
499
529
  else
@@ -726,6 +756,26 @@ module Vivarium
726
756
  events.ringbuf_submit(ev, 0);
727
757
  }
728
758
 
759
+ static __always_inline void submit_env_event(u32 pid, const char *op, u32 op_len, const char *name_ptr)
760
+ {
761
+ struct event_t ev = {};
762
+ ev.pid = pid;
763
+ __builtin_memcpy(ev.event_name, "env_caccess", 12);
764
+
765
+ if (op && op_len > 0) {
766
+ if (op_len > #{ENV_PAYLOAD_OP_SIZE} - 1) {
767
+ op_len = #{ENV_PAYLOAD_OP_SIZE} - 1;
768
+ }
769
+ __builtin_memcpy(&ev.payload[0], op, op_len);
770
+ }
771
+
772
+ if (name_ptr) {
773
+ bpf_probe_read_user_str(&ev.payload[#{ENV_PAYLOAD_KEY_OFFSET}], #{ENV_PAYLOAD_KEY_SIZE}, name_ptr);
774
+ }
775
+
776
+ submit_event(&ev);
777
+ }
778
+
729
779
  static __always_inline int is_dns_destination(void *addr)
730
780
  {
731
781
  u16 family = 0;
@@ -1519,6 +1569,80 @@ module Vivarium
1519
1569
  return 0;
1520
1570
  }
1521
1571
 
1572
+ int on_getenv(struct pt_regs *ctx)
1573
+ {
1574
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1575
+ u32 pid = pid_tgid >> 32;
1576
+ u32 tid = (u32)pid_tgid;
1577
+ const char *name = (const char *)PT_REGS_PARM1(ctx);
1578
+
1579
+ if (!target_enabled(pid, tid) || !name) {
1580
+ return 0;
1581
+ }
1582
+
1583
+ submit_env_event(pid, "getenv", 6, name);
1584
+ return 0;
1585
+ }
1586
+
1587
+ int on_setenv(struct pt_regs *ctx)
1588
+ {
1589
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1590
+ u32 pid = pid_tgid >> 32;
1591
+ u32 tid = (u32)pid_tgid;
1592
+ const char *name = (const char *)PT_REGS_PARM1(ctx);
1593
+
1594
+ if (!target_enabled(pid, tid) || !name) {
1595
+ return 0;
1596
+ }
1597
+
1598
+ submit_env_event(pid, "setenv", 6, name);
1599
+ return 0;
1600
+ }
1601
+
1602
+ int on_unsetenv(struct pt_regs *ctx)
1603
+ {
1604
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1605
+ u32 pid = pid_tgid >> 32;
1606
+ u32 tid = (u32)pid_tgid;
1607
+ const char *name = (const char *)PT_REGS_PARM1(ctx);
1608
+
1609
+ if (!target_enabled(pid, tid) || !name) {
1610
+ return 0;
1611
+ }
1612
+
1613
+ submit_env_event(pid, "unsetenv", 8, name);
1614
+ return 0;
1615
+ }
1616
+
1617
+ int on_putenv(struct pt_regs *ctx)
1618
+ {
1619
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1620
+ u32 pid = pid_tgid >> 32;
1621
+ u32 tid = (u32)pid_tgid;
1622
+ const char *string = (const char *)PT_REGS_PARM1(ctx);
1623
+
1624
+ if (!target_enabled(pid, tid) || !string) {
1625
+ return 0;
1626
+ }
1627
+
1628
+ submit_env_event(pid, "putenv", 6, string);
1629
+ return 0;
1630
+ }
1631
+
1632
+ int on_clearenv(struct pt_regs *ctx)
1633
+ {
1634
+ u64 pid_tgid = bpf_get_current_pid_tgid();
1635
+ u32 pid = pid_tgid >> 32;
1636
+ u32 tid = (u32)pid_tgid;
1637
+
1638
+ if (!target_enabled(pid, tid)) {
1639
+ return 0;
1640
+ }
1641
+
1642
+ submit_env_event(pid, "clearenv", 8, 0);
1643
+ return 0;
1644
+ }
1645
+
1522
1646
  int on_span_raise(struct pt_regs *ctx)
1523
1647
  {
1524
1648
  u64 pid_tgid = bpf_get_current_pid_tgid();
@@ -1551,11 +1675,12 @@ module Vivarium
1551
1675
  CLANG
1552
1676
 
1553
1677
  def initialize(pin_dir: Vivarium.bpf_pin_dir, ssl_trace: true, libssl_path: nil,
1554
- dlopen_trace: true, libc_path: nil)
1678
+ dlopen_trace: true, env_trace: true, libc_path: nil)
1555
1679
  @pin_dir = pin_dir
1556
1680
  @ssl_trace = ssl_trace
1557
1681
  @libssl_path = libssl_path
1558
1682
  @dlopen_trace = dlopen_trace
1683
+ @env_trace = env_trace
1559
1684
  @libc_path = libc_path
1560
1685
  end
1561
1686
 
@@ -1580,6 +1705,7 @@ module Vivarium
1580
1705
 
1581
1706
  attach_ssl_write_uprobe(bpf) if @ssl_trace
1582
1707
  attach_dlopen_uprobe(bpf) if @dlopen_trace
1708
+ attach_env_uprobes(bpf) if @env_trace
1583
1709
 
1584
1710
  config_root_targets = bpf["config_root_targets"]
1585
1711
  config_spawned_targets = bpf["config_spawned_targets"]
@@ -1652,6 +1778,30 @@ module Vivarium
1652
1778
  warn "[vivariumd] dlopen uprobe attach failed: #{e.class}: #{e.message}"
1653
1779
  end
1654
1780
 
1781
+ def attach_env_uprobes(bpf)
1782
+ path = resolve_libc_path
1783
+ unless path
1784
+ warn "[vivariumd] libc not found; ENV uprobes disabled " \
1785
+ "(set --libc PATH or VIVARIUM_LIBC_PATH to override)"
1786
+ return
1787
+ end
1788
+
1789
+ {
1790
+ "getenv" => "on_getenv",
1791
+ "setenv" => "on_setenv",
1792
+ "unsetenv" => "on_unsetenv",
1793
+ "putenv" => "on_putenv",
1794
+ "clearenv" => "on_clearenv"
1795
+ }.each do |sym, fn_name|
1796
+ begin
1797
+ bpf.attach_uprobe(name: path, sym: sym, fn_name: fn_name)
1798
+ puts "[vivariumd] #{sym} uprobe attached via #{path}"
1799
+ rescue StandardError => e
1800
+ warn "[vivariumd] #{sym} uprobe attach failed: #{e.class}: #{e.message}"
1801
+ end
1802
+ end
1803
+ end
1804
+
1655
1805
  def resolve_libc_path
1656
1806
  if @libc_path
1657
1807
  return @libc_path if File.exist?(@libc_path)
@@ -1892,18 +2042,23 @@ module Vivarium
1892
2042
  next
1893
2043
  end
1894
2044
 
1895
- signature = "#{tp.defined_class}##{tp.method_id}"
2045
+ signature = if tp.self.equal?(ENV)
2046
+ "ENV##{tp.method_id}"
2047
+ else
2048
+ "#{tp.defined_class}##{tp.method_id}"
2049
+ end
1896
2050
  is_target = allowlist.include?(signature) || \
1897
2051
  allow_classes.any? { |klass| tp.defined_class == klass } || \
1898
2052
  allow_classes.any? { |klass| tp.defined_class == klass.singleton_class }
1899
2053
  next unless is_target
1900
2054
 
1901
2055
  file_arg = tail_fit_string(tp.path, SPAN_FILE_ARG_MAX)
2056
+ span_class_name = tp.self.equal?(ENV) ? "ENV" : tp.defined_class.to_s
1902
2057
  case tp.event
1903
2058
  when :call, :c_call
1904
- Vivarium::Usdt.start(tp.defined_class.to_s, tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
2059
+ Vivarium::Usdt.start(span_class_name, tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
1905
2060
  when :return, :c_return
1906
- Vivarium::Usdt.stop(tp.defined_class.to_s, tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
2061
+ Vivarium::Usdt.stop(span_class_name, tp.method_id.to_s, file: file_arg, lineno: tp.lineno)
1907
2062
  end
1908
2063
  end
1909
2064
  end
@@ -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
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.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Uchio Kondo
@@ -66,6 +66,8 @@ files:
66
66
  - Rakefile
67
67
  - examples/dlopen_demo.rb
68
68
  - examples/drop_demo.rb
69
+ - examples/env_access_external_demo.rb
70
+ - examples/env_access_ruby_demo.rb
69
71
  - examples/execve_demo.rb
70
72
  - examples/file_operation_demo.rb
71
73
  - examples/network_client_demo.rb