patchelf 0.0.0 → 1.2.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.
@@ -1,8 +1,11 @@
1
- require 'elftools'
2
- require 'fileutils'
1
+ # encoding: ascii-8bit
2
+ # frozen_string_literal: true
3
3
 
4
+ require 'elftools/elf_file'
5
+
6
+ require 'patchelf/exceptions'
4
7
  require 'patchelf/logger'
5
- require 'patchelf/mm'
8
+ require 'patchelf/saver'
6
9
 
7
10
  module PatchELF
8
11
  # Class to handle all patching things.
@@ -10,13 +13,36 @@ module PatchELF
10
13
  # @!macro [new] note_apply
11
14
  # @note This setting will be saved after {#save} being invoked.
12
15
 
16
+ attr_reader :elf # @return [ELFTools::ELFFile] ELF parser object.
17
+
13
18
  # Instantiate a {Patcher} object.
14
19
  # @param [String] filename
15
20
  # Filename of input ELF.
16
- def initialize(filename)
21
+ # @param [Boolean] logging
22
+ # *deprecated*: use +on_error+ instead
23
+ # @param [:log, :silent, :exception] on_error
24
+ # action when the desired segment/tag field isn't present
25
+ # :log = logs to stderr
26
+ # :exception = raise exception related to the error
27
+ # :silent = ignore the errors
28
+ def initialize(filename, on_error: :log, logging: true)
17
29
  @in_file = filename
18
30
  @elf = ELFTools::ELFFile.new(File.open(filename))
19
31
  @set = {}
32
+ @rpath_sym = :runpath
33
+ @on_error = !logging ? :exception : on_error
34
+
35
+ on_error_syms = %i[exception log silent]
36
+ raise ArgumentError, "on_error must be one of #{on_error_syms}" unless on_error_syms.include?(@on_error)
37
+ end
38
+
39
+ # @return [String?]
40
+ # Get interpreter's name.
41
+ # @example
42
+ # PatchELF::Patcher.new('/bin/ls').interpreter
43
+ # #=> "/lib64/ld-linux-x86-64.so.2"
44
+ def interpreter
45
+ @set[:interpreter] || interpreter_
20
46
  end
21
47
 
22
48
  # Set interpreter's name.
@@ -26,11 +52,73 @@ module PatchELF
26
52
  # @param [String] interp
27
53
  # @macro note_apply
28
54
  def interpreter=(interp)
29
- return if interpreter.nil? # will also show warning if there's no interp segment.
55
+ return if interpreter_.nil? # will also show warning if there's no interp segment.
30
56
 
31
57
  @set[:interpreter] = interp
32
58
  end
33
59
 
60
+ # Get needed libraries.
61
+ # @return [Array<String>]
62
+ # @example
63
+ # patcher = PatchELF::Patcher.new('/bin/ls')
64
+ # patcher.needed
65
+ # #=> ["libselinux.so.1", "libc.so.6"]
66
+ def needed
67
+ @set[:needed] || needed_
68
+ end
69
+
70
+ # Set needed libraries.
71
+ # @param [Array<String>] needs
72
+ # @macro note_apply
73
+ def needed=(needs)
74
+ @set[:needed] = needs
75
+ end
76
+
77
+ # Add the needed library.
78
+ # @param [String] need
79
+ # @return [void]
80
+ # @macro note_apply
81
+ def add_needed(need)
82
+ @set[:needed] ||= needed_
83
+ @set[:needed] << need
84
+ end
85
+
86
+ # Remove the needed library.
87
+ # @param [String] need
88
+ # @return [void]
89
+ # @macro note_apply
90
+ def remove_needed(need)
91
+ @set[:needed] ||= needed_
92
+ @set[:needed].delete(need)
93
+ end
94
+
95
+ # Replace needed library +src+ with +tar+.
96
+ #
97
+ # @param [String] src
98
+ # Library to be replaced.
99
+ # @param [String] tar
100
+ # Library replace with.
101
+ # @return [void]
102
+ # @macro note_apply
103
+ def replace_needed(src, tar)
104
+ @set[:needed] ||= needed_
105
+ @set[:needed].map! { |v| v == src ? tar : v }
106
+ end
107
+
108
+ # Get the soname of a shared library.
109
+ # @return [String?] The name.
110
+ # @example
111
+ # patcher = PatchELF::Patcher.new('/bin/ls')
112
+ # patcher.soname
113
+ # # [WARN] Entry DT_SONAME not found, not a shared library?
114
+ # #=> nil
115
+ # @example
116
+ # PatchELF::Patcher.new('/lib/x86_64-linux-gnu/libc.so.6').soname
117
+ # #=> "libc.so.6"
118
+ def soname
119
+ @set[:soname] || soname_
120
+ end
121
+
34
122
  # Set soname.
