seccomp-tools 1.1.0 → 1.5.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.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +112 -30
  3. data/bin/seccomp-tools +1 -0
  4. data/ext/ptrace/extconf.rb +2 -0
  5. data/ext/ptrace/ptrace.c +107 -5
  6. data/lib/seccomp-tools.rb +5 -0
  7. data/lib/seccomp-tools/asm/asm.rb +5 -2
  8. data/lib/seccomp-tools/asm/compiler.rb +96 -18
  9. data/lib/seccomp-tools/asm/tokenizer.rb +25 -8
  10. data/lib/seccomp-tools/bpf.rb +7 -4
  11. data/lib/seccomp-tools/cli/asm.rb +16 -6
  12. data/lib/seccomp-tools/cli/base.rb +10 -4
  13. data/lib/seccomp-tools/cli/cli.rb +9 -6
  14. data/lib/seccomp-tools/cli/disasm.rb +6 -2
  15. data/lib/seccomp-tools/cli/dump.rb +37 -6
  16. data/lib/seccomp-tools/cli/emu.rb +41 -22
  17. data/lib/seccomp-tools/const.rb +47 -16
  18. data/lib/seccomp-tools/consts/sys_arg.rb +432 -0
  19. data/lib/seccomp-tools/consts/sys_nr/aarch64.rb +284 -0
  20. data/lib/seccomp-tools/consts/{amd64.rb → sys_nr/amd64.rb} +6 -1
  21. data/lib/seccomp-tools/consts/{i386.rb → sys_nr/i386.rb} +18 -15
  22. data/lib/seccomp-tools/disasm/context.rb +125 -34
  23. data/lib/seccomp-tools/disasm/disasm.rb +5 -2
  24. data/lib/seccomp-tools/dumper.rb +75 -8
  25. data/lib/seccomp-tools/emulator.rb +19 -8
  26. data/lib/seccomp-tools/instruction/alu.rb +7 -2
  27. data/lib/seccomp-tools/instruction/base.rb +5 -3
  28. data/lib/seccomp-tools/instruction/instruction.rb +2 -0
  29. data/lib/seccomp-tools/instruction/jmp.rb +28 -14
  30. data/lib/seccomp-tools/instruction/ld.rb +28 -12
  31. data/lib/seccomp-tools/instruction/ldx.rb +2 -0
  32. data/lib/seccomp-tools/instruction/misc.rb +2 -0
  33. data/lib/seccomp-tools/instruction/ret.rb +14 -2
  34. data/lib/seccomp-tools/instruction/st.rb +4 -2
  35. data/lib/seccomp-tools/instruction/stx.rb +2 -0
  36. data/lib/seccomp-tools/logger.rb +40 -0
  37. data/lib/seccomp-tools/syscall.rb +24 -13
  38. data/lib/seccomp-tools/templates/asm.amd64.asm +26 -0
  39. data/lib/seccomp-tools/templates/asm.c +17 -0
  40. data/lib/seccomp-tools/templates/asm.i386.asm +33 -0
  41. data/lib/seccomp-tools/util.rb +24 -3
  42. data/lib/seccomp-tools/version.rb +3 -1
  43. metadata +51 -44
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'set'
2
4
 
3
5
  require 'seccomp-tools/bpf'
@@ -28,10 +30,11 @@ module SeccompTools
28
30
  code.contexts = ctxs
29
31
  code.disasm
30
32
  end.join("\n")
31
- <<EOS + dis + "\n"
33
+ <<-EOS
32
34
  line CODE JT JF K
33
35
  =================================
34
- EOS
36
+ #{dis}
37
+ EOS
35
38
  end
36
39
 
37
40
  # Convert raw bpf string to array of {BPF}.
@@ -1,10 +1,19 @@
1
- require 'seccomp-tools/ptrace'
1
+ # frozen_string_literal: true
2
+
3
+ require 'os'
4
+
5
+ require 'seccomp-tools/logger'
6
+ require 'seccomp-tools/ptrace' if OS.linux?
2
7
  require 'seccomp-tools/syscall'
