seccomp-tools 0.1.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.
@@ -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