seccomp-tools 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,128 @@
1
+ require 'seccomp-tools/ptrace'
2
+ require 'seccomp-tools/syscall'
3
+
4
+ module SeccompTools
5
+ # Dump seccomp-bpf using ptrace of binary.
6
+ # Currently only support x86_64.
7
+ module Dumper
8
+ module_function
9
+
10
+ # Main bpf dump function.
11
+ # Yield seccomp bpf whenever find a +prctl(SET_SECCOMP)+ call.
12
+ #
13
+ # @param [Array<String>] args
14
+ # The arguments for target execution file.
15
+ # @param [Integer] limit
16
+ # By default, +dump+ will only dump the first +SET_SECCOMP+ call.
17
+ # Set +limit+ to the number of calling +prctl(SET_SECCOMP)+ then the child process will be killed when number of
18
+ # calling +prctl+ reaches +limit+.
19
+ #
20
+ # Negative number for unlimited.
21
+ # @yieldparam [String] bpf
22
+ # Seccomp bpf in raw bytes.
23
+ # @yieldparam [Symbol] arch
24
+ # Architecture of the target process.
25
+ # @return [Array<Object>, Array<String>]
26
+ # Return the block returned. If block is not given, array of raw bytes will be returned.
27
+ # @example
28
+ # dump('ls', '-l', '-a')
29
+ # #=> []
30
+ # dump('spec/binary/twctf-2016-diary') { |c| c[0, 10] }
31
+ # #=> [" \x00\x00\x00\x00\x00\x00\x00\x15\x00"]
32
+ # @todo
33
+ # Detect execution file architecture to know which syscall number should be traced.
34
+ # @todo
35
+ # +timeout+ option.
36
+ def dump(*args, limit: 1, &block)
37
+ pid = fork { handle_child(*args) }
38
+ Handler.new(pid).handle(limit, &block)
39
+ end
40
+
41
+ # Do the tracer things.
42
+ class Handler
43
+ def initialize(pid)
44
+ Process.waitpid(pid)
45
+ opt = Ptrace::O_TRACESYSGOOD | Ptrace::O_TRACECLONE | Ptrace::O_TRACEFORK | Ptrace::O_TRACEVFORK
46
+ Ptrace.setoptions(pid, 0, opt)
47
+ Ptrace.syscall(pid, 0, 0)
48
+ end
49
+
50
+ # Tracer.
51
+ #
52
+ # @param [Integer] limit
53
+ # Child will be killed when number of calling +prctl(SET_SECCOMP)+ reaches +limit+.
54
+ # @yieldparam [String] bpf
55
+ # Seccomp bpf in raw bytes.
56
+ # @return [Array<Object>, Array<String>]
57
+ # Return the block returned. If block is not given, array of raw bytes will be returned.
58
+ def handle(limit, &block)
59
+ collect = []
60
+ syscalls = {} # record last syscall
61
+ loop while wait_syscall do |child|
62
+ if syscalls[child].nil? # invoke syscall
63
+ syscalls[child] = syscall(child)
64
+ next true
65
+ end
66
+ # syscall finished
67
+ sys = syscalls[child]
68
+ syscalls[child] = nil
69
+ if sys.set_seccomp? && syscall(child).ret.zero? # consider successful call only
70
+ bpf = sys.dump_bpf
71
+ collect << (block.nil? ? bpf : yield(bpf, sys.arch))
72
+ limit -= 1
73
+ end
74
+ !limit.zero?
75
+ end
76
+ syscalls.keys.each { |cpid| Process.kill('KILL', cpid) if alive?(cpid) }
77
+ collect
78
+ end
79
+
80
+ private
81
+
82
+ # @yieldparam [Integer] pid
83
+ # @return [Boolean]
84
+ # +true+ for continue,
85
+ # +false+ for break.
86
+ def wait_syscall
87
+ child, status = Process.wait2
88
+ cont = true
89
+ # TODO: Test if clone / vfork works
90
+ if [Ptrace::EVENT_CLONE, Ptrace::EVENT_FORK, Ptrace::EVENT_VFORK].include?(status >> 16)
91
+ # New child launched!
92
+ # newpid = SeccompTools::Ptrace.geteventmsg(child)
93
+ elsif status.stopped? && status.stopsig & 0x80 != 0
94
+ cont = yield(child)
95
+ end
96
+ Ptrace.syscall(child, 0, 0) unless status.exited?
97
+ return cont
98
+ rescue Errno::ECHILD
99
+ return false
100
+ end
101
+
102
+ # @return [SeccompTools::Syscall]
103
+ def syscall(pid)
104
+ SeccompTools::Syscall.new(pid)
105
+ end
106
+
107
+ def alive?(pid)
108
+ Process.getpgid(pid)
109
+ true
110
+ rescue Errno::ESRCH
111
+ false
112
+ end
113
+ end
114
+
115
+ class << self
116
+ private
117
+
118
+ def handle_child(*args)
119
+ Ptrace.traceme_and_stop
120
+ exec(*args)
121
+ rescue # exec fail
122
+ # TODO: use logger
123
+ $stderr.puts("Failed to execute #{args.join(' ')}")
124
+ exit(1)
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,42 @@
1
+ require 'seccomp-tools/instruction/base'
2
+
3
+ module SeccompTools
4
+ module Instruction
5
+ # Instruction alu.
6
+ class ALU < Base
7
+ # Decompile instruction.
8
+ def decompile
9
+ return 'A = -A' if op == :neg
10
+ "A #{op_sym}= #{src}"
11
+ end
12
+
13
+ private
14
+
15
+ def op
16
+ o = OP.invert[code & 0xf0]
17
+ invalid('unknown op') if o.nil?
18
+ o
19
+ end
20
+
21
+ def op_sym
22
+ case op
23
+ when :add then :+
24
+ when :sub then :-
25
+ when :mul then :*
26
+ when :div then :/
27
+ when :or then :|
28
+ when :and then :&
29
+ when :lsh then :<<
30
+ when :rsh then :>>
31
+ # when :neg then :- # should not invoke this method
32
+ # when :mod then :% # unsupported
33
+ when :xor then :^
34
+ end
35
+ end
36
+
37
+ def src
38
+ SRC.invert[code & 0x8] == :k ? k : 'X'
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,30 @@
1
+ require 'seccomp-tools/const'
2
+
3
+ module SeccompTools
4
+ # For instructions' class.
5
+ module Instruction
6
+ # Base class for different instruction.
7
+ class Base
8
+ include SeccompTools::Const::BPF
9
+
10
+ # @param [SeccompTools::BPF] bpf
11
+ # An instruction.
12
+ def initialize(bpf)
13
+ @bpf = bpf
14
+ end
15
+
16
+ # @raise [ArgumentError]
17
+ def invalid(msg = 'unknown')
18
+ raise ArgumentError, "Line #{line} is invalid: #{msg}"
19
+ end
20
+
21
+ private
22
+
23
+ %i(code jt jf k arch line contexts).each do |sym|
24
+ define_method(sym) do
25
+ @bpf.send(sym)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,8 @@
1
+ require 'seccomp-tools/instruction/alu'
2
+ require 'seccomp-tools/instruction/jmp'
3
+ require 'seccomp-tools/instruction/ld'
4
+ require 'seccomp-tools/instruction/ldx'
5
+ require 'seccomp-tools/instruction/misc'
6
+ require 'seccomp-tools/instruction/ret'
7
+ require 'seccomp-tools/instruction/st'
8
+ require 'seccomp-tools/instruction/stx'
@@ -0,0 +1,76 @@
1
+ require 'seccomp-tools/const'
2
+ require 'seccomp-tools/instruction/base'
3
+
4
+ module SeccompTools
5
+ module Instruction
6
+ # Instruction jmp.
7
+ class JMP < Base
8
+ # Decompile instruction.
9
+ def decompile
10
+ return goto(k) if jop == :none
11
+ # if jt == 0 && jf == 0 => no-op # should not happen
12
+ # jt == 0 => if(!) goto jf
13
+ # jf == 0 => if() goto jt;
14
+ # otherwise => if () goto jt; else goto jf;
15
+ return '/* no-op */' if jt.zero? && jf.zero?
16
+ 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)
20
+ end
21
+
22
+ # @return [Array<(Integer, Context)>]
23
+ def branch(context)
24
+ return [[at(k), context]] if jop == :none
25
+ return [[at(jt), context]] if jt == jf
26
+ [[at(jt), context], [at(jf), context]]
27
+ end
28
+
29
+ private
30
+
31
+ def jop
32
+ case Const::BPF::JMP.invert[code & 0x70]
33
+ when :ja then :none
34
+ when :jgt then :>
35
+ when :jge then :>=
36
+ when :jeq then :==
37
+ when :jset then :&
38
+ else invalid('unknown jmp type')
39
+ end
40
+ end
41
+
42
+ def src_str
43
+ return 'X' if SRC.invert[code & 8] == :x
44
+ # if A in all contexts are same
45
+ a = contexts.map(&:a).uniq
46
+ return k.to_s if a.size != 1
47
+ a = a[0]
48
+ return k.to_s unless a.instance_of?(Array) && a.first == :data
49
+ case a.last
50
+ when 0 then Util.colorize((Const::Syscall.const_get(arch.upcase.to_sym).invert[k] || k).to_s, t: :syscall)
51
+ when 4 then Util.colorize(Const::Audit::ARCH.invert[k] || k.to_s(16), t: :arch)
52
+ else '0x' + k.to_s(16)
53
+ end
54
+ end
55
+
56
+ def goto(off)
57
+ format('goto %04d', at(off))
58
+ end
59
+
60
+ def at(off)
61
+ line + off + 1
62
+ end
63
+
64
+ def if_str(neg = false)
65
+ return "if (A #{jop} #{src_str}) " unless neg
66
+ return "if (!(A & #{src_str})) " if jop == :&
67
+ op = case jop
68
+ when :>= then :<
69
+ when :> then :<=
70
+ when :== then :!=
71
+ end
72
+ "if (A #{op} #{src_str}) "
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,69 @@
1
+ require 'seccomp-tools/instruction/base'
2
+
3
+ module SeccompTools
4
+ module Instruction
5
+ # Instruction ld.
6
+ class LD < Base
7
+ # Decompile instruction.
8
+ def decompile
9
+ ret = reg + ' = '
10
+ type = load_val
11
+ return ret + type[:val].to_s if type[:rel] == :immi
12
+ return ret + "mem[#{type[:val]}]" if type[:rel] == :mem
13
+ ret + seccomp_data_str
14
+ end
15
+
16
+ # Accumulator register.
17
+ # @return ['A']
18
+ def reg
19
+ 'A'
20
+ end
21
+
22
+ # @return [Array<(Integer, Context)>]
23
+ def branch(context)
24
+ nctx = context.dup
25
+ type = load_val
26
+ nctx[reg] = case type[:rel]
27
+ when :immi then type[:val]
28
+ when :mem then context.mem[type[:val]]
29
+ when :data then [:data, type[:val]]
30
+ end
31
+ [[line + 1, nctx]]
32
+ end
33
+
34
+ private
35
+
36
+ def mode
37
+ @mode ||= MODE.invert[code & 0xe0]
38
+ # Seccomp doesn't support this mode
39
+ invalid if @mode.nil? || @mode == :ind
40
+ @mode
41
+ end
42
+
43
+ def load_val
44
+ return { rel: :immi, val: k } if mode == :imm
45
+ return { rel: :immi, val: SIZEOF_SECCOMP_DATA } if mode == :len
46
+ return { rel: :mem, val: k } if mode == :mem
47
+ { rel: :data, val: k }
48
+ end
49
+
50
+ # struct seccomp_data {
51
+ # int nr;
52
+ # __u32 arch;
53
+ # __u64 instruction_pointer;
54
+ # __u64 args[6];
55
+ # };
56
+ def seccomp_data_str
57
+ case k
58
+ when 0 then 'sys_number'
59
+ when 4 then 'arch'
60
+ when 8 then 'instruction_pointer'
61
+ else
62
+ idx = Array.new(12) { |i| i * 4 + 16 }.index(k)
63
+ return 'INVALID' if idx.nil?
64
+ idx.even? ? "args[#{idx / 2}]" : "args[#{idx / 2}] >> 32"
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,14 @@
1
+ require 'seccomp-tools/instruction/ld'
2
+
3
+ module SeccompTools
4
+ module Instruction
5
+ # Instruction ldx.
6
+ class LDX < LD
7
+ # Index register.
8
+ # @return ['X']
9
+ def reg
10
+ 'X'
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,24 @@
1
+ require 'seccomp-tools/instruction/base'
2
+
3
+ module SeccompTools
4
+ module Instruction
5
+ # Instruction misc.
6
+ class MISC < Base
7
+ # Decompile instruction.
8
+ def decompile
9
+ case op
10
+ when :txa then 'A = X'
11
+ when :tax then 'X = A'
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def op
18
+ o = MISCOP.invert[code & 0xf8]
19
+ invalid('MISC operation only supports txa/tax') if o.nil?
20
+ o
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ require 'seccomp-tools/instruction/base'
2
+
3
+ module SeccompTools
4
+ module Instruction
5
+ # Instruction ret.
6
+ class RET < Base
7
+ # Decompile instruction.
8
+ def decompile
9
+ return 'return A' if code & 0x18 == SRC[:a]
10
+ "return #{ACTION.invert[k & 0x7fff0000]}"
11
+ end
12
+
13
+ # @return [[]]
14
+ def branch(*)
15
+ []
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ require 'seccomp-tools/instruction/ld'
2
+
3
+ module SeccompTools
4
+ module Instruction
5
+ # Instruction st.
6
+ class ST < LD
7
+ # Decompile instruction.
8
+ def decompile
9
+ "mem[#{k}] = #{reg}"
10
+ end
11
+
12
+ # @return [Array<(Integer, Context)>]
13
+ def branch(context)
14
+ ctx = context.dup
15
+ ctx.mem[k] = ctx[reg]
16
+ [[line + 1, ctx]]
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ require 'seccomp-tools/instruction/st'
2
+
3
+ module SeccompTools
4
+ module Instruction
5
+ # Instruction stx.
6
+ class STX < ST
7
+ # Index register.
8
+ # @return ['X']
9
+ def reg
10
+ 'X'
11
+ end
12
+ end
13
+ end
14
+ end