3
8
 
4
9
  module SeccompTools
5
10
  # Dump seccomp-bpf using ptrace of binary.
6
- # Currently only support x86_64.
11
+ # Currently only support x86_64 and aarch64.
7
12
  module Dumper
13
+ # Whether the dumper is supported.
14
+ # Dumper works based on ptrace, so we need the platform be Linux.
15
+ SUPPORTED = OS.linux?
16
+
8
17
  module_function
9
18
 
10
19
  # Main bpf dump function.
@@ -32,6 +41,8 @@ module SeccompTools
32
41
  # @todo
33
42
  # +timeout+ option.
34
43
  def dump(*args, limit: 1, &block)
44
+ return [] unless SUPPORTED
45
+
35
46
  pid = fork { handle_child(*args) }
36
47
  Handler.new(pid).handle(limit, &block)
37
48
  end
@@ -54,6 +65,8 @@ module SeccompTools
54
65
  # Child will be killed when number of calling +prctl(SET_SECCOMP)+ reaches +limit+.
55
66
  # @yieldparam [String] bpf
56
67
  # Seccomp bpf in raw bytes.
68
+ # @yieldparam [Symbol] arch
69
+ # Architecture, either :i386 or :amd64.
57
70
  # @return [Array<Object>, Array<String>]
58
71
  # Return the block returned. If block is not given, array of raw bytes will be returned.
59
72
  def handle(limit, &block)
@@ -74,7 +87,8 @@ module SeccompTools
74
87
  end
75
88
  !limit.zero?
76
89
  end
77
- syscalls.keys.each { |cpid| Process.kill('KILL', cpid) if alive?(cpid) }
90
+ syscalls.each_key { |cpid| Process.kill('KILL', cpid) if alive?(cpid) }
91
+ Process.waitall
78
92
  collect
79
93
  end
80
94
 
@@ -95,9 +109,9 @@ module SeccompTools
95
109
  cont = yield(child)
96
110
  end
97
111
  Ptrace.syscall(child, 0, 0) unless status.exited?
98
- return cont
112
+ cont
99
113
  rescue Errno::ECHILD
100
- return false
114
+ false
101
115
  end
102
116
 
103
117
  # @return [SeccompTools::Syscall]
@@ -119,11 +133,64 @@ module SeccompTools
119
133
  def handle_child(*args)
120
134
  Ptrace.traceme_and_stop
121
135
  exec(*args)
122
- rescue # exec fail
123
- # TODO: use logger
124
- $stderr.puts("Failed to execute #{args.join(' ')}")
136
+ rescue # rubocop:disable Style/RescueStandardError # exec fail
137
+ Logger.error("Failed to execute #{args.join(' ')}")
125
138
  exit(1)
126
139
  end
127
140
  end
