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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +197 -65
- data/README.md +96 -22
- data/lib/one_gadget/abi.rb +41 -5
- data/lib/one_gadget/builds/libc-2.31-93b46e0027747153e336c3fd9431ce9a5d82ad00.rb +563 -0
- data/lib/one_gadget/builds/libc-2.35-891c1403437a4e30e684e0c8e34b87a09e4298e5.rb +434 -0
- data/lib/one_gadget/builds/libc-2.39-a1d1cf4badf1f5dfe57ff1d17bd692ccc6fcd5c5.rb +531 -0
- data/lib/one_gadget/builds/libc-2.39-cd8f5a207dd67aea370d2b471a54c3e56f44ab18.rb +531 -0
- data/lib/one_gadget/builds/libc-2.43-b50ceafbd17dc6bceee344a66671c7eaa152bef4.rb +15 -0
- data/lib/one_gadget/emulators/aarch64.rb +5 -2
- data/lib/one_gadget/emulators/amd64.rb +1 -0
- data/lib/one_gadget/emulators/arm.rb +27 -18
- data/lib/one_gadget/emulators/arm_family.rb +35 -160
- data/lib/one_gadget/emulators/conditional.rb +28 -22
- data/lib/one_gadget/emulators/constraints.rb +269 -0
- data/lib/one_gadget/emulators/data_processing.rb +167 -0
- data/lib/one_gadget/emulators/i386.rb +4 -6
- data/lib/one_gadget/emulators/instruction.rb +24 -1
- data/lib/one_gadget/emulators/lambda.rb +3 -1
- data/lib/one_gadget/emulators/mips.rb +289 -0
- data/lib/one_gadget/emulators/processor.rb +33 -455
- data/lib/one_gadget/emulators/register_file.rb +19 -10
- data/lib/one_gadget/emulators/riscv64.rb +265 -0
- data/lib/one_gadget/emulators/safe_calls.rb +9 -3
- data/lib/one_gadget/emulators/tracked_memory.rb +209 -0
- data/lib/one_gadget/emulators/x86.rb +32 -22
- data/lib/one_gadget/fetchers/aarch64.rb +0 -27
- data/lib/one_gadget/fetchers/amd64.rb +0 -24
- data/lib/one_gadget/fetchers/argument_resolution.rb +340 -0
- data/lib/one_gadget/fetchers/arm.rb +99 -44
- data/lib/one_gadget/fetchers/base.rb +142 -640
- data/lib/one_gadget/fetchers/candidate_walk.rb +150 -0
- data/lib/one_gadget/fetchers/disassembly.rb +252 -0
- data/lib/one_gadget/fetchers/dynamic_symbols.rb +104 -0
- data/lib/one_gadget/fetchers/i386.rb +5 -8
- data/lib/one_gadget/fetchers/mips.rb +478 -0
- data/lib/one_gadget/fetchers/objdump.rb +24 -2
- data/lib/one_gadget/fetchers/riscv64.rb +78 -0
- data/lib/one_gadget/fetchers/x86.rb +19 -0
- data/lib/one_gadget/fetchers.rb +22 -6
- data/lib/one_gadget/gadget.rb +25 -34
- data/lib/one_gadget/helper.rb +19 -5
- data/lib/one_gadget/one_gadget.rb +6 -0
- data/lib/one_gadget/version.rb +1 -1
- data/lib/one_gadget.rb +1 -1
- metadata +18 -6
- data/lib/one_gadget/builds/libc-2.26-2104f3d4ad5cf68603afbe7ba1a17f5ac99c5988.rb +0 -227
- data/lib/one_gadget/builds/libc-2.26-ddcc13122ddbfe5e5ef77d4ebe66d124ae5762c2.rb +0 -300
- 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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|