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,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'seccomp-tools/const'
|
|
4
|
+
require 'seccomp-tools/explain/analysis'
|
|
5
|
+
require 'seccomp-tools/explain/qword'
|
|
6
|
+
require 'seccomp-tools/explain/renderer'
|
|
7
|
+
require 'seccomp-tools/explain/verdict'
|
|
8
|
+
require 'seccomp-tools/util'
|
|
9
|
+
|
|
10
|
+
module SeccompTools
|
|
11
|
+
class Explain
|
|
12
|
+
# Turns the raw {Symbolic::Executor::Leaf}s collected by the walk into a human-readable policy,
|
|
13
|
+
# grouped by architecture and then by action (+ALLOW+, +KILL+, +ERRNO(n)+, ...).
|
|
14
|
+
#
|
|
15
|
+
# This class owns the presentation: sections, buckets and the default rule. It reads each
|
|
16
|
+
# leaf's path through {PathFacts}, decodes the returned action with {Verdict}, reassembles
|
|
17
|
+
# 64-bit word checks with {QwordFusion}, and stringifies conditions with {Renderer}.
|
|
18
|
+
class Summary
|
|
19
|
+
# Display name of the syscall-number field, for the range subjects.
|
|
20
|
+
SYS_NAME = Const::BPF::SeccompData::NAMES.fetch(Const::BPF::SeccompData::SYS_NUMBER)
|
|
21
|
+
# The x32 ABI bit (+__X32_SYSCALL_BIT+); a lower-bound-only range at exactly this value is the
|
|
22
|
+
# conventional x32 guard, worth annotating.
|
|
23
|
+
X32_SYSCALL_BIT = 0x40000000
|
|
24
|
+
# Widest a wrapped bucket line may get, in columns.
|
|
25
|
+
WRAP_WIDTH = 72
|
|
26
|
+
|
|
27
|
+
# @param [Array<Symbolic::Executor::Leaf>] leaves
|
|
28
|
+
# @param [Symbol] arch
|
|
29
|
+
# The filter's declared architecture, used when the filter itself does not branch on +arch+.
|
|
30
|
+
# @param [String?] source
|
|
31
|
+
# Label shown in the header.
|
|
32
|
+
# @param [Boolean] truncated
|
|
33
|
+
# Whether the walk hit {Symbolic::Executor::STEP_CAP}.
|
|
34
|
+
def initialize(leaves, arch:, source: nil, truncated: false)
|
|
35
|
+
@arch = arch
|
|
36
|
+
@source = source
|
|
37
|
+
@truncated = truncated
|
|
38
|
+
@fusion = QwordFusion.new(arch)
|
|
39
|
+
@renderer = Renderer.new(@fusion)
|
|
40
|
+
@analysis = Analysis.new(leaves)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Renders the policy.
|
|
44
|
+
# @return [String]
|
|
45
|
+
def to_s
|
|
46
|
+
out = +''
|
|
47
|
+
out << "Seccomp policy for #{@source}\n" if @source
|
|
48
|
+
out << "WARNING: analysis truncated (filter too large); results may be incomplete.\n" if @truncated
|
|
49
|
+
@analysis.sections(@arch).each do |_arch_val, arch_sym, title, leaves|
|
|
50
|
+
out << "\n" << render_section(title, section_buckets(arch_sym, leaves))
|
|
51
|
+
end
|
|
52
|
+
out << render_other_arches
|
|
53
|
+
out
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# The {PathFacts} of +leaf+, computed once (shared with {Analysis}).
|
|
59
|
+
def facts(leaf)
|
|
60
|
+
@analysis.facts(leaf)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Renders what happens on the architectures the filter does not explicitly check for. Usually
|
|
64
|
+
# those paths just fall to one action and a one-liner suffices; when they carry rules of their
|
|
65
|
+
# own, a full section is rendered so the rules are not silently dropped.
|
|
66
|
+
def render_other_arches
|
|
67
|
+
return '' if @analysis.arch_values.empty?
|
|
68
|
+
|
|
69
|
+
leaves = @analysis.other_leaves
|
|
70
|
+
default = @analysis.default_label(leaves)
|
|
71
|
+
return '' unless default
|
|
72
|
+
|
|
73
|
+
buckets = rule_buckets(nil, leaves, default)
|
|
74
|
+
return "\nOther architectures: #{default}\n" if buckets.empty?
|
|
75
|
+
|
|
76
|
+
add_default(buckets, default)
|
|
77
|
+
"\n#{render_section('<any other>', buckets)}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The action buckets of one section: its non-default rules plus the default rule. +arch_sym+
|
|
81
|
+
# names syscalls/arguments; +nil+ (architecture unknown) leaves them numeric.
|
|
82
|
+
def section_buckets(arch_sym, leaves)
|
|
83
|
+
default = @analysis.default_label(leaves)
|
|
84
|
+
buckets = rule_buckets(arch_sym, leaves, default)
|
|
85
|
+
add_default(buckets, default)
|
|
86
|
+
buckets
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Renders one architecture section from its prebuilt +buckets+.
|
|
90
|
+
def render_section(title, buckets)
|
|
91
|
+
out = "Architecture: #{Util.colorize(title, t: :arch)}\n"
|
|
92
|
+
return out << "\n (no return reached; filter runs off the end)\n" if buckets.empty?
|
|
93
|
+
|
|
94
|
+
sorted_buckets(buckets).each { |label, b| out << render_bucket(label, b) }
|
|
95
|
+
out
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Buckets the non-default rules of a section by action label. Every leaf falls into exactly
|
|
99
|
+
# one bucket source: it pins a syscall number, restricts a range of numbers, checks arguments
|
|
100
|
+
# only, or is the catch-all (rendered by {#add_default}).
|
|
101
|
+
def rule_buckets(arch_sym, leaves, default)
|
|
102
|
+
named, rest = leaves.partition { |l| facts(l).sys_eq }
|
|
103
|
+
ranged, rest = rest.partition { |l| facts(l).sys_range }
|
|
104
|
+
conditional, = rest.partition { |l| !facts(l).residual.empty? }
|
|
105
|
+
|
|
106
|
+
buckets = {}
|
|
107
|
+
add_named(buckets, arch_sym, named, default)
|
|
108
|
+
add_ranges(buckets, ranged)
|
|
109
|
+
add_conditional(buckets, conditional, default)
|
|
110
|
+
buckets
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Explicitly matched syscalls (+A == nr+), grouped by number then verdict.
|
|
114
|
+
def add_named(buckets, arch_sym, leaves, default)
|
|
115
|
+
leaves.group_by { |l| facts(l).sys_eq }.sort_by(&:first).each do |nr, group|
|
|
116
|
+
sys = syscall_name(arch_sym, nr)
|
|
117
|
+
name = Util.colorize(sys ? sys.to_s : "0x#{nr.to_s(16)}", t: :syscall)
|
|
118
|
+
group.group_by { |l| Verdict.label(l.ret) }.each do |label, ls|
|
|
119
|
+
next if label == default # falls through to the default action
|
|
120
|
+
|
|
121
|
+
conds = merged_conds(ls, sys)
|
|
122
|
+
plain = conds.include?('') # some path reaches this verdict with no extra condition
|
|
123
|
+
entry = plain ? name : "#{name} when #{conds.join(' or ')}"
|
|
124
|
+
add(buckets, label, entry, simple: plain)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Fall-through rules that restrict a range of syscall numbers, e.g. the x32 ABI guard,
|
|
130
|
+
# together with whatever else those paths check. A range whose action is the default is still
|
|
131
|
+
# shown when unconditional (the explicit guard is worth surfacing), and its conditional
|
|
132
|
+
# variants are shown too so no check is silently dropped.
|
|
133
|
+
def add_ranges(buckets, leaves)
|
|
134
|
+
leaves.group_by { |l| facts(l).sys_range }.each do |(lo, hi), group|
|
|
135
|
+
range = "#{SYS_NAME} >= 0x#{lo.to_s(16)}"
|
|
136
|
+
range << " && #{SYS_NAME} <= 0x#{hi.to_s(16)}" if hi
|
|
137
|
+
group.group_by { |l| Verdict.label(l.ret) }.each do |label, ls|
|
|
138
|
+
conds = merged_conds(ls, nil)
|
|
139
|
+
entry = conds.include?('') ? range.dup : "#{range} when #{conds.join(' or ')}"
|
|
140
|
+
entry << ' (x32 ABI)' if x32?(lo, hi)
|
|
141
|
+
add(buckets, label, entry, simple: false)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Fall-through rules that inspect arguments (or a transformed syscall number) without pinning a
|
|
147
|
+
# specific syscall. Kept so such checks are never silently dropped.
|
|
148
|
+
def add_conditional(buckets, leaves, default)
|
|
149
|
+
leaves.group_by { |l| Verdict.label(l.ret) }.each do |label, ls|
|
|
150
|
+
next if label == default
|
|
151
|
+
|
|
152
|
+
conds = merged_conds(ls, nil)
|
|
153
|
+
add(buckets, label, "any syscall when #{conds.join(' or ')}", simple: false)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# The rendered or-branch conditions of the leaves +ls+, deduplicated, with 64-bit word checks
|
|
158
|
+
# fused back into whole-field facts (see {QwordFusion}).
|
|
159
|
+
def merged_conds(ls, sys)
|
|
160
|
+
@fusion.merge_or(ls.map { |l| facts(l).residual })
|
|
161
|
+
.map { |list| @renderer.conjunction(@fusion.fold(list), sys) }.uniq
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def add_default(buckets, default)
|
|
165
|
+
return unless default
|
|
166
|
+
|
|
167
|
+
# "other" only makes sense when some syscall was singled out; otherwise the default is the
|
|
168
|
+
# whole policy.
|
|
169
|
+
text = buckets.empty? ? '<default> (any syscall)' : '<default> (any other syscall)'
|
|
170
|
+
add(buckets, default, text, simple: false)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def add(buckets, label, text, simple:)
|
|
174
|
+
b = buckets[label] ||= { simple: [], complex: [] }
|
|
175
|
+
(simple ? b[:simple] : b[:complex]) << text
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def sorted_buckets(buckets)
|
|
179
|
+
buckets.sort_by { |label, _b| Verdict.rank(label) }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def render_bucket(label, bucket)
|
|
183
|
+
out = "\n #{label}:\n"
|
|
184
|
+
wrap(bucket[:simple]).each { |line| out << " #{line}\n" }
|
|
185
|
+
bucket[:complex].each { |line| out << " #{line}\n" }
|
|
186
|
+
out
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Wraps a list of short tokens into comma-separated lines no wider than 72 columns.
|
|
190
|
+
def wrap(tokens)
|
|
191
|
+
return [] if tokens.empty?
|
|
192
|
+
|
|
193
|
+
lines = []
|
|
194
|
+
line = +''
|
|
195
|
+
tokens.each do |tok|
|
|
196
|
+
piece = line.empty? ? tok : ", #{tok}"
|
|
197
|
+
if !line.empty? && line.size + piece.size > WRAP_WIDTH
|
|
198
|
+
lines << line
|
|
199
|
+
line = +tok
|
|
200
|
+
else
|
|
201
|
+
line << piece
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
lines << line
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def x32?(lo, hi)
|
|
208
|
+
lo == X32_SYSCALL_BIT && hi.nil?
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def syscall_name(arch_sym, nr)
|
|
212
|
+
arch_sym && Const::Syscall.const_get(arch_sym.upcase).invert[nr]
|
|
213
|
+
rescue NameError
|
|
214
|
+
nil
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'seccomp-tools/const'
|
|
4
|
+
|
|
5
|
+
module SeccompTools
|
|
6
|
+
class Explain
|
|
7
|
+
# Decodes the value a filter path returns into the action it stands for: the label a policy
|
|
8
|
+
# shows (+ALLOW+, +ERRNO(5)+, ...) and where that action sorts among the buckets.
|
|
9
|
+
module Verdict
|
|
10
|
+
# Buckets are printed in this order; unlisted actions sort last.
|
|
11
|
+
ORDER = %i[ALLOW USER_NOTIF LOG TRACE TRAP ERRNO KILL KILL_PROCESS UNKNOWN].freeze
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# The label for the value +ret+ a leaf returns, including the +SECCOMP_RET_DATA+ part when
|
|
16
|
+
# it is meaningful for the action.
|
|
17
|
+
# @param [Symbolic::Expr] ret
|
|
18
|
+
# @return [String]
|
|
19
|
+
def label(ret)
|
|
20
|
+
return 'UNKNOWN' unless ret.imm?
|
|
21
|
+
|
|
22
|
+
# An unrecognized action value loads fine; the kernel treats it as KILL_PROCESS
|
|
23
|
+
# (KILL_THREAD before Linux 4.14). See seccomp(2).
|
|
24
|
+
Const::BPF.action_label(ret.val) || format('KILL_PROCESS (unknown action 0x%x)', ret.val)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Where the bucket labeled +label+ sorts: by its action's position in {ORDER}, then
|
|
28
|
+
# alphabetically. Works on any label {.label} produces, data and annotations included.
|
|
29
|
+
# @param [String] label
|
|
30
|
+
# @return [Array(Integer, String)]
|
|
31
|
+
def rank(label)
|
|
32
|
+
[ORDER.index(action_of(label)) || ORDER.size, label]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The bare action symbol of a label, recovered by stripping any +(data)+ / + (annotation)+
|
|
36
|
+
# suffix {.label} appended. Safe because no action name itself contains +" ("+.
|
|
37
|
+
# @param [String] label
|
|
38
|
+
# @return [Symbol]
|
|
39
|
+
def action_of(label)
|
|
40
|
+
label.sub(/\s*\(.*/, '').to_sym
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'seccomp-tools/explain/summary'
|
|
4
|
+
require 'seccomp-tools/symbolic/executor'
|
|
5
|
+
|
|
6
|
+
module SeccompTools
|
|
7
|
+
# Analyzes a whole seccomp filter across all execution paths and summarizes it as a per-action
|
|
8
|
+
# policy: which syscalls end in +ALLOW+, +KILL+, +ERRNO(n)+, etc., and under what argument
|
|
9
|
+
# constraints.
|
|
10
|
+
#
|
|
11
|
+
# It runs the generic {Symbolic::Executor} over the filter to collect every reachable +return+
|
|
12
|
+
# together with the path condition that leads to it, then hands those leaves to {Summary}, which
|
|
13
|
+
# interprets them with seccomp semantics (syscall numbers, architectures, actions, ...).
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# insts = SeccompTools::Disasm.to_bpf(raw, :amd64).map(&:inst)
|
|
17
|
+
# puts SeccompTools::Explain.new(insts, arch: :amd64).summarize
|
|
18
|
+
class Explain
|
|
19
|
+
# @param [Array<Instruction::Base>] instructions
|
|
20
|
+
# The filter, as +SeccompTools::Disasm.to_bpf(raw, arch).map(&:inst)+.
|
|
21
|
+
# @param [Symbol] arch
|
|
22
|
+
# The architecture the filter is written for, used for syscall/argument names.
|
|
23
|
+
# @param [String?] source
|
|
24
|
+
# A label for the filter (e.g. a filename) shown in the summary header.
|
|
25
|
+
def initialize(instructions, arch:, source: nil)
|
|
26
|
+
@instructions = instructions
|
|
27
|
+
@arch = arch
|
|
28
|
+
@source = source
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Walks the filter and returns a printable {Summary}.
|
|
32
|
+
# @return [Summary]
|
|
33
|
+
def summarize
|
|
34
|
+
leaves, truncated = Symbolic::Executor.new(@instructions).run
|
|
35
|
+
Summary.new(leaves, arch: @arch, source: @source, truncated:)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -4,7 +4,9 @@ require 'seccomp-tools/instruction/base'
|
|
|
4
4
|
|
|
5
5
|
module SeccompTools
|
|
6
6
|
module Instruction
|
|
7
|
-
# Instruction alu.
|
|
7
|
+
# Instruction alu, performs an arithmetic or bitwise operation on the accumulator register A.
|
|
8
|
+
#
|
|
9
|
+
# The right operand is either the X register or the immediate +k+.
|
|
8
10
|
class ALU < Base
|
|
9
11
|
# Mapping from name to operator.
|
|
10
12
|
OP_SYM = {
|
|
@@ -17,10 +19,12 @@ module SeccompTools
|
|
|
17
19
|
lsh: :<<,
|
|
18
20
|
rsh: :>>,
|
|
19
21
|
# neg: :-, # should not be invoked
|
|
20
|
-
# mod: :%, #
|
|
22
|
+
# mod: :%, # the kernel rejects BPF_MOD (see Const::BPF::OP), so it never reaches here
|
|
21
23
|
xor: :^
|
|
22
24
|
}.freeze
|
|
23
25
|
# Decompile instruction.
|
|
26
|
+
# @return [String]
|
|
27
|
+
# The operation as assembly, e.g. +"A &= 0x7fff"+.
|
|
24
28
|
def decompile
|
|
25
29
|
return 'A = -A' if op == :neg
|
|
26
30
|
|
|
@@ -29,6 +33,8 @@ module SeccompTools
|
|
|
29
33
|
|
|
30
34
|
# See {Instruction::Base#symbolize}.
|
|
31
35
|
# @return [[:alu, Symbol, (:x, Integer, nil)]]
|
|
36
|
+
# The operator and its right operand, which is +:x+ for the X register, an Integer for an
|
|
37
|
+
# immediate, or +nil+ for the unary +neg+.
|
|
32
38
|
def symbolize
|
|
33
39
|
return [:alu, :neg, nil] if op == :neg
|
|
34
40
|
|
|
@@ -36,13 +42,12 @@ module SeccompTools
|
|
|
36
42
|
end
|
|
37
43
|
|
|
38
44
|
# See {Base#branch}.
|
|
39
|
-
# @param [
|
|
40
|
-
# Current
|
|
41
|
-
# @return [Array<(Integer,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
[[line + 1, ctx]]
|
|
45
|
+
# @param [Symbolic::State] state
|
|
46
|
+
# Current state.
|
|
47
|
+
# @return [Array<(Integer, Symbolic::State)>]
|
|
48
|
+
# Always the next line, with A marked as no longer tracked.
|
|
49
|
+
def branch(state)
|
|
50
|
+
[[line + 1, state.with(a: Symbolic::Expr.opaque)]]
|
|
46
51
|
end
|
|
47
52
|
|
|
48
53
|
private
|
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
require 'seccomp-tools/const'
|
|
4
4
|
|
|
5
5
|
module SeccompTools
|
|
6
|
-
#
|
|
6
|
+
# Classes of BPF instructions, one per opcode class.
|
|
7
|
+
#
|
|
8
|
+
# Each instruction wraps a {SeccompTools::BPF} and knows how to render itself as assembly
|
|
9
|
+
# (+decompile+), as tokens ({Base#symbolize}), and how it moves the disassembler's state
|
|
10
|
+
# forward ({Base#branch}).
|
|
7
11
|
module Instruction
|
|
8
12
|
# Base class of instructions.
|
|
13
|
+
#
|
|
14
|
+
# Subclasses must implement {#branch} and {#symbolize}.
|
|
9
15
|
class Base
|
|
10
16
|
include SeccompTools::Const::BPF
|
|
11
17
|
|
|
@@ -25,29 +31,52 @@ module SeccompTools
|
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
# Returns the possible branches after executing this instruction.
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
34
|
+
#
|
|
35
|
+
# Each branch is the line number to be executed next, paired with the {Symbolic::State} that
|
|
36
|
+
# reaching that line implies. Non-jump instructions have exactly one branch, the following
|
|
37
|
+
# line.
|
|
38
|
+
# @param [Symbolic::State] _state
|
|
39
|
+
# Current state.
|
|
40
|
+
# @return [Array<(Integer, Symbolic::State)>]
|
|
41
|
+
# Pairs of the next line number and the state at that line.
|
|
42
|
+
# @raise [NotImplementedError]
|
|
43
|
+
# Always, subclasses must override this method.
|
|
31
44
|
# @example
|
|
32
45
|
# # For ALU, LD, LDX, ST, STX
|
|
33
46
|
# inst.line #=> 10
|
|
34
|
-
# inst.branch(
|
|
35
|
-
# #=> [[11,
|
|
36
|
-
def branch(
|
|
47
|
+
# inst.branch(state)
|
|
48
|
+
# #=> [[11, state]]
|
|
49
|
+
def branch(_state); raise NotImplementedError
|
|
37
50
|
end
|
|
38
51
|
|
|
39
|
-
#
|
|
52
|
+
# Returns tokens that represent this instruction.
|
|
40
53
|
# @return [Array<Symbol, Integer>]
|
|
54
|
+
# The instruction as a tuple, the exact shape depends on the instruction class.
|
|
55
|
+
# @raise [NotImplementedError]
|
|
56
|
+
# Always, subclasses must override this method.
|
|
41
57
|
# @example
|
|
42
58
|
# ret_a.symbolize #=> [:ret, :a]
|
|
43
59
|
# ret_k.symbolize #=> [:ret, 0x7fff0000]
|
|
44
60
|
# jeq.symbolize #=> [:cmp, :==, 0, 0, 1]
|
|
45
|
-
def symbolize; raise
|
|
61
|
+
def symbolize; raise NotImplementedError
|
|
46
62
|
end
|
|
47
63
|
|
|
48
64
|
private
|
|
49
65
|
|
|
50
|
-
|
|
66
|
+
# The architecture this instruction's operands should be read as: the one the filter has
|
|
67
|
+
# branched on (+arch == AUDIT_ARCH_*+) when the reaching states pin a single value, else +nil+
|
|
68
|
+
# so callers fall back to the declared {#arch}. Lets syscall/argument names stay correct even
|
|
69
|
+
# when the filter is disassembled under a different +--arch+ than it targets.
|
|
70
|
+
# @return [Symbol?]
|
|
71
|
+
def infer_arch
|
|
72
|
+
arches = states.map { |st| st.pinned(Const::BPF::SeccompData::ARCH) }.uniq
|
|
73
|
+
return nil unless arches.size == 1 && !arches.first.nil?
|
|
74
|
+
|
|
75
|
+
Const::Audit.arch_symbol(arches.first)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Delegate the accessors of the wrapped {SeccompTools::BPF} so subclasses can use them directly.
|
|
79
|
+
%i(code jt jf k arch line states show_arg_infer?).each do |sym|
|
|
51
80
|
define_method(sym) do
|
|
52
81
|
@bpf.__send__(sym)
|
|
53
82
|
end
|
|
@@ -5,9 +5,13 @@ require 'seccomp-tools/instruction/base'
|
|
|
5
5
|
|
|
6
6
|
module SeccompTools
|
|
7
7
|
module Instruction
|
|
8
|
-
# Instruction jmp.
|
|
8
|
+
# Instruction jmp, an unconditional jump or a comparison of A against X or an immediate.
|
|
9
|
+
#
|
|
10
|
+
# Jumps are always forward, +jt+ and +jf+ being offsets relative to the following line.
|
|
9
11
|
class JMP < Base
|
|
10
12
|
# Decompile instruction.
|
|
13
|
+
# @return [String]
|
|
14
|
+
# The jump as assembly, e.g. +"if (A == read) goto 0003"+.
|
|
11
15
|
def decompile
|
|
12
16
|
return goto(k) if jop == :none
|
|
13
17
|
# if jt == 0 && jf == 0 => no-op # should not happen
|
|
@@ -23,6 +27,8 @@ module SeccompTools
|
|
|
23
27
|
|
|
24
28
|
# See {Instruction::Base#symbolize}.
|
|
25
29
|
# @return [[:cmp, Symbol, (:x, Integer), Integer, Integer], [:jmp, Integer]]
|
|
30
|
+
# +[:jmp, offset]+ for an unconditional jump, otherwise +[:cmp, operator, right operand,
|
|
31
|
+
# jt, jf]+.
|
|
26
32
|
def symbolize
|
|
27
33
|
return [:jmp, k] if jop == :none
|
|
28
34
|
|
|
@@ -30,19 +36,49 @@ module SeccompTools
|
|
|
30
36
|
end
|
|
31
37
|
|
|
32
38
|
# See {Base#branch}.
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
#
|
|
40
|
+
# Unlike the other instructions, a conditional jump has two possible successors. On the
|
|
41
|
+
# taken branch of an equality test the state is narrowed, recording that A is known to equal
|
|
42
|
+
# the compared value.
|
|
43
|
+
# @param [Symbolic::State] state
|
|
44
|
+
# Current state.
|
|
45
|
+
# @return [Array<(Integer, Symbolic::State)>]
|
|
46
|
+
# One pair for an unconditional jump, two otherwise.
|
|
47
|
+
# @example
|
|
48
|
+
# # 0000: if (A == 0) goto 0002 else goto 0003
|
|
49
|
+
# jeq.branch(state) #=> [[2, narrowed_state], [3, state]]
|
|
50
|
+
def branch(state)
|
|
51
|
+
return [[at(k), state]] if jop == :none
|
|
52
|
+
return [[at(jt), state]] if jt == jf
|
|
53
|
+
return [[at(jt), narrow(state)], [at(jf), state]] if jop == :==
|
|
54
|
+
|
|
55
|
+
[[at(jt), state], [at(jf), state]]
|
|
42
56
|
end
|
|
43
57
|
|
|
44
58
|
private
|
|
45
59
|
|
|
60
|
+
# The taken branch of +A == src+ learns +A == value+. When A holds a plain data word and the
|
|
61
|
+
# compared side is (or resolves to) a constant, that pins the word - recorded as a
|
|
62
|
+
# {Symbolic::Constraint} on the path. Anything else leaves the state unchanged.
|
|
63
|
+
# @param [Symbolic::State] state
|
|
64
|
+
# @return [Symbolic::State]
|
|
65
|
+
def narrow(state)
|
|
66
|
+
return state unless state.a.plain_data?
|
|
67
|
+
|
|
68
|
+
rhs = src == :x ? resolve(state, state.x) : Symbolic::Expr.imm(k)
|
|
69
|
+
return state unless rhs.imm?
|
|
70
|
+
|
|
71
|
+
state.with(path: state.path + [Symbolic::Constraint.new(state.a, :==, rhs)])
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Replaces a data-word +expr+ with the constant it is pinned to on +state+'s path, if any;
|
|
75
|
+
# otherwise returns it unchanged.
|
|
76
|
+
def resolve(state, expr)
|
|
77
|
+
return expr unless expr.plain_data?
|
|
78
|
+
|
|
79
|
+
state.path.find { |c| c.plain_data_eq?(expr.offset) }&.rhs || expr
|
|
80
|
+
end
|
|
81
|
+
|
|
46
82
|
def jop
|
|
47
83
|
case Const::BPF::JMP.invert[code & 0x70]
|
|
48
84
|
when :ja then :none
|
|
@@ -57,15 +93,15 @@ module SeccompTools
|
|
|
57
93
|
def src_str
|
|
58
94
|
return 'X' if src == :x
|
|
59
95
|
|
|
60
|
-
#
|
|
61
|
-
a =
|
|
96
|
+
# only when A holds the same data word across every reaching state
|
|
97
|
+
a = states.map(&:a).uniq
|
|
62
98
|
return k.to_s if a.size != 1
|
|
63
99
|
|
|
64
100
|
a = a[0]
|
|
65
|
-
return k.to_s unless a.
|
|
101
|
+
return k.to_s unless a.plain_data?
|
|
66
102
|
|
|
67
103
|
hex = "0x#{k.to_s(16)}"
|
|
68
|
-
case a.
|
|
104
|
+
case a.offset
|
|
69
105
|
# interpret as syscalls only if it's an equality test
|
|
70
106
|
when 0 then Util.colorize(jop == :== ? sysname_by_k || hex : hex, t: :syscall)
|
|
71
107
|
when 4 then Util.colorize(Const::Audit::ARCH.invert[k] || hex, t: :arch)
|
|
@@ -81,15 +117,6 @@ module SeccompTools
|
|
|
81
117
|
a == arch ? name : "#{a}.#{name}"
|
|
82
118
|
end
|
|
83
119
|
|
|
84
|
-
# Infers the architecture from context.
|
|
85
|
-
# @return [Symbol?]
|
|
86
|
-
def infer_arch
|
|
87
|
-
arches = contexts.map { |ctx| ctx.known_data[4] }.uniq
|
|
88
|
-
return nil unless arches.size == 1 && !arches.first.nil?
|
|
89
|
-
|
|
90
|
-
Const::Audit::ARCH_NAME.invert[Const::Audit::ARCH.invert[arches.first]]
|
|
91
|
-
end
|
|
92
|
-
|
|
93
120
|
def src
|
|
94
121
|
SRC.invert[code & 8] == :x ? :x : k
|
|
95
122
|
end
|
|
@@ -6,9 +6,14 @@ require 'seccomp-tools/util'
|
|
|
6
6
|
|
|
7
7
|
module SeccompTools
|
|
8
8
|
module Instruction
|
|
9
|
-
# Instruction ld.
|
|
9
|
+
# Instruction ld, loads a value into the accumulator register A.
|
|
10
|
+
#
|
|
11
|
+
# The value can be an immediate, a word of +struct seccomp_data+, or a slot of the scratch
|
|
12
|
+
# memory. {LDX} inherits from this class and targets the X register instead.
|
|
10
13
|
class LD < Base
|
|
11
14
|
# Decompile instruction.
|
|
15
|
+
# @return [String]
|
|
16
|
+
# The assignment as assembly, e.g. +"A = sys_number"+.
|
|
12
17
|
def decompile
|
|
13
18
|
ret = "#{reg} = "
|
|
14
19
|
_, _reg, type = symbolize
|
|
@@ -18,26 +23,35 @@ module SeccompTools
|
|
|
18
23
|
ret + seccomp_data_str
|
|
19
24
|
end
|
|
20
25
|
|
|
21
|
-
#
|
|
26
|
+
# See {Instruction::Base#symbolize}.
|
|
27
|
+
# @return [[:ld, Symbol, {rel: Symbol, val: Integer}]]
|
|
28
|
+
# The target register and the value being loaded, whose +:rel+ is one of +:immi+, +:mem+
|
|
29
|
+
# or +:data+.
|
|
22
30
|
def symbolize
|
|
23
31
|
type = load_val
|
|
24
32
|
[:ld, reg.downcase.to_sym, type]
|
|
25
33
|
end
|
|
26
34
|
|
|
27
|
-
#
|
|
28
|
-
# @return [
|
|
35
|
+
# Name of the register being loaded into.
|
|
36
|
+
# @return [String]
|
|
37
|
+
# The accumulator register, +"A"+.
|
|
29
38
|
def reg
|
|
30
39
|
'A'
|
|
31
40
|
end
|
|
32
41
|
|
|
33
42
|
# See {Base#branch}.
|
|
34
|
-
# @param [
|
|
35
|
-
# Current
|
|
36
|
-
# @return [Array<(Integer,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
# @param [Symbolic::State] state
|
|
44
|
+
# Current state.
|
|
45
|
+
# @return [Array<(Integer, Symbolic::State)>]
|
|
46
|
+
# Always the next line, with the loaded value recorded in the register.
|
|
47
|
+
def branch(state)
|
|
48
|
+
v = load_val
|
|
49
|
+
val = case v[:rel]
|
|
50
|
+
when :immi then Symbolic::Expr.imm(v[:val])
|
|
51
|
+
when :mem then state.mem[v[:val]]
|
|
52
|
+
when :data then Symbolic::Expr.data(v[:val])
|
|
53
|
+
end
|
|
54
|
+
[[line + 1, reg == 'X' ? state.with(x: val) : state.with(a: val)]]
|
|
41
55
|
end
|
|
42
56
|
|
|
43
57
|
private
|
|
@@ -64,33 +78,44 @@ module SeccompTools
|
|
|
64
78
|
# __u64 args[6];
|
|
65
79
|
# };
|
|
66
80
|
def seccomp_data_str
|
|
81
|
+
data = Const::BPF::SeccompData
|
|
67
82
|
case k
|
|
68
|
-
when
|
|
69
|
-
when
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
when data::SYS_NUMBER, data::ARCH then data::NAMES[k]
|
|
84
|
+
when data::INSTRUCTION_POINTER, data::INSTRUCTION_POINTER + 4
|
|
85
|
+
ip = data::NAMES[data::INSTRUCTION_POINTER]
|
|
86
|
+
hi_word?(k) ? "#{ip} >> 32" : ip
|
|
72
87
|
else
|
|
73
|
-
idx =
|
|
88
|
+
idx = (data::ARGS...data::SIZE).step(4).to_a.index(k)
|
|
74
89
|
return 'INVALID' if idx.nil?
|
|
75
90
|
|
|
76
91
|
args_name(idx)
|
|
77
92
|
end
|
|
78
93
|
end
|
|
79
94
|
|
|
95
|
+
# Is the 32-bit word at byte offset +k+ the high half of its 64-bit +seccomp_data+ field?
|
|
96
|
+
# The high word comes second on little-endian architectures but first on big-endian ones
|
|
97
|
+
# (s390x); see +arch_arg_offset_hi+ in libseccomp.
|
|
98
|
+
def hi_word?(k)
|
|
99
|
+
(k % 8 == 4) ^ Const::Endian.big?(arch)
|
|
100
|
+
end
|
|
101
|
+
|
|
80
102
|
def args_name(idx)
|
|
81
|
-
|
|
103
|
+
hi = hi_word?((idx * 4) + Const::BPF::SeccompData::ARGS)
|
|
104
|
+
default = hi ? "args[#{idx / 2}] >> 32" : "args[#{idx / 2}]"
|
|
82
105
|
return default unless show_arg_infer?
|
|
83
106
|
|
|
84
|
-
sys_nrs =
|
|
107
|
+
sys_nrs = states.map { |st| st.pinned(Const::BPF::SeccompData::SYS_NUMBER) }.uniq
|
|
85
108
|
return default if sys_nrs.size != 1 || sys_nrs.first.nil?
|
|
86
109
|
|
|
87
|
-
|
|
110
|
+
a = infer_arch || arch
|
|
111
|
+
sys = Const::Syscall.const_get(a.upcase.to_sym).invert[sys_nrs.first]
|
|
88
112
|
args = Const::SYS_ARG[sys]
|
|
89
113
|
return default if args.nil? || args[idx / 2].nil? # function prototype doesn't have that argument
|
|
90
114
|
|
|
91
|
-
|
|
115
|
+
name = a == arch ? sys : "#{a}.#{sys}"
|
|
116
|
+
comment = "# #{name}(#{args.join(', ')})"
|
|
92
117
|
arg_name = Util.colorize(args[idx / 2], t: :args)
|
|
93
|
-
"#{
|
|
118
|
+
"#{hi ? "#{arg_name} >> 32" : arg_name} #{Util.colorize(comment, t: :gray)}"
|
|
94
119
|
end
|
|
95
120
|
end
|
|
96
121
|
end
|