141
+
142
+ # Dump installed seccomp-bpf of an existing process using PTRACE_SECCOMP_GET_FILTER.
143
+ #
144
+ # Dump the installed seccomp-bpf from a running process. This is achieved by the ptrace command
145
+ # PTRACE_SECCOMP_GET_FILTER, which needs CAP_SYS_ADMIN capability.
146
+ #
147
+ # @param [Integer] pid
148
+ # Target process identifier.
149
+ # @param [Integer] limit
150
+ # Number of filters to dump. Negative number for unlimited.
151
+ # @yieldparam [String] bpf
152
+ # Seccomp bpf in raw bytes.
153
+ # @yieldparam [Symbol] arch
154
+ # Architecture of the target process (always nil right now).
155
+ # @return [Array<Object>, Array<String>]
156
+ # Return the block returned. If block is not given, array of raw bytes will be returned.
157
+ # @raise [Errno::ESRCH]
158
+ # Raises when the target process does not exist.
159
+ # @raise [Errno::EPERM]
160
+ # Raises the error if not allowed to attach.
161
+ # @raise [Errno::EACCES]
162
+ # Raises the error if not allowed to dump (e.g. no CAP_SYS_ADMIN).
163
+ # @example
164
+ # pid1 = Process.spawn('sleep inf')
165
+ # dump_by_pid(pid1, 1)
166
+ # # empty because there is no seccomp installed
167
+ # #=> []
168
+ # @example
169
+ # pid2 = Process.spawn('spec/binary/twctf-2016-diary')
170
+ # # give it some time to install the filter
171
+ # sleep(1)
172
+ # dump_by_pid(pid2, 1) { |c| c[0, 10] }
173
+ # #=> [" \x00\x00\x00\x00\x00\x00\x00\x15\x00"]
174
+ def dump_by_pid(pid, limit, &block)
175
+ return [] unless SUPPORTED
176
+
177
+ collect = []
178
+ Ptrace.attach_and_wait(pid)
179
+ begin
180
+ i = 0
181
+ while limit.negative? || i < limit
182
+ begin
183
+ bpf = Ptrace.seccomp_get_filter(pid, i)
184
+ rescue Errno::ENOENT, Errno::EINVAL
185
+ break
186
+ end
187
+ collect << (block.nil? ? bpf : yield(bpf, nil))
188
+ i += 1
189
+ end
190
+ ensure
191
+ Ptrace.detach(pid)
192
+ end
193
+ collect
194
+ end
128
195
  end
129
196
  end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'seccomp-tools/const'
2
4
 
3
5
  module SeccompTools
4
- # For emulation seccomp.
6
+ # For emulating seccomp.
5
7
  class Emulator
6
8
  # Instantiate a {Emulator} object.
7
9
  #
@@ -31,6 +33,7 @@ module SeccompTools
31
33
  @values = { pc: 0, a: 0, x: 0 }
32
34
  loop do
33
35
  break if @values[:ret] # break when returned
36
+
34
37
  yield(@values) if block_given?
35
38
  inst = @instructions[pc]
36
39
  op, *args = inst.symbolize
@@ -39,7 +42,7 @@ module SeccompTools
39
42
  when :ld then ld(args[0], args[1]) # ld/ldx
40
43
  when :st then st(args[0], args[1]) # st/stx
41
44
  when :jmp then jmp(args[0]) # directly jmp
42
- when :cmp then cmp(*args[0, 4]) # jmp with comparsion
45
+ when :cmp then cmp(*args[0, 4]) # jmp with comparison
43
46
  when :alu then alu(args[0], args[1]) # alu
44
47
  when :misc then misc(args[0]) # misc: txa/tax
45
48
  end
@@ -55,10 +58,11 @@ module SeccompTools
55
58
  end
56
59
 
57
60
  def audit(arch)
58
- type = case arch
59
- when :amd64 then 'ARCH_X86_64'
60
- when :i386 then 'ARCH_I386'
61
- end
61
+ type = {
62
+ amd64: 'ARCH_X86_64',
63
+ i386: 'ARCH_I386',
64
+ aarch64: 'ARCH_AARCH64'
65
+ }[arch]
62
66
  Const::Audit::ARCH[type]
63
67
  end
64
68
 
@@ -79,6 +83,7 @@ module SeccompTools
79
83
 
80
84
  def st(reg, index)
81
85
  raise IndexError, "Expect 0 <= index < 16, got: #{index}" unless index.between?(0, 15)
86
+
82
87
  set(:mem, index, get(reg))
83
88
  end
84
89
 
@@ -86,10 +91,11 @@ module SeccompTools
86
91
  set(:pc, get(:pc) + k + 1)
87
92
  end
88
93
 
94
+ # Emulates cmp instruction.
89
95
  def cmp(op, src, jt, jf)
90
96
  src = get(:x) if src == :x
91
97
  a = get(:a)
92
- val = a.send(op, src)
98
+ val = a.__send__(op, src)
93
99
  val = (val != 0) if val.is_a?(Integer) # handle & operator
