diff --git a/lib/one_gadget.rb b/lib/one_gadget.rb index 2ab9016..f0a112f 100644 --- a/lib/one_gadget.rb +++ b/lib/one_gadget.rb @@ -16,7 +16,7 @@ require 'one_gadget/version' # @param [String?] arg # Can be either +build_id+ or path to libc. # @param [Mixed] options -# See {OneGadget#gadgets} for ore information. +# See {OneGadget#gadgets} for more information. # @return [Array, Array] # The gadgets found. # @example diff --git a/lib/one_gadget/emulators/aarch64.rb b/lib/one_gadget/emulators/aarch64.rb index 76ea240..8c042d7 100644 --- a/lib/one_gadget/emulators/aarch64.rb +++ b/lib/one_gadget/emulators/aarch64.rb @@ -19,6 +19,8 @@ module OneGadget end # @see OneGadget::Emulators::X86#process! + # @param [String] cmd One line from result of objdump. + # @return [Boolean] If successfully processed. def process!(cmd) resolve_pending_branch(cmd) cmd = cmd.gsub(/#-?(0x)?[0-9a-f]+/) { |v| v[1..] } @@ -60,6 +62,7 @@ module OneGadget # A landing pad marks where an indirect branch may arrive; nothing about the # state changes at one. + # @return [void] alias inst_bti inst_nop # Return the argument value of calling a function. @@ -157,6 +160,7 @@ module OneGadget class << self # AArch64 is 64-bit. + # @return [Integer] def bits 64 end diff --git a/lib/one_gadget/emulators/amd64.rb b/lib/one_gadget/emulators/amd64.rb index 9cbd863..98ec7cb 100644 --- a/lib/one_gadget/emulators/amd64.rb +++ b/lib/one_gadget/emulators/amd64.rb @@ -9,6 +9,7 @@ module OneGadget class Amd64 < X86 class << self # Bits. + # @return [Integer] def bits 64 end diff --git a/lib/one_gadget/emulators/arm.rb b/lib/one_gadget/emulators/arm.rb index e48b021..0d5b3e9 100644 --- a/lib/one_gadget/emulators/arm.rb +++ b/lib/one_gadget/emulators/arm.rb @@ -28,11 +28,15 @@ module OneGadget end # Memoized bytes of +file+ (the target libc), shared across emulator instances. + # @param [String] file Path to the target libc. + # @return [String] Its bytes. def self.file_data(file) (@file_data ||= {})[file] ||= File.binread(file) end # @see OneGadget::Emulators::AArch64#process! + # @param [String] cmd One line from result of objdump. + # @return [Boolean] If successfully processed. def process!(cmd) resolve_pending_branch(cmd) line = cmd.strip @@ -112,6 +116,7 @@ module OneGadget # Thumb candidate offers no evidence either and is no better served than # before. # @param [Array] lines The candidate's objdump lines. + # @return [void] def note_instruction_set(lines) return if @thumb @@ -297,6 +302,7 @@ module OneGadget class << self # ARM (32-bit) is 32-bit. + # @return [Integer] def bits 32 end diff --git a/lib/one_gadget/emulators/conditional.rb b/lib/one_gadget/emulators/conditional.rb index 9a86f3f..0aa6a1c 100644 --- a/lib/one_gadget/emulators/conditional.rb +++ b/lib/one_gadget/emulators/conditional.rb @@ -188,6 +188,7 @@ module OneGadget # # fall-through path emits x0 != 0 ; taken path emits x0 == 0 # @example x86 reuses it for +jrcxz+/+jecxz+/+jcxz+ (always branch-if-zero) # branch_on_zero(0x4a200, 'rcx', negate: false) + # @return [true] Registering the branch is how the line is handled. def branch_on_zero(target, operand, negate:) reg = operand_str(operand) hit = negate ? '!=' : '==' # taken (not negated) => reg == 0 @@ -205,6 +206,7 @@ module OneGadget # @example aarch64 +tbz w0, #4, 4a200+ - branch taken when bit 4 of +w0+ is 0 # branch_on_bit(0x4a200, 'w0', 4, negate: false) #=> true # # taken path emits (w0 & 0x10) == 0 ; fall-through emits (w0 & 0x10) != 0 + # @return [true] Registering the branch is how the line is handled. def branch_on_bit(target, operand, bit, negate:) reg = operand_str(operand) mask = OneGadget::Helper.hex(1 << bit) @@ -227,6 +229,9 @@ module OneGadget # end # # if the previous line did +branch_on_compare(:ne, 0x4a200)+ and this # # +cmd+ sits at 0x4a200, the branch was taken; otherwise it fell through + # @return [void] + # @raise [OneGadget::Error::InfeasiblePathError] + # When the resolved relation cannot hold with the ones already recorded. def resolve_pending_branch(cmd) return if @pending.nil? @@ -244,6 +249,8 @@ module OneGadget # each register's current value, so two constraints printing the same left # side really are about the same tracked value (and a differing signedness # cast is part of that text, keeping incomparable ones apart). + # @param [String] expr The left side, as rendered. + # @return [Array<(String, String, String)>] The comparisons recorded on it. def comparisons_on(expr) @constraints.filter_map { |type, obj| obj if type == :cmp && obj.first == expr } end diff --git a/lib/one_gadget/emulators/constraints.rb b/lib/one_gadget/emulators/constraints.rb index 30142fc..6ec1c4a 100644 --- a/lib/one_gadget/emulators/constraints.rb +++ b/lib/one_gadget/emulators/constraints.rb @@ -58,6 +58,9 @@ module OneGadget # Whether +(type, obj)+ is an address constraint on a bare (deref-0) target, # i.e. one carrying a base register and offset to normalise. + # @param [Symbol] type The constraint's type. + # @param [Object] obj Its payload. + # @return [Boolean] def address_deref0?(type, obj) ADDRESS_TYPES.include?(type) && obj.deref_count.zero? end @@ -65,6 +68,9 @@ module OneGadget # De-duplication key: an address constraint collapses per (type, base) so # constraints of different types on the same register stay distinct; a raw # constraint keys on its own text. + # @param [Symbol] type The constraint's type. + # @param [Object] obj Its payload. + # @return [Object] Equal for two constraints that impose the same requirement. def constraint_key(type, obj) return obj unless ADDRESS_TYPES.include?(type) @@ -72,6 +78,9 @@ module OneGadget end # Render a constraint to its output string. + # @param [Symbol] type The constraint's type. + # @param [Object] obj Its payload. + # @return [String] The constraint as reported. def render_constraint(type, obj) case type when :writable then "writable: #{obj}" diff --git a/lib/one_gadget/emulators/i386.rb b/lib/one_gadget/emulators/i386.rb index e72dbe8..021ddff 100644 --- a/lib/one_gadget/emulators/i386.rb +++ b/lib/one_gadget/emulators/i386.rb @@ -9,6 +9,7 @@ module OneGadget class I386 < X86 class << self # Yap, bits. + # @return [Integer] def bits 32 end diff --git a/lib/one_gadget/emulators/instruction.rb b/lib/one_gadget/emulators/instruction.rb index 54fc1f3..13d4984 100644 --- a/lib/one_gadget/emulators/instruction.rb +++ b/lib/one_gadget/emulators/instruction.rb @@ -72,6 +72,8 @@ module OneGadget private + # Split an operand list on the commas that separate operands, which are the + # ones outside brackets: a memory operand carries commas of its own. def parse_args(str) args = [] cur = +'' diff --git a/lib/one_gadget/emulators/lambda.rb b/lib/one_gadget/emulators/lambda.rb index baba506..a0815dd 100644 --- a/lib/one_gadget/emulators/lambda.rb +++ b/lib/one_gadget/emulators/lambda.rb @@ -186,7 +186,9 @@ module OneGadget private - # @return [(String, Integer)] + # Split a memory operand into what it is based on and how far from it. + # @param [String] arg The operand, with its brackets already removed. + # @return [(String, Integer)] The base, and the offset from it. def mem_obj(arg) # We have three forms: # 0. reg diff --git a/lib/one_gadget/emulators/processor.rb b/lib/one_gadget/emulators/processor.rb index 89dab74..da423f9 100644 --- a/lib/one_gadget/emulators/processor.rb +++ b/lib/one_gadget/emulators/processor.rb @@ -174,6 +174,9 @@ module OneGadget raise Error::InstructionArgumentError, "#{reg.inspect} is not a valid register" unless register?(reg) end + # Whether the +idx+-th argument meets +expect+, a requirement a catalogued + # call states about it that is not about the pointer being mapped (those go + # through {#record_pointer}). See {SafeCalls}. def check_argument(idx, expect) case expect when :global_var? then global_var?(argument(idx)) @@ -316,6 +319,9 @@ module OneGadget self.class.bits / 8 end + # Whether +obj+ is reached through the program counter, which is what a + # libc global looks like once resolved -- as opposed to a value the caller + # supplies. def global_var?(obj) obj.to_s.include?(pc) end diff --git a/lib/one_gadget/emulators/register_file.rb b/lib/one_gadget/emulators/register_file.rb index 4b373c6..c2c7d70 100644 --- a/lib/one_gadget/emulators/register_file.rb +++ b/lib/one_gadget/emulators/register_file.rb @@ -36,15 +36,22 @@ module OneGadget @entry_halves = {} end + # @param [String] name Any name the architecture accepts, narrow or full. + # @return [OneGadget::Emulators::Lambda, Integer] What that name currently reads as. def [](name) value = super(full(name)) narrow?(name) ? narrowed(name, value) : value end + # @param [String] name Any name the architecture accepts, narrow or full. + # @param [OneGadget::Emulators::Lambda, Integer] value + # @return [void] def []=(name, value) super(full(name), value) end + # @param [String] name Any name the architecture accepts, narrow or full. + # @return [Boolean] Whether its storage holds a value. def key?(name) super(full(name)) end diff --git a/lib/one_gadget/emulators/riscv64.rb b/lib/one_gadget/emulators/riscv64.rb index 4b8e183..3205515 100644 --- a/lib/one_gadget/emulators/riscv64.rb +++ b/lib/one_gadget/emulators/riscv64.rb @@ -17,6 +17,8 @@ module OneGadget end # @see OneGadget::Emulators::X86#process! + # @param [String] cmd One line from result of objdump. + # @return [Boolean] If successfully processed. def process!(cmd) resolve_pending_branch(cmd) @cur_addr = cmd[/\A\s*([0-9a-f]+):/, 1]&.to_i(16) @@ -253,6 +255,7 @@ module OneGadget class << self # RV64 is 64-bit. + # @return [Integer] def bits 64 end diff --git a/lib/one_gadget/emulators/tracked_memory.rb b/lib/one_gadget/emulators/tracked_memory.rb index 596cfc8..5867960 100644 --- a/lib/one_gadget/emulators/tracked_memory.rb +++ b/lib/one_gadget/emulators/tracked_memory.rb @@ -20,6 +20,7 @@ module OneGadget # gadget staging data at +[bp+imm]+ (e.g. an argv array off the frame # pointer) is recovered instead of collapsing to a bare +writable:+. A nil # +bp+ leaves the arch +sp+-only. Call from the arch initializer after +super+. + # @param [String, nil] bp The frame register's name, or nil for an arch with none. # @return [void] def setup_frame_pointer(bp) @bp = bp diff --git a/lib/one_gadget/emulators/x86.rb b/lib/one_gadget/emulators/x86.rb index 8e11d55..8b3af56 100644 --- a/lib/one_gadget/emulators/x86.rb +++ b/lib/one_gadget/emulators/x86.rb @@ -10,6 +10,10 @@ module OneGadget # Super class for amd64 and i386 processor. class X86 < Processor # Constructor for a x86 processor. + # @param [Array] registers All the register names this architecture accepts. + # @param [String] sp The stack pointer's name. + # @param [String] bp The frame pointer's name. + # @param [String] pc The program counter's name. def initialize(registers, sp, bp, pc) super(registers, sp) @pc = pc @@ -124,6 +128,8 @@ module OneGadget branch_on_compare(JCC[mnem], jump_target(cmd)) end + # The counter register a +jcxz+-family branch tests, at the width its + # mnemonic names. def cx_reg(mnem) { 'jcxz' => 'cx', 'jecxz' => 'ecx', 'jrcxz' => 'rcx' }[mnem] end @@ -337,6 +343,9 @@ module OneGadget dispatch_safe_call(addr) end + # A vector register holds one value per lane, so it becomes an array of + # them -- each lane named by the shift that reaches it -- where an ordinary + # register becomes a single {Lambda}. def to_lambda(reg) return super unless reg =~ /^xmm\d+$/ diff --git a/lib/one_gadget/fetchers/argument_resolution.rb b/lib/one_gadget/fetchers/argument_resolution.rb index b470ac1..0aa4bfc 100644 --- a/lib/one_gadget/fetchers/argument_resolution.rb +++ b/lib/one_gadget/fetchers/argument_resolution.rb @@ -130,10 +130,14 @@ module OneGadget generate_argv_without_sh(argv_ptr, argv, allow_null) end + # Whether the array is usable as it stands: an empty argv, or one whose only + # entry is a libc global. def argv_already_valid?(argv) argv[0] == '0' || (global_var?(argv[0]) && argv[1] == '0') end + # The constraint for an argv whose first entry libc has already filled in + # with the shell's own name, leaving the caller to arrange the rest. def generate_argv_with_sh(argv) # argv[0] is not controlled by the user, argv[0] probably is "/bin/sh" or "sh" (but actually, the content of # argv[0] doesn't quite matter, just need to make sure it's readable) diff --git a/lib/one_gadget/fetchers/arm.rb b/lib/one_gadget/fetchers/arm.rb index ff213cd..8a996a4 100644 --- a/lib/one_gadget/fetchers/arm.rb +++ b/lib/one_gadget/fetchers/arm.rb @@ -42,6 +42,8 @@ module OneGadget sites.uniq end + # Whether the two halfwords are a Thumb +BL+: the first names the encoding, + # and the second's top bits say it is the long form rather than +BLX+. def thumb_bl?(high, low) high.between?(0xf000, 0xf7ff) && low.allbits?(0xd000) end @@ -52,6 +54,9 @@ module OneGadget ((low >> 8) & 0x0f) == 0x0b end + # Where a Thumb +BL+ goes. Its offset is scattered across both halfwords, + # with the two middle bits stored inverted against the sign bit, and is + # taken from the address two instructions on. def thumb_bl_target(addr, high, low) s = (high >> 10) & 1 i1 = (~(((low >> 13) & 1) ^ s)) & 1 diff --git a/lib/one_gadget/fetchers/base.rb b/lib/one_gadget/fetchers/base.rb index ef6e0fb..9ce07c5 100644 --- a/lib/one_gadget/fetchers/base.rb +++ b/lib/one_gadget/fetchers/base.rb @@ -37,6 +37,10 @@ module OneGadget # (its output and everything derived from it), so re-analysing the same # file - common in the specs, harmless for the CLI which reads a file once - # doesn't redo the disassembly or the whole-binary scans. + # @param [Symbol] kind What is being cached, so two kinds may share a command. + # @param [String] command The objdump command the value is a function of. + # @yieldreturn [Object] The value, computed only on a miss. + # @return [Object] The cached value. def self.cached(kind, command) @cached ||= Hash.new { |h, k| h[k] = {} } @cached[kind][command] ||= yield @@ -101,6 +105,8 @@ module OneGadget # also matches the +posix_spawn+ setup helpers, which emulation runs through. # The mnemonic must be a call, so a branch whose target symbol merely looks # similar (++) doesn't end the window. + # @param [String] line One disassembled line. + # @return [Boolean] def terminal_call_line?(line) return false unless line[/\A\s*[0-9a-f]+:\s*(\S+)/, 1]&.match?(/\A#{call_str}x?(?:\.[wn])?\z/) @@ -123,6 +129,8 @@ module OneGadget end # Emulate a candidate suffix and turn it into a gadget, or +nil+ if it isn't one. + # @param [Array] lines The suffix, ending at the terminal call. + # @return [OneGadget::Gadget::Gadget, nil] def resolve_suffix(lines) processor = emulate(lines) (@refused ||= {})[processor.refused_line] = true if processor.refused_line diff --git a/lib/one_gadget/fetchers/i386.rb b/lib/one_gadget/fetchers/i386.rb index 9cd0aed..3ef81f5 100644 --- a/lib/one_gadget/fetchers/i386.rb +++ b/lib/one_gadget/fetchers/i386.rb @@ -77,6 +77,8 @@ module OneGadget end end + # How far +"/bin/sh"+ sits before the GOT, which is how PIC code names it: + # a window reaches the string as an offset from the register holding the base. def rel_sh @rel_sh ||= got_offset - str_offset('/bin/sh') end diff --git a/lib/one_gadget/fetchers/objdump.rb b/lib/one_gadget/fetchers/objdump.rb index d76a124..fcf1957 100644 --- a/lib/one_gadget/fetchers/objdump.rb +++ b/lib/one_gadget/fetchers/objdump.rb @@ -34,6 +34,7 @@ module OneGadget # @param [Array] options The options. # @example # objdump.extra_options = %w[-M intel] + # @return [void] def extra_options=(options) @options = options end diff --git a/lib/one_gadget/gadget.rb b/lib/one_gadget/gadget.rb index 9abc681..dcda13a 100644 --- a/lib/one_gadget/gadget.rb +++ b/lib/one_gadget/gadget.rb @@ -424,6 +424,8 @@ module OneGadget 0.9**(depth + base) end + # The constraints as a reader wants them: every +writable:+ folded into one + # sentence naming all of the addresses, the rest left alone. def merge_constraints key = 'writable: ' w_cons, normal = constraints.partition { |c| c.start_with?(key) } diff --git a/lib/one_gadget/one_gadget.rb b/lib/one_gadget/one_gadget.rb index 0d91d66..4897be9 100644 --- a/lib/one_gadget/one_gadget.rb +++ b/lib/one_gadget/one_gadget.rb @@ -79,6 +79,12 @@ module OneGadget take_until(high, 3) end + # The +count+ best-scoring of +ary+, in the order they were given, plus + # anything scoring as well as the last of them -- so a tie is never broken + # arbitrarily, and more than +count+ may come back. + # @param [Array] ary + # @param [Integer] count + # @return [Array] def take_until(ary, count) return ary if ary.size <= count