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
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'seccomp-tools/cli/asm'
|
|
4
|
+
require 'seccomp-tools/cli/audit'
|
|
5
|
+
require 'seccomp-tools/cli/completion'
|
|
4
6
|
require 'seccomp-tools/cli/disasm'
|
|
5
7
|
require 'seccomp-tools/cli/dump'
|
|
6
8
|
require 'seccomp-tools/cli/emu'
|
|
9
|
+
require 'seccomp-tools/cli/explain'
|
|
7
10
|
require 'seccomp-tools/version'
|
|
8
11
|
|
|
9
12
|
module SeccompTools
|
|
@@ -12,9 +15,12 @@ module SeccompTools
|
|
|
12
15
|
# Handled commands
|
|
13
16
|
COMMANDS = {
|
|
14
17
|
'asm' => SeccompTools::CLI::Asm,
|
|
18
|
+
'audit' => SeccompTools::CLI::Audit,
|
|
19
|
+
'completion' => SeccompTools::CLI::Completion,
|
|
15
20
|
'disasm' => SeccompTools::CLI::Disasm,
|
|
16
21
|
'dump' => SeccompTools::CLI::Dump,
|
|
17
|
-
'emu' => SeccompTools::CLI::Emu
|
|
22
|
+
'emu' => SeccompTools::CLI::Emu,
|
|
23
|
+
'explain' => SeccompTools::CLI::Explain
|
|
18
24
|
}.freeze
|
|
19
25
|
|
|
20
26
|
# Main usage message.
|
|
@@ -57,14 +63,12 @@ EOS
|
|
|
57
63
|
COMMANDS[cmd].new(argv).handle
|
|
58
64
|
end
|
|
59
65
|
|
|
60
|
-
#
|
|
66
|
+
# Writes a message to stdout, followed by a newline.
|
|
61
67
|
# @param [String] msg
|
|
62
|
-
# The message.
|
|
63
|
-
# @return [
|
|
64
|
-
# Always return +false+.
|
|
68
|
+
# The message to be written.
|
|
69
|
+
# @return [void]
|
|
65
70
|
def show(msg)
|
|
66
71
|
puts msg
|
|
67
|
-
false
|
|
68
72
|
end
|
|
69
73
|
|
|
70
74
|
class << self
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'seccomp-tools/cli/base'
|
|
4
|
+
require 'seccomp-tools/logger'
|
|
5
|
+
|
|
6
|
+
module SeccompTools
|
|
7
|
+
module CLI
|
|
8
|
+
# Handle 'completion' command.
|
|
9
|
+
class Completion < Base
|
|
10
|
+
# Summary of this command.
|
|
11
|
+
SUMMARY = 'Print a shell completion script.'
|
|
12
|
+
# Usage of this command.
|
|
13
|
+
USAGE = "completion - #{SUMMARY}\n\nUsage: seccomp-tools completion <bash|zsh|fish>".freeze
|
|
14
|
+
|
|
15
|
+
# Maps a shell name to the completion script that ships with the gem.
|
|
16
|
+
SCRIPTS = { 'bash' => 'seccomp-tools.bash', 'zsh' => '_seccomp-tools', 'fish' => 'seccomp-tools.fish' }.freeze
|
|
17
|
+
|
|
18
|
+
# Define option parser.
|
|
19
|
+
# @return [OptionParser]
|
|
20
|
+
# The parser of this command's options.
|
|
21
|
+
def parser
|
|
22
|
+
@parser ||= OptionParser.new do |opt|
|
|
23
|
+
opt.banner = usage
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Writes the completion script for the requested shell to stdout.
|
|
28
|
+
# @return [void]
|
|
29
|
+
def handle
|
|
30
|
+
return unless super
|
|
31
|
+
|
|
32
|
+
shell = argv.shift
|
|
33
|
+
file = SCRIPTS[shell]
|
|
34
|
+
return CLI.show(parser.help) if file.nil?
|
|
35
|
+
|
|
36
|
+
output { File.read(File.join(__dir__, '..', '..', '..', 'completions', file)) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -10,8 +10,11 @@ module SeccompTools
|
|
|
10
10
|
# Summary of this command.
|
|
11
11
|
SUMMARY = 'Disassemble seccomp bpf.'
|
|
12
12
|
# Usage of this command.
|
|
13
|
-
USAGE = "disasm - #{SUMMARY}\n\nUsage: seccomp-tools disasm BPF_FILE [options]"
|
|
13
|
+
USAGE = "disasm - #{SUMMARY}\n\nUsage: seccomp-tools disasm BPF_FILE [options]".freeze
|
|
14
14
|
|
|
15
|
+
# Instantiate a {Disasm} object, showing raw BPF and inferred arguments by default.
|
|
16
|
+
#
|
|
17
|
+
# Takes the same arguments as {Base#initialize}.
|
|
15
18
|
def initialize(*)
|
|
16
19
|
super
|
|
17
20
|
option[:bpf] = true
|
|
@@ -20,10 +23,11 @@ module SeccompTools
|
|
|
20
23
|
|
|
21
24
|
# Define option parser.
|
|
22
25
|
# @return [OptionParser]
|
|
26
|
+
# The parser of this command's options.
|
|
23
27
|
def parser
|
|
24
28
|
@parser ||= OptionParser.new do |opt|
|
|
25
29
|
opt.banner = usage
|
|
26
|
-
opt.on('-o', '--output FILE', '
|
|
30
|
+
opt.on('-o', '--output FILE', 'Write output to FILE instead of stdout.') do |o|
|
|
27
31
|
option[:ofile] = o
|
|
28
32
|
end
|
|
29
33
|
option_arch(opt)
|
|
@@ -35,9 +39,9 @@ module SeccompTools
|
|
|
35
39
|
'Default: true') do |f|
|
|
36
40
|
option[:arg_infer] = f
|
|
37
41
|
end
|
|
38
|
-
opt.on('--asm-able', '
|
|
39
|
-
'By default, "seccomp-tools disasm"
|
|
40
|
-
'
|
|
42
|
+
opt.on('--asm-able', 'Make the output valid input for "seccomp-tools asm".',
|
|
43
|
+
'By default, "seccomp-tools disasm" outputs a human-readable format meant for analysis.',
|
|
44
|
+
'With this flag the output is simplified so it can be fed back to "seccomp-tools asm".',
|
|
41
45
|
'This flag implies "--no-bpf --no-arg-infer".',
|
|
42
46
|
'Default: false') do |_f|
|
|
43
47
|
option[:bpf] = false
|
|
@@ -46,7 +50,7 @@ module SeccompTools
|
|
|
46
50
|
end
|
|
47
51
|
end
|
|
48
52
|
|
|
49
|
-
#
|
|
53
|
+
# Disassembles the input file and writes the result.
|
|
50
54
|
# @return [void]
|
|
51
55
|
def handle
|
|
52
56
|
return unless super
|
|
@@ -1,39 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'shellwords'
|
|
4
|
-
|
|
5
3
|
require 'seccomp-tools/cli/base'
|
|
4
|
+
require 'seccomp-tools/cli/filter_input'
|
|
6
5
|
require 'seccomp-tools/disasm/disasm'
|
|
7
6
|
require 'seccomp-tools/dumper'
|
|
8
|
-
require 'seccomp-tools/logger'
|
|
9
7
|
|
|
10
8
|
module SeccompTools
|
|
11
9
|
module CLI
|
|
12
10
|
# Handle 'dump' command.
|
|
13
11
|
class Dump < Base
|
|
12
|
+
include FilterInput
|
|
13
|
+
|
|
14
14
|
# Summary of this command.
|
|
15
|
-
SUMMARY = 'Automatically dump seccomp bpf from
|
|
15
|
+
SUMMARY = 'Automatically dump seccomp bpf from executable(s).'
|
|
16
16
|
# Usage of this command.
|
|
17
|
-
USAGE = "dump - #{SUMMARY}\nNOTE
|
|
18
|
-
"\n\nUsage: seccomp-tools dump [
|
|
17
|
+
USAGE = "dump - #{SUMMARY}\nNOTE: This command is only available on Linux." \
|
|
18
|
+
"\n\nUsage: seccomp-tools dump [EXEC] [options]".freeze
|
|
19
19
|
|
|
20
|
+
# Instantiate a {Dump} object, dumping the first filter as disassembly by default.
|
|
21
|
+
#
|
|
22
|
+
# Takes the same arguments as {Base#initialize}.
|
|
20
23
|
def initialize(*)
|
|
21
24
|
super
|
|
22
25
|
option[:format] = :disasm
|
|
23
|
-
option[:limit] = 1
|
|
24
|
-
option[:pid] = nil
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
# Define option parser.
|
|
28
29
|
# @return [OptionParser]
|
|
30
|
+
# The parser of this command's options.
|
|
29
31
|
def parser
|
|
30
32
|
@parser ||= OptionParser.new do |opt|
|
|
31
33
|
opt.banner = usage
|
|
32
|
-
opt
|
|
33
|
-
'Use this option if want to pass arguments or do pipe things to the execution file.',
|
|
34
|
-
'e.g. use `-c "./bin > /dev/null"` to dump seccomp without being mixed with stdout.') do |command|
|
|
35
|
-
option[:command] = command
|
|
36
|
-
end
|
|
34
|
+
option_filter_source(opt, 'dump')
|
|
37
35
|
|
|
38
36
|
opt.on('-f', '--format FORMAT', %i[disasm raw inspect],
|
|
39
37
|
'Output format. FORMAT can only be one of <disasm|raw|inspect>.',
|
|
@@ -41,55 +39,42 @@ module SeccompTools
|
|
|
41
39
|
option[:format] = f
|
|
42
40
|
end
|
|
43
41
|
|
|
44
|
-
opt.on('-
|
|
45
|
-
'The target process will be killed whenever its calling times reaches LIMIT.',
|
|
46
|
-
'Default: 1', Integer) do |l|
|
|
47
|
-
option[:limit] = l
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
opt.on('-o', '--output FILE', 'Output result into FILE instead of stdout.',
|
|
42
|
+
opt.on('-o', '--output FILE', 'Write output to FILE instead of stdout.',
|
|
51
43
|
'If multiple seccomp syscalls have been invoked (see --limit),',
|
|
52
|
-
'results
|
|
53
|
-
'For example, "--output out.bpf"
|
|
44
|
+
'results are written to FILE, FILE_1, FILE_2, etc.',
|
|
45
|
+
'For example, with "--output out.bpf" the output files are out.bpf, out_1.bpf, ...') do |o|
|
|
54
46
|
option[:ofile] = o
|
|
55
47
|
end
|
|
56
|
-
|
|
57
|
-
opt.on('-p', '--pid PID', 'Dump installed seccomp filters of the existing process.',
|
|
58
|
-
'You must have CAP_SYS_ADMIN (e.g. be root) in order to use this option.',
|
|
59
|
-
Integer) do |p|
|
|
60
|
-
option[:pid] = p
|
|
61
|
-
end
|
|
62
48
|
end
|
|
63
49
|
end
|
|
64
50
|
|
|
65
|
-
#
|
|
51
|
+
# Traces the target process and writes out the seccomp filters it installs.
|
|
52
|
+
#
|
|
53
|
+
# Only available on Linux, logs an error and returns otherwise.
|
|
66
54
|
# @return [void]
|
|
67
55
|
def handle
|
|
68
|
-
return
|
|
56
|
+
return unless dumping_supported?
|
|
69
57
|
return unless super
|
|
70
58
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
EOS
|
|
91
|
-
exit(1)
|
|
92
|
-
end
|
|
59
|
+
collect_filters.each { |bpf, arch| emit(bpf, arch) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# A positional argument is always an executable to trace, never a raw BPF blob to read: that is
|
|
65
|
+
# +disasm+'s job, and a non-ELF executable (a shell script wrapping the target) must still run.
|
|
66
|
+
# @return [Boolean]
|
|
67
|
+
def accepts_raw_bpf?
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Writes one dumped filter in the requested format.
|
|
72
|
+
# @return [void]
|
|
73
|
+
def emit(bpf, arch)
|
|
74
|
+
case option[:format]
|
|
75
|
+
when :inspect then output { "\"#{bpf.bytes.map { |b| format('\\x%02X', b) }.join}\"\n" }
|
|
76
|
+
when :raw then output { bpf }
|
|
77
|
+
when :disasm then output { SeccompTools::Disasm.disasm(bpf, arch:) }
|
|
93
78
|
end
|
|
94
79
|
end
|
|
95
80
|
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
5
|
+
require 'seccomp-tools/dumper'
|
|
6
|
+
require 'seccomp-tools/logger'
|
|
7
|
+
|
|
8
|
+
module SeccompTools
|
|
9
|
+
module CLI
|
|
10
|
+
# Shared helpers for the commands that dump seccomp filters via ptrace ({Dump} and {Explain}).
|
|
11
|
+
module Dumpable
|
|
12
|
+
# Dumps the seccomp filters from a command run via +sh+, or from an existing process, yielding
|
|
13
|
+
# each installed filter.
|
|
14
|
+
#
|
|
15
|
+
# When +pid+ is given the process is traced (requiring +CAP_SYS_ADMIN+); otherwise +command+
|
|
16
|
+
# is executed. On a permission error while tracing a pid, {#dump_permission_error} exits.
|
|
17
|
+
# Warns when nothing was installed, so a caller that produces no output still tells the user why.
|
|
18
|
+
# @param [String?] command
|
|
19
|
+
# The command to run, when +pid+ is +nil+.
|
|
20
|
+
# @param [Integer?] pid
|
|
21
|
+
# The process to trace, or +nil+ to run +command+.
|
|
22
|
+
# @param [Integer] limit
|
|
23
|
+
# Stop after this many installed filters.
|
|
24
|
+
# @param [Float?] timeout
|
|
25
|
+
# Seconds to wait for +command+, ignored when tracing a pid.
|
|
26
|
+
# @yieldparam [String] bpf
|
|
27
|
+
# One installed filter, as raw bytes.
|
|
28
|
+
# @yieldparam [Symbol?] arch
|
|
29
|
+
# The architecture of the traced process, if known.
|
|
30
|
+
# @return [Array]
|
|
31
|
+
# One entry per filter: the block's return values. Empty when nothing was installed.
|
|
32
|
+
def dump_seccomp(command:, pid:, limit:, timeout:, &)
|
|
33
|
+
filters = if pid
|
|
34
|
+
dump_seccomp_by_pid(pid, limit, &)
|
|
35
|
+
else
|
|
36
|
+
SeccompTools::Dumper.dump('/bin/sh', '-c', command, limit:, timeout:, &)
|
|
37
|
+
end
|
|
38
|
+
Logger.warn('No seccomp filter was installed.') if filters.empty?
|
|
39
|
+
filters
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Whether tracer-based dumping is available on this platform (Linux only). Logs an error when
|
|
43
|
+
# it is not, so callers can guard with +return unless dumping_supported?+.
|
|
44
|
+
# @return [Boolean]
|
|
45
|
+
def dumping_supported?
|
|
46
|
+
return true if SeccompTools::Dumper::SUPPORTED
|
|
47
|
+
|
|
48
|
+
Logger.error('Dumping a filter from an executable or process is only available on Linux.')
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Traces +pid+, translating a permission error into the standard hint.
|
|
55
|
+
def dump_seccomp_by_pid(pid, limit, &)
|
|
56
|
+
SeccompTools::Dumper.dump_by_pid(pid, limit, &)
|
|
57
|
+
rescue Errno::EPERM, Errno::EACCES => e
|
|
58
|
+
dump_permission_error(e)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Reports a permission error from tracing a process and exits.
|
|
62
|
+
#
|
|
63
|
+
# Dumping a filter by pid needs +CAP_SYS_ADMIN+ for +PTRACE_SECCOMP_GET_FILTER+.
|
|
64
|
+
# @param [SystemCallError] err
|
|
65
|
+
# The +Errno::EPERM+ or +Errno::EACCES+ that was raised.
|
|
66
|
+
# @raise [SystemExit]
|
|
67
|
+
# Always; the process is terminated with exit status 1.
|
|
68
|
+
def dump_permission_error(err)
|
|
69
|
+
Logger.error(<<~EOS)
|
|
70
|
+
#{err}
|
|
71
|
+
PTRACE_SECCOMP_GET_FILTER requires CAP_SYS_ADMIN
|
|
72
|
+
Try:
|
|
73
|
+
sudo env "PATH=$PATH" #{(%w[seccomp-tools] + ARGV).shelljoin}
|
|
74
|
+
EOS
|
|
75
|
+
exit(1)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -15,8 +15,11 @@ module SeccompTools
|
|
|
15
15
|
# Summary of this command.
|
|
16
16
|
SUMMARY = 'Emulate seccomp rules.'
|
|
17
17
|
# Usage of this command.
|
|
18
|
-
USAGE = "emu - #{SUMMARY}\n\nUsage: seccomp-tools emu [options] BPF_FILE [sys_nr [arg0 [arg1 ... arg5]]]"
|
|
18
|
+
USAGE = "emu - #{SUMMARY}\n\nUsage: seccomp-tools emu [options] BPF_FILE [sys_nr [arg0 [arg1 ... arg5]]]".freeze
|
|
19
19
|
|
|
20
|
+
# Instantiate an {Emu} object, defaulting to verbose output.
|
|
21
|
+
#
|
|
22
|
+
# Takes the same arguments as {Base#initialize}.
|
|
20
23
|
def initialize(*)
|
|
21
24
|
super
|
|
22
25
|
option[:verbose] = 1
|
|
@@ -24,6 +27,7 @@ module SeccompTools
|
|
|
24
27
|
|
|
25
28
|
# Define option parser.
|
|
26
29
|
# @return [OptionParser]
|
|
30
|
+
# The parser of this command's options.
|
|
27
31
|
def parser
|
|
28
32
|
@parser ||= OptionParser.new do |opt|
|
|
29
33
|
opt.banner = usage
|
|
@@ -33,10 +37,14 @@ module SeccompTools
|
|
|
33
37
|
opt.on('-q', '--[no-]quiet', 'Run quietly, only show emulation result.') do |v|
|
|
34
38
|
option[:verbose] = 0 if v
|
|
35
39
|
end
|
|
40
|
+
|
|
41
|
+
opt.on('-i', '--ip=VAL', Integer, 'Set instruction pointer.') do |val|
|
|
42
|
+
option[:instruction_pointer] = val
|
|
43
|
+
end
|
|
36
44
|
end
|
|
37
45
|
end
|
|
38
46
|
|
|
39
|
-
#
|
|
47
|
+
# Emulates the filter against the given syscall and shows the resulting action.
|
|
40
48
|
# @return [void]
|
|
41
49
|
def handle
|
|
42
50
|
return unless super
|
|
@@ -50,9 +58,13 @@ module SeccompTools
|
|
|
50
58
|
sys = evaluate_sys_nr(sys) if sys
|
|
51
59
|
args.map! { |v| Integer(v) }
|
|
52
60
|
trace = Set.new
|
|
53
|
-
res = SeccompTools::Emulator.new(
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
res = SeccompTools::Emulator.new(
|
|
62
|
+
insts,
|
|
63
|
+
sys_nr: sys,
|
|
64
|
+
args:,
|
|
65
|
+
instruction_pointer: option[:instruction_pointer] && Integer(option[:instruction_pointer]),
|
|
66
|
+
arch: option[:arch]
|
|
67
|
+
).run
|
|
56
68
|
|
|
57
69
|
if option[:verbose] >= 1
|
|
58
70
|
disasm = SeccompTools::Disasm.disasm(raw, arch: option[:arch]).lines
|
|
@@ -67,17 +79,28 @@ module SeccompTools
|
|
|
67
79
|
|
|
68
80
|
private
|
|
69
81
|
|
|
82
|
+
# Resolves a syscall given on the command line, by name or by number.
|
|
83
|
+
#
|
|
70
84
|
# @param [String] str
|
|
85
|
+
# A syscall name valid for +option[:arch]+, or an integer literal.
|
|
71
86
|
# @return [Integer]
|
|
87
|
+
# The syscall number.
|
|
88
|
+
# @raise [ArgumentError]
|
|
89
|
+
# If +str+ is neither a known syscall name nor a valid integer.
|
|
72
90
|
def evaluate_sys_nr(str)
|
|
73
91
|
consts = SeccompTools::Const::Syscall.const_get(option[:arch].to_s.upcase)
|
|
74
92
|
consts[str.to_sym] || Integer(str)
|
|
75
93
|
end
|
|
76
94
|
|
|
77
|
-
#
|
|
95
|
+
# Outputs the disassembly, highlighting the lines that were executed during emulation.
|
|
96
|
+
#
|
|
78
97
|
# @param [Array<String>] disasm
|
|
79
|
-
#
|
|
80
|
-
# @param [
|
|
98
|
+
# Lines of the disassembly, including the two header lines.
|
|
99
|
+
# @param [Set<Integer>] trace
|
|
100
|
+
# Line numbers that were reached.
|
|
101
|
+
# @param [{Symbol => Integer}] result
|
|
102
|
+
# The emulation result, as returned by {SeccompTools::Emulator#run}.
|
|
103
|
+
# @return [void]
|
|
81
104
|
def output_emulate_path(disasm, trace, result)
|
|
82
105
|
output { disasm.shift }
|
|
83
106
|
output { disasm.shift }
|
|
@@ -87,7 +110,7 @@ module SeccompTools
|
|
|
87
110
|
|
|
88
111
|
Util.colorize(line, t: :gray)
|
|
89
112
|
end
|
|
90
|
-
# Too
|
|
113
|
+
# Too many lines remain; omit them.
|
|
91
114
|
rem = disasm.size - idx - 1
|
|
92
115
|
break output { Util.colorize("... (omitting #{rem} lines)\n", t: :gray) } if rem > 3 && idx > result[:pc] + 4
|
|
93
116
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'seccomp-tools/cli/base'
|
|
4
|
+
require 'seccomp-tools/cli/filter_input'
|
|
5
|
+
require 'seccomp-tools/disasm/disasm'
|
|
6
|
+
require 'seccomp-tools/explain'
|
|
7
|
+
require 'seccomp-tools/logger'
|
|
8
|
+
|
|
9
|
+
module SeccompTools
|
|
10
|
+
module CLI
|
|
11
|
+
# Handle 'explain' command.
|
|
12
|
+
class Explain < Base
|
|
13
|
+
include FilterInput
|
|
14
|
+
|
|
15
|
+
# Summary of this command.
|
|
16
|
+
SUMMARY = 'Summarize a seccomp filter as a per-action policy.'
|
|
17
|
+
# Usage of this command.
|
|
18
|
+
USAGE = "explain - #{SUMMARY}\n\nUsage: seccomp-tools explain [options] [BPF_FILE|EXEC]".freeze
|
|
19
|
+
|
|
20
|
+
# Define option parser.
|
|
21
|
+
# @return [OptionParser]
|
|
22
|
+
# The parser of this command's options.
|
|
23
|
+
def parser
|
|
24
|
+
@parser ||= OptionParser.new do |opt|
|
|
25
|
+
opt.banner = usage
|
|
26
|
+
|
|
27
|
+
option_filter_source(opt, 'explain')
|
|
28
|
+
option_arch(opt, 'With an executable or --pid the architecture is auto-detected instead.')
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Reads the filter(s) from a BPF file, an executable, or an existing process, then prints the
|
|
33
|
+
# policy of each.
|
|
34
|
+
# @return [void]
|
|
35
|
+
def handle
|
|
36
|
+
return unless super
|
|
37
|
+
|
|
38
|
+
filters = collect_filters
|
|
39
|
+
if filters.size > 1
|
|
40
|
+
Logger.warn("#{filters.size} filters are installed; they stack, so a syscall must pass every one " \
|
|
41
|
+
'(most restrictive wins). Each is explained separately below.')
|
|
42
|
+
end
|
|
43
|
+
filters.each_with_index do |(raw, arch, source), idx|
|
|
44
|
+
label = filters.size > 1 ? "#{source} (filter ##{idx})" : source
|
|
45
|
+
insts = SeccompTools::Disasm.to_bpf(raw, arch).map(&:inst)
|
|
46
|
+
output { SeccompTools::Explain.new(insts, arch:, source: label).summarize.to_s }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'seccomp-tools/cli/dumpable'
|
|
4
|
+
require 'seccomp-tools/logger'
|
|
5
|
+
require 'seccomp-tools/util'
|
|
6
|
+
|
|
7
|
+
module SeccompTools
|
|
8
|
+
module CLI
|
|
9
|
+
# Shared input handling for the commands that take a seccomp filter from a BPF file, stdin, an
|
|
10
|
+
# executable, or a running process ({Explain} and {Audit}): it resolves the positional argument
|
|
11
|
+
# and options into +[raw_bpf, arch, source]+ tuples, dumping via ptrace ({Dumpable}) when the
|
|
12
|
+
# input is a command or +--pid+. The including command must provide +option+, +argv+, +parser+,
|
|
13
|
+
# +input+ and +warn_ignored_arguments+ (all from {Base}).
|
|
14
|
+
module FilterInput
|
|
15
|
+
include Dumpable
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
# Registers the options every command taking its filter from a process shares - +-c/--sh-exec+,
|
|
20
|
+
# +-l/--limit+, +-p/--pid+ and +-t/--timeout+ - and their defaults, so the four stay described
|
|
21
|
+
# and parsed the same way wherever they appear. The counterpart of {Base#option_arch}.
|
|
22
|
+
#
|
|
23
|
+
# The descriptions hold for every including command, because the behaviour they describe lives
|
|
24
|
+
# in the shared code: {#collect_filters} gives +-c+ precedence, and {Dumpable#dump_seccomp}
|
|
25
|
+
# passes +--timeout+ only when running an executable, never when attaching to a +--pid+.
|
|
26
|
+
# @param [OptionParser] opt
|
|
27
|
+
# @param [String] action
|
|
28
|
+
# What the command does with each filter, woven into the descriptions.
|
|
29
|
+
# @return [void]
|
|
30
|
+
# @example
|
|
31
|
+
# option_filter_source(opt, 'explain')
|
|
32
|
+
def option_filter_source(opt, action)
|
|
33
|
+
option[:limit] = 1
|
|
34
|
+
opt.on('-c', '--sh-exec <command>', "Executes the given command (via sh) and #{action}s its seccomp.",
|
|
35
|
+
'Use this to pass arguments or pipe things to the executable.',
|
|
36
|
+
'e.g. use `-c "./bin > /dev/null"` to keep the program output out of the result.',
|
|
37
|
+
'Takes precedence over the positional argument.') { |command| option[:command] = command }
|
|
38
|
+
|
|
39
|
+
opt.on('-l', '--limit LIMIT', Integer, "#{action.capitalize} only the first LIMIT installed filters.",
|
|
40
|
+
'Only meaningful when the input is an executable or --pid. Default: 1',
|
|
41
|
+
'An executable is killed once it reaches LIMIT.') { |l| option[:limit] = l }
|
|
42
|
+
|
|
43
|
+
opt.on('-p', '--pid PID', Integer, "#{action.capitalize} the seccomp filters installed on an existing process.",
|
|
44
|
+
'You must have CAP_SYS_ADMIN (e.g. be root) to use this option.') { |p| option[:pid] = p }
|
|
45
|
+
|
|
46
|
+
opt.on('-t', '--timeout SEC', Float, 'Timeout (seconds) for the execution. Default: no timeout',
|
|
47
|
+
'This option is ignored when --pid is given.') { |t| option[:timeout] = t }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Resolves the input into an array of +[raw_bpf, arch, source]+ tuples, empty when there is
|
|
51
|
+
# nothing to process (help shown, or an error was logged).
|
|
52
|
+
#
|
|
53
|
+
# The input is one of three kinds:
|
|
54
|
+
# * a running process, when +--pid+ is given;
|
|
55
|
+
# * a raw BPF file (or stdin), when the positional argument is not an executable;
|
|
56
|
+
# * a command to run and trace - either +-c+, or a positional executable.
|
|
57
|
+
# @return [Array<Array(String, Symbol, String?)>]
|
|
58
|
+
def collect_filters
|
|
59
|
+
# -c/--sh-exec and --pid take precedence over a positional BPF file or executable.
|
|
60
|
+
option[:ifile] = argv.shift unless option[:command] || option[:pid]
|
|
61
|
+
warn_ignored_arguments
|
|
62
|
+
|
|
63
|
+
return dump_filters(command: nil, pid: option[:pid], source: "pid #{option[:pid]}") if option[:pid]
|
|
64
|
+
|
|
65
|
+
command = option[:command] || option[:ifile]
|
|
66
|
+
if command.nil? # nothing to process
|
|
67
|
+
CLI.show(parser.help)
|
|
68
|
+
return []
|
|
69
|
+
end
|
|
70
|
+
return read_raw_bpf if raw_bpf_file?
|
|
71
|
+
|
|
72
|
+
dump_filters(command:, pid: nil, source: command)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Reads the positional file (or stdin) as a raw BPF blob, logging an error instead of
|
|
76
|
+
# crashing when it cannot be read.
|
|
77
|
+
# @return [Array<Array(String, Symbol, String?)>]
|
|
78
|
+
def read_raw_bpf
|
|
79
|
+
[[input, option[:arch], source_name(option[:ifile])]]
|
|
80
|
+
rescue SystemCallError => e
|
|
81
|
+
Logger.error(e.message)
|
|
82
|
+
[]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Should the input be read directly as a raw BPF blob, rather than run as a command? True when
|
|
86
|
+
# the command accepts blobs at all, no +-c+ was given, and the positional argument is not an
|
|
87
|
+
# executable (a plain file or stdin).
|
|
88
|
+
# @return [Boolean]
|
|
89
|
+
def raw_bpf_file?
|
|
90
|
+
accepts_raw_bpf? && !option[:command] && !executable?(option[:ifile])
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Does a positional argument that is not an ELF mean "read this filter"? True for the commands
|
|
94
|
+
# that analyze a filter ({Explain}, {Audit}); {Dump} overrides it to +false+, because its
|
|
95
|
+
# positional is always something to execute - a shell script is not an ELF but must still be
|
|
96
|
+
# run, not parsed as BPF.
|
|
97
|
+
# @return [Boolean]
|
|
98
|
+
def accepts_raw_bpf?
|
|
99
|
+
true
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Dumps filters from a command or pid and labels each with +source+.
|
|
103
|
+
# @return [Array<Array(String, Symbol, String?)>]
|
|
104
|
+
# The filter tuples, empty when dumping is unsupported or nothing was installed.
|
|
105
|
+
def dump_filters(command:, pid:, source:)
|
|
106
|
+
return [] unless dumping_supported?
|
|
107
|
+
|
|
108
|
+
dump_seccomp(command:, pid:, limit: option[:limit], timeout: option[:timeout]) do |bpf, arch|
|
|
109
|
+
[bpf, arch || option[:arch], source]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Is +file+ an ELF executable to run, rather than a raw BPF blob or stdin to read?
|
|
114
|
+
# @param [String?] file
|
|
115
|
+
# The path to check. +nil+ (no argument) and +-+ (stdin) are not executables.
|
|
116
|
+
# @return [Boolean]
|
|
117
|
+
def executable?(file)
|
|
118
|
+
file && file != '-' && Util.elf?(file)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The label shown in the header for +file+; +<STDIN>+ when reading from stdin.
|
|
122
|
+
# @param [String] file
|
|
123
|
+
# The input path, or +-+ for stdin.
|
|
124
|
+
# @return [String]
|
|
125
|
+
def source_name(file)
|
|
126
|
+
file == '-' ? '<STDIN>' : file
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|