94
100
  j = val ? jt : jf
95
101
  set(:pc, get(:pc) + j + 1)
@@ -100,7 +106,7 @@ module SeccompTools
100
106
  set(:a, 2**32 - get(:a))
101
107
  else
102
108
  src = get(:x) if src == :x
103
- set(:a, get(:a).send(op, src))
109
+ set(:a, get(:a).__send__(op, src))
104
110
  end
105
111
  end
106
112
 
@@ -115,10 +121,12 @@ module SeccompTools
115
121
  if arg.size == 1
116
122
  arg = arg.first
117
123
  raise ArgumentError, "Invalid #{arg}" unless %i[a x pc ret].include?(arg)
124
+
118
125
  @values[arg] = val & 0xffffffff
119
126
  else
120
127
  raise ArgumentError, arg.to_s unless arg.first == :mem
121
128
  raise IndexError, "Invalid index: #{arg[1]}" unless arg[1].between?(0, 15)
129
+
122
130
  @values[arg[1]] = val & 0xffffffff
123
131
  end
124
132
  end
@@ -127,15 +135,18 @@ module SeccompTools
127
135
  if arg.size == 1
128
136
  arg = arg.first
129
137
  raise ArgumentError, "Invalid #{arg}" unless %i[a x pc ret].include?(arg)
138
+
130
139
  undefined(arg.upcase) if @values[arg].nil?
131
140
  return @values[arg]
132
141
  end
133
142
  return @values[arg[1]] if arg.first == :mem
143
+
134
144
  data_of(arg[1])
135
145
  end
136
146
 
137
147
  def data_of(index)
138
148
  raise IndexError, "Invalid index: #{index}" unless (index & 3).zero? && index.between?(0, 63)
149
+
139
150
  index /= 4
140
151
  case index
141
152
  when 0 then @sys_nr || undefined('sys_number')
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'seccomp-tools/instruction/base'
2
4
 
3
5
  module SeccompTools
@@ -21,6 +23,7 @@ module SeccompTools
21
23
  # Decompile instruction.
22
24
  def decompile
23
25
  return 'A = -A' if op == :neg
26
+
24
27
  "A #{op_sym}= #{src_str}"
25
28
  end
26
29
 
@@ -28,6 +31,7 @@ module SeccompTools
28
31
  # @return [[:alu, Symbol, (:x, Integer, nil)]]
29
32
  def symbolize
30
33
  return [:alu, :neg, nil] if op == :neg
34
+
31
35
  [:alu, op_sym, src]
32
36
  end
33
37
 
@@ -37,7 +41,7 @@ module SeccompTools
37
41
  # @return [Array<(Integer, Context)>]
38
42
  def branch(context)
39
43
  ctx = context.dup
40
- ctx[:a] = nil
44
+ ctx[:a] = Disasm::Context::Value.new
41
45
  [[line + 1, ctx]]
42
46
  end
43
47
 
@@ -55,9 +59,10 @@ module SeccompTools
55
59
 
56
60
  def src_str
57
61
  return 'X' if src == :x
62
+
58
63
  case op
59
64
  when :lsh, :rsh then src.to_s
60
- else '0x' + src.to_s(16)
65
+ else "0x#{src.to_s(16)}"
61
66
  end
62
67
  end
63
68
 
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'seccomp-tools/const'
2
4
 
3
5
  module SeccompTools
4
6
  # For instructions' class.
5
7
  module Instruction
6
- # Base class for different instruction.
8
+ # Base class of instructions.
7
9
  class Base
8
10
  include SeccompTools::Const::BPF
9
11
 
@@ -22,7 +24,7 @@ module SeccompTools
22
24
  raise ArgumentError, "Line #{line} is invalid: #{msg}"
23
25
  end
24
26
 
25
- # Return the possible branches after executing this instruction.
27
+ # Returns the possible branches after executing this instruction.
26
28
  # @param [Context] _context
27
29
  # Current context.