35
123
  #
36
124
  # If the input ELF is not a shared library with a soname,
@@ -38,115 +126,81 @@ module PatchELF
38
126
  # @param [String] name
39
127
  # @macro note_apply
40
128
  def soname=(name)
41
- return if soname.nil?
129
+ return if soname_.nil?
42
130
 
43
131
  @set[:soname] = name
44
132
  end
45
133
 
46
- # Set rpath.
134
+ # Get runpath.
135
+ # @return [String?]
136
+ def runpath
137
+ @set[@rpath_sym] || runpath_(@rpath_sym)
138
+ end
139
+
140
+ # Get rpath
141
+ # return [String?]
142
+ def rpath
143
+ @set[:rpath] || runpath_(:rpath)
144
+ end
145
+
146
+ # Set rpath
47
147
  #
48
- # If DT_RPATH is not presented in the input ELF,
49
- # a new DT_RPATH attribute will be inserted into the DYNAMIC segment.
148
+ # Modify / set DT_RPATH of the given ELF.
149
+ # similar to runpath= except DT_RPATH is modifed/created in DYNAMIC segment.
50
150
  # @param [String] rpath
51
151
  # @macro note_apply
52
152
  def rpath=(rpath)
53
153
  @set[:rpath] = rpath
54
154
  end
55
155
 
156
+ # Set runpath.
157
+ #
158
+ # If DT_RUNPATH is not presented in the input ELF,
159
+ # a new DT_RUNPATH attribute will be inserted into the DYNAMIC segment.
160
+ # @param [String] runpath
161
+ # @macro note_apply
162
+ def runpath=(runpath)
163
+ @set[@rpath_sym] = runpath
164
+ end
165
+
166
+ # Set all operations related to DT_RUNPATH to use DT_RPATH.
167
+ # @return [self]
168
+ def use_rpath!
169
+ @rpath_sym = :rpath
170
+ self
171
+ end
172
+
56
173
  # Save the patched ELF as +out_file+.
57
174
  # @param [String?] out_file
58
175
  # If +out_file+ is +nil+, the original input file will be modified.
59
176
  # @return [void]
60
177
  def save(out_file = nil)
61
- # TODO: Test if we can save twice, and the output files are exactly same.
62
178
  # If nothing is modified, return directly.
63
179
  return if out_file.nil? && !dirty?
64
180
 
65
181
  out_file ||= @in_file
66
- # [{Integer => String}]
67
- @inline_patch = {}
68
- @mm = PatchELF::MM.new(@elf)
69
- # Patching interpreter is the easiest.
70
- patch_interpreter(@set[:interpreter])
71
-
72
- @mm.dispatch!
73
-
74
- FileUtils.cp(@in_file, out_file) if out_file != @in_file
75
- # if @mm.extend_size != 0:
76
- # 1. Remember all data after the original second LOAD
77
- # 2. Apply patches before the second LOAD.
78
- # 3. Apply patches located after the second LOAD.
79
-
80
- File.open(out_file, 'r+') do |f|
81
- if @mm.extended?
82
- original_head = @mm.threshold
83
- extra = {}
84
- # Copy all data after the second load
85
- @elf.stream.pos = original_head
86
- extra[original_head + @mm.extend_size] = @elf.stream.read # read to end
87
- # zero out the 'gap' we created
88
- extra[original_head] = "\x00" * @mm.extend_size
89
- extra.each do |pos, str|
90
- f.pos = pos
91
- f.write(str)
92
- end
93
- end
94
- @elf.patches.each do |pos, str|
95
- f.pos = @mm.extended_offset(pos)
96
- f.write(str)
97
- end
182
+ saver = PatchELF::Saver.new(@in_file, out_file, @set)
98
183
 
