# frozen_string_literal: true # # Copyright (c) 2006-2023 Hal Brodigan (postmodern.mod3 at gmail.com) # # Ronin is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ronin is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ronin. If not, see . # require 'ronin/cli/file_processor_command' require 'hexdump' module Ronin class CLI module Commands # # Hexdumps data in a variety of encodings and formats. # # ## Usage # # ronin hexdump [options] [FILE ...] # # ## Options # # -t int8|uint8|char|uchar|byte|int16|int16_le|int16_be|int16_ne|uint16|uint16_le|uint16_be|uint16_ne|short|short_le|short_be|short_ne|ushort|ushort_le|ushort_be|ushort_ne|int32|int32_le|int32_be|int32_ne|uint32|uint32_le|uint32_be|uint32_ne|int|long|long_le|long_be|long_ne|uint|ulong|ulong_le|ulong_be|ulong_ne|int64|int64_le|int64_be|int64_ne|uint64|uint64_le|uint64_be|uint64_ne|long_long|long_long_le|long_long_be|long_long_ne|ulong_long|ulong_long_le|ulong_long_be|ulong_long_ne|float|float_le|float_be|float_ne|double|double_le|double_be|double_ne, # --type The binary data type to decode the data as (Default: byte) # -O, --offset INDEX Offset within the data to start hexdumping at # -L, --length LEN Length of data to hexdump # -Z, --zero-pad Enables zero-padding the input data # -c, --columns WIDTH The number of bytes to hexdump per line # -g, --group-columns WIDTH Groups columns together # -G, --group-chars WIDTH|type Group characters into columns # -r, --[no-]repeating Allows repeating lines in hexdump output # -b, --base 2|8|10|16 Base to print numbers in # -B, --index-base 2|8|10|16 Base to print the index addresses in # -I, --index-offset INT Starting number for the index addresses # -C, --[no-]chars-column Enables/disables the characters column # -E, --encoding ascii|utf8 Encoding to display the characters in (Default: ascii) # --style-index STYLE ANSI styles the index column # --style-numeric STYLE ANSI styles the numeric columns # --style-chars STYLE ANSI styles the characters column # --highlight-index PATTERN:STYLE # Applies ANSI highlighting to the index column # --highlight-numeric PATTERN:STYLE # Applies ANSI highlighting to the numeric column # --highlight-chars PATTERN:STYLE # Applies ANSI highlighting to the characters column # -h, --help Print help information # # ## Arguments # # [FILE] Optional file to hexdump # class Hexdump < FileProcessorCommand # Supported types for the `-t,--type` option. TYPES = [ :int8, :uint8, :char, :uchar, :byte, # default :int16, :int16_le, :int16_be, :int16_ne, :uint16, :uint16_le, :uint16_be, :uint16_ne, :short, :short_le, :short_be, :short_ne, :ushort, :ushort_le, :ushort_be, :ushort_ne, :int32, :int32_le, :int32_be, :int32_ne, :uint32, :uint32_le, :uint32_be, :uint32_ne, :int, :long, :long_le, :long_be, :long_ne, :uint, :ulong, :ulong_le, :ulong_be, :ulong_ne, :int64, :int64_le, :int64_be, :int64_ne, :uint64, :uint64_le, :uint64_be, :uint64_ne, :long_long, :long_long_le, :long_long_be, :long_long_ne, :ulong_long, :ulong_long_le, :ulong_long_be, :ulong_long_ne, :float, :float_le, :float_be, :float_ne, :double, :double_le, :double_be, :double_ne ] usage '[options] [FILE]' option :type, short: '-t', value: { type: TYPES, default: :byte }, desc: 'The binary data type to decode the data as' option :offset, short: '-O', value: { type: Integer, usage: 'INDEX' }, desc: 'Offset within the data to start hexdumping at' option :length, short: '-L', value: { type: Integer, usage: 'LEN' }, desc: 'Length of data to hexdump' option :zero_pad, short: '-Z', desc: 'Enables zero-padding the input data' option :columns, short: '-c', value: { type: Integer, usage: 'WIDTH' }, desc: 'The number of bytes to hexdump per line' option :group_columns, short: '-g', value: { type: Integer, usage: 'WIDTH' }, desc: 'Groups columns together' option :group_chars, short: '-G', value: {usage: 'WIDTH|type'}, desc: 'Group characters into columns' do |value| options[:group_chars] = parse_group_chars(value) end option :repeating, short: '-r', long: '--[no-]repeating', desc: 'Allows repeating lines in hexdump output' # Mapping of supported values for the `-b,--base` option. BASES = {'2' => 2, '8' => 8, '10' => 10, '16' => 16} option :base, short: '-b', value: {type: BASES}, desc: 'Base to print numbers in' option :index_base, short: '-B', value: {type: BASES}, desc: 'Base to print the index addresses in' option :index_offset, short: '-I', value: {type: Integer}, desc: 'Starting number for the index addresses' option :chars_column, short: '-C', long: '--[no-]chars-column', desc: 'Enables/disables the characters column' option :encoding, short: '-E', value: { type: [:ascii, :utf8], default: :ascii }, desc: 'Encoding to display the characters in' option :style_index, value: {usage: 'STYLE'}, desc: 'ANSI styles the index column' do |value| options[:style_index] = parse_style(value) end option :style_numeric, value: {usage: 'STYLE'}, desc: 'ANSI styles the numeric columns' do |value| options[:style_numeric] = parse_style(value) end option :style_chars, value: {usage: 'STYLE'}, desc: 'ANSI styles the characts column' do |value| options[:style_chars] = parse_style(value) end option :highlight_index, value: {usage: 'PATTERN:STYLE'}, desc: 'Applies ANSI highlighting to the index column' do |value| pattern, style = parse_highlight(value) @highlight_index[pattern] = style end option :highlight_numeric, value: {usage: 'PATTERN:STYLE'}, desc: 'Applies ANSI highlighting to the numeric column' do |value| pattern, style = parse_highlight(value) @highlight_numeric[pattern] = style end option :highlight_chars, value: {usage: 'PATTERN:STYLE'}, desc: 'Applies ANSI highlighting to the characters column' do |value| pattern, style = parse_highlight(value) @highlight_chars[pattern] = style end description 'Hexdumps data in a variaty of encodings and formats' man_page 'ronin-hexdump.1' # The highlighting rules to apply to the index column. # # @return [Array<(Regexp, Array)>, # Array<(String, Array)>] attr_reader :highlight_index # The highlighting rules to apply to the numeric column. # # @return [Array<(Regexp, Array)>, # Array<(String, Array)>] attr_reader :highlight_numeric # The highlighting rules to apply to the characters column. # # @return [Array<(Regexp, Array)>, # Array<(String, Array)>] attr_reader :highlight_chars # # Initializes the `hexdump` command. # def initialize(**kwargs) super(**kwargs) @highlight_index = {} @highlight_numeric = {} @highlight_chars = {} end # # Runs the `ronin hexdump` command. # # @param [Array] files # Additional files to hexdump. # def run(*files) @hexdump = ::Hexdump::Hexdump.new(**hexdump_kwargs) super(*files) end # # Opens the file in binary mode. # # @yield [file] # If a block is given, the newly opened file will be yielded. # Once the block returns the file will automatically be closed. # # @yieldparam [File] file # The newly opened file. # # @return [File, nil] # If no block is given, the newly opened file object will be returned. # If no block was given, then `nil` will be returned. # def open_file(file,&block) File.open(file,'rb',&block) end # # Hexdumps the input stream. # # @param [IO, StringIO] input # The input stream to hexdump. # def process_input(input) @hexdump.hexdump(input) end # # Parses the value passed to the `-G,--group-chars` option. # # @param [String] value # The `-G,--group-chars` argument value. # # @return [Integer, :type] # The parsed integer or `:type` if the `type` argument was given. # def parse_group_chars(value) case value when 'type' then :type when /^\d+$/ then value.to_i else raise(OptionParser::InvalidArgument,"invalid value: #{value}") end end # Mapping of style names to Symbols. STYLES = { # font styling 'bold' => :bold, 'faint' => :faint, 'italic' => :italic, 'underline' => :underline, 'invert' => :invert, 'strike' => :strike, # foreground colors 'black' => :black, 'red' => :red, 'green' => :green, 'yellow' => :yellow, 'blue' => :blue, 'magenta' => :magenta, 'cyan' => :cyan, 'white' => :white, # background colors 'on_black' => :on_black, 'on_red' => :on_red, 'on_green' => :on_green, 'on_yellow' => :on_yellow, 'on_blue' => :on_blue, 'on_magenta' => :on_magenta, 'on_cyan' => :on_cyan, 'on_white' => :on_white } # # Parses a style string. # # @param [String] value # The comma-separated list of style names. # # @return [Array] # The array of parsed style names. # def parse_style(value) value.split(/\s*,\s*/).map do |style_name| STYLES.fetch(style_name) do raise(OptionParser::InvalidArgument,"unknown style name: #{style_name}") end end end # # Parses a highlight rule of the form `/REGEXP/:STYLE` or # `STRING:STYLE`. # # @param [String] value # The raw string value to parse. # # @return [(Regexp, Array), (String, Array)] # The Regexp or String pattern to match and the style rules to apply # to it. # def parse_highlight(value) if value.start_with?('/') unless (index = value.rindex('/:')) raise(OptionParser::InvalidArgument,"argument must be of the form /REGEXP/:STYLE but was: #{value}") end regexp = Regexp.new(value[1...index]) style = parse_style(value[(index + 2)..]) return [regexp, style] else unless (index = value.rindex(':')) raise(OptionParser::InvalidArgument,"argument must be of the form STRING:STYLE but was: #{value}") end pattern = value[0...index] style = parse_style(value[(index + 1)..]) return [pattern, style] end end # List of command `options` that directly map to the keyword arguments # of `Hexdump.hexdump`. HEXDUMP_OPTIONS = [ :type, :format, :offset, :length, :zero_pad, :columns, :group_columns, :group_chars, :repeating, :base, :index_base, :index_offset, :chars_column, :encoding ] # # Creates a keyword arguments `Hash` of all command `options` that will # be directly passed to `Hexdump.hexdump`. # # @return [Hash{Symbol => Object}] # def hexdump_kwargs kwargs = {} HEXDUMP_OPTIONS.each do |key| kwargs[key] = options[key] if options.has_key?(key) end if options.has_key?(:style_index) || options.has_key?(:style_numeric) || options.has_key?(:style_chars) kwargs[:style] = hexdump_style_kwargs end if !@highlight_index.empty? || !@highlight_numeric.empty? || !@highlight_chars.empty? kwargs[:highlights] = hexdump_highlights_kwargs end return kwargs end # # The hexdump `style:` keyword arguments. # # @return [Hash{Symbol => Object}] # def hexdump_style_kwargs style = {} if (index_style = options[:style_index]) style[:index] = index_style end if (numeric_style = options[:style_numeric]) style[:numeric] = numeric_style end if (chars_style = options[:style_chars]) style[:chars] = chars_style end return style end # # The hexdump `highlights:` keyword arguments. # # @return [Hash{Symbol => Object}] # def hexdump_highlights_kwargs highlights = {} unless @highlight_index.empty? highlights[:index] = @highlight_index end unless @highlight_numeric.empty? highlights[:numeric] = @highlight_numeric end unless @highlight_chars.empty? highlights[:chars] = @highlight_chars end return highlights end end end end end