28
30
  # @return [Array<(Integer, Context)>]
@@ -47,7 +49,7 @@ module SeccompTools
47
49
 
48
50
  %i(code jt jf k arch line contexts).each do |sym|
49
51
  define_method(sym) do
50
- @bpf.send(sym)
52
+ @bpf.__send__(sym)
51
53
  end
52
54
  end
53
55
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'seccomp-tools/instruction/alu'
2
4
  require 'seccomp-tools/instruction/jmp'
3
5
  require 'seccomp-tools/instruction/ld'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'seccomp-tools/const'
2
4
  require 'seccomp-tools/instruction/base'
3
5
 
@@ -9,20 +11,21 @@ module SeccompTools
9
11
  def decompile
10
12
  return goto(k) if jop == :none
11
13
  # if jt == 0 && jf == 0 => no-op # should not happen
12
- # jt == 0 => if(!) goto jf
14
+ # jt == 0 => if(!) goto jf;
13
15
  # jf == 0 => if() goto jt;
14
16
  # otherwise => if () goto jt; else goto jf;
15
17
  return '/* no-op */' if jt.zero? && jf.zero?
16
18
  return goto(jt) if jt == jf
17
- return if_str + goto(jt) + ' else ' + goto(jf) unless jt.zero? || jf.zero?
18
- return if_str + goto(jt) if jf.zero?
19
- if_str(true) + goto(jf)
19
+ return if_str(neg: true) + goto(jf) if jt.zero?
20
+
21
+ if_str + goto(jt) + (jf.zero? ? '' : " else #{goto(jf)}")
20
22
  end
21
23
 
22
24
  # See {Instruction::Base#symbolize}.
23
25
  # @return [[:cmp, Symbol, (:x, Integer), Integer, Integer], [:jmp, Integer]]
24
26
  def symbolize
25
27
  return [:jmp, k] if jop == :none
28
+
26
29
  [:cmp, jop, src, jt, jf]
27
30
  end
28
31
 
@@ -33,6 +36,8 @@ module SeccompTools
33
36
  def branch(context)
34
37
  return [[at(k), context]] if jop == :none
35
38
  return [[at(jt), context]] if jt == jf
39
+ return [[at(jt), context.dup.eql!(src)], [at(jf), context]] if jop == :==
40
+
36
41
  [[at(jt), context], [at(jf), context]]
37
42
  end
38
43
 
@@ -51,19 +56,27 @@ module SeccompTools
51
56
 
52
57
  def src_str
53
58
  return 'X' if src == :x
59
+
54
60
  # if A in all contexts are same
55
61
  a = contexts.map(&:a).uniq
56
62
  return k.to_s if a.size != 1
63
+
57
64
  a = a[0]
58
- return k.to_s if a.nil?
59
- hex = '0x' + k.to_s(16)
60
- case a
61
- when 0 then Util.colorize(Const::Syscall.const_get(arch.upcase.to_sym).invert[k] || hex, t: :syscall)
65
+ return k.to_s unless a.data?
66
+
67
+ hex = "0x#{k.to_s(16)}"
68
+ case a.val
69
+ # interpret as syscalls only if it's an equality test
70
+ when 0 then Util.colorize(jop == :== ? sysname_by_k || hex : hex, t: :syscall)
62
71
  when 4 then Util.colorize(Const::Audit::ARCH.invert[k] || hex, t: :arch)
63
72
  else hex
64
73
  end
65
74
  end
66
75
 
76
+ def sysname_by_k
77
+ Const::Syscall.const_get(arch.upcase.to_sym).invert[k]
78
+ end
79
+
67
80
  def src
68
81
  SRC.invert[code & 8] == :x ? :x : k
69
82
  end
@@ -76,14 +89,15 @@ module SeccompTools
76
89
  line + off + 1
77
90
  end
78
91
 
79
- def if_str(neg = false)
92
+ def if_str(neg: false)
80
93
  return "if (A #{jop} #{src_str}) " unless neg