99
- @inline_patch.each do |pos, str|
100
- f.pos = pos
101
- f.write(str)
102
- end
103
- end
104
-
105
- # Let output file have the same permission as input.
106
- FileUtils.chmod(File.stat(@in_file).mode, out_file)
184
+ saver.save!
107
185
  end
108
186
 
109
- # Get name(s) of interpreter, needed libraries, rpath, or soname.
110
- #
111
- # @param [:interpreter, :needed, :rpath, :soname] name
112
- # @return [String, Array<String>, nil]
113
- # Returns name(s) fetched from ELF.
114
- # @example
115
- # patcher = Patcher.new('/bin/ls')
116
- # patcher.get(:interpreter)
117
- # #=> "/lib64/ld-linux-x86-64.so.2"
118
- # patcher.get(:needed)
119
- # #=> ["libselinux.so.1", "libc.so.6"]
120
- #
121
- # patcher.get(:soname)
122
- # # [WARN] Entry DT_SONAME not found, not a shared library?
123
- # #=> nil
124
- # @example
125
- # Patcher.new('/lib/x86_64-linux-gnu/libc.so.6').get(:soname)
126
- # #=> "libc.so.6"
127
- def get(name)
128
- return unless %i[interpreter needed rpath soname].include?(name)
129
- return @set[name] if @set[name]
187
+ private
130
188
 
131
- __send__(name)
132
- end
189
+ def log_or_raise(msg, exception = PatchELF::PatchError)
190
+ raise exception, msg if @on_error == :exception
133
191
 
134
- private
192
+ PatchELF::Logger.warn(msg) if @on_error == :log
193
+ end
135
194
 
136
- # @return [String?]
137
- # Get interpreter's name.
138
- # @example
139
- # Patcher.new('/bin/ls').interpreter
140
- # #=> "/lib64/ld-linux-x86-64.so.2"
141
- def interpreter
195
+ def interpreter_
142
196
  segment = @elf.segment_by_type(:interp)
143
- return PatchELF::Logger.warn('No interpreter found.') if segment.nil?
197
+ return log_or_raise 'No interpreter found.', PatchELF::MissingSegmentError if segment.nil?
144
198
 
145
199
  segment.interp_name
146
200
  end
147
201
 
148
202
  # @return [Array<String>]
149
- def needed
203
+ def needed_
150
204
  segment = dynamic_or_log
151
205
  return if segment.nil?
152
206
 
@@ -154,85 +208,36 @@ module PatchELF
154
208
  end
155
209
 
156
210
  # @return [String?]
157
- def rpath
158
- # TODO: consider both rpath and runpath
159
- tag_name_or_log(:rpath, 'Entry DT_RPATH not found.')
211
+ def runpath_(rpath_sym = :runpath)
212
+ tag_name_or_log(rpath_sym, "Entry DT_#{rpath_sym.to_s.upcase} not found.")
160
213
  end
161
214
 
162
215
  # @return [String?]
163
- def soname
216
+ def soname_
164
217
  tag_name_or_log(:soname, 'Entry DT_SONAME not found, not a shared library?')
165
218
  end
166
219
 
167
- def patch_interpreter(new_interp)
168
- return if new_interp.nil?
169
-
170
- new_interp += "\x00"
171
- old_interp = interpreter + "\x00"
172
- return if old_interp == new_interp
173
-
174
- # These headers must be found here but not in the proc.
175
- seg_header = @elf.segment_by_type(:interp).header
176
- sec_header = section_header('.interp')
177
-
178
- patch = proc do |off, vaddr|
179
- # Register an inline patching
180
- inline_patch(off, new_interp)
181
-
182
- # The patching feature of ELFTools
183
- seg_header.p_offset = off
184
- seg_header.p_vaddr = seg_header.p_paddr = vaddr
185
- seg_header.p_filesz = seg_header.p_memsz = new_interp.size
186
-
187
- if sec_header
188
- sec_header.sh_offset = off
189
- sec_header.sh_size = new_interp.size
190
- end
191
- end
192
-
193
- if new_interp.size <= old_interp.size
194
- # easy case
195
- patch.call(seg_header.p_offset.to_i, seg_header.p_vaddr.to_i)
196
- else
197
- # hard case, we have to request a new LOAD area
198
- @mm.malloc(new_interp.size, &patch)
199
- end
200
- end
201
-
202
220
  # @return [Boolean]
