seccomp-tools 1.6.1 → 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/LICENSE +21 -0
- data/README.md +267 -46
- 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 +39 -9
- data/lib/seccomp-tools/asm/sasm.tab.rb +31 -21
- 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 +40 -8
- 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 +7 -3
- 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 +10 -6
- data/lib/seccomp-tools/cli/completion.rb +40 -0
- data/lib/seccomp-tools/cli/disasm.rb +10 -6
- 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 +32 -9
- 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/amd64.rb +1 -1
- data/lib/seccomp-tools/consts/sys_nr/riscv64.rb +332 -0
- data/lib/seccomp-tools/disasm/disasm.rb +31 -13
- data/lib/seccomp-tools/dumper.rb +67 -35
- data/lib/seccomp-tools/emulator.rb +41 -16
- 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 +15 -2
- 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 +74 -22
- data/lib/seccomp-tools/util.rb +70 -11
- data/lib/seccomp-tools/version.rb +1 -1
- data/lib/seccomp-tools.rb +10 -1
- metadata +83 -11
- data/lib/seccomp-tools/disasm/context.rb +0 -171
|
@@ -10,116 +10,159 @@ module SeccompTools
|
|
|
10
10
|
# @private
|
|
11
11
|
module Scalar
|
|
12
12
|
# To be used to denote a register (A / X), an argument (data[]), or a memory data (mem[]).
|
|
13
|
+
#
|
|
14
|
+
# Every predicate returns +false+ here, each subclass overrides exactly the one that
|
|
15
|
+
# identifies it. The registers and +len+ carry no payload and are singletons, the remaining
|
|
16
|
+
# subclasses keep theirs in {#val}.
|
|
13
17
|
class Base
|
|
18
|
+
# @return [Integer?]
|
|
19
|
+
# The payload of this scalar, whose meaning depends on the subclass: the immediate value
|
|
20
|
+
# for {ConstVal}, the index for {Data} and {Mem}. +nil+ for the singletons.
|
|
14
21
|
attr_reader :val
|
|
15
22
|
|
|
23
|
+
# Is this the A register?
|
|
24
|
+
# @return [Boolean]
|
|
16
25
|
def a?
|
|
17
26
|
false
|
|
18
27
|
end
|
|
19
28
|
|
|
29
|
+
# Is this the X register?
|
|
30
|
+
# @return [Boolean]
|
|
20
31
|
def x?
|
|
21
32
|
false
|
|
22
33
|
end
|
|
23
34
|
|
|
35
|
+
# Is this +len+?
|
|
36
|
+
# @return [Boolean]
|
|
24
37
|
def len?
|
|
25
38
|
false
|
|
26
39
|
end
|
|
27
40
|
|
|
41
|
+
# Is this a +data[]+ access?
|
|
42
|
+
# @return [Boolean]
|
|
28
43
|
def data?
|
|
29
44
|
false
|
|
30
45
|
end
|
|
31
46
|
|
|
47
|
+
# Is this a +mem[]+ access?
|
|
48
|
+
# @return [Boolean]
|
|
32
49
|
def mem?
|
|
33
50
|
false
|
|
34
51
|
end
|
|
35
52
|
|
|
53
|
+
# Is this a constant value?
|
|
54
|
+
# @return [Boolean]
|
|
36
55
|
def const?
|
|
37
56
|
false
|
|
38
57
|
end
|
|
39
58
|
end
|
|
40
59
|
|
|
41
|
-
# Register A.
|
|
60
|
+
# Register A, the accumulator. A singleton, use +A.instance+.
|
|
42
61
|
class A < Base
|
|
43
62
|
include Singleton
|
|
44
63
|
|
|
64
|
+
# @return [Boolean] Always +true+.
|
|
45
65
|
def a?
|
|
46
66
|
true
|
|
47
67
|
end
|
|
48
68
|
end
|
|
49
69
|
|
|
50
|
-
# Register X.
|
|
70
|
+
# Register X, the index register. A singleton, use +X.instance+.
|
|
51
71
|
class X < Base
|
|
52
72
|
include Singleton
|
|
53
73
|
|
|
74
|
+
# @return [Boolean] Always +true+.
|
|
54
75
|
def x?
|
|
55
76
|
true
|
|
56
77
|
end
|
|
57
78
|
end
|
|
58
79
|
|
|
59
|
-
# Len.
|
|
80
|
+
# The +len+ keyword, the size of +struct seccomp_data+. A singleton, use +Len.instance+.
|
|
60
81
|
class Len < Base
|
|
61
82
|
include Singleton
|
|
62
83
|
|
|
84
|
+
# @return [Boolean] Always +true+.
|
|
63
85
|
def len?
|
|
64
86
|
true
|
|
65
87
|
end
|
|
66
88
|
end
|
|
67
89
|
|
|
68
|
-
# A constant value.
|
|
90
|
+
# A constant value, i.e. an immediate operand.
|
|
69
91
|
class ConstVal < Base
|
|
92
|
+
# Instantiates a {ConstVal} object.
|
|
93
|
+
#
|
|
70
94
|
# @param [Integer] val
|
|
95
|
+
# The constant value.
|
|
71
96
|
def initialize(val)
|
|
72
97
|
@val = val
|
|
73
98
|
super()
|
|
74
99
|
end
|
|
75
100
|
|
|
101
|
+
# Compares by value, so a {ConstVal} equals the bare Integer it wraps.
|
|
102
|
+
#
|
|
76
103
|
# @param [ConstVal, Integer] other
|
|
104
|
+
# The object to be compared with.
|
|
105
|
+
# @return [Boolean]
|
|
77
106
|
def ==(other)
|
|
78
107
|
to_i == other.to_i
|
|
79
108
|
end
|
|
80
109
|
|
|
110
|
+
# @return [Boolean] Always +true+.
|
|
81
111
|
def const?
|
|
82
112
|
true
|
|
83
113
|
end
|
|
84
114
|
|
|
115
|
+
# @!method to_i
|
|
116
|
+
# The wrapped constant value.
|
|
117
|
+
# @return [Integer]
|
|
85
118
|
alias to_i val
|
|
86
119
|
end
|
|
87
120
|
|
|
88
|
-
# data[]
|
|
121
|
+
# A +data[]+ access, one word of +struct seccomp_data+.
|
|
89
122
|
class Data < Base
|
|
90
123
|
# Instantiates a {Data} object.
|
|
91
124
|
#
|
|
92
125
|
# @param [Integer] index
|
|
126
|
+
# Byte offset into +struct seccomp_data+.
|
|
93
127
|
def initialize(index)
|
|
94
128
|
@val = index
|
|
95
129
|
super()
|
|
96
130
|
end
|
|
97
131
|
|
|
98
132
|
# @param [Data] other
|
|
133
|
+
# The object to be compared with.
|
|
134
|
+
# @return [Boolean]
|
|
135
|
+
# +true+ only if +other+ is a {Data} with the same index.
|
|
99
136
|
def ==(other)
|
|
100
137
|
other.is_a?(Data) && val == other.val
|
|
101
138
|
end
|
|
102
139
|
|
|
140
|
+
# @return [Boolean] Always +true+.
|
|
103
141
|
def data?
|
|
104
142
|
true
|
|
105
143
|
end
|
|
106
144
|
end
|
|
107
145
|
|
|
108
|
-
# mem[]
|
|
146
|
+
# A +mem[]+ access, one slot of the scratch memory.
|
|
109
147
|
class Mem < Base
|
|
110
148
|
# Instantiates a {Mem} object.
|
|
111
149
|
#
|
|
112
150
|
# @param [Integer] index
|
|
151
|
+
# Index of the scratch memory slot.
|
|
113
152
|
def initialize(index)
|
|
114
153
|
@val = index
|
|
115
154
|
super()
|
|
116
155
|
end
|
|
117
156
|
|
|
118
|
-
# @param [
|
|
157
|
+
# @param [Mem] other
|
|
158
|
+
# The object to be compared with.
|
|
159
|
+
# @return [Boolean]
|
|
160
|
+
# +true+ only if +other+ is a {Mem} with the same index.
|
|
119
161
|
def ==(other)
|
|
120
162
|
other.is_a?(Mem) && val == other.val
|
|
121
163
|
end
|
|
122
164
|
|
|
165
|
+
# @return [Boolean] Always +true+.
|
|
123
166
|
def mem?
|
|
124
167
|
true
|
|
125
168
|
end
|
|
@@ -11,43 +11,54 @@ module SeccompTools
|
|
|
11
11
|
#
|
|
12
12
|
# Maintains columns and rows to have informative error messages.
|
|
13
13
|
#
|
|
14
|
-
# Internally used by
|
|
14
|
+
# Internally used by the seccomp asm parser.
|
|
15
15
|
class Scanner
|
|
16
|
+
# @return [{Symbol => Integer}]
|
|
17
|
+
# Syscall name to number, for the architecture this scanner was created with.
|
|
16
18
|
attr_reader :syscalls
|
|
19
|
+
# @return [Symbol]
|
|
20
|
+
# The architecture this scanner was created with.
|
|
21
|
+
attr_reader :arch
|
|
17
22
|
|
|
18
23
|
# Keywords with special meanings in our assembly. Keywords are all case-insensitive.
|
|
19
24
|
KEYWORDS = %w[a x if else return mem args args_h data len sys_number arch instruction_pointer].freeze
|
|
20
25
|
# Regexp for matching keywords.
|
|
21
|
-
KEYWORD_MATCHER = /\A\b(#{KEYWORDS.join('|')})\b/i
|
|
26
|
+
KEYWORD_MATCHER = /\A\b(#{KEYWORDS.join('|')})\b/i
|
|
22
27
|
# Action strings can be used in a return statement. Actions must be in upper case.
|
|
23
28
|
# See {SeccompTools::Const::BPF::ACTION}.
|
|
24
29
|
ACTIONS = Const::BPF::ACTION.keys.map(&:to_s)
|
|
25
30
|
# Regexp for matching actions.
|
|
26
|
-
ACTION_MATCHER = /\A\b(#{ACTIONS.join('|')})\b
|
|
31
|
+
ACTION_MATCHER = /\A\b(#{ACTIONS.join('|')})\b/
|
|
27
32
|
# Special constants for checking the current architecture. See {SeccompTools::Const::Audit::ARCH}. These constants
|
|
28
33
|
# are case-insensitive.
|
|
29
34
|
AUDIT_ARCHES = Const::Audit::ARCH.keys
|
|
30
35
|
# Regexp for matching arch values.
|
|
31
|
-
AUDIT_ARCH_MATCHER = /\A\b(#{AUDIT_ARCHES.join('|')})\b/i
|
|
36
|
+
AUDIT_ARCH_MATCHER = /\A\b(#{AUDIT_ARCHES.join('|')})\b/i
|
|
32
37
|
# Comparisons.
|
|
33
38
|
COMPARE = %w[== != >= <= > <].freeze
|
|
34
39
|
# Regexp for matching comparisons.
|
|
35
|
-
COMPARE_MATCHER = /\A(#{COMPARE.join('|')})
|
|
40
|
+
COMPARE_MATCHER = /\A(#{COMPARE.join('|')})/
|
|
36
41
|
# All valid arithmetic operators.
|
|
37
42
|
ALU_OP = %w[+ - * / | ^ << >>].freeze
|
|
38
43
|
# Regexp for matching ALU operators.
|
|
39
|
-
ALU_OP_MATCHER = /\A(#{ALU_OP.map { |o| ::Regexp.escape(o) }.join('|')})
|
|
44
|
+
ALU_OP_MATCHER = /\A(#{ALU_OP.map { |o| ::Regexp.escape(o) }.join('|')})/
|
|
40
45
|
# Supported architectures
|
|
41
46
|
ARCHES = SeccompTools::Syscall::ABI.keys.map(&:to_s)
|
|
42
47
|
|
|
48
|
+
# Instantiates a {Scanner} object.
|
|
49
|
+
#
|
|
43
50
|
# @param [String] str
|
|
51
|
+
# The assembly source to be scanned.
|
|
44
52
|
# @param [Symbol] arch
|
|
53
|
+
# Target architecture, decides which syscall names are recognized.
|
|
45
54
|
# @param [String?] filename
|
|
55
|
+
# Only used for error messages, defaults to +<inline>+.
|
|
46
56
|
# @example
|
|
47
57
|
# Scanner.new('return ALLOW', :amd64)
|
|
48
58
|
def initialize(str, arch, filename: nil)
|
|
49
59
|
@filename = filename || '<inline>'
|
|
50
60
|
@str = str
|
|
61
|
+
@arch = arch
|
|
51
62
|
@syscalls =
|
|
52
63
|
begin; Const::Syscall.const_get(arch.to_s.upcase); rescue NameError; []; end
|
|
53
64
|
@syscall_all = ARCHES.each_with_object({}) do |ar, memo|
|
|
@@ -57,7 +68,9 @@ module SeccompTools
|
|
|
57
68
|
|
|
58
69
|
# Scans the whole string and raises errors when there are unrecognized tokens.
|
|
59
70
|
# @return [self]
|
|
71
|
+
# The scanner itself, so calls can be chained.
|
|
60
72
|
# @raise [UnrecognizedTokenError]
|
|
73
|
+
# If any token could not be recognized. The message points at every such token.
|
|
61
74
|
def validate!
|
|
62
75
|
errors = validate
|
|
63
76
|
return self if errors.empty?
|
|
@@ -68,11 +81,17 @@ module SeccompTools
|
|
|
68
81
|
# Same as {#validate!} but returns the array of errors instead of raising an exception.
|
|
69
82
|
#
|
|
70
83
|
# @return [Array<Token>]
|
|
84
|
+
# The +:unknown+ tokens, empty when the source is valid.
|
|
71
85
|
def validate
|
|
72
86
|
scan.select { |t| t.sym == :unknown }
|
|
73
87
|
end
|
|
74
88
|
|
|
89
|
+
# Scans the whole string into tokens.
|
|
90
|
+
#
|
|
91
|
+
# The result is memoized, so repeated calls are free.
|
|
75
92
|
# @return [Array<Token>]
|
|
93
|
+
# All tokens in source order. Unrecognized text becomes +:unknown+ tokens rather than
|
|
94
|
+
# raising, see {#validate!}.
|
|
76
95
|
def scan
|
|
77
96
|
return @tokens if defined?(@tokens)
|
|
78
97
|
|
|
@@ -138,9 +157,22 @@ module SeccompTools
|
|
|
138
157
|
# Let tab on terminal be 4 spaces wide.
|
|
139
158
|
TAB_WIDTH = 4
|
|
140
159
|
|
|
160
|
+
# Formats an error message that points at a token in the source.
|
|
161
|
+
#
|
|
141
162
|
# @param [Token] tok
|
|
163
|
+
# The token the message refers to.
|
|
142
164
|
# @param [String] msg
|
|
165
|
+
# The message to be shown next to the source location.
|
|
143
166
|
# @return [String]
|
|
167
|
+
# The location and message, the offending source line, and a row of carets underlining
|
|
168
|
+
# the token.
|
|
169
|
+
# @example
|
|
170
|
+
# scanner.format_error(tok, 'unknown token "foo"')
|
|
171
|
+
# #=> <<-EOS
|
|
172
|
+
# # <inline>:1:1 unknown token "foo"
|
|
173
|
+
# # foo
|
|
174
|
+
# # ^^^
|
|
175
|
+
# # EOS
|
|
144
176
|
def format_error(tok, msg)
|
|
145
177
|
@lines = @str.lines unless defined?(@lines)
|
|
146
178
|
line = @lines[tok.line]
|
|
@@ -149,14 +181,14 @@ module SeccompTools
|
|
|
149
181
|
<<-EOS
|
|
150
182
|
#{@filename}:#{tok.line + 1}:#{tok.col + 1} #{msg}
|
|
151
183
|
#{line}
|
|
152
|
-
#{' ' * calculate_spaces(@lines[tok.line][0...tok.col]) + '^' * tok.str.size}
|
|
184
|
+
#{(' ' * calculate_spaces(@lines[tok.line][0...tok.col])) + ('^' * tok.str.size)}
|
|
153
185
|
EOS
|
|
154
186
|
end
|
|
155
187
|
|
|
156
188
|
private
|
|
157
189
|
|
|
158
190
|
def calculate_spaces(str)
|
|
159
|
-
str.size + str.count("\t") * (TAB_WIDTH - 1)
|
|
191
|
+
str.size + (str.count("\t") * (TAB_WIDTH - 1))
|
|
160
192
|
end
|
|
161
193
|
end
|
|
162
194
|
end
|
|
@@ -8,15 +8,21 @@ module SeccompTools
|
|
|
8
8
|
# Internally used by sasm.y.
|
|
9
9
|
# @private
|
|
10
10
|
class Statement
|
|
11
|
-
|
|
11
|
+
# @return [Symbol] Kind of this statement, one of +:alu+, +:assign+, +:if+ and +:ret+.
|
|
12
|
+
attr_reader :type
|
|
13
|
+
# @return [Object] The operands of this statement, see {#initialize}.
|
|
14
|
+
attr_reader :data
|
|
15
|
+
# @return [Array<Token>] Labels that refer to this statement.
|
|
16
|
+
attr_reader :symbols
|
|
12
17
|
|
|
13
18
|
# Instantiates a {Statement} object.
|
|
14
19
|
#
|
|
15
20
|
# @param [:alu, :assign, :if, :ret] type
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
21
|
+
# Kind of this statement.
|
|
22
|
+
# @param [Object] data
|
|
23
|
+
# The data for describing this statement. Type of +data+ varies according to the value of +type+.
|
|
24
|
+
# @param [Array<Token>] symbols
|
|
25
|
+
# Label tokens that refer to this statement.
|
|
20
26
|
def initialize(type, data, symbols)
|
|
21
27
|
@type = type
|
|
22
28
|
@data = data
|
|
@@ -24,6 +30,9 @@ module SeccompTools
|
|
|
24
30
|
end
|
|
25
31
|
|
|
26
32
|
# @param [Statement] other
|
|
33
|
+
# The statement to be compared with.
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
# +true+ only if the type, data and symbols are all equal.
|
|
27
36
|
def ==(other)
|
|
28
37
|
[type, data, symbols] == [other.type, other.data, other.symbols]
|
|
29
38
|
end
|
|
@@ -3,14 +3,30 @@
|
|
|
3
3
|
module SeccompTools
|
|
4
4
|
module Asm
|
|
5
5
|
# Records information of a token.
|
|
6
|
+
#
|
|
7
|
+
# Beyond the token itself, a {Token} remembers where it was found so that
|
|
8
|
+
# {Scanner#format_error} can point at it in the source.
|
|
6
9
|
class Token
|
|
7
|
-
|
|
10
|
+
# @return [Symbol]
|
|
11
|
+
# Type of this token, e.g. +:NEWLINE+, +:SYMBOL+, +:GOTO+, or +:unknown+ for text the
|
|
12
|
+
# scanner failed to recognize.
|
|
13
|
+
attr_reader :sym
|
|
14
|
+
# @return [String] The matched text.
|
|
15
|
+
attr_reader :str
|
|
16
|
+
# @return [Integer] Zero-based line number where this token starts.
|
|
17
|
+
attr_reader :line
|
|
18
|
+
# @return [Integer] Zero-based column number where this token starts.
|
|
19
|
+
attr_reader :col
|
|
8
20
|
|
|
9
21
|
# Instantiates a {Token} object.
|
|
10
22
|
# @param [Symbol] sym
|
|
23
|
+
# Type of this token.
|
|
11
24
|
# @param [String] str
|
|
25
|
+
# The matched text.
|
|
12
26
|
# @param [Integer] line
|
|
27
|
+
# Zero-based line number where this token starts.
|
|
13
28
|
# @param [Integer] col
|
|
29
|
+
# Zero-based column number where this token starts.
|
|
14
30
|
def initialize(sym, str, line, col)
|
|
15
31
|
@sym = sym
|
|
16
32
|
@str = str
|
|
@@ -20,7 +36,9 @@ module SeccompTools
|
|
|
20
36
|
|
|
21
37
|
# To compare with another {Token} object.
|
|
22
38
|
# @param [Token] other
|
|
39
|
+
# The token to be compared with.
|
|
23
40
|
# @return [Boolean]
|
|
41
|
+
# +true+ only if all four attributes are equal.
|
|
24
42
|
def ==(other)
|
|
25
43
|
[other.sym, other.str, other.line, other.col] == [sym, str, line, col]
|
|
26
44
|
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SeccompTools
|
|
4
|
+
class Audit
|
|
5
|
+
# The seccomp-domain knowledge the checks classify against: which syscalls are dangerous, which
|
|
6
|
+
# are equivalents of each other, and which form a file read/write chain. Entries are by syscall
|
|
7
|
+
# *name*; each check resolves them against the target architecture's own syscall table (a name a
|
|
8
|
+
# given arch lacks is simply skipped), and the +*_BY_ARCH+ maps add arch-specific knowledge on a
|
|
9
|
+
# clean extension point - no arch is hardcoded in the checks themselves.
|
|
10
|
+
module Catalog
|
|
11
|
+
# Dangerous-if-reachable syscalls => +{severity:, why:}+. Applied on every architecture. Kept to
|
|
12
|
+
# a low-noise, high-signal set: syscalls a real sandbox almost never wants. Ubiquitous-but-risky
|
|
13
|
+
# ones (+mmap+/+mprotect+ RWX, decided by the +prot+ argument) are intentionally out of v1 -
|
|
14
|
+
# flagging them needs argument-flag analysis, else every normal allowlist trips them.
|
|
15
|
+
DANGEROUS = {
|
|
16
|
+
execve: { severity: :high, why: 'arbitrary program execution' },
|
|
17
|
+
execveat: { severity: :high, why: 'arbitrary program execution' },
|
|
18
|
+
ptrace: { severity: :high, why: 'inspect/inject into other processes' },
|
|
19
|
+
process_vm_readv: { severity: :high, why: "read another process's memory" },
|
|
20
|
+
process_vm_writev: { severity: :high, why: "write another process's memory" },
|
|
21
|
+
socket: { severity: :medium, why: 'network access (exfiltration)' },
|
|
22
|
+
connect: { severity: :medium, why: 'network access (exfiltration)' }
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
# Per-arch additions to {DANGEROUS} (e.g. i386 multiplexes the socket API through +socketcall+).
|
|
26
|
+
DANGEROUS_BY_ARCH = {
|
|
27
|
+
i386: { socketcall: { severity: :medium, why: 'multiplexed socket API (exfiltration)' } }
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
# Equivalent-syscall groups: denying one member while another is allowed is a bypass gap.
|
|
31
|
+
ALT_GROUPS = {
|
|
32
|
+
exec: %i[execve execveat],
|
|
33
|
+
open: %i[open openat openat2],
|
|
34
|
+
read: %i[read readv pread64 preadv preadv2],
|
|
35
|
+
write: %i[write writev pwrite64 pwritev sendfile],
|
|
36
|
+
fork: %i[fork vfork clone clone3]
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
# Per-arch additions to {ALT_GROUPS}.
|
|
40
|
+
ALT_GROUPS_BY_ARCH = {}.freeze
|
|
41
|
+
|
|
42
|
+
# The open/read/write families whose joint availability is a file read/exfil chain.
|
|
43
|
+
ORW = {
|
|
44
|
+
open: %i[open openat openat2],
|
|
45
|
+
read: %i[read readv pread64 preadv preadv2],
|
|
46
|
+
write: %i[write writev pwrite64 pwritev sendfile]
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
module_function
|
|
50
|
+
|
|
51
|
+
# {DANGEROUS} plus any per-arch additions for +arch_sym+.
|
|
52
|
+
# @param [Symbol?] arch_sym
|
|
53
|
+
# @return [Hash{Symbol=>Hash}]
|
|
54
|
+
def dangerous(arch_sym)
|
|
55
|
+
DANGEROUS.merge(DANGEROUS_BY_ARCH.fetch(arch_sym, {}))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# {ALT_GROUPS} plus any per-arch additions for +arch_sym+.
|
|
59
|
+
# @param [Symbol?] arch_sym
|
|
60
|
+
# @return [Hash{Symbol=>Array<Symbol>}]
|
|
61
|
+
def alt_groups(arch_sym)
|
|
62
|
+
ALT_GROUPS.merge(ALT_GROUPS_BY_ARCH.fetch(arch_sym, {}))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'seccomp-tools/audit/finding'
|
|
4
|
+
require 'seccomp-tools/explain/verdict'
|
|
5
|
+
|
|
6
|
+
module SeccompTools
|
|
7
|
+
class Audit
|
|
8
|
+
module Checks
|
|
9
|
+
# The filter does not validate the architecture, or lets an unlisted one reach +ALLOW+. Syscall
|
|
10
|
+
# numbers are ABI-relative, so number-only rules are dodged by invoking under another ABI.
|
|
11
|
+
module ArchUnchecked
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# @param [Explain::Analysis] analysis
|
|
15
|
+
# @return [Array<Audit::Finding>]
|
|
16
|
+
def call(analysis)
|
|
17
|
+
if analysis.arch_values.empty?
|
|
18
|
+
[finding('Architecture is never validated',
|
|
19
|
+
'The filter checks syscall numbers without ever comparing data[4] (arch). ' \
|
|
20
|
+
'Numbers mean different syscalls under another AUDIT_ARCH, so the checks can be ' \
|
|
21
|
+
'dodged by invoking through a different ABI (e.g. i386 numbering on amd64).')]
|
|
22
|
+
elsif analysis.other_leaves.any? { |l| Explain::Verdict.label(l.ret) == 'ALLOW' }
|
|
23
|
+
[finding('Unlisted architectures reach ALLOW',
|
|
24
|
+
'Some paths reach ALLOW on an architecture the filter does not explicitly ' \
|
|
25
|
+
'check, so a different-ABI call can slip past the syscall-number rules.')]
|
|
26
|
+
else
|
|
27
|
+
[]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @!visibility private
|
|
32
|
+
def finding(title, detail)
|
|
33
|
+
Finding.new(id: 'arch-unchecked', severity: :high, arch: nil, title:, detail:,
|
|
34
|
+
syscalls: [], condition: nil,
|
|
35
|
+
remediation: 'Compare data[4] against your AUDIT_ARCH_* and KILL every ' \
|
|
36
|
+
'architecture you do not explicitly handle.')
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'seccomp-tools/audit/catalog'
|
|
4
|
+
require 'seccomp-tools/audit/finding'
|
|
5
|
+
|
|
6
|
+
module SeccompTools
|
|
7
|
+
class Audit
|
|
8
|
+
module Checks
|
|
9
|
+
# A syscall a sandbox almost never wants ({Catalog#dangerous}) is reachable as +ALLOW+. One
|
|
10
|
+
# finding per syscall, carrying the argument condition (if any) under which it is allowed.
|
|
11
|
+
module DangerousAllow
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# @param [Audit::Policy] policy
|
|
15
|
+
# @return [Array<Audit::Finding>]
|
|
16
|
+
def call(policy)
|
|
17
|
+
Catalog.dangerous(policy.arch_sym).filter_map do |name, meta|
|
|
18
|
+
nr = policy.number(name)
|
|
19
|
+
next unless nr && policy.reachable_as_allow?(nr)
|
|
20
|
+
|
|
21
|
+
cond = policy.condition_for(nr)
|
|
22
|
+
Finding.new(
|
|
23
|
+
id: 'dangerous-allow', severity: meta[:severity], arch: policy.arch_name,
|
|
24
|
+
title: "#{name} is allowed",
|
|
25
|
+
detail: "#{name} reaches ALLOW - #{meta[:why]}.",
|
|
26
|
+
syscalls: [name.to_s], condition: cond,
|
|
27
|
+
remediation: "Block #{name} unless the program genuinely needs it."
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'seccomp-tools/audit/catalog'
|
|
4
|
+
require 'seccomp-tools/audit/finding'
|
|
5
|
+
|
|
6
|
+
module SeccompTools
|
|
7
|
+
class Audit
|
|
8
|
+
module Checks
|
|
9
|
+
# An open-family, a read-family and a write/send-family syscall are all reachable as +ALLOW+ -
|
|
10
|
+
# the classic "open the flag, read it, write it out" chain.
|
|
11
|
+
module OrwChain
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# @param [Audit::Policy] policy
|
|
15
|
+
# @return [Array<Audit::Finding>]
|
|
16
|
+
def call(policy)
|
|
17
|
+
o = first_allowed(policy, Catalog::ORW[:open])
|
|
18
|
+
r = first_allowed(policy, Catalog::ORW[:read])
|
|
19
|
+
w = first_allowed(policy, Catalog::ORW[:write])
|
|
20
|
+
return [] unless o && r && w
|
|
21
|
+
|
|
22
|
+
[Finding.new(
|
|
23
|
+
id: 'orw-chain', severity: :high, arch: policy.arch_name,
|
|
24
|
+
title: 'A file can be opened and its contents copied out',
|
|
25
|
+
# The syscall names are painted wherever they appear, so keep them out of prose that
|
|
26
|
+
# merely describes the actions - only name one where the syscall itself is meant.
|
|
27
|
+
detail: "#{o}, #{r} and #{w} all reach ALLOW, so the contents of an arbitrary file " \
|
|
28
|
+
'(e.g. the flag) can be copied straight back out.',
|
|
29
|
+
syscalls: [o, r, w].map(&:to_s), condition: nil,
|
|
30
|
+
remediation: 'Deny the open-family syscalls unless the program genuinely needs arbitrary files.'
|
|
31
|
+
)]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @!visibility private
|
|
35
|
+
def first_allowed(policy, names)
|
|
36
|
+
names.find { |n| (nr = policy.number(n)) && policy.reachable_as_allow?(nr) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'seccomp-tools/audit/finding'
|
|
4
|
+
|
|
5
|
+
module SeccompTools
|
|
6
|
+
class Audit
|
|
7
|
+
module Checks
|
|
8
|
+
# The catch-all action is +ALLOW+: a denylist, bypassable by anything the author forgot.
|
|
9
|
+
module PermissiveDefault
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# @param [Audit::Policy] policy
|
|
13
|
+
# @return [Array<Audit::Finding>]
|
|
14
|
+
def call(policy)
|
|
15
|
+
return [] unless policy.default_label == 'ALLOW'
|
|
16
|
+
|
|
17
|
+
[Finding.new(
|
|
18
|
+
id: 'permissive-default', severity: :high, arch: policy.arch_name,
|
|
19
|
+
title: 'Default action is ALLOW (denylist)',
|
|
20
|
+
detail: 'Any syscall the filter does not explicitly block is allowed; a denylist is ' \
|
|
21
|
+
'bypassable by any syscall the author overlooked.',
|
|
22
|
+
syscalls: [], condition: nil,
|
|
23
|
+
remediation: 'Use an allowlist: default to KILL/ERRNO and permit only the needed syscalls.'
|
|
24
|
+
)]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'seccomp-tools/audit/catalog'
|
|
4
|
+
require 'seccomp-tools/audit/finding'
|
|
5
|
+
|
|
6
|
+
module SeccompTools
|
|
7
|
+
class Audit
|
|
8
|
+
module Checks
|
|
9
|
+
# An equivalent-syscall gap: a group member is singled out for denial while a sibling with the
|
|
10
|
+
# same capability still reaches +ALLOW+ (blocked +execve+ but not +execveat+, +open+ but not
|
|
11
|
+
# +openat+, +fork+ but not +clone+).
|
|
12
|
+
module SyscallAltGap
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# @param [Audit::Policy] policy
|
|
16
|
+
# @return [Array<Audit::Finding>]
|
|
17
|
+
def call(policy)
|
|
18
|
+
Catalog.alt_groups(policy.arch_sym).filter_map do |group, names|
|
|
19
|
+
present = names.filter_map { |n| [n, policy.number(n)] if policy.number(n) }
|
|
20
|
+
denied = present.select { |_n, nr| policy.explicitly_denied?(nr) }.map(&:first)
|
|
21
|
+
allowed = present.select { |_n, nr| policy.reachable_as_allow?(nr) }.map(&:first)
|
|
22
|
+
next if denied.empty? || allowed.empty?
|
|
23
|
+
|
|
24
|
+
finding(policy, group, denied, allowed)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @!visibility private
|
|
29
|
+
def finding(policy, group, denied, allowed)
|
|
30
|
+
Finding.new(
|
|
31
|
+
id: 'syscall-alt-gap', severity: %i[exec open].include?(group) ? :high : :medium,
|
|
32
|
+
arch: policy.arch_name,
|
|
33
|
+
title: "#{denied.join('/')} blocked but #{allowed.join('/')} allowed",
|
|
34
|
+
detail: "#{denied.join(', ')} denied, but the equivalent #{allowed.join(', ')} reaches " \
|
|
35
|
+
'ALLOW - same capability, different syscall number.',
|
|
36
|
+
# Both sides: the finding is about the pair, and the detail says which is which.
|
|
37
|
+
syscalls: (denied + allowed).map(&:to_s), condition: nil,
|
|
38
|
+
remediation: "Deny every equivalent in the group: also block #{allowed.join(', ')}."
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'seccomp-tools/audit/finding'
|
|
4
|
+
|
|
5
|
+
module SeccompTools
|
|
6
|
+
class Audit
|
|
7
|
+
module Checks
|
|
8
|
+
# amd64's x32 ABI quirk: x32 syscalls share +AUDIT_ARCH_X86_64+ but number as +nr | 0x40000000+.
|
|
9
|
+
# Without a +sys_number >= 0x40000000+ (or +jset 0x40000000+) guard, a syscall blocked by its
|
|
10
|
+
# native number is still reachable through its x32 number. Registered only for amd64, so no
|
|
11
|
+
# other architecture is ever mis-flagged.
|
|
12
|
+
module X32Guard
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# @param [Audit::Policy] policy
|
|
16
|
+
# @return [Array<Audit::Finding>]
|
|
17
|
+
def call(policy)
|
|
18
|
+
sharp = policy.table.filter_map do |name, nr|
|
|
19
|
+
next if name.to_s.start_with?('x32_')
|
|
20
|
+
|
|
21
|
+
x = policy.number(:"x32_#{name}")
|
|
22
|
+
name if x && policy.reachable_as_allow?(x) && !policy.reachable_as_allow?(nr)
|
|
23
|
+
end
|
|
24
|
+
return [] if sharp.empty?
|
|
25
|
+
|
|
26
|
+
[finding(policy, sharp)]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @!visibility private
|
|
30
|
+
def finding(policy, sharp)
|
|
31
|
+
shown = sharp.first(8).map(&:to_s)
|
|
32
|
+
more = sharp.size > shown.size ? ", ... (+#{sharp.size - shown.size} more)" : ''
|
|
33
|
+
Finding.new(
|
|
34
|
+
id: 'x32-guard', severity: :high, arch: policy.arch_name,
|
|
35
|
+
title: 'x32 ABI is not guarded',
|
|
36
|
+
detail: 'Syscalls blocked by their native number are reachable via their x32 number ' \
|
|
37
|
+
"(nr | 0x40000000): #{shown.join(', ')}#{more}.",
|
|
38
|
+
syscalls: shown, condition: nil,
|
|
39
|
+
remediation: 'After the arch check, KILL when sys_number >= 0x40000000 ' \
|
|
40
|
+
'(or jset 0x40000000).'
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|