seccomp-tools 1.6.2 → 1.7.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/CHANGELOG.md +153 -0
- data/README.md +263 -42
- data/completions/_seccomp-tools +83 -0
- data/completions/seccomp-tools.bash +59 -0
- data/completions/seccomp-tools.fish +52 -0
- data/ext/ptrace/ptrace.c +2 -2
- data/lib/seccomp-tools/asm/asm.rb +9 -4
- data/lib/seccomp-tools/asm/compiler.rb +32 -2
- data/lib/seccomp-tools/asm/sasm.tab.rb +27 -19
- data/lib/seccomp-tools/asm/sasm.y +15 -7
- data/lib/seccomp-tools/asm/scalar.rb +50 -7
- data/lib/seccomp-tools/asm/scanner.rb +33 -1
- data/lib/seccomp-tools/asm/statement.rb +14 -5
- data/lib/seccomp-tools/asm/token.rb +19 -1
- data/lib/seccomp-tools/audit/catalog.rb +66 -0
- data/lib/seccomp-tools/audit/checks/arch_unchecked.rb +41 -0
- data/lib/seccomp-tools/audit/checks/dangerous_allow.rb +34 -0
- data/lib/seccomp-tools/audit/checks/orw_chain.rb +41 -0
- data/lib/seccomp-tools/audit/checks/permissive_default.rb +29 -0
- data/lib/seccomp-tools/audit/checks/syscall_alt_gap.rb +44 -0
- data/lib/seccomp-tools/audit/checks/x32_guard.rb +46 -0
- data/lib/seccomp-tools/audit/checks.rb +42 -0
- data/lib/seccomp-tools/audit/finding.rb +25 -0
- data/lib/seccomp-tools/audit/policy.rb +126 -0
- data/lib/seccomp-tools/audit/report.rb +98 -0
- data/lib/seccomp-tools/audit.rb +48 -0
- data/lib/seccomp-tools/bpf.rb +27 -17
- data/lib/seccomp-tools/cli/asm.rb +6 -2
- data/lib/seccomp-tools/cli/audit.rb +86 -0
- data/lib/seccomp-tools/cli/base.rb +51 -8
- data/lib/seccomp-tools/cli/cli.rb +9 -3
- data/lib/seccomp-tools/cli/completion.rb +40 -0
- data/lib/seccomp-tools/cli/disasm.rb +9 -5
- data/lib/seccomp-tools/cli/dump.rb +37 -52
- data/lib/seccomp-tools/cli/dumpable.rb +79 -0
- data/lib/seccomp-tools/cli/emu.rb +20 -5
- data/lib/seccomp-tools/cli/explain.rb +51 -0
- data/lib/seccomp-tools/cli/filter_input.rb +130 -0
- data/lib/seccomp-tools/const.rb +98 -15
- data/lib/seccomp-tools/consts/sys_nr/riscv64.rb +332 -0
- data/lib/seccomp-tools/disasm/disasm.rb +30 -12
- data/lib/seccomp-tools/dumper.rb +65 -33
- data/lib/seccomp-tools/emulator.rb +40 -15
- data/lib/seccomp-tools/error.rb +4 -2
- data/lib/seccomp-tools/explain/analysis.rb +67 -0
- data/lib/seccomp-tools/explain/path_facts.rb +110 -0
- data/lib/seccomp-tools/explain/qword.rb +204 -0
- data/lib/seccomp-tools/explain/renderer.rb +128 -0
- data/lib/seccomp-tools/explain/summary.rb +218 -0
- data/lib/seccomp-tools/explain/verdict.rb +44 -0
- data/lib/seccomp-tools/explain.rb +38 -0
- data/lib/seccomp-tools/instruction/alu.rb +14 -9
- data/lib/seccomp-tools/instruction/base.rb +39 -10
- data/lib/seccomp-tools/instruction/jmp.rb +50 -23
- data/lib/seccomp-tools/instruction/ld.rb +46 -21
- data/lib/seccomp-tools/instruction/ldx.rb +4 -3
- data/lib/seccomp-tools/instruction/misc.rb +11 -9
- data/lib/seccomp-tools/instruction/ret.rb +12 -6
- data/lib/seccomp-tools/instruction/st.rb +15 -6
- data/lib/seccomp-tools/instruction/stx.rb +4 -3
- data/lib/seccomp-tools/logger.rb +14 -1
- data/lib/seccomp-tools/symbolic/constraint.rb +80 -0
- data/lib/seccomp-tools/symbolic/executor.rb +210 -0
- data/lib/seccomp-tools/symbolic/expr.rb +185 -0
- data/lib/seccomp-tools/symbolic/state.rb +82 -0
- data/lib/seccomp-tools/syscall.rb +72 -20
- data/lib/seccomp-tools/util.rb +70 -11
- data/lib/seccomp-tools/version.rb +1 -1
- data/lib/seccomp-tools.rb +10 -1
- metadata +36 -4
- data/lib/seccomp-tools/disasm/context.rb +0 -171
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'seccomp-tools/symbolic/expr'
|
|
4
|
+
|
|
5
|
+
module SeccompTools
|
|
6
|
+
module Symbolic
|
|
7
|
+
# A snapshot of the classic-BPF machine at one point along one path of the walk: the two
|
|
8
|
+
# registers (A and X), the 16 scratch-memory slots, and the path condition (the {Constraint}s
|
|
9
|
+
# that must hold to have reached here). The walk fills values in as it interprets
|
|
10
|
+
# instructions, starting from {.initial}.
|
|
11
|
+
#
|
|
12
|
+
# A {State} is treated as immutable - stepping an instruction produces a *new* state via {#with},
|
|
13
|
+
# so the many paths of the walk can safely share the parts they have in common.
|
|
14
|
+
class State
|
|
15
|
+
# @return [Expr] Register A (the accumulator).
|
|
16
|
+
attr_reader :a
|
|
17
|
+
# @return [Expr] Register X (the index register).
|
|
18
|
+
attr_reader :x
|
|
19
|
+
# @return [Array<Expr>] The 16 scratch-memory slots (+mem[0..15]+).
|
|
20
|
+
attr_reader :mem
|
|
21
|
+
# @return [Array<Constraint>] The path condition accumulated so far.
|
|
22
|
+
attr_reader :path
|
|
23
|
+
|
|
24
|
+
# The starting state, as the kernel sets it up: both registers zero (a classic BPF program is
|
|
25
|
+
# guaranteed +A = X = 0+ on entry - the cBPF-to-eBPF converter clears them first), the
|
|
26
|
+
# scratch slots unknown, and no facts assumed. The slots stay {Expr.opaque} because nothing
|
|
27
|
+
# can rely on them: the kernel rejects a filter that reads a slot before writing it.
|
|
28
|
+
# @return [State]
|
|
29
|
+
def self.initial
|
|
30
|
+
new(a: Expr.imm(0), x: Expr.imm(0), mem: Array.new(16, Expr.opaque), path: [])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param [Expr] a
|
|
34
|
+
# @param [Expr] x
|
|
35
|
+
# @param [Array<Expr>] mem
|
|
36
|
+
# @param [Array<Constraint>] path
|
|
37
|
+
def initialize(a:, x:, mem:, path:)
|
|
38
|
+
@a = a
|
|
39
|
+
@x = x
|
|
40
|
+
@mem = mem
|
|
41
|
+
@path = path
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns a copy with the given fields replaced; unreplaced fields are shared (the state is
|
|
45
|
+
# immutable).
|
|
46
|
+
# @return [State]
|
|
47
|
+
def with(a: @a, x: @x, mem: @mem, path: @path)
|
|
48
|
+
State.new(a:, x:, mem:, path:)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A string that identifies this state exactly, joined from each part's {Expr#key} /
|
|
52
|
+
# {Constraint#key} (the same identity, one level up). The walk uses it to skip a
|
|
53
|
+
# +(line, state)+ pair it has already visited, which keeps merging control-flow from blowing
|
|
54
|
+
# up. Both {#==} and {#hash} are decided on it; see {Expr#key} for why the key is a string.
|
|
55
|
+
# The +;+ / +,+ separators appear in no part's key, so the join stays injective.
|
|
56
|
+
# @return [String]
|
|
57
|
+
def key
|
|
58
|
+
@key ||= "#{a.key};#{x.key};#{mem.map(&:key).join(',')};#{path.map(&:key).join(',')}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The constant the data word at byte +offset+ is pinned to on this path (by an +==+ fact), or
|
|
62
|
+
# +nil+ when it is not pinned.
|
|
63
|
+
# @param [Integer] offset
|
|
64
|
+
# @return [Integer?]
|
|
65
|
+
def pinned(offset)
|
|
66
|
+
path.find { |c| c.plain_data_eq?(offset) }&.rhs&.val
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @param [State] other
|
|
70
|
+
# @return [Boolean]
|
|
71
|
+
def ==(other)
|
|
72
|
+
other.is_a?(State) && key == other.key
|
|
73
|
+
end
|
|
74
|
+
alias eql? ==
|
|
75
|
+
|
|
76
|
+
# @return [Integer]
|
|
77
|
+
def hash
|
|
78
|
+
key.hash
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
# encoding: ascii-8bit
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
require 'os'
|
|
4
5
|
|
|
5
6
|
require 'seccomp-tools/const'
|
|
7
|
+
require 'seccomp-tools/util'
|
|
6
8
|
require 'seccomp-tools/ptrace' if OS.linux?
|
|
7
9
|
|
|
8
10
|
module SeccompTools
|
|
@@ -13,25 +15,32 @@ module SeccompTools
|
|
|
13
15
|
amd64: { number: 120, args: [112, 104, 96, 56, 72, 44], ret: 80, SYS_prctl: 157, SYS_seccomp: 317 },
|
|
14
16
|
i386: { number: 44, args: [0, 4, 8, 12, 16, 20], ret: 24, SYS_prctl: 172, SYS_seccomp: 354 },
|
|
15
17
|
aarch64: { number: 64, args: [0, 8, 16, 24, 32, 40, 48], ret: 0, SYS_prctl: 167, SYS_seccomp: 277 },
|
|
18
|
+
riscv64: { number: 136, args: [80, 88, 96, 104, 112, 120], ret: 80, SYS_prctl: 167, SYS_seccomp: 277 },
|
|
16
19
|
# Most software invokes syscalls through "svc 0", in which case the syscall number is in r1.
|
|
17
20
|
# However, it's also possible to use "svc NR": this case is not handled here.
|
|
18
21
|
s390x: { number: 24, args: [32, 40, 48, 56, 64, 72], ret: 32, SYS_prctl: 172, SYS_seccomp: 348 }
|
|
19
22
|
}.freeze
|
|
20
23
|
|
|
21
|
-
# @return [Integer]
|
|
24
|
+
# @return [Integer] Id of the traced process.
|
|
22
25
|
attr_reader :pid
|
|
23
|
-
# @return [{Symbol => Integer, Array<Integer>}]
|
|
26
|
+
# @return [{Symbol => Integer, Array<Integer>}]
|
|
27
|
+
# The {ABI} entry of this syscall's architecture.
|
|
24
28
|
attr_reader :abi
|
|
25
29
|
# @return [Integer] Syscall number.
|
|
26
30
|
attr_reader :number
|
|
27
|
-
# @return [Integer] Syscall arguments.
|
|
31
|
+
# @return [Array<Integer>] Syscall arguments, in register order.
|
|
28
32
|
attr_reader :args
|
|
29
33
|
# @return [Integer] Syscall return value.
|
|
30
34
|
attr_reader :ret
|
|
31
35
|
|
|
32
36
|
# Instantiate a {Syscall} object.
|
|
33
|
-
#
|
|
34
|
-
#
|
|
37
|
+
#
|
|
38
|
+
# Reads the syscall number, arguments and return value of +pid+ through ptrace, so the process
|
|
39
|
+
# must already be stopped and attached.
|
|
40
|
+
# @param [Integer] pid
|
|
41
|
+
# Id of the traced process.
|
|
42
|
+
# @raise [ArgumentError]
|
|
43
|
+
# If the architecture of +pid+ is not one of {ABI}'s keys.
|
|
35
44
|
def initialize(pid)
|
|
36
45
|
@pid = pid
|
|
37
46
|
raise ArgumentError, "Only supports #{ABI.keys.join(', ')}" if ABI[arch].nil?
|
|
@@ -42,38 +51,80 @@ module SeccompTools
|
|
|
42
51
|
@ret = peek(abi[:ret])
|
|
43
52
|
end
|
|
44
53
|
|
|
45
|
-
# Is this a
|
|
54
|
+
# Is this a seccomp installation syscall?
|
|
46
55
|
#
|
|
56
|
+
# Both +SECCOMP_MODE_FILTER+ and +SECCOMP_MODE_STRICT+ installations are recognized, invoked
|
|
57
|
+
# through either +seccomp+ or +prctl(PR_SET_SECCOMP, ...)+.
|
|
47
58
|
# @return [Boolean]
|
|
48
|
-
# +true+
|
|
59
|
+
# +true+ if this is a seccomp installation syscall.
|
|
49
60
|
def set_seccomp?
|
|
50
|
-
|
|
61
|
+
filter_mode? || strict_mode?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Is this a +seccomp(SECCOMP_SET_MODE_FILTER, ..)+/+prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, ..)+ syscall?
|
|
65
|
+
#
|
|
66
|
+
# @return [Boolean]
|
|
67
|
+
def filter_mode?
|
|
51
68
|
return true if number == abi[:SYS_seccomp] && args[0] == Const::BPF::SECCOMP_SET_MODE_FILTER
|
|
52
69
|
|
|
53
70
|
number == abi[:SYS_prctl] && args[0] == Const::BPF::PR_SET_SECCOMP && args[1] == Const::BPF::SECCOMP_MODE_FILTER
|
|
54
71
|
end
|
|
55
72
|
|
|
56
|
-
#
|
|
73
|
+
# Is this a +seccomp(SECCOMP_SET_MODE_STRICT, ..)+/+prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT)+ syscall?
|
|
74
|
+
#
|
|
75
|
+
# @return [Boolean]
|
|
76
|
+
def strict_mode?
|
|
77
|
+
return true if number == abi[:SYS_seccomp] && args[0] == Const::BPF::SECCOMP_SET_MODE_STRICT
|
|
78
|
+
|
|
79
|
+
number == abi[:SYS_prctl] && args[0] == Const::BPF::PR_SET_SECCOMP && args[1] == Const::BPF::SECCOMP_MODE_STRICT
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Constructs a BPF program equivalent to what +SECCOMP_MODE_STRICT+ enforces: only read, write,
|
|
83
|
+
# exit, and sigreturn are allowed, while any other syscall kills the thread.
|
|
84
|
+
#
|
|
85
|
+
# @param [Symbol] arch
|
|
86
|
+
# Target architecture, one of {ABI}'s keys.
|
|
87
|
+
# @return [String]
|
|
88
|
+
# Raw BPF bytes.
|
|
89
|
+
def self.strict_bpf(arch)
|
|
90
|
+
# Required lazily because the assembler's scanner refers to {ABI} at load time.
|
|
91
|
+
require 'seccomp-tools/asm/asm'
|
|
92
|
+
|
|
93
|
+
# The kernel checks sigreturn on architectures that have it and rt_sigreturn on the others.
|
|
94
|
+
sigreturn = Const::Syscall.const_get(arch.to_s.upcase)[:sigreturn] ? :sigreturn : :rt_sigreturn
|
|
95
|
+
Asm.asm(<<-EOS, arch: arch)
|
|
96
|
+
A = sys_number
|
|
97
|
+
A == read ? ok : next
|
|
98
|
+
A == write ? ok : next
|
|
99
|
+
A == exit ? ok : next
|
|
100
|
+
A == #{sigreturn} ? ok : next
|
|
101
|
+
return KILL
|
|
102
|
+
ok:
|
|
103
|
+
return ALLOW
|
|
104
|
+
EOS
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Dumps the BPF of the filter being installed.
|
|
108
|
+
#
|
|
109
|
+
# Only meaningful when {#set_seccomp?} is +true+. In filter mode +args[2]+ points to a
|
|
110
|
+
# +struct sock_fprog+ in the traced process, whose BPF bytes are read out. Strict mode installs
|
|
111
|
+
# no BPF, so an equivalent filter built by {.strict_bpf} is returned instead.
|
|
57
112
|
# @return [String]
|
|
113
|
+
# The raw BPF bytes of the filter being installed.
|
|
58
114
|
def dump_bpf
|
|
115
|
+
return self.class.strict_bpf(arch) if strict_mode?
|
|
116
|
+
|
|
59
117
|
addr = args[2]
|
|
60
118
|
len = Ptrace.peekdata(pid, addr, 0) & 0xffff # len is unsigned short
|
|
61
119
|
filter = Ptrace.peekdata(pid, addr + (bits / 8), 0) & ((1 << bits) - 1)
|
|
62
120
|
Array.new(len) { |i| Ptrace.peekdata(pid, filter + (i * 8), 0) }.pack('Q*')
|
|
63
121
|
end
|
|
64
122
|
|
|
65
|
-
#
|
|
66
|
-
#
|
|
123
|
+
# Architecture of this syscall, i.e. of the traced process.
|
|
124
|
+
# @return [Symbol?]
|
|
125
|
+
# See {SeccompTools::Util.process_arch}.
|
|
67
126
|
def arch
|
|
68
|
-
@arch ||=
|
|
69
|
-
f.pos = 18
|
|
70
|
-
{
|
|
71
|
-
"\x03\x00" => :i386,
|
|
72
|
-
"\x3e\x00" => :amd64,
|
|
73
|
-
"\xb7\x00" => :aarch64,
|
|
74
|
-
"\x00\x16" => :s390x
|
|
75
|
-
}[f.read(2)]
|
|
76
|
-
end
|
|
127
|
+
@arch ||= Util.process_arch(pid)
|
|
77
128
|
end
|
|
78
129
|
|
|
79
130
|
private
|
|
@@ -83,6 +134,7 @@ module SeccompTools
|
|
|
83
134
|
i386: 32,
|
|
84
135
|
amd64: 64,
|
|
85
136
|
aarch64: 64,
|
|
137
|
+
riscv64: 64,
|
|
86
138
|
s390x: 64
|
|
87
139
|
}[arch]
|
|
88
140
|
end
|
data/lib/seccomp-tools/util.rb
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SeccompTools
|
|
4
|
-
#
|
|
4
|
+
# Utility methods shared across the library: architecture detection and terminal colorizing.
|
|
5
5
|
module Util
|
|
6
6
|
module_function
|
|
7
7
|
|
|
8
8
|
# Get currently supported architectures.
|
|
9
|
+
#
|
|
10
|
+
# Derived from the syscall tables shipped under +consts/sys_nr/+.
|
|
9
11
|
# @return [Array<Symbol>]
|
|
10
|
-
#
|
|
12
|
+
# Architecture names, sorted.
|
|
11
13
|
def supported_archs
|
|
12
14
|
@supported_archs ||= Dir.glob(File.join(__dir__, 'consts', 'sys_nr', '*.rb'))
|
|
13
15
|
.map { |f| File.basename(f, '.rb').to_sym }
|
|
@@ -16,23 +18,57 @@ module SeccompTools
|
|
|
16
18
|
|
|
17
19
|
# Detect system architecture.
|
|
18
20
|
# @return [Symbol]
|
|
21
|
+
# One of {supported_archs}, or +:unknown+ if the host CPU is not supported.
|
|
19
22
|
def system_arch
|
|
20
23
|
case RbConfig::CONFIG['host_cpu']
|
|
21
24
|
when /x86_64/ then :amd64
|
|
22
25
|
when /i386/ then :i386
|
|
23
26
|
when /aarch64/ then :aarch64
|
|
27
|
+
when /riscv64/ then :riscv64
|
|
24
28
|
when /s390x/ then :s390x
|
|
25
29
|
else :unknown
|
|
26
30
|
end
|
|
27
31
|
end
|
|
28
32
|
|
|
33
|
+
# ELF +e_machine+ values (the halfword at offset 18) of the architectures seccomp-tools supports,
|
|
34
|
+
# as they are laid out in the file - so the big-endian s390x reads the other way around. The keys
|
|
35
|
+
# are binary strings (+.b+) to match what is read off the file: this source is UTF-8, where a
|
|
36
|
+
# literal such as +"\xb7\x00"+ would never compare equal to those bytes.
|
|
37
|
+
ELF_MACHINE = {
|
|
38
|
+
"\x03\x00".b => :i386,
|
|
39
|
+
"\x3e\x00".b => :amd64,
|
|
40
|
+
"\xb7\x00".b => :aarch64,
|
|
41
|
+
"\xf3\x00".b => :riscv64,
|
|
42
|
+
"\x00\x16".b => :s390x
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
# The architecture a running process was built for, read from the ELF header of its executable.
|
|
46
|
+
#
|
|
47
|
+
# Filters dumped from another process are numbered for *its* architecture, so this is what makes
|
|
48
|
+
# the syscall names right when it differs from the host.
|
|
49
|
+
# @param [Integer] pid
|
|
50
|
+
# Process identifier.
|
|
51
|
+
# @return [Symbol?]
|
|
52
|
+
# One of {supported_archs}, or +nil+ when the executable cannot be read (no +/proc+, the
|
|
53
|
+
# process is gone, permission denied) or its machine type is not one we know.
|
|
54
|
+
def process_arch(pid)
|
|
55
|
+
File.open("/proc/#{pid}/exe", 'rb') do |f|
|
|
56
|
+
f.pos = 18
|
|
57
|
+
ELF_MACHINE[f.read(2)]
|
|
58
|
+
end
|
|
59
|
+
rescue SystemCallError
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
29
63
|
# Enable colorize.
|
|
64
|
+
#
|
|
65
|
+
# Colors are still only emitted when the output is a tty, see {colorize_enabled?}.
|
|
30
66
|
# @return [void]
|
|
31
67
|
def enable_color!
|
|
32
68
|
@disable_color = false
|
|
33
69
|
end
|
|
34
70
|
|
|
35
|
-
# Disable colorize.
|
|
71
|
+
# Disable colorize, {colorize} becomes a no-op regardless of the output being a tty.
|
|
36
72
|
# @return [void]
|
|
37
73
|
def disable_color!
|
|
38
74
|
@disable_color = true
|
|
@@ -40,28 +76,35 @@ module SeccompTools
|
|
|
40
76
|
|
|
41
77
|
# Is colorize enabled?
|
|
42
78
|
# @return [Boolean]
|
|
79
|
+
# +true+ only if colors have not been disabled by {disable_color!} and +$stdout+ is a tty.
|
|
43
80
|
def colorize_enabled?
|
|
44
81
|
!@disable_color && $stdout.tty?
|
|
45
82
|
end
|
|
46
83
|
|
|
47
84
|
# color code of light yellow
|
|
48
85
|
LIGHT_YELLOW = "\e[38;5;230m"
|
|
49
|
-
# Color codes for pretty print.
|
|
86
|
+
# Color codes for pretty print. +error+, +warn+ and +info+ are severities rather than parts of a
|
|
87
|
+
# filter, shading from alarming to quiet; {SeccompTools::Logger} colors its own levels by the
|
|
88
|
+
# same names.
|
|
50
89
|
COLOR_CODE = {
|
|
51
90
|
esc_m: "\e[0m",
|
|
52
91
|
syscall: "\e[38;5;120m", # light green
|
|
53
92
|
arch: LIGHT_YELLOW,
|
|
54
93
|
args: LIGHT_YELLOW,
|
|
55
94
|
gray: "\e[2m",
|
|
56
|
-
error: "\e[38;5;196m" # heavy red
|
|
95
|
+
error: "\e[38;5;196m", # heavy red
|
|
96
|
+
warn: LIGHT_YELLOW,
|
|
97
|
+
info: "\e[38;5;110m" # light blue
|
|
57
98
|
}.freeze
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
#
|
|
99
|
+
# Wrap contents with terminal color codes.
|
|
100
|
+
#
|
|
101
|
+
# Returns +s+ unchanged when {colorize_enabled?} is +false+.
|
|
102
|
+
# @param [#to_s] s
|
|
103
|
+
# Contents to be wrapped.
|
|
61
104
|
# @param [Symbol?] t
|
|
62
|
-
#
|
|
105
|
+
# Which kind of color to use, valid symbols are the keys of {Util::COLOR_CODE}.
|
|
63
106
|
# @return [String]
|
|
64
|
-
#
|
|
107
|
+
# +s+ wrapped with color codes.
|
|
65
108
|
def colorize(s, t: nil)
|
|
66
109
|
s = s.to_s
|
|
67
110
|
return s unless colorize_enabled?
|
|
@@ -71,13 +114,29 @@ module SeccompTools
|
|
|
71
114
|
"#{color}#{s.sub(cc[:esc_m], cc[:esc_m] + color)}#{cc[:esc_m]}"
|
|
72
115
|
end
|
|
73
116
|
|
|
117
|
+
# Does the file at +path+ start with the ELF magic bytes?
|
|
118
|
+
#
|
|
119
|
+
# Used to tell an executable apart from a raw BPF blob.
|
|
120
|
+
# @param [String] path
|
|
121
|
+
# Path to the file to check.
|
|
122
|
+
# @return [Boolean]
|
|
123
|
+
# +false+ when the file cannot be read.
|
|
124
|
+
def elf?(path)
|
|
125
|
+
File.binread(path, 4) == "\x7fELF".b
|
|
126
|
+
rescue SystemCallError
|
|
127
|
+
false
|
|
128
|
+
end
|
|
129
|
+
|
|
74
130
|
# Get content of filename under directory templates/.
|
|
75
131
|
#
|
|
76
132
|
# @param [String] filename
|
|
77
|
-
#
|
|
133
|
+
# Basename of a file under +lib/seccomp-tools/templates/+.
|
|
78
134
|
#
|
|
79
135
|
# @return [String]
|
|
80
136
|
# Content of the file.
|
|
137
|
+
#
|
|
138
|
+
# @raise [Errno::ENOENT]
|
|
139
|
+
# If no such template exists.
|
|
81
140
|
def template(filename)
|
|
82
141
|
File.binread(File.join(__dir__, 'templates', filename))
|
|
83
142
|
end
|
data/lib/seccomp-tools.rb
CHANGED
|
@@ -2,7 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
# @author david942j
|
|
4
4
|
|
|
5
|
-
#
|
|
5
|
+
# Toolkit for working with seccomp BPF filters.
|
|
6
|
+
#
|
|
7
|
+
# The library entry points are {Asm.asm} to compile assembly into raw BPF, {Disasm.disasm} to turn
|
|
8
|
+
# raw BPF back into readable assembly, {Dumper.dump} to capture the filters a process installs, and
|
|
9
|
+
# {Emulator} to run a filter against a hypothetical syscall.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# raw = SeccompTools::Asm.asm("return ALLOW\n", arch: :amd64)
|
|
13
|
+
# SeccompTools::Disasm.disasm(raw, arch: :amd64, display_bpf: false)
|
|
14
|
+
# #=> "0000: return ALLOW\n"
|
|
6
15
|
module SeccompTools
|
|
7
16
|
end
|
|
8
17
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: seccomp-tools
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- david942j
|
|
@@ -85,14 +85,14 @@ dependencies:
|
|
|
85
85
|
requirements:
|
|
86
86
|
- - "~>"
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
|
-
version: '0
|
|
88
|
+
version: '1.0'
|
|
89
89
|
type: :development
|
|
90
90
|
prerelease: false
|
|
91
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
92
92
|
requirements:
|
|
93
93
|
- - "~>"
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
|
-
version: '0
|
|
95
|
+
version: '1.0'
|
|
96
96
|
- !ruby/object:Gem::Dependency
|
|
97
97
|
name: yard
|
|
98
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -166,9 +166,13 @@ extensions:
|
|
|
166
166
|
- ext/ptrace/extconf.rb
|
|
167
167
|
extra_rdoc_files: []
|
|
168
168
|
files:
|
|
169
|
+
- CHANGELOG.md
|
|
169
170
|
- LICENSE
|
|
170
171
|
- README.md
|
|
171
172
|
- bin/seccomp-tools
|
|
173
|
+
- completions/_seccomp-tools
|
|
174
|
+
- completions/seccomp-tools.bash
|
|
175
|
+
- completions/seccomp-tools.fish
|
|
172
176
|
- ext/ptrace/extconf.rb
|
|
173
177
|
- ext/ptrace/ptrace.c
|
|
174
178
|
- lib/seccomp-tools.rb
|
|
@@ -180,24 +184,48 @@ files:
|
|
|
180
184
|
- lib/seccomp-tools/asm/scanner.rb
|
|
181
185
|
- lib/seccomp-tools/asm/statement.rb
|
|
182
186
|
- lib/seccomp-tools/asm/token.rb
|
|
187
|
+
- lib/seccomp-tools/audit.rb
|
|
188
|
+
- lib/seccomp-tools/audit/catalog.rb
|
|
189
|
+
- lib/seccomp-tools/audit/checks.rb
|
|
190
|
+
- lib/seccomp-tools/audit/checks/arch_unchecked.rb
|
|
191
|
+
- lib/seccomp-tools/audit/checks/dangerous_allow.rb
|
|
192
|
+
- lib/seccomp-tools/audit/checks/orw_chain.rb
|
|
193
|
+
- lib/seccomp-tools/audit/checks/permissive_default.rb
|
|
194
|
+
- lib/seccomp-tools/audit/checks/syscall_alt_gap.rb
|
|
195
|
+
- lib/seccomp-tools/audit/checks/x32_guard.rb
|
|
196
|
+
- lib/seccomp-tools/audit/finding.rb
|
|
197
|
+
- lib/seccomp-tools/audit/policy.rb
|
|
198
|
+
- lib/seccomp-tools/audit/report.rb
|
|
183
199
|
- lib/seccomp-tools/bpf.rb
|
|
184
200
|
- lib/seccomp-tools/cli/asm.rb
|
|
201
|
+
- lib/seccomp-tools/cli/audit.rb
|
|
185
202
|
- lib/seccomp-tools/cli/base.rb
|
|
186
203
|
- lib/seccomp-tools/cli/cli.rb
|
|
204
|
+
- lib/seccomp-tools/cli/completion.rb
|
|
187
205
|
- lib/seccomp-tools/cli/disasm.rb
|
|
188
206
|
- lib/seccomp-tools/cli/dump.rb
|
|
207
|
+
- lib/seccomp-tools/cli/dumpable.rb
|
|
189
208
|
- lib/seccomp-tools/cli/emu.rb
|
|
209
|
+
- lib/seccomp-tools/cli/explain.rb
|
|
210
|
+
- lib/seccomp-tools/cli/filter_input.rb
|
|
190
211
|
- lib/seccomp-tools/const.rb
|
|
191
212
|
- lib/seccomp-tools/consts/sys_arg.rb
|
|
192
213
|
- lib/seccomp-tools/consts/sys_nr/aarch64.rb
|
|
193
214
|
- lib/seccomp-tools/consts/sys_nr/amd64.rb
|
|
194
215
|
- lib/seccomp-tools/consts/sys_nr/i386.rb
|
|
216
|
+
- lib/seccomp-tools/consts/sys_nr/riscv64.rb
|
|
195
217
|
- lib/seccomp-tools/consts/sys_nr/s390x.rb
|
|
196
|
-
- lib/seccomp-tools/disasm/context.rb
|
|
197
218
|
- lib/seccomp-tools/disasm/disasm.rb
|
|
198
219
|
- lib/seccomp-tools/dumper.rb
|
|
199
220
|
- lib/seccomp-tools/emulator.rb
|
|
200
221
|
- lib/seccomp-tools/error.rb
|
|
222
|
+
- lib/seccomp-tools/explain.rb
|
|
223
|
+
- lib/seccomp-tools/explain/analysis.rb
|
|
224
|
+
- lib/seccomp-tools/explain/path_facts.rb
|
|
225
|
+
- lib/seccomp-tools/explain/qword.rb
|
|
226
|
+
- lib/seccomp-tools/explain/renderer.rb
|
|
227
|
+
- lib/seccomp-tools/explain/summary.rb
|
|
228
|
+
- lib/seccomp-tools/explain/verdict.rb
|
|
201
229
|
- lib/seccomp-tools/instruction/alu.rb
|
|
202
230
|
- lib/seccomp-tools/instruction/base.rb
|
|
203
231
|
- lib/seccomp-tools/instruction/instruction.rb
|
|
@@ -209,6 +237,10 @@ files:
|
|
|
209
237
|
- lib/seccomp-tools/instruction/st.rb
|
|
210
238
|
- lib/seccomp-tools/instruction/stx.rb
|
|
211
239
|
- lib/seccomp-tools/logger.rb
|
|
240
|
+
- lib/seccomp-tools/symbolic/constraint.rb
|
|
241
|
+
- lib/seccomp-tools/symbolic/executor.rb
|
|
242
|
+
- lib/seccomp-tools/symbolic/expr.rb
|
|
243
|
+
- lib/seccomp-tools/symbolic/state.rb
|
|
212
244
|
- lib/seccomp-tools/syscall.rb
|
|
213
245
|
- lib/seccomp-tools/templates/asm.amd64.asm
|
|
214
246
|
- lib/seccomp-tools/templates/asm.c
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SeccompTools
|
|
4
|
-
module Disasm
|
|
5
|
-
# @private
|
|
6
|
-
#
|
|
7
|
-
# Context for disassembler to analyze.
|
|
8
|
-
#
|
|
9
|
-
# This class maintains:
|
|
10
|
-
# * if +reg/mem+ can be one of +data[*]+
|
|
11
|
-
# * if +data[0]+ (i.e. sys_number) is a known value
|
|
12
|
-
class Context
|
|
13
|
-
# @private
|
|
14
|
-
#
|
|
15
|
-
# Records the type and value.
|
|
16
|
-
class Value
|
|
17
|
-
attr_reader :val # @return [Integer]
|
|
18
|
-
|
|
19
|
-
# @param [:imm, :data, :mem] rel
|
|
20
|
-
# @param [Integer?] val
|
|
21
|
-
def initialize(rel: :imm, val: nil)
|
|
22
|
-
@rel = rel
|
|
23
|
-
@val = val
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# @return [Boolean]
|
|
27
|
-
def data?
|
|
28
|
-
@rel == :data
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# @return [Boolean]
|
|
32
|
-
def imm?
|
|
33
|
-
@rel == :imm && @val.is_a?(Integer)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Defines hash function.
|
|
37
|
-
# @return [Integer]
|
|
38
|
-
def hash
|
|
39
|
-
[@rel, @val].hash
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Defines +eql?+.
|
|
43
|
-
#
|
|
44
|
-
# @param [Context::Value] other
|
|
45
|
-
# @return [Boolean]
|
|
46
|
-
def eql?(other)
|
|
47
|
-
@val == other.val && @rel == other.instance_variable_get(:@rel)
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# @return [{Integer, Symbol => Context::Value}] Records reg and mem values.
|
|
52
|
-
attr_reader :values
|
|
53
|
-
# @return [Array<Integer?>] Records the known value of data.
|
|
54
|
-
attr_reader :known_data
|
|
55
|
-
|
|
56
|
-
# Instantiate a {Context} object.
|
|
57
|
-
# @param [{Integer, Symbol => Context::Value?}] values
|
|
58
|
-
# Value to be set to +reg/mem+.
|
|
59
|
-
# @param [Array<Integer?>] known_data
|
|
60
|
-
# Records which index of data is known.
|
|
61
|
-
# It's used for tracking when the syscall number is known, which can be used to display argument names of the
|
|
62
|
-
# syscall.
|
|
63
|
-
def initialize(values: {}, known_data: [])
|
|
64
|
-
@values = values
|
|
65
|
-
16.times { |i| @values[i] ||= Value.new(rel: :mem, val: i) } # make @values always has all keys
|
|
66
|
-
@values[:a] ||= Value.new
|
|
67
|
-
@values[:x] ||= Value.new
|
|
68
|
-
@known_data = known_data
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Is used for the ld/ldx instructions.
|
|
72
|
-
#
|
|
73
|
-
# @param [#downcase, :a, :x] reg
|
|
74
|
-
# Register to be set
|
|
75
|
-
# @return [void]
|
|
76
|
-
def load(reg, rel: nil, val: nil)
|
|
77
|
-
reg = reg.downcase.to_sym
|
|
78
|
-
values[reg] = if rel == :mem
|
|
79
|
-
values[val]
|
|
80
|
-
else
|
|
81
|
-
Value.new(rel:, val:)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Is used for the st/stx instructions.
|
|
86
|
-
#
|
|
87
|
-
# @param [Integer] idx
|
|
88
|
-
# Index of +mem+ array.
|
|
89
|
-
# @param [#downcase, :a, :x] reg
|
|
90
|
-
# Register.
|
|
91
|
-
#
|
|
92
|
-
# @return [void]
|
|
93
|
-
def store(idx, reg)
|
|
94
|
-
raise RangeError, "Expect 0 <= idx < 16, got #{idx}." unless idx.between?(0, 15)
|
|
95
|
-
|
|
96
|
-
values[idx] = values[reg.downcase.to_sym]
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# Hints context that current value of register A equals to +val+.
|
|
100
|
-
#
|
|
101
|
-
# @param [Integer, :x] val
|
|
102
|
-
# An immediate value or the symbol x.
|
|
103
|
-
# @return [self]
|
|
104
|
-
# Returns the object itself.
|
|
105
|
-
def eql!(val)
|
|
106
|
-
tap do
|
|
107
|
-
# only cares when A is fetched from data
|
|
108
|
-
next unless a.data?
|
|
109
|
-
next known_data[a.val] = val if val.is_a?(Integer)
|
|
110
|
-
# A == X, we can handle these cases:
|
|
111
|
-
# * X is an immi
|
|
112
|
-
# * X is a known data
|
|
113
|
-
next unless x.data? || x.imm?
|
|
114
|
-
next known_data[a.val] = x.val if x.imm?
|
|
115
|
-
|
|
116
|
-
known_data[a.val] = known_data[x.val]
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# Implements a deep dup.
|
|
121
|
-
# @return [Context]
|
|
122
|
-
def dup
|
|
123
|
-
Context.new(values: values.dup, known_data: known_data.dup)
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
# Register A.
|
|
127
|
-
# @return [Context::Value]
|
|
128
|
-
def a
|
|
129
|
-
values[:a]
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Register X.
|
|
133
|
-
# @return [Context::Value]
|
|
134
|
-
def x
|
|
135
|
-
values[:x]
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# For conveniently get instance variable.
|
|
139
|
-
# @param [String, Symbol, Integer] key
|
|
140
|
-
# @return [Context::Value]
|
|
141
|
-
def [](key)
|
|
142
|
-
return values[key] if key.is_a?(Integer) # mem
|
|
143
|
-
|
|
144
|
-
values[key.downcase.to_sym]
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# For conveniently set an instance variable.
|
|
148
|
-
# @param [#downcase, :a, :x] reg
|
|
149
|
-
# Can be +'A', 'a', :a, 'X', 'x', :x+.
|
|
150
|
-
# @param [Value] val
|
|
151
|
-
# Value to set.
|
|
152
|
-
# @return [void]
|
|
153
|
-
def []=(reg, val)
|
|
154
|
-
values[reg.downcase.to_sym] = val
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
# For +Set+ to compare two {Context} objects.
|
|
158
|
-
# @param [Context] other
|
|
159
|
-
# @return [Boolean]
|
|
160
|
-
def eql?(other)
|
|
161
|
-
values.eql?(other.values) && known_data.eql?(other.known_data)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# For +Set+ to get the hash value.
|
|
165
|
-
# @return [Integer]
|
|
166
|
-
def hash
|
|
167
|
-
[values, known_data].hash
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
end
|