81
94
  return "if (!(A & #{src_str})) " if jop == :&
82
- op = case jop
83
- when :>= then :<
84
- when :> then :<=
85
- when :== then :!=
86
- end
95
+
96
+ op = {
97
+ :>= => :<,
98
+ :> => :<=,
99
+ :== => :!=
100
+ }[jop]
87
101
  "if (A #{op} #{src_str}) "
88
102
  end
89
103
  end
@@ -1,4 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'seccomp-tools/const'
1
4
  require 'seccomp-tools/instruction/base'
5
+ require 'seccomp-tools/util'
2
6
 
3
7
  module SeccompTools
4
8
  module Instruction
@@ -6,10 +10,11 @@ module SeccompTools
6
10
  class LD < Base
7
11
  # Decompile instruction.
8
12
  def decompile
9
- ret = reg + ' = '
13
+ ret = "#{reg} = "
10
14
  _, _reg, type = symbolize
11
15
  return ret + type[:val].to_s if type[:rel] == :immi
12
16
  return ret + "mem[#{type[:val]}]" if type[:rel] == :mem
17
+
13
18
  ret + seccomp_data_str
14
19
  end
15
20
 
@@ -30,22 +35,17 @@ module SeccompTools
30
35
  # Current context.
31
36
  # @return [Array<(Integer, Context)>]
32
37
  def branch(context)
33
- nctx = context.dup
34
- type = load_val
35
- nctx[reg] = case type[:rel]
36
- when :immi then nil
37
- when :mem then context[type[:val]]
38
- when :data then type[:val]
39
- end
40
- [[line + 1, nctx]]
38
+ ctx = context.dup
39
+ ctx.load(reg, **load_val)
40
+ [[line + 1, ctx]]
41
41
  end
42
42
 
43
43
  private
44
44
 
45
45
  def mode
46
46
  @mode ||= MODE.invert[code & 0xe0]
47
- # Seccomp doesn't support this mode
48
- invalid if @mode.nil? || @mode == :ind
47
+ # Seccomp doesn't support these modes
48
+ invalid if @mode.nil? || @mode == :ind || @mode == :msh
49
49
  @mode
50
50
  end
51
51
 
@@ -53,6 +53,7 @@ module SeccompTools
53
53
  return { rel: :immi, val: k } if mode == :imm
54
54
  return { rel: :immi, val: SIZEOF_SECCOMP_DATA } if mode == :len
55
55
  return { rel: :mem, val: k } if mode == :mem
56
+
56
57
  { rel: :data, val: k }
57
58
  end
58
59
 
@@ -71,9 +72,24 @@ module SeccompTools
71
72
  else
72
73
  idx = Array.new(12) { |i| i * 4 + 16 }.index(k)
73
74
  return 'INVALID' if idx.nil?
74
- idx.even? ? "args[#{idx / 2}]" : "args[#{idx / 2}] >> 32"
75
+
76
+ args_name(idx)
75
77
  end
76
78
  end
79
+
80
+ def args_name(idx)
81
+ sys_nrs = contexts.map { |ctx| ctx.known_data[0] }.uniq
82
+ default = idx.even? ? "args[#{idx / 2}]" : "args[#{idx / 2}] >> 32"
83
+ return default if sys_nrs.size != 1 || sys_nrs.first.nil?
84
+
85
+ sys = Const::Syscall.const_get(arch.upcase.to_sym).invert[sys_nrs.first]
86
+ args = Const::SYS_ARG[sys]
87
+ return default if args.nil? || args[idx / 2].nil? # function prototype doesn't have that argument
88
+
89
+ comment = "# #{sys}(#{args.join(', ')})"
90
+ arg_name = Util.colorize(args[idx / 2], t: :args)
91
+ "#{idx.even? ? arg_name : "#{arg_name} >> 32"} #{Util.colorize(comment, t: :gray)}"
92
+ end
77
93
  end
78
94
  end
79
95
  end