203
221
  def dirty?
204
222
  @set.any?
205
223
  end
206
224
 
207
- # @return [ELFTools::Sections::Section?]
208
- def section_header(name)
209
- sec = @elf.section_by_name(name)
210
- return if sec.nil?
211
-
212
- sec.header
213
- end
214
-
215
225
  def tag_name_or_log(type, log_msg)
216
226
  segment = dynamic_or_log
217
227
  return if segment.nil?
218
228
 
219
229
  tag = segment.tag_by_type(type)
220
- return PatchELF::Logger.warn(log_msg) if tag.nil?
230
+ return log_or_raise log_msg, PatchELF::MissingTagError if tag.nil?
221
231
 
222
232
  tag.name
223
233
  end
224
234
 
225
235
  def dynamic_or_log
226
236
  @elf.segment_by_type(:dynamic).tap do |s|
227
- PatchELF::Logger.warn('DYNAMIC segment not found, might be a statically-linked ELF?') if s.nil?
237
+ if s.nil?
238
+ log_or_raise 'DYNAMIC segment not found, might be a statically-linked ELF?', PatchELF::MissingSegmentError
239
+ end
228
240
  end
229
241
  end
230
-
231
- # This can only be used for patching interpreter's name
232
- # or set strings in a malloc-ed area.
233
- # i.e. NEVER intend to change the string defined in strtab
234
- def inline_patch(off, str)
235
- @inline_patch[@mm.extended_offset(off)] = str
236
- end
237
242
  end
238
243
  end
