Module: PWN::Plugins::BinaryAnalysis

Defined in:
lib/pwn/plugins/binary_analysis.rb

Overview

Bounded, argv-only read-only backend shared by normalized analysis adapters.

Class Method Summary collapse

Class Method Details

.analyze(opts = {}) ⇒ Object

Raises:

  • (ArgumentError)


43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/pwn/plugins/binary_analysis.rb', line 43

public_class_method def self.analyze(opts = {})
  path = File.realpath(File.expand_path(opts[:path].to_s))
  raise ArgumentError, 'binary must be a regular file' unless File.file?(path)

  result = { backend: 'binutils', status: 'degraded', risk_level: 'low', path: path, sha256: Digest::SHA256.file(path).hexdigest, functions: [], strings: [], imports: [], symbols: [], types: [], warnings: ['heavy analysis backend unavailable or binutils requested; no decompiled C or complete xrefs'] }
  { symbols: ['nm', '-a', path], imports: ['readelf', '--dyn-syms', '--wide', path], strings: ['strings', '-a', '-t', 'x', path], disassembly: ['objdump', '-d', path] }.each do |key, command|
    text = run(argv: command, timeout: opts.fetch(:timeout, 60))
    result[key] = case key
                  when :symbols
                    text.lines.filter_map do |line|
                      m = line.match(/^([0-9a-fA-F]+)\s+(\w)\s+(.+)$/)
                      { address: m[1].to_i(16), type: m[2], name: m[3] } if m
                    end
                  when :imports
                    text.lines.filter_map { |line| { name: line.split.last } if line.include?(' UND ') && line.split.length >= 8 }
                  when :strings
                    text.lines.filter_map do |line|
                      m = line.match(/^\s*([0-9a-f]+) (.*)$/)
                      { offset: m[1].to_i(16), string: m[2] } if m
                    end
                  else
                    text
                  end
  rescue StandardError => e
    result[:warnings] << "#{command.first}: #{e.message}"
  end
  result[:functions] = result.fetch(:disassembly, '').scan(/^([0-9a-fA-F]+) <([^>]+)>:/).map { |address, name| { address: address.to_i(16), name: name } }
  result
end

.authorsObject



73
74
75
# File 'lib/pwn/plugins/binary_analysis.rb', line 73

public_class_method def self.authors
  "AUTHOR(S):\n  0day Inc. <[email protected]>\n"
end

.available?(opts = {}) ⇒ Boolean

Returns:

  • (Boolean)


11
12
13
14
# File 'lib/pwn/plugins/binary_analysis.rb', line 11

public_class_method def self.available?(opts = {})
  name = opts[:name]
  ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).any? { |dir| File.executable?(File.join(dir, name)) && !File.directory?(File.join(dir, name)) }
end

.helpObject



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/pwn/plugins/binary_analysis.rb', line 77

public_class_method def self.help
  puts "USAGE:
    # Check whether a named backend executable is present on PATH.
    #{self}.available?(
      name: 'required - backend executable filename to search on PATH'
    )

    # Run a bounded argv subprocess without shell interpolation.
    #{self}.run(
      argv: 'required - array of executable name and separate argument strings',
      limit: 'optional - maximum captured subprocess output bytes before termination',
      timeout: 'optional - positive subprocess or HTTP deadline in seconds'
    )

    # Read binary symbols, strings, imports and disassembly through binutils.
    #{self}.analyze(
      path: 'required - filesystem path to the local artifact or binary',
      timeout: 'optional - positive subprocess or HTTP deadline in seconds'
    )

    # Display module authors.
    #{self}.authors
  "
  constants.sort
end

.run(opts = {}) ⇒ Object



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/pwn/plugins/binary_analysis.rb', line 16

public_class_method def self.run(opts = {})
  argv = opts[:argv]
  timeout = opts.fetch(:timeout, 60)
  limit = opts.fetch(:limit, 2_000_000)
  output = +''
  Open3.popen2e(*argv, pgroup: true) do |stdin, stream, waiter|
    stdin.close
    begin
      Timeout.timeout(timeout) do
        loop do
          output << stream.readpartial(16_384)
          raise 'binary backend output limit exceeded' if output.bytesize > limit
        rescue EOFError
          break
        end
        raise "backend exited #{waiter.value.exitstatus}: #{output[-1000, 1000] || output}" unless waiter.value.success?
      end
    ensure
      if waiter.alive?
        Process.kill('KILL', -waiter.pid)
        waiter.join
      end
    end
  end
  output.encode('UTF-8', invalid: :replace, undef: :replace)
end