mirror of
https://github.com/david942j/one_gadget.git
synced 2026-10-12 02:12:49 +00:00
Document what a reader cannot infer (#436)
Every public method now states its parameters and its return: 18 were missing a @param and 22 a @return, including every arch's process! override, RegisterFile's Hash accessors, Base.cached and the branch registrations in Conditional. Thirteen private methods gained a line, the ones whose name does not say what they do -- take_until, which keeps ties and so may return more than it is asked for; X86#to_lambda, which answers a vector register with one value per lane; the Thumb BL encoding helpers, whose offset is scattered across both halfwords; PIC's distance from the GOT to "/bin/sh"; and the argv builders, where "with sh" means libc has already filled in argv[0]. The rest -- inst_mov, emulator, call_str and their kind -- are left bare, since their names already say it. Descriptions are left alone. A method whose name tells may still carry a one-line summary, and the ones here do; nothing in lib carries more than a line without earning it.
This commit is contained in:
+1
-1
@@ -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<OneGadget::Gadget::Gadget>, Array<Integer>]
|
||||
# The gadgets found.
|
||||
# @example
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,6 +9,7 @@ module OneGadget
|
||||
class Amd64 < X86
|
||||
class << self
|
||||
# Bits.
|
||||
# @return [Integer]
|
||||
def bits
|
||||
64
|
||||
end
|
||||
|
||||
@@ -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<String>] 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -9,6 +9,7 @@ module OneGadget
|
||||
class I386 < X86
|
||||
class << self
|
||||
# Yap, bits.
|
||||
# @return [Integer]
|
||||
def bits
|
||||
32
|
||||
end
|
||||
|
||||
@@ -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 = +''
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,6 +10,10 @@ module OneGadget
|
||||
# Super class for amd64 and i386 processor.
|
||||
class X86 < Processor
|
||||
# Constructor for a x86 processor.
|
||||
# @param [Array<String>] 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+$/
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (+<execlp@@GLIBC_2.4+0x136>+) 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<String>] 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -34,6 +34,7 @@ module OneGadget
|
||||
# @param [Array<String>] options The options.
|
||||
# @example
|
||||
# objdump.extra_options = %w[-M intel]
|
||||
# @return [void]
|
||||
def extra_options=(options)
|
||||
@options = options
|
||||
end
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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<OneGadget::Gadget::Gadget>] ary
|
||||
# @param [Integer] count
|
||||
# @return [Array<OneGadget::Gadget::Gadget>]
|
||||
def take_until(ary, count)
|
||||
return ary if ary.size <= count
|
||||
|
||||
|
||||
Reference in New Issue
Block a user