@@ -0,0 +1,285 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'elftools/constants'
4
+ require 'elftools/elf_file'
5
+ require 'elftools/structs'
6
+ require 'elftools/util'
7
+ require 'fileutils'
8
+
9
+ require 'patchelf/mm'
10
+
11
+ module PatchELF
12
+ # Internal use only.
13
+ #
14
+ # For {Patcher} to do patching things and save to file.
15
+ # @private
16
+ class Saver
17
+ attr_reader :in_file # @return [String] Input filename.
18
+ attr_reader :out_file # @return [String] Output filename.
19
+
20
+ # Instantiate a {Saver} object.
21
+ # @param [String] in_file
22
+ # @param [String] out_file
23
+ # @param [{Symbol => String, Array}] set
24
+ def initialize(in_file, out_file, set)
25
+ @in_file = in_file
26
+ @out_file = out_file
27
+ @set = set
28
+ # [{Integer => String}]
29
+ @inline_patch = {}
30
+ @elf = ELFTools::ELFFile.new(File.open(in_file))
31
+ @mm = PatchELF::MM.new(@elf)
32
+ @strtab_extend_requests = []
33
+ @append_dyn = []
34
+ end
35
+
36
+ # @return [void]
37
+ def save!
38
+ # In this method we assume all attributes that should exist do exist.
39
+ # e.g. DT_INTERP, DT_DYNAMIC. These should have been checked in the patcher.
40
+ patch_interpreter
41
+ patch_dynamic
42
+
43
+ @mm.dispatch!
44
+
45
+ FileUtils.cp(in_file, out_file) if out_file != in_file
46
+ patch_out(@out_file)
47
+ # Let output file have the same permission as input.
48
+ FileUtils.chmod(File.stat(in_file).mode, out_file)
49
+ end
50
+
51
+ private
52
+
53
+ def patch_interpreter
54
+ return if @set[:interpreter].nil?
55
+
56
+ new_interp = @set[:interpreter] + "\x00"
57
+ old_interp = @elf.segment_by_type(:interp).interp_name + "\x00"
58
+ return if old_interp == new_interp
59
+
60
+ # These headers must be found here but not in the proc.
61
+ seg_header = @elf.segment_by_type(:interp).header
62
+ sec_header = section_header('.interp')
63
+
64
+ patch = proc do |off, vaddr|
65
+ # Register an inline patching
66
+ inline_patch(off, new_interp)
67
+
68
+ # The patching feature of ELFTools
69
+ seg_header.p_offset = off
70
+ seg_header.p_vaddr = seg_header.p_paddr = vaddr
71
+ seg_header.p_filesz = seg_header.p_memsz = new_interp.size
72
+
73
+ if sec_header
74
+ sec_header.sh_offset = off
75
+ sec_header.sh_size = new_interp.size
76
+ end
77
+ end
78
+
79
+ if new_interp.size <= old_interp.size
80
+ # easy case
81
+ patch.call(seg_header.p_offset.to_i, seg_header.p_vaddr.to_i)
82
+ else
83
+ # hard case, we have to request a new LOAD area
84
+ @mm.malloc(new_interp.size, &patch)
85
+ end
86
+ end
87
+
88
+ def patch_dynamic
89
+ # We never do inline patching on strtab's string.
90
+ # 1. Search if there's useful string exists
91
+ # - only need header patching
92
+ # 2. Append a new string to the strtab.
93
+ # - register strtab extension
94
+ dynamic.tags # HACK, force @tags to be defined
95
+ patch_soname if @set[:soname]
96
+ patch_runpath if @set[:runpath]
97
+ patch_runpath(:rpath) if @set[:rpath]
98
+ patch_needed if @set[:needed]
99
+ malloc_strtab!
100
+ expand_dynamic!
101
+ end
102
+
103
+ def patch_soname
104
+ # The tag must exist.
105
+ so_tag = dynamic.tag_by_type(:soname)
106
+ reg_str_table(@set[:soname]) do |idx|
107
+ so_tag.header.d_val = idx
108
+ end
109
+ end
110
+
111
+ def patch_runpath(sym = :runpath)
112
+ tag = dynamic.tag_by_type(sym)
113
+ tag = tag.nil? ? lazy_dyn(sym) : tag.header
114
+ reg_str_table(@set[sym]) do |idx|
115
+ tag.d_val = idx
116
+ end
117
+ end
118
+
119
+ # To mark a not-using tag
120
+ IGNORE = ELFTools::Constants::DT_LOOS
121
+ def patch_needed
122
+ original_needs = dynamic.tags_by_type(:needed)
123
+ @set[:needed].uniq!
124
+ # 3 sets:
125
+ # 1. in original and in needs - remain unchanged
126
+ # 2. in original but not in needs - remove
127
+ # 3. not in original and in needs - append
128
+ original_needs.each do |n|
129
+ next if @set[:needed].include?(n.name)
130
+
131
+ n.header.d_tag = IGNORE # temporarily mark
132
+ end
133
+
134
+ extra = @set[:needed] - original_needs.map(&:name)
135
+ original_needs.each do |n|
136
+ break if extra.empty?
137
+ next if n.header.d_tag != IGNORE
138
+
139
+ n.header.d_tag = ELFTools::Constants::DT_NEEDED
140
+ reg_str_table(extra.shift) { |idx| n.header.d_val = idx }
141
+ end
142
+ return if extra.empty?
143
+
144
+ # no spaces, need append
145
+ extra.each do |name|
146
+ tag = lazy_dyn(:needed)
147
+ reg_str_table(name) { |idx| tag.d_val = idx }
148
+ end
149
+ end
150
+
151
+ # Create a temp tag header.
152
+ # @return [ELFTools::Structs::ELF_Dyn]
153
+ def lazy_dyn(sym)
154
+ ELFTools::Structs::ELF_Dyn.new(endian: @elf.endian).tap do |dyn|
155
+ @append_dyn << dyn
156
+ dyn.elf_class = @elf.elf_class
157
+ dyn.d_tag = ELFTools::Util.to_constant(ELFTools::Constants::DT, sym)
158
+ end
159
+ end
160
+
161
+ def expand_dynamic!
162
+ return if @append_dyn.empty?
163
+
164
+ dyn_sec = section_header('.dynamic')
165
+ total = dynamic.tags.map(&:header)
166
+ # the last must be a null-tag
167
+ total = total[0..-2] + @append_dyn + [total.last]
168
+ bytes = total.first.num_bytes * total.size
169
+ @mm.malloc(bytes) do |off, vaddr|
170
+ inline_patch(off, total.map(&:to_binary_s).join)
171
+ dynamic.header.p_offset = off
172
+ dynamic.header.p_vaddr = dynamic.header.p_paddr = vaddr
173
+ dynamic.header.p_filesz = dynamic.header.p_memsz = bytes
174
+ if dyn_sec
175
+ dyn_sec.sh_offset = off
176
+ dyn_sec.sh_addr = vaddr
177
+ dyn_sec.sh_size = bytes
178
+ end
179
+ end
180
+ end
181
+
182
+ def malloc_strtab!
183
+ return if @strtab_extend_requests.empty?
184
+
185
+ strtab = dynamic.tag_by_type(:strtab)
186
+ # Process registered requests
187
+ need_size = strtab_string.size + @strtab_extend_requests.reduce(0) { |sum, (str, _)| sum + str.size + 1 }
188
+ dynstr = section_header('.dynstr')
189
+ @mm.malloc(need_size) do |off, vaddr|
190
+ new_str = strtab_string + @strtab_extend_requests.map(&:first).join("\x00") + "\x00"
191
+ inline_patch(off, new_str)
192
+ cur = strtab_string.size
193
+ @strtab_extend_requests.each do |str, block|
194
+ block.call(cur)
195
+ cur += str.size + 1
196
+ end
197
+ # Now patching strtab header
198
+ strtab.header.d_val = vaddr
199
+ # We also need to patch dynstr to let readelf have correct output.
200
+ if dynstr
201
+ dynstr.sh_size = new_str.size
202
+ dynstr.sh_offset = off
203
+ dynstr.sh_addr = vaddr
204
+ end
205
+ end
206
+ end
207
+
208
+ # @param [String] str
209
+ # @yieldparam [Integer] idx
210
+ # @yieldreturn [void]
211
+ def reg_str_table(str, &block)
212
+ idx = strtab_string.index(str + "\x00")
213
+ # Request string is already exist
214
+ return yield idx if idx
215
+
216
+ # Record the request
217
+ @strtab_extend_requests << [str, block]
218
+ end
219
+
220
+ def strtab_string
221
+ return @strtab_string if defined?(@strtab_string)
222
+
223
+ # TODO: handle no strtab exists..
224
+ offset = @elf.offset_from_vma(dynamic.tag_by_type(:strtab).value)
225
+ # This is a little tricky since no length information is stored in the tag.
226
+ # We first get the file offset of the string then 'guess' where the end is.
227
+ @elf.stream.pos = offset
228
+ @strtab_string = +''
229
+ loop do
230
+ c = @elf.stream.read(1)
231
+ break unless c =~ /\x00|[[:print:]]/
232
+
233
+ @strtab_string << c
234
+ end
235
+ @strtab_string
236
+ end
237
+
238
+ # This can only be used for patching interpreter's name
239
+ # or set strings in a malloc-ed area.
240
+ # i.e. NEVER intend to change the string defined in strtab
241
+ def inline_patch(off, str)
242
+ @inline_patch[off] = str
243
+ end
244
+
245
+ # Modify the out_file according to registered patches.
246
+ def patch_out(out_file)
247
+ File.open(out_file, 'r+') do |f|
248
+ if @mm.extended?
249
+ original_head = @mm.threshold
250
+ extra = {}
251
+ # Copy all data after the second load
252
+ @elf.stream.pos = original_head
253
+ extra[original_head + @mm.extend_size] = @elf.stream.read # read to end
254
+ # zero out the 'gap' we created
255
+ extra[original_head] = "\x00" * @mm.extend_size
256
+ extra.each do |pos, str|
257
+ f.pos = pos
258
+ f.write(str)
259
+ end
260
+ end
261
+ @elf.patches.each do |pos, str|
262
+ f.pos = @mm.extended_offset(pos)
263
+ f.write(str)
264
+ end
265
+
266
+ @inline_patch.each do |pos, str|
267
+ f.pos = pos
268
+ f.write(str)
269
+ end
270
+ end
271
+ end
272
+
273
+ # @return [ELFTools::Sections::Section?]
274
+ def section_header(name)
275
+ sec = @elf.section_by_name(name)
276
+ return if sec.nil?
277
+
278
+ sec.header
279
+ end
280
+
281
+ def dynamic
282
+ @dynamic ||= @elf.segment_by_type(:dynamic)
283
+ end
284
+ end
285
+ end