seccomp-tools 1.1.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
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