one_gadget 2.0.0 → 2.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +197 -65
  3. data/README.md +96 -22
  4. data/lib/one_gadget/abi.rb +41 -5
  5. data/lib/one_gadget/builds/libc-2.31-93b46e0027747153e336c3fd9431ce9a5d82ad00.rb +563 -0
  6. data/lib/one_gadget/builds/libc-2.35-891c1403437a4e30e684e0c8e34b87a09e4298e5.rb +434 -0
  7. data/lib/one_gadget/builds/libc-2.39-a1d1cf4badf1f5dfe57ff1d17bd692ccc6fcd5c5.rb +531 -0
  8. data/lib/one_gadget/builds/libc-2.39-cd8f5a207dd67aea370d2b471a54c3e56f44ab18.rb +531 -0
  9. data/lib/one_gadget/builds/libc-2.43-b50ceafbd17dc6bceee344a66671c7eaa152bef4.rb +15 -0
  10. data/lib/one_gadget/emulators/aarch64.rb +5 -2
  11. data/lib/one_gadget/emulators/amd64.rb +1 -0
  12. data/lib/one_gadget/emulators/arm.rb +27 -18
  13. data/lib/one_gadget/emulators/arm_family.rb +35 -160
  14. data/lib/one_gadget/emulators/conditional.rb +28 -22
  15. data/lib/one_gadget/emulators/constraints.rb +269 -0
  16. data/lib/one_gadget/emulators/data_processing.rb +167 -0
  17. data/lib/one_gadget/emulators/i386.rb +4 -6
  18. data/lib/one_gadget/emulators/instruction.rb +24 -1
  19. data/lib/one_gadget/emulators/lambda.rb +3 -1
  20. data/lib/one_gadget/emulators/mips.rb +289 -0
  21. data/lib/one_gadget/emulators/processor.rb +33 -455
  22. data/lib/one_gadget/emulators/register_file.rb +19 -10
  23. data/lib/one_gadget/emulators/riscv64.rb +265 -0
  24. data/lib/one_gadget/emulators/safe_calls.rb +9 -3
  25. data/lib/one_gadget/emulators/tracked_memory.rb +209 -0
  26. data/lib/one_gadget/emulators/x86.rb +32 -22
  27. data/lib/one_gadget/fetchers/aarch64.rb +0 -27
  28. data/lib/one_gadget/fetchers/amd64.rb +0 -24
  29. data/lib/one_gadget/fetchers/argument_resolution.rb +340 -0
  30. data/lib/one_gadget/fetchers/arm.rb +99 -44
  31. data/lib/one_gadget/fetchers/base.rb +142 -640
  32. data/lib/one_gadget/fetchers/candidate_walk.rb +150 -0
  33. data/lib/one_gadget/fetchers/disassembly.rb +252 -0
  34. data/lib/one_gadget/fetchers/dynamic_symbols.rb +104 -0
  35. data/lib/one_gadget/fetchers/i386.rb +5 -8
  36. data/lib/one_gadget/fetchers/mips.rb +478 -0
  37. data/lib/one_gadget/fetchers/objdump.rb +24 -2
  38. data/lib/one_gadget/fetchers/riscv64.rb +78 -0
  39. data/lib/one_gadget/fetchers/x86.rb +19 -0
  40. data/lib/one_gadget/fetchers.rb +22 -6
  41. data/lib/one_gadget/gadget.rb +25 -34
  42. data/lib/one_gadget/helper.rb +19 -5
  43. data/lib/one_gadget/one_gadget.rb +6 -0
  44. data/lib/one_gadget/version.rb +1 -1
  45. data/lib/one_gadget.rb +1 -1
  46. metadata +18 -6
  47. data/lib/one_gadget/builds/libc-2.26-2104f3d4ad5cf68603afbe7ba1a17f5ac99c5988.rb +0 -227
  48. data/lib/one_gadget/builds/libc-2.26-ddcc13122ddbfe5e5ef77d4ebe66d124ae5762c2.rb +0 -300
  49. data/lib/one_gadget/builds/libc-2.26-f65648a832414f2144ce795d75b6045a1ec2e252.rb +0 -199
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OneGadget
4
+ module Fetchers
5
+ # Where a gadget could start: a backward control-flow walk from each terminal
6
+ # call, yielding every instruction sequence that reaches it. A conditional
7
+ # branch on the way is explored both ways and its decision becomes a constraint
8
+ # on the candidate, bounded so a loop-heavy region cannot fork forever. Mixed
9
+ # into {Base}.
10
+ module CandidateWalk
11
+ # Give up on a control-flow path once it has crossed this many conditional branches.
12
+ MAX_FORKS = 4
13
+
14
+ # Hard cap on a single path's length (loop/runaway guard).
15
+ PATH_BUDGET = 80
16
+
17
+ # Fetch candidates that end with call exec*.
18
+ #
19
+ # Provide a block to filter gadget candidates.
20
+ # @yieldparam [String] cand
21
+ # Is this candidate valid?
22
+ # @yieldreturn [Boolean]
23
+ # True for valid.
24
+ # @return [Array<String>]
25
+ # Each +String+ returned is multi-lines of assembly code.
26
+ def candidates(&)
27
+ branch_aware_candidates(&)
28
+ end
29
+
30
+ private
31
+
32
+ # Enumerate gadget candidates by walking the control-flow graph *backward*
33
+ # from each terminal call along its predecessors (the instruction that falls
34
+ # through to it, plus any branch that targets it), forking at conditional
35
+ # edges up to {MAX_FORKS} times. Each emitted candidate is a flat,
36
+ # address-preserving line-list ending at the terminal call, consumed
37
+ # unchanged by {#find}; the emulator turns each crossed conditional edge into
38
+ # a constraint (see {OneGadget::Emulators::Conditional}).
39
+ def branch_aware_candidates(&)
40
+ cands = []
41
+ re = /#{terminal_call_regexp}/
42
+ disasm_lines.each_with_index do |line, idx|
43
+ next unless line.match?(re)
44
+
45
+ back_walk(idx, 0, Set.new, []) { |lines| cands << lines.join("\n") }
46
+ end
47
+ cands.uniq!
48
+ cands.select!(&) if block_given?
49
+ cands
50
+ end
51
+
52
+ # Depth-first backward walk. +visited+ (a Set) and +path+ are mutated with
53
+ # backtracking so a fork explores independently without per-step copies;
54
+ # +path+ is built in forward order (the terminal call stays last). Emits a
55
+ # candidate at each leaf - {#find} then tries every start line within it.
56
+ def back_walk(idx, forks, visited, path, &blk)
57
+ addr = addr_at(idx)
58
+ # Out of budget is a place to stop walking, not a reason to throw away
59
+ # what has been walked: the lines already on the path reach the call
60
+ # whether or not anything before them is ever looked at.
61
+ return blk.call(path.dup) if path.size >= PATH_BUDGET
62
+
63
+ visited.add(addr)
64
+ path.unshift(disasm_lines[idx])
65
+ edges = predecessors(idx).reject do |pidx, cond|
66
+ visited.include?(addr_at(pidx)) || (cond && forks >= MAX_FORKS)
67
+ end
68
+ if edges.empty?
69
+ blk.call(path.dup)
70
+ else
71
+ edges.each { |pidx, cond| back_walk(pidx, forks + (cond ? 1 : 0), visited, path, &blk) }
72
+ end
73
+ path.shift
74
+ visited.delete(addr)
75
+ end
76
+
77
+ # The address of the instruction at +idx+, remembered as it is asked for.
78
+ # Only the lines the backward walk reaches ever need one, a small part of a
79
+ # whole libc's disassembly.
80
+ def addr_at(idx)
81
+ (@addr_at ||= {})[idx] ||= offset_of(disasm_lines[idx])
82
+ end
83
+
84
+ # Predecessors of +disasm_lines[idx]+ as +[pred_index, conditional_edge?]+:
85
+ # the instruction that falls through to it, plus any branch that targets it.
86
+ # Computed lazily (the walk only touches lines near terminal calls) and cached.
87
+ def predecessors(idx)
88
+ (@predecessors ||= {})[idx] ||= begin
89
+ preds = []
90
+ # Nothing falls through into the first line of a window: the line before
91
+ # it is the last of another window, a different part of the binary.
92
+ if idx.positive? && !window_starts.key?(idx)
93
+ kind = branch_kind(disasm_lines[idx - 1])
94
+ preds << [idx - 1, kind == :conditional] unless %i[unconditional terminator].include?(kind)
95
+ end
96
+ (branch_pred_map[addr_at(idx)] || []).each do |b|
97
+ preds << [b, branch_kind(disasm_lines[b]) == :conditional]
98
+ end
99
+ preds
100
+ end
101
+ end
102
+
103
+ # Map of target-address => indexes of (conditional or unconditional) direct
104
+ # branches that jump there. Scans the whole disassembly once (a branch can
105
+ # target a call region from anywhere), so keep the per-line test cheap.
106
+ def branch_pred_map
107
+ @branch_pred_map ||= Base.cached(:branch_pred, @objdump.command) do
108
+ lead = branch_lead_regex
109
+ disasm_lines.each_with_index.with_object(Hash.new { |h, k| h[k] = [] }) do |(line, i), map|
110
+ # Cheap precompiled reject (no per-line allocation) before the full test.
111
+ next unless line.match?(lead)
112
+ next unless %i[conditional unconditional].include?(branch_kind(line))
113
+
114
+ tgt = branch_target(line)
115
+ map[tgt] << i if tgt
116
+ end
117
+ end
118
+ end
119
+
120
+ # Precompiled over-approximation of a branch line, built from the arch's
121
+ # {#branch_lead_chars}, to skip the full test for the (majority) non-branch
122
+ # lines during the whole-binary scan.
123
+ def branch_lead_regex
124
+ @branch_lead_regex ||= /\A[0-9a-f]+:\s+[#{branch_lead_chars}]/
125
+ end
126
+
127
+ # Parse the (direct) target address of a branch line, or +nil+ if indirect.
128
+ def branch_target(line)
129
+ line.sub(/\A[0-9a-f]+:\s*\S+\s*/, '')[/\b([0-9a-f]+)\b\s*(?:<|\z)/, 1]&.to_i(16)
130
+ end
131
+
132
+ # The mnemonic of an objdump line, memoized because each line is tested by
133
+ # several branch predicates during the CFG scan.
134
+ # @example
135
+ # mnemonic('4a1d0: b.ne 4a200 <foo>') #=> 'b.ne'
136
+ # mnemonic('4a1c0: cbz x0, 4a200') #=> 'cbz'
137
+ def mnemonic(line)
138
+ (@mnemonic ||= {})[line] ||= line[/\A[0-9a-f]+:\s*(\S+)/, 1] || ''
139
+ end
140
+
141
+ # Where in {#disasm_lines} each window begins, keyed for lookup. Empty when
142
+ # the whole file was disassembled, since then every line does follow the one
143
+ # before it.
144
+ # @return [Hash{Integer => true}]
145
+ def window_starts
146
+ disassembly[:starts]
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'elftools'
4
+
5
+ module OneGadget
6
+ module Fetchers
7
+ # Reading the target file: where its terminal calls are, and the disassembly
8
+ # around them. Disassembling a whole libc is the dominant cost of a search, so
9
+ # an architecture that can find its call sites cheaply gets windows around them
10
+ # instead (see {Base#scan_calls}), and everything derived from one objdump
11
+ # command is cached. Mixed into {Base}.
12
+ module Disassembly
13
+ # What a terminal function's name starts with. Read by {DynamicSymbols} too,
14
+ # which finds the same functions by another route.
15
+ TERMINAL_PREFIXES = %w[exec posix_spawn].freeze
16
+
17
+ # A call to +posix_spawn+ itself, not one of the setup helpers that share its
18
+ # prefix. The name ends at the version marker, or at the closing bracket when
19
+ # there is none -- glibc's symbols are versioned, musl's are not, and neither
20
+ # is one recovered from a file with no symbol table (see {DynamicSymbols}).
21
+ TERMINAL_SPAWN = /posix_spawn[@>]/
22
+
23
+ # Enough bytes of a name to tell: a symbol table's string table is read as
24
+ # one blob, so a name is a slice of it.
25
+ TERMINAL_PREFIX_BYTES = 12
26
+ private_constant :TERMINAL_PREFIX_BYTES
27
+
28
+ # How much to disassemble around each terminal call when an architecture can
29
+ # locate the calls cheaply (see {#terminal_call_sites} / {#windowed_disasm}).
30
+ #
31
+ # Measured against a full disassembly of every fixture: 0x368 back and 0x350
32
+ # forward is the least that reports the same gadgets. Both must stay a
33
+ # multiple of four -- objdump decodes from wherever it is told to start, so
34
+ # an offset landing mid-instruction turns the whole window into rubble
35
+ # rather than shifting it.
36
+ WINDOW_BACK = 0x1000
37
+
38
+ # As far past the call, for a predecessor that branches back into the region.
39
+ WINDOW_FWD = 0x1000
40
+
41
+ private
42
+
43
+ # Regexp (as a String) matching an objdump line that calls a terminal
44
+ # function (+exec*+ / +posix_spawn*+) we can turn into a gadget.
45
+ def terminal_call_regexp
46
+ "#{call_str}.*<(exec[^+]*|posix_spawn[^+]*)>$"
47
+ end
48
+
49
+ # The target's objdump disassembly as stripped +"ADDR: insn"+ lines.
50
+ def disasm_lines
51
+ disassembly[:lines]
52
+ end
53
+
54
+ # The disassembly and the shape it was taken in, cached (per objdump command)
55
+ # for the lifetime of the fetcher.
56
+ # @return [Hash{Symbol => Array<String>, Hash}]
57
+ def disassembly
58
+ @disassembly ||= begin
59
+ # Before the command is read: it is what says how to disassemble, and
60
+ # what the cache is keyed on.
61
+ prepare_raw_disassembly if sectionless?
62
+ Base.cached(:disasm, @objdump.command) do
63
+ sites = terminal_call_sites
64
+ sites.nil? || sites.empty? ? full_disasm : windowed_disasm(sites)
65
+ end
66
+ end
67
+ end
68
+
69
+ # Disassemble the whole file (the exhaustive default).
70
+ def full_disasm
71
+ { lines: objdump_lines, starts: {} }
72
+ end
73
+
74
+ # Disassemble only [call-WINDOW_BACK, call+WINDOW_FWD] around each call site,
75
+ # merging overlaps. Used when {#terminal_call_sites} located the calls without
76
+ # a full disassembly (the win on the slow-to-objdump Thumb-2 arm libcs).
77
+ def windowed_disasm(sites)
78
+ windows = sites.sort.map { |a| [[a - WINDOW_BACK, 0].max, a + WINDOW_FWD] }
79
+ # One objdump per range, all at once: they are separate processes that
80
+ # wait on each other for nothing, and a libc comes to a handful of windows.
81
+ disassembled = merge_ranges(windows)
82
+ .flat_map { |lo, hi| decode_ranges(lo, hi) }
83
+ .map { |lo, hi, extra| Thread.new { objdump_lines(start: lo, stop: hi, extra:) } }
84
+ .map(&:value)
85
+ starts = {}
86
+ lines = disassembled.each_with_object([]) do |window, acc|
87
+ starts[acc.size] = true
88
+ acc.concat(window)
89
+ end
90
+ { lines:, starts: }
91
+ end
92
+
93
+ # Merge a list of sorted [lo, hi] ranges, coalescing any that overlap.
94
+ def merge_ranges(ranges)
95
+ ranges.each_with_object([]) do |(lo, hi), merged|
96
+ if merged.last && lo <= merged.last[1]
97
+ merged.last[1] = hi if hi > merged.last[1]
98
+ else
99
+ merged << [lo, hi]
100
+ end
101
+ end
102
+ end
103
+
104
+ # An objdump line carries an instruction when it opens with its address.
105
+ DISASSEMBLED = /\A[0-9a-f]+:/
106
+ private_constant :DISASSEMBLED
107
+
108
+ # Whether this file ships without section headers, which is what makes the
109
+ # ordinary route to its code and symbols unavailable (see {DynamicSymbols}).
110
+ # @return [Boolean]
111
+ def sectionless?
112
+ return @sectionless unless @sectionless.nil?
113
+
114
+ @sectionless = File.open(file) { |fd| ELFTools::ELFFile.new(fd).num_sections.zero? }
115
+ end
116
+
117
+ # Point objdump at the bytes rather than the ELF, once, when there is no
118
+ # other way to read the file.
119
+ # @return [void]
120
+ def prepare_raw_disassembly
121
+ File.open(file) do |fd|
122
+ elf = ELFTools::ELFFile.new(fd)
123
+ seg = executable_segment(elf)
124
+ next if seg.nil?
125
+
126
+ @raw_symbols = dynamic_symbols(elf)
127
+ record_instruction_sets(elf)
128
+ @objdump.read_raw(machine: OneGadget::Helper.objdump_arch(OneGadget::Helper.architecture(file)),
129
+ endian: elf.endian,
130
+ vma: seg.mem_head - seg.file_head)
131
+ end
132
+ end
133
+
134
+ # How +lo+ to +hi+ must be disassembled: as one range by default. An
135
+ # architecture that encodes different parts of its code differently splits
136
+ # it where the encoding changes, since objdump reads a whole range one way.
137
+ # @param [Integer] lo
138
+ # @param [Integer] hi
139
+ # @return [Array<(Integer, Integer, Array<String>)>]
140
+ # Each range, with the objdump options that read it correctly.
141
+ def decode_ranges(lo, hi)
142
+ [[lo, hi, []]]
143
+ end
144
+
145
+ # Note how the file says its code is encoded, for {#decode_ranges} to split
146
+ # on. Nothing to note for an architecture with one encoding.
147
+ # @param [ELFTools::ELFFile] elf
148
+ # @return [void]
149
+ def record_instruction_sets(elf); end
150
+
151
+ def objdump_lines(start: nil, stop: nil, extra: [])
152
+ # One pass, one string per line: a whole libc is hundreds of thousands of
153
+ # them, and only the instructions are wanted.
154
+ symbols = @raw_symbols
155
+ `#{@objdump.command(start:, stop:, extra:)}`.each_line.filter_map do |line|
156
+ line = line.strip
157
+ next unless DISASSEMBLED.match?(line)
158
+
159
+ symbols ? symbolize(line, symbols) : line
160
+ end
161
+ end
162
+
163
+ # Addresses of the calls reaching a terminal function, found without
164
+ # disassembling anything: the +exec*+/+posix_spawn*+ symbols are read out of
165
+ # the ELF and the segment holding the code is scanned for direct calls into
166
+ # them (see {#scan_calls}). +nil+ when the file cannot be read that way, so
167
+ # the caller disassembles everything instead.
168
+ # @return [Array<Integer>, nil]
169
+ def terminal_call_sites
170
+ File.open(file) do |fd|
171
+ elf = ELFTools::ELFFile.new(fd)
172
+ targets = terminal_symbol_addresses(elf)
173
+ return [] if targets.empty?
174
+
175
+ seg = executable_segment(elf)
176
+ return nil if seg.nil?
177
+
178
+ scan_calls(seg.mem_head, seg.data, targets)
179
+ end
180
+ rescue ELFTools::ELFError
181
+ nil # not something we can scan; disassemble everything
182
+ end
183
+
184
+ # The loadable segment holding the code, which is where a call scan runs and
185
+ # what raw disassembly is taken from. It holds a little besides the code, and
186
+ # is used in place of a section because a stripped file has none.
187
+ # @param [ELFTools::ELFFile] elf
188
+ # @return [ELFTools::Segments::LoadSegment, nil]
189
+ def executable_segment(elf)
190
+ elf.segments_by_type(:load).find(&:executable?)
191
+ end
192
+
193
+ # Where the +exec*+/+posix_spawn*+ symbols live, keyed for lookup. Read
194
+ # straight out of the tables rather than built into an object per symbol: a
195
+ # libc has thousands, and only two fields of each are wanted.
196
+ # @param [ELFTools::ELFFile] elf
197
+ # @return [Hash{Integer => true}]
198
+ def terminal_symbol_addresses(elf)
199
+ return sectionless_terminal_addresses(elf) if elf.num_sections.zero?
200
+
201
+ addrs = {}
202
+ %w[.dynsym .symtab].each do |name|
203
+ sec = elf.section_by_name(name)
204
+ next if sec.nil?
205
+
206
+ strtab = elf.sections[sec.header.sh_link.to_i].data
207
+ each_symbol(elf, sec) do |name_offset, value|
208
+ symbol = strtab.byteslice(name_offset, TERMINAL_PREFIX_BYTES)
209
+ addrs[symbol_address(value)] = true if symbol&.start_with?(*TERMINAL_PREFIXES)
210
+ end
211
+ end
212
+ addrs
213
+ end
214
+
215
+ # Each symbol's name offset and value, unpacked from the table's bytes. A
216
+ # 32-bit entry is four words with the value second; a 64-bit one is six, the
217
+ # value being the pair from the third. The table is the file's own data, so
218
+ # it is read in the byte order the file states.
219
+ # @param [ELFTools::ELFFile] elf
220
+ # @param [ELFTools::Sections::Section] section
221
+ # @yieldparam [Integer] name_offset
222
+ # @yieldparam [Integer] value
223
+ # @return [void]
224
+ def each_symbol(elf, section)
225
+ wide = elf.elf_class == 64
226
+ big = elf.endian == :big
227
+ section.data.unpack(big ? 'N*' : 'V*').each_slice(wide ? 6 : 4) do |words|
228
+ value = wide ? wide_value(words, big) : words[1]
229
+ next if value.nil? || value.zero?
230
+
231
+ yield(words[0], value)
232
+ end
233
+ end
234
+
235
+ # A 64-bit value stated as two words, the more significant of which comes
236
+ # first in a big-endian file.
237
+ # @param [Array<Integer>] words One symbol table entry.
238
+ # @param [Boolean] big
239
+ # @return [Integer, nil] +nil+ for a truncated entry.
240
+ def wide_value(words, big)
241
+ low, high = big ? [words[3], words[2]] : [words[2], words[3]]
242
+ low && high && (low | (high << 32))
243
+ end
244
+
245
+ # Map from an instruction's address to its index in {#disasm_lines}, so a
246
+ # given address can be located in the disassembly in O(1).
247
+ def disasm_index
248
+ @disasm_index ||= disasm_lines.each_with_index.to_h { |line, i| [line[/\A([0-9a-f]+):/, 1].to_i(16), i] }
249
+ end
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'elftools'
4
+
5
+ require 'one_gadget/emulators/processor'
6
+ require 'one_gadget/emulators/safe_calls'
7
+
8
+ module OneGadget
9
+ module Fetchers
10
+ # Reading a shared object that ships without section headers, as an embedded
11
+ # libc commonly does: OpenWrt strips them from musl to save flash, and a
12
+ # vendor firmware may do the same to glibc.
13
+ #
14
+ # Nothing is actually missing from such a file -- the loader still has to find
15
+ # the symbols, so they are reachable through +PT_DYNAMIC+ -- but the usual
16
+ # route to them is not. objdump disassembles sections and so produces nothing
17
+ # at all, and the engine recognises a terminal call by the symbol objdump
18
+ # prints beside it. Both are answered here: the symbols are read out of the
19
+ # dynamic segment, and the raw disassembly is rewritten to name them, so
20
+ # everything downstream reads what it always reads.
21
+ module DynamicSymbols
22
+ # Where a line that transfers control says it goes. Raw disassembly writes
23
+ # every destination as +0x<addr>+, and the rest of the line is whatever the
24
+ # architecture puts around it: the destination may be separated by a comma,
25
+ # and followed by the note objdump appends to name the mnemonic's alias.
26
+ # @example
27
+ # 'call 0x94180'
28
+ # 'beqz a5,0x43060'
29
+ # 'b.ls 0x47dcc // b.plast'
30
+ CONTROL_TARGET = %r{\A(.*[\s,])0x([0-9a-f]+)(?:\s+//.*)?\z}
31
+
32
+ # An address any other line names, which it states last and alone -- so a
33
+ # value the line merely operates on is left as it is written.
34
+ # @example An architecture may resolve a pc-relative operand from this.
35
+ # 'lea rcx,[rip+0x19dabe] # 0x1eb960'
36
+ TRAILING_ADDRESS = /\A(.*\s)0x([0-9a-f]+)\z/
37
+
38
+ private
39
+
40
+ # Where the terminal +exec*+/+posix_spawn*+ entry points live, read through
41
+ # the dynamic segment. Keyed for lookup, as {#terminal_symbol_addresses} is.
42
+ # @param [ELFTools::ELFFile] elf
43
+ # @return [Hash{Integer => true}]
44
+ def sectionless_terminal_addresses(elf)
45
+ dynamic_symbols(elf).each_with_object({}) do |(addr, name), addrs|
46
+ addrs[symbol_address(addr)] = true if name.start_with?(*Disassembly::TERMINAL_PREFIXES)
47
+ end
48
+ end
49
+
50
+ # Every function the dynamic symbol table names, as +{address => name}+ --
51
+ # where a file stripped of its sections still records them.
52
+ # @param [ELFTools::ELFFile] elf
53
+ # @return [Hash{Integer => String}]
54
+ # @example Several names may share an address; the one the engine can act on
55
+ # wins (see {#name_rank}).
56
+ # dynamic_symbols(elf)[0x43470] #=> '__sigsuspend'
57
+ def dynamic_symbols(elf)
58
+ @dynamic_symbols ||= (elf.dynamic&.symbols || []).each_with_object({}) do |symbol, symbols|
59
+ value = symbol.value
60
+ name = symbol.name.to_s
61
+ next if value.zero? || name.empty?
62
+
63
+ addr = symbol_address(value)
64
+ symbols[addr] = name if !symbols.key?(addr) || name_rank(name) >= name_rank(symbols[addr])
65
+ end
66
+ end
67
+
68
+ # How much the engine can do with +name+: it recognises a terminal entry
69
+ # point by naming it exactly, a call it can step over by containing one of
70
+ # the catalog's names, and nothing else by name at all. Higher is more.
71
+ # @param [String] name One symbol name.
72
+ # @return [Integer]
73
+ # @example Two names for one address, of which only the second is read.
74
+ # name_rank('sigaction') #=> 0
75
+ # name_rank('__sigaction') #=> 1
76
+ def name_rank(name)
77
+ return 2 if OneGadget::Emulators::Processor::TERMINAL_CALL_RE.match?(name)
78
+ return 1 if OneGadget::Emulators::SafeCalls::COMMON.keys.any? { |known| name.include?(known) }
79
+
80
+ 0
81
+ end
82
+
83
+ # Rewrite a raw-disassembly line into what reading the ELF would have
84
+ # produced: an address objdump wrote as +0x<addr>+ becomes the bare address,
85
+ # named when a symbol is there. The engine recognises a terminal call, reads
86
+ # a branch target, and matches a safe call by name, from exactly that.
87
+ #
88
+ # @param [String] line One disassembled line.
89
+ # @param [Hash{Integer => String}] symbols
90
+ # @return [String]
91
+ # @example A destination is rewritten and named; an operand that merely looks
92
+ # like one is left alone ({CONTROL_TARGET} against {TRAILING_ADDRESS}).
93
+ # symbolize('e6570: call 0x94180', symbols) #=> 'e6570: call 94180 <execve>'
94
+ # symbolize('e6570: mov rax,0x94180', symbols) #=> 'e6570: mov rax,0x94180'
95
+ def symbolize(line, symbols)
96
+ m = line.match(control_transfer?(line) ? CONTROL_TARGET : TRAILING_ADDRESS) or return line
97
+
98
+ name = symbols[m[2].to_i(16)]
99
+ named = name ? " <#{name}>" : ''
100
+ "#{m[1]}#{m[2]}#{named}"
101
+ end
102
+ end
103
+ end
104
+ end
@@ -11,14 +11,9 @@ module OneGadget
11
11
  class I386 < OneGadget::Fetchers::X86
12
12
  private
13
13
 
14
- def candidates
15
- rel_sh_hex = rel_sh.to_s(16)
16
- super do |candidate|
17
- next false unless candidate.include?(rel_sh_hex)
18
-
19
- true
20
- end
21
- end
14
+ # PIC reaches the string through the GOT, so what a window holds is the
15
+ # distance between them rather than the string's own offset.
16
+ def bin_sh_reference = rel_sh
22
17
 
23
18
  def emulator
24
19
  OneGadget::Emulators::I386.new
@@ -82,6 +77,8 @@ module OneGadget
82
77
  end
83
78
  end
84
79
 
80
+ # How far +"/bin/sh"+ sits before the GOT, which is how PIC code names it:
81
+ # a window reaches the string as an offset from the register holding the base.
85
82
  def rel_sh
86
83
  @rel_sh ||= got_offset - str_offset('/bin/sh')
87
84
  end