patchelf 0.1.0 → 1.3.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/README.md +1 -1
- data/lib/patchelf/alt_saver.rb +831 -0
- data/lib/patchelf/cli.rb +19 -6
- data/lib/patchelf/exceptions.rb +13 -0
- data/lib/patchelf/logger.rb +1 -1
- data/lib/patchelf/mm.rb +3 -1
- data/lib/patchelf/patcher.rb +54 -9
- data/lib/patchelf/saver.rb +17 -19
- data/lib/patchelf/version.rb +1 -1
- metadata +14 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8182cf70ee88eceed07b85e8edda947e647842fb0e1767b36c0bfc61651ae9a6
|
4
|
+
data.tar.gz: a739c96f21f48a4ae7036ba184e46e3ca6a6d3c1cc79215b0e8e4a5b4b970562
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed2e115d7925aef19b3b192f0d9fe70744bd267fe3ef1e9b4245340e741bd6a3cf62bb82d6a847edd8c99878dfaf2e64b50e82ff1049be1f8e89881279fbd682
|
7
|
+
data.tar.gz: 02bfde530e73b448ec73932a1be1dd378bb44d74235fa9ab9096bf3c45607f0194ccfe3979857e4b4389004ee1cef8d4852b687fd571c4d4cfb291e57c576f8a
|
data/README.md
CHANGED
@@ -134,4 +134,4 @@ patcher.save('ls.patch')
|
|
134
134
|
|
135
135
|
## Environment
|
136
136
|
|
137
|
-
patchelf.rb is implemented in pure Ruby, so it should work in all environments include Linux,
|
137
|
+
patchelf.rb is implemented in pure Ruby, so it should work in all environments include Linux, macOS, and Windows!
|
@@ -0,0 +1,831 @@
|
|
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/helper'
|
10
|
+
|
11
|
+
#:nodoc:
|
12
|
+
module PatchELF
|
13
|
+
# TODO: refactor buf_* methods here
|
14
|
+
# TODO: move all refinements into a seperate file / helper file.
|
15
|
+
# refinements for cleaner syntax / speed / memory optimizations
|
16
|
+
module Refinements
|
17
|
+
refine StringIO do
|
18
|
+
# behaves like C memset. Equivalent to calling stream.write(char * nbytes)
|
19
|
+
# the benefit of preferring this over `stream.write(char * nbytes)` is only when data to be written is large.
|
20
|
+
# @param [String] char
|
21
|
+
# @param [Integer] nbytes
|
22
|
+
# @return[void]
|
23
|
+
def fill(char, nbytes)
|
24
|
+
at_once = Helper::PAGE_SIZE
|
25
|
+
pending = nbytes
|
26
|
+
|
27
|
+
if pending > at_once
|
28
|
+
to_write = char * at_once
|
29
|
+
while pending >= at_once
|
30
|
+
write(to_write)
|
31
|
+
pending -= at_once
|
32
|
+
end
|
33
|
+
end
|
34
|
+
write(char * pending) if pending.positive?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
using Refinements
|
39
|
+
|
40
|
+
# Internal use only.
|
41
|
+
# alternative to +Saver+, that aims to be byte to byte equivalent with NixOS/patchelf.
|
42
|
+
#
|
43
|
+
# *DISCLAIMER*: This differs from +Saver+ in number of ways. No lazy reading,
|
44
|
+
# inconsistent use of existing internal API(e.g: manual reading of data instead of calling +section.data+)
|
45
|
+
# @private
|
46
|
+
class AltSaver
|
47
|
+
attr_reader :in_file # @return [String] Input filename.
|
48
|
+
attr_reader :out_file # @return [String] Output filename.
|
49
|
+
|
50
|
+
# Instantiate a {AltSaver} object.
|
51
|
+
# the params passed are the same as the ones passed to +Saver+
|
52
|
+
# @param [String] in_file
|
53
|
+
# @param [String] out_file
|
54
|
+
# @param [{Symbol => String, Array}] set
|
55
|
+
def initialize(in_file, out_file, set)
|
56
|
+
@in_file = in_file
|
57
|
+
@out_file = out_file
|
58
|
+
@set = set
|
59
|
+
|
60
|
+
f = File.open(in_file, 'rb')
|
61
|
+
# the +@buffer+ and +@elf+ both could work on same +StringIO+ stream,
|
62
|
+
# the updating of @buffer in place blocks us from looking up old values.
|
63
|
+
# TODO: cache the values needed later, use same stream for +@buffer+ and +@elf+.
|
64
|
+
# also be sure to update the stream offset passed to Segments::Segment.
|
65
|
+
@elf = ELFTools::ELFFile.new(f)
|
66
|
+
@buffer = StringIO.new(f.tap(&:rewind).read) # StringIO makes easier to work with Bindata
|
67
|
+
|
68
|
+
@ehdr = @elf.header
|
69
|
+
@endian = @elf.endian
|
70
|
+
@elf_class = @elf.elf_class
|
71
|
+
|
72
|
+
@segments = @elf.segments # usage similar to phdrs
|
73
|
+
@sections = @elf.sections # usage similar to shdrs
|
74
|
+
update_section_idx!
|
75
|
+
|
76
|
+
# {String => String}
|
77
|
+
# section name to its data mapping
|
78
|
+
@replaced_sections = {}
|
79
|
+
@section_alignment = ehdr.e_phoff.num_bytes
|
80
|
+
|
81
|
+
# using the same environment flag as patchelf, makes it easier for debugging
|
82
|
+
Logger.level = ::Logger.const_get(ENV['PATCHELF_DEBUG'] ? :DEBUG : :WARN)
|
83
|
+
end
|
84
|
+
|
85
|
+
# @return [void]
|
86
|
+
def save!
|
87
|
+
@set.each { |mtd, val| send(:"modify_#{mtd}") if val }
|
88
|
+
rewrite_sections
|
89
|
+
|
90
|
+
FileUtils.cp(in_file, out_file) if out_file != in_file
|
91
|
+
patch_out
|
92
|
+
# Let output file have the same permission as input.
|
93
|
+
FileUtils.chmod(File.stat(in_file).mode, out_file)
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
attr_reader :ehdr, :endian, :elf_class
|
99
|
+
|
100
|
+
def old_sections
|
101
|
+
@old_sections ||= @elf.sections
|
102
|
+
end
|
103
|
+
|
104
|
+
def buf_cstr(off)
|
105
|
+
cstr = []
|
106
|
+
with_buf_at(off) do |buf|
|
107
|
+
loop do
|
108
|
+
c = buf.read 1
|
109
|
+
break if c.nil? || c == "\x00"
|
110
|
+
|
111
|
+
cstr.push c
|
112
|
+
end
|
113
|
+
end
|
114
|
+
cstr.join
|
115
|
+
end
|
116
|
+
|
117
|
+
def buf_move!(dst_idx, src_idx, n_bytes)
|
118
|
+
with_buf_at(src_idx) do |buf|
|
119
|
+
to_write = buf.read(n_bytes)
|
120
|
+
buf.seek dst_idx
|
121
|
+
buf.write to_write
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def dynstr
|
126
|
+
find_section '.dynstr'
|
127
|
+
end
|
128
|
+
|
129
|
+
# yields dynamic tag, and offset in buffer
|
130
|
+
def each_dynamic_tags
|
131
|
+
return unless block_given?
|
132
|
+
|
133
|
+
sec = find_section '.dynamic'
|
134
|
+
return unless sec
|
135
|
+
|
136
|
+
shdr = sec.header
|
137
|
+
with_buf_at(shdr.sh_offset) do |buf|
|
138
|
+
dyn = ELFTools::Structs::ELF_Dyn.new(elf_class: elf_class, endian: endian)
|
139
|
+
loop do
|
140
|
+
buf_dyn_offset = buf.tell
|
141
|
+
dyn.clear
|
142
|
+
dyn.read(buf)
|
143
|
+
break if dyn.d_tag == ELFTools::Constants::DT_NULL
|
144
|
+
|
145
|
+
yield dyn, buf_dyn_offset
|
146
|
+
# there's a possibility for caller to modify @buffer.pos, seek to avoid such issues
|
147
|
+
buf.seek buf_dyn_offset + dyn.num_bytes
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# the idea of uniquely identifying section by its name has its problems
|
153
|
+
# but this is how patchelf operates and is prone to bugs.
|
154
|
+
# e.g: https://github.com/NixOS/patchelf/issues/197
|
155
|
+
def find_section(sec_name)
|
156
|
+
idx = find_section_idx sec_name
|
157
|
+
return unless idx
|
158
|
+
|
159
|
+
@sections[idx]
|
160
|
+
end
|
161
|
+
|
162
|
+
def find_section_idx(sec_name)
|
163
|
+
@section_idx_by_name[sec_name]
|
164
|
+
end
|
165
|
+
|
166
|
+
def buf_grow!(newsz)
|
167
|
+
bufsz = @buffer.size
|
168
|
+
return if newsz <= bufsz
|
169
|
+
|
170
|
+
@buffer.truncate newsz
|
171
|
+
end
|
172
|
+
|
173
|
+
def modify_interpreter
|
174
|
+
@replaced_sections['.interp'] = @set[:interpreter] + "\x00"
|
175
|
+
end
|
176
|
+
|
177
|
+
def modify_needed
|
178
|
+
# due to gsoc time constraints only implmenting features used by brew.
|
179
|
+
raise NotImplementedError
|
180
|
+
end
|
181
|
+
|
182
|
+
# not checking for nil as modify_rpath is only called if @set[:rpath]
|
183
|
+
def modify_rpath
|
184
|
+
modify_rpath_helper @set[:rpath], force_rpath: true
|
185
|
+
end
|
186
|
+
|
187
|
+
# not checking for nil as modify_runpath is only called if @set[:runpath]
|
188
|
+
def modify_runpath
|
189
|
+
modify_rpath_helper @set[:runpath]
|
190
|
+
end
|
191
|
+
|
192
|
+
def collect_runpath_tags
|
193
|
+
tags = {}
|
194
|
+
each_dynamic_tags do |dyn, off|
|
195
|
+
case dyn.d_tag
|
196
|
+
when ELFTools::Constants::DT_RPATH
|
197
|
+
tag_type = :rpath
|
198
|
+
when ELFTools::Constants::DT_RUNPATH
|
199
|
+
tag_type = :runpath
|
200
|
+
else
|
201
|
+
next
|
202
|
+
end
|
203
|
+
|
204
|
+
# clone does shallow copy, and for some reason d_tag and d_val can't be pass as argument
|
205
|
+
dyn_rpath = ELFTools::Structs::ELF_Dyn.new(endian: endian, elf_class: elf_class)
|
206
|
+
dyn_rpath.assign({ d_tag: dyn.d_tag.to_i, d_val: dyn.d_val.to_i })
|
207
|
+
tags[tag_type] = { offset: off, header: dyn_rpath }
|
208
|
+
end
|
209
|
+
tags
|
210
|
+
end
|
211
|
+
|
212
|
+
def resolve_rpath_tag_conflict(dyn_tags, force_rpath: false)
|
213
|
+
dyn_runpath, dyn_rpath = dyn_tags.values_at(:runpath, :rpath)
|
214
|
+
|
215
|
+
update_sym =
|
216
|
+
if !force_rpath && dyn_rpath && dyn_runpath.nil?
|
217
|
+
:runpath
|
218
|
+
elsif force_rpath && dyn_runpath
|
219
|
+
:rpath
|
220
|
+
end
|
221
|
+
return unless update_sym
|
222
|
+
|
223
|
+
delete_sym, = %i[rpath runpath] - [update_sym]
|
224
|
+
dyn_tag = dyn_tags[update_sym] = dyn_tags[delete_sym]
|
225
|
+
dyn = dyn_tag[:header]
|
226
|
+
dyn.d_tag = ELFTools::Constants.const_get("DT_#{update_sym.upcase}")
|
227
|
+
with_buf_at(dyn_tag[:offset]) { |buf| dyn.write(buf) }
|
228
|
+
dyn_tags.delete(delete_sym)
|
229
|
+
end
|
230
|
+
|
231
|
+
def modify_rpath_helper(new_rpath, force_rpath: false)
|
232
|
+
shdr_dynstr = dynstr.header
|
233
|
+
|
234
|
+
dyn_tags = collect_runpath_tags
|
235
|
+
resolve_rpath_tag_conflict(dyn_tags, force_rpath: force_rpath)
|
236
|
+
# (:runpath, :rpath) order_matters.
|
237
|
+
resolved_rpath_dyns = dyn_tags.values_at(:runpath, :rpath).compact
|
238
|
+
|
239
|
+
old_rpath = ''
|
240
|
+
rpath_off = nil
|
241
|
+
resolved_rpath_dyns.each do |dyn|
|
242
|
+
rpath_off = shdr_dynstr.sh_offset + dyn[:header].d_val
|
243
|
+
old_rpath = buf_cstr(rpath_off)
|
244
|
+
break
|
245
|
+
end
|
246
|
+
return if old_rpath == new_rpath
|
247
|
+
|
248
|
+
with_buf_at(rpath_off) { |b| b.write('X' * old_rpath.size) } if rpath_off
|
249
|
+
if new_rpath.size <= old_rpath.size
|
250
|
+
with_buf_at(rpath_off) { |b| b.write "#{new_rpath}\x00" }
|
251
|
+
return
|
252
|
+
end
|
253
|
+
|
254
|
+
Logger.debug 'rpath is too long, resizing...'
|
255
|
+
new_dynstr = replace_section '.dynstr', shdr_dynstr.sh_size + new_rpath.size + 1
|
256
|
+
new_rpath_strtab_idx = shdr_dynstr.sh_size.to_i
|
257
|
+
new_dynstr[new_rpath_strtab_idx..(new_rpath_strtab_idx + new_rpath.size)] = "#{new_rpath}\x00"
|
258
|
+
|
259
|
+
dyn_tags.each do |_, dyn|
|
260
|
+
dyn[:header].d_val = new_rpath_strtab_idx
|
261
|
+
with_buf_at(dyn[:offset]) { |b| dyn[:header].write(b) }
|
262
|
+
end
|
263
|
+
|
264
|
+
return unless dyn_tags.empty?
|
265
|
+
|
266
|
+
add_dt_rpath!(
|
267
|
+
d_tag: force_rpath ? ELFTools::Constants::DT_RPATH : ELFTools::Constants::DT_RUNPATH,
|
268
|
+
d_val: new_rpath_strtab_idx
|
269
|
+
)
|
270
|
+
end
|
271
|
+
|
272
|
+
def modify_soname
|
273
|
+
return unless ehdr.e_type == ELFTools::Constants::ET_DYN
|
274
|
+
|
275
|
+
# due to gsoc time constraints only implmenting features used by brew.
|
276
|
+
raise NotImplementedError
|
277
|
+
end
|
278
|
+
|
279
|
+
def add_segment!(**phdr_vals)
|
280
|
+
new_phdr = ELFTools::Structs::ELF_Phdr[elf_class].new(endian: endian, **phdr_vals)
|
281
|
+
# nil = no reference to stream; we only want @segments[i].header
|
282
|
+
new_segment = ELFTools::Segments::Segment.new(new_phdr, nil)
|
283
|
+
@segments.push new_segment
|
284
|
+
ehdr.e_phnum += 1
|
285
|
+
nil
|
286
|
+
end
|
287
|
+
|
288
|
+
def add_dt_rpath!(d_tag: nil, d_val: nil)
|
289
|
+
dyn_num_bytes = nil
|
290
|
+
dt_null_idx = 0
|
291
|
+
each_dynamic_tags do |dyn|
|
292
|
+
dyn_num_bytes ||= dyn.num_bytes
|
293
|
+
dt_null_idx += 1
|
294
|
+
end
|
295
|
+
|
296
|
+
# allot for new dt_runpath
|
297
|
+
shdr_dynamic = find_section('.dynamic').header
|
298
|
+
new_dynamic_data = replace_section '.dynamic', shdr_dynamic.sh_size + dyn_num_bytes
|
299
|
+
|
300
|
+
# consider DT_NULL when copying
|
301
|
+
replacement_size = (dt_null_idx + 1) * dyn_num_bytes
|
302
|
+
|
303
|
+
# make space for dt_runpath tag at the top, shift data by one tag positon
|
304
|
+
new_dynamic_data[dyn_num_bytes..(replacement_size + dyn_num_bytes)] = new_dynamic_data[0..replacement_size]
|
305
|
+
|
306
|
+
dyn_rpath = ELFTools::Structs::ELF_Dyn.new endian: endian, elf_class: elf_class
|
307
|
+
dyn_rpath.d_tag = d_tag
|
308
|
+
dyn_rpath.d_val = d_val
|
309
|
+
|
310
|
+
zi = StringIO.new
|
311
|
+
dyn_rpath.write zi
|
312
|
+
zi.rewind
|
313
|
+
new_dynamic_data[0...dyn_num_bytes] = zi.read
|
314
|
+
end
|
315
|
+
|
316
|
+
# given a index into old_sections table
|
317
|
+
# returns the corresponding section index in @sections
|
318
|
+
#
|
319
|
+
# raises ArgumentError if old_shndx can't be found in old_sections
|
320
|
+
# TODO: handle case of non existing section in (new) @sections.
|
321
|
+
def new_section_idx(old_shndx)
|
322
|
+
return if old_shndx == ELFTools::Constants::SHN_UNDEF || old_shndx >= ELFTools::Constants::SHN_LORESERVE
|
323
|
+
|
324
|
+
raise ArgumentError if old_shndx >= old_sections.count
|
325
|
+
|
326
|
+
old_sec = old_sections[old_shndx]
|
327
|
+
raise PatchError, "old_sections[#{shndx}] is nil" if old_sec.nil?
|
328
|
+
|
329
|
+
# TODO: handle case of non existing section in (new) @sections.
|
330
|
+
find_section_idx(old_sec.name)
|
331
|
+
end
|
332
|
+
|
333
|
+
def page_size
|
334
|
+
Helper::PAGE_SIZE
|
335
|
+
end
|
336
|
+
|
337
|
+
def patch_out
|
338
|
+
with_buf_at(0) { |b| ehdr.write(b) }
|
339
|
+
|
340
|
+
File.open(out_file, 'wb') do |f|
|
341
|
+
@buffer.rewind
|
342
|
+
f.write @buffer.read
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
# size includes NUL byte
|
347
|
+
def replace_section(section_name, size)
|
348
|
+
data = @replaced_sections[section_name]
|
349
|
+
unless data
|
350
|
+
shdr = find_section(section_name).header
|
351
|
+
# avoid calling +section.data+ as the @buffer contents may vary from
|
352
|
+
# the stream provided to section at initialization.
|
353
|
+
# ideally, calling section.data should work, however avoiding it to prevent
|
354
|
+
# future traps.
|
355
|
+
with_buf_at(shdr.sh_offset) { |b| data = b.read shdr.sh_size }
|
356
|
+
end
|
357
|
+
rep_data = if data.size == size
|
358
|
+
data
|
359
|
+
elsif data.size < size
|
360
|
+
data.ljust(size, "\x00")
|
361
|
+
else
|
362
|
+
data[0...size] + "\x00"
|
363
|
+
end
|
364
|
+
@replaced_sections[section_name] = rep_data
|
365
|
+
end
|
366
|
+
|
367
|
+
def write_phdrs_to_buf!
|
368
|
+
sort_phdrs!
|
369
|
+
with_buf_at(ehdr.e_phoff) do |buf|
|
370
|
+
@segments.each { |seg| seg.header.write(buf) }
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
def write_shdrs_to_buf!
|
375
|
+
raise PatchError, 'ehdr.e_shnum != @sections.count' if ehdr.e_shnum != @sections.count
|
376
|
+
|
377
|
+
sort_shdrs!
|
378
|
+
with_buf_at(ehdr.e_shoff) do |buf|
|
379
|
+
@sections.each { |section| section.header.write(buf) }
|
380
|
+
end
|
381
|
+
sync_dyn_tags!
|
382
|
+
end
|
383
|
+
|
384
|
+
# data for manual packing and unpacking of symbols in symtab sections.
|
385
|
+
def meta_sym_pack
|
386
|
+
return @meta_sym_pack if @meta_sym_pack
|
387
|
+
|
388
|
+
# resort to manual packing and unpacking of data,
|
389
|
+
# as using bindata is painfully slow :(
|
390
|
+
if elf_class == 32
|
391
|
+
sym_num_bytes = 16 # u32 u32 u32 u8 u8 u16
|
392
|
+
pack_code = endian == :little ? 'VVVCCv' : 'NNNCCn'
|
393
|
+
pack_st_info = 3
|
394
|
+
pack_st_shndx = 5
|
395
|
+
pack_st_value = 1
|
396
|
+
else # 64
|
397
|
+
sym_num_bytes = 24 # u32 u8 u8 u16 u64 u64
|
398
|
+
pack_code = endian == :little ? 'VCCvQ<Q<' : 'NCCnQ>Q>'
|
399
|
+
pack_st_info = 1
|
400
|
+
pack_st_shndx = 3
|
401
|
+
pack_st_value = 4
|
402
|
+
end
|
403
|
+
|
404
|
+
@meta_sym_pack = {
|
405
|
+
num_bytes: sym_num_bytes, code: pack_code,
|
406
|
+
st_info: pack_st_info, st_shndx: pack_st_shndx, st_value: pack_st_value
|
407
|
+
}
|
408
|
+
end
|
409
|
+
|
410
|
+
# yields +symbol+, +entry+
|
411
|
+
def each_symbol(shdr)
|
412
|
+
return unless [ELFTools::Constants::SHT_SYMTAB, ELFTools::Constants::SHT_DYNSYM].include?(shdr.sh_type)
|
413
|
+
|
414
|
+
pack_code, sym_num_bytes = meta_sym_pack.values_at(:code, :num_bytes)
|
415
|
+
|
416
|
+
with_buf_at(shdr.sh_offset) do |buf|
|
417
|
+
num_symbols = shdr.sh_size / sym_num_bytes
|
418
|
+
num_symbols.times do |entry|
|
419
|
+
sym = buf.read(sym_num_bytes).unpack(pack_code)
|
420
|
+
sym_modified = yield sym, entry
|
421
|
+
|
422
|
+
if sym_modified
|
423
|
+
buf.seek buf.tell - sym_num_bytes
|
424
|
+
buf.write sym.pack(pack_code)
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
def rewrite_headers(phdr_address)
|
431
|
+
# there can only be a single program header table according to ELF spec
|
432
|
+
@segments.find { |seg| seg.header.p_type == ELFTools::Constants::PT_PHDR }&.tap do |seg|
|
433
|
+
phdr = seg.header
|
434
|
+
phdr.p_offset = ehdr.e_phoff.to_i
|
435
|
+
phdr.p_vaddr = phdr.p_paddr = phdr_address.to_i
|
436
|
+
phdr.p_filesz = phdr.p_memsz = phdr.num_bytes * @segments.count # e_phentsize * e_phnum
|
437
|
+
end
|
438
|
+
write_phdrs_to_buf!
|
439
|
+
write_shdrs_to_buf!
|
440
|
+
|
441
|
+
pack = meta_sym_pack
|
442
|
+
@sections.each do |sec|
|
443
|
+
each_symbol(sec.header) do |sym, entry|
|
444
|
+
old_shndx = sym[pack[:st_shndx]]
|
445
|
+
|
446
|
+
begin
|
447
|
+
new_index = new_section_idx(old_shndx)
|
448
|
+
next unless new_index
|
449
|
+
rescue ArgumentError
|
450
|
+
Logger.warn "entry #{entry} in symbol table refers to a non existing section, skipping"
|
451
|
+
end
|
452
|
+
|
453
|
+
sym[pack[:st_shndx]] = new_index
|
454
|
+
|
455
|
+
# right 4 bits in the st_info field is st_type
|
456
|
+
if (sym[pack[:st_info]] & 0xF) == ELFTools::Constants::STT_SECTION
|
457
|
+
sym[pack[:st_value]] = @sections[new_index].header.sh_addr.to_i
|
458
|
+
end
|
459
|
+
true
|
460
|
+
end
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
def rewrite_sections
|
465
|
+
return if @replaced_sections.empty?
|
466
|
+
|
467
|
+
case ehdr.e_type
|
468
|
+
when ELFTools::Constants::ET_DYN
|
469
|
+
rewrite_sections_library
|
470
|
+
when ELFTools::Constants::ET_EXEC
|
471
|
+
rewrite_sections_executable
|
472
|
+
else
|
473
|
+
raise PatchError, 'unknown ELF type'
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
def replaced_section_indices
|
478
|
+
return enum_for(:replaced_section_indices) unless block_given?
|
479
|
+
|
480
|
+
last_replaced = 0
|
481
|
+
@sections.each_with_index do |sec, idx|
|
482
|
+
if @replaced_sections[sec.name]
|
483
|
+
last_replaced = idx
|
484
|
+
yield last_replaced
|
485
|
+
end
|
486
|
+
end
|
487
|
+
raise PatchError, 'last_replaced = 0' if last_replaced.zero?
|
488
|
+
raise PatchError, 'last_replaced + 1 >= @sections.size' if last_replaced + 1 >= @sections.size
|
489
|
+
end
|
490
|
+
|
491
|
+
def start_replacement_shdr
|
492
|
+
last_replaced = replaced_section_indices.max
|
493
|
+
start_replacement_hdr = @sections[last_replaced + 1].header
|
494
|
+
|
495
|
+
prev_sec_name = ''
|
496
|
+
(1..last_replaced).each do |idx|
|
497
|
+
sec = @sections[idx]
|
498
|
+
shdr = sec.header
|
499
|
+
if (sec.type == ELFTools::Constants::SHT_PROGBITS && sec.name != '.interp') || prev_sec_name == '.dynstr'
|
500
|
+
start_replacement_hdr = shdr
|
501
|
+
break
|
502
|
+
elsif @replaced_sections[sec.name].nil?
|
503
|
+
Logger.debug " replacing section #{sec.name} which is in the way"
|
504
|
+
replace_section(sec.name, shdr.sh_size)
|
505
|
+
end
|
506
|
+
prev_sec_name = sec.name
|
507
|
+
end
|
508
|
+
|
509
|
+
start_replacement_hdr
|
510
|
+
end
|
511
|
+
|
512
|
+
def copy_shdrs_to_eof
|
513
|
+
shoff_new = @buffer.size
|
514
|
+
# honestly idk why `ehdr.e_shoff` is considered when we are only moving shdrs.
|
515
|
+
sh_size = ehdr.e_shoff + ehdr.e_shnum * ehdr.e_shentsize
|
516
|
+
buf_grow! @buffer.size + sh_size
|
517
|
+
ehdr.e_shoff = shoff_new
|
518
|
+
raise PatchError, 'ehdr.e_shnum != @sections.size' if ehdr.e_shnum != @sections.size
|
519
|
+
|
520
|
+
with_buf_at(ehdr.e_shoff + @sections.first.header.num_bytes) do |buf| # skip writing to NULL section
|
521
|
+
@sections.each_with_index do |sec, idx|
|
522
|
+
next if idx.zero?
|
523
|
+
|
524
|
+
sec.header.write buf
|
525
|
+
end
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
def rewrite_sections_executable
|
530
|
+
sort_shdrs!
|
531
|
+
shdr = start_replacement_shdr
|
532
|
+
start_offset = shdr.sh_offset
|
533
|
+
start_addr = shdr.sh_addr
|
534
|
+
first_page = start_addr - start_offset
|
535
|
+
|
536
|
+
Logger.debug "first reserved offset/addr is 0x#{start_offset.to_i.to_s 16}/0x#{start_addr.to_i.to_s 16}"
|
537
|
+
|
538
|
+
unless start_addr % page_size == start_offset % page_size
|
539
|
+
raise PatchError, 'start_addr != start_offset (mod PAGE_SIZE)'
|
540
|
+
end
|
541
|
+
|
542
|
+
Logger.debug "first page is 0x#{first_page.to_i.to_s 16}"
|
543
|
+
|
544
|
+
copy_shdrs_to_eof if ehdr.e_shoff < start_offset
|
545
|
+
|
546
|
+
seg_num_bytes = @segments.first.header.num_bytes
|
547
|
+
needed_space = (
|
548
|
+
ehdr.num_bytes +
|
549
|
+
(@segments.count * seg_num_bytes) +
|
550
|
+
@replaced_sections.sum { |_, str| Helper.alignup(str.size, @section_alignment) }
|
551
|
+
)
|
552
|
+
|
553
|
+
if needed_space > start_offset
|
554
|
+
needed_space += seg_num_bytes # new load segment is required
|
555
|
+
|
556
|
+
needed_pages = Helper.alignup(needed_space - start_offset, page_size) / page_size
|
557
|
+
Logger.debug "needed pages is #{needed_pages}"
|
558
|
+
raise PatchError, 'virtual address space underrun' if needed_pages * page_size > first_page
|
559
|
+
|
560
|
+
first_page -= needed_pages * page_size
|
561
|
+
start_offset += needed_pages * page_size
|
562
|
+
|
563
|
+
shift_file(needed_pages, first_page)
|
564
|
+
end
|
565
|
+
Logger.debug "needed space is #{needed_space}"
|
566
|
+
|
567
|
+
cur_off = ehdr.num_bytes + (@segments.count * seg_num_bytes)
|
568
|
+
Logger.debug "clearing first #{start_offset - cur_off} bytes"
|
569
|
+
with_buf_at(cur_off) { |buf| buf.fill("\x00", (start_offset - cur_off)) }
|
570
|
+
|
571
|
+
cur_off = write_replaced_sections cur_off, first_page, 0
|
572
|
+
raise PatchError, "cur_off(#{cur_off}) != needed_space" if cur_off != needed_space
|
573
|
+
|
574
|
+
rewrite_headers first_page + ehdr.e_phoff
|
575
|
+
end
|
576
|
+
|
577
|
+
def replace_sections_in_the_way_of_phdr!
|
578
|
+
pht_size = ehdr.num_bytes + (@segments.count + 1) * @segments.first.header.num_bytes
|
579
|
+
|
580
|
+
# replace sections that may overlap with expanded program header table
|
581
|
+
@sections.each_with_index do |sec, idx|
|
582
|
+
shdr = sec.header
|
583
|
+
next if idx.zero? || @replaced_sections[sec.name]
|
584
|
+
break if shdr.sh_addr > pht_size
|
585
|
+
|
586
|
+
replace_section sec.name, shdr.sh_size
|
587
|
+
end
|
588
|
+
end
|
589
|
+
|
590
|
+
def seg_end_addr(seg)
|
591
|
+
phdr = seg.header
|
592
|
+
Helper.alignup(phdr.p_vaddr + phdr.p_memsz, page_size)
|
593
|
+
end
|
594
|
+
|
595
|
+
def rewrite_sections_library
|
596
|
+
start_page = seg_end_addr(@segments.max_by(&method(:seg_end_addr)))
|
597
|
+
|
598
|
+
Logger.debug "Last page is 0x#{start_page.to_s 16}"
|
599
|
+
replace_sections_in_the_way_of_phdr!
|
600
|
+
needed_space = @replaced_sections.sum { |_, str| Helper.alignup(str.size, @section_alignment) }
|
601
|
+
Logger.debug "needed space = #{needed_space}"
|
602
|
+
|
603
|
+
start_offset = Helper.alignup(@buffer.size, page_size)
|
604
|
+
buf_grow! start_offset + needed_space
|
605
|
+
|
606
|
+
# executable shared object
|
607
|
+
if start_offset > start_page && @segments.any? { |seg| seg.header.p_type == ELFTools::Constants::PT_INTERP }
|
608
|
+
Logger.debug(
|
609
|
+
"shifting new PT_LOAD segment by #{start_offset - start_page} bytes to work around a Linux kernel bug"
|
610
|
+
)
|
611
|
+
start_page = start_offset
|
612
|
+
end
|
613
|
+
|
614
|
+
ehdr.e_phoff = ehdr.num_bytes
|
615
|
+
add_segment!(
|
616
|
+
p_type: ELFTools::Constants::PT_LOAD,
|
617
|
+
p_offset: start_offset,
|
618
|
+
p_vaddr: start_page,
|
619
|
+
p_paddr: start_page,
|
620
|
+
p_filesz: needed_space,
|
621
|
+
p_memsz: needed_space,
|
622
|
+
p_flags: ELFTools::Constants::PF_R | ELFTools::Constants::PF_W,
|
623
|
+
p_align: page_size
|
624
|
+
)
|
625
|
+
|
626
|
+
cur_off = write_replaced_sections start_offset, start_page, start_offset
|
627
|
+
raise PatchError, 'cur_off != start_offset + needed_space' if cur_off != start_offset + needed_space
|
628
|
+
|
629
|
+
rewrite_headers ehdr.e_phoff
|
630
|
+
end
|
631
|
+
|
632
|
+
def shift_file(extra_pages, start_page)
|
633
|
+
oldsz = @buffer.size
|
634
|
+
shift = extra_pages * page_size
|
635
|
+
buf_grow!(oldsz + shift)
|
636
|
+
buf_move! shift, 0, oldsz
|
637
|
+
with_buf_at(ehdr.num_bytes) { |buf| buf.write "\x00" * (shift - ehdr.num_bytes) }
|
638
|
+
|
639
|
+
ehdr.e_phoff = ehdr.num_bytes
|
640
|
+
ehdr.e_shoff = ehdr.e_shoff + shift
|
641
|
+
|
642
|
+
@sections.each_with_index do |sec, i|
|
643
|
+
next if i.zero? # dont touch NULL section
|
644
|
+
|
645
|
+
shdr = sec.header
|
646
|
+
shdr.sh_offset += shift
|
647
|
+
end
|
648
|
+
|
649
|
+
@segments.each do |seg|
|
650
|
+
phdr = seg.header
|
651
|
+
phdr.p_offset += shift
|
652
|
+
phdr.p_align = page_size if phdr.p_align != 0 && (phdr.p_vaddr - phdr.p_offset) % phdr.p_align != 0
|
653
|
+
end
|
654
|
+
|
655
|
+
add_segment!(
|
656
|
+
p_type: ELFTools::Constants::PT_LOAD,
|
657
|
+
p_offset: 0,
|
658
|
+
p_vaddr: start_page,
|
659
|
+
p_paddr: start_page,
|
660
|
+
p_filesz: shift,
|
661
|
+
p_memsz: shift,
|
662
|
+
p_flags: ELFTools::Constants::PF_R | ELFTools::Constants::PF_W,
|
663
|
+
p_align: page_size
|
664
|
+
)
|
665
|
+
end
|
666
|
+
|
667
|
+
def sort_phdrs!
|
668
|
+
pt_phdr = ELFTools::Constants::PT_PHDR
|
669
|
+
@segments.sort! do |me, you|
|
670
|
+
next 1 if you.header.p_type == pt_phdr
|
671
|
+
next -1 if me.header.p_type == pt_phdr
|
672
|
+
|
673
|
+
me.header.p_paddr.to_i <=> you.header.p_paddr.to_i
|
674
|
+
end
|
675
|
+
end
|
676
|
+
|
677
|
+
# section headers may contain sh_info and sh_link values that are
|
678
|
+
# references to another section
|
679
|
+
def collect_section_to_section_refs
|
680
|
+
rel_syms = [ELFTools::Constants::SHT_REL, ELFTools::Constants::SHT_RELA]
|
681
|
+
# Translate sh_link, sh_info mappings to section names.
|
682
|
+
@sections.each_with_object({ linkage: {}, info: {} }) do |s, collected|
|
683
|
+
hdr = s.header
|
684
|
+
collected[:linkage][s.name] = @sections[hdr.sh_link].name if hdr.sh_link.nonzero?
|
685
|
+
collected[:info][s.name] = @sections[hdr.sh_info].name if hdr.sh_info.nonzero? && rel_syms.include?(hdr.sh_type)
|
686
|
+
end
|
687
|
+
end
|
688
|
+
|
689
|
+
# @param collected
|
690
|
+
# this must be the value returned by +collect_section_to_section_refs+
|
691
|
+
def restore_section_to_section_refs!(collected)
|
692
|
+
rel_syms = [ELFTools::Constants::SHT_REL, ELFTools::Constants::SHT_RELA]
|
693
|
+
linkage, info = collected.values_at(:linkage, :info)
|
694
|
+
@sections.each do |sec|
|
695
|
+
hdr = sec.header
|
696
|
+
hdr.sh_link = find_section_idx(linkage[sec.name]) if hdr.sh_link.nonzero?
|
697
|
+
hdr.sh_info = find_section_idx(info[sec.name]) if hdr.sh_info.nonzero? && rel_syms.include?(hdr.sh_type)
|
698
|
+
end
|
699
|
+
end
|
700
|
+
|
701
|
+
def sort_shdrs!
|
702
|
+
section_dep_values = collect_section_to_section_refs
|
703
|
+
shstrtab_name = @sections[ehdr.e_shstrndx].name
|
704
|
+
@sections.sort! { |me, you| me.header.sh_offset.to_i <=> you.header.sh_offset.to_i }
|
705
|
+
update_section_idx!
|
706
|
+
restore_section_to_section_refs!(section_dep_values)
|
707
|
+
ehdr.e_shstrndx = find_section_idx shstrtab_name
|
708
|
+
end
|
709
|
+
|
710
|
+
# given a +dyn.d_tag+, returns the section name it must be synced to.
|
711
|
+
# it may return nil, when given tag maps to no section,
|
712
|
+
# or when its okay to skip if section is not found.
|
713
|
+
def dyn_tag_to_section_name(d_tag)
|
714
|
+
case d_tag
|
715
|
+
when ELFTools::Constants::DT_STRTAB, ELFTools::Constants::DT_STRSZ
|
716
|
+
'.dynstr'
|
717
|
+
when ELFTools::Constants::DT_SYMTAB
|
718
|
+
'.dynsym'
|
719
|
+
when ELFTools::Constants::DT_HASH
|
720
|
+
'.hash'
|
721
|
+
when ELFTools::Constants::DT_GNU_HASH
|
722
|
+
'.gnu.hash'
|
723
|
+
when ELFTools::Constants::DT_JMPREL
|
724
|
+
sec_name = %w[.rel.plt .rela.plt .rela.IA_64.pltoff].find { |s| find_section(s) }
|
725
|
+
raise PatchError, 'cannot find section corresponding to DT_JMPREL' unless sec_name
|
726
|
+
|
727
|
+
sec_name
|
728
|
+
when ELFTools::Constants::DT_REL
|
729
|
+
# regarding .rel.got, NixOS/patchelf says
|
730
|
+
# "no idea if this makes sense, but it was needed for some program"
|
731
|
+
#
|
732
|
+
# return nil if not found, patchelf claims no problem in skipping
|
733
|
+
%w[.rel.dyn .rel.got].find { |s| find_section(s) }
|
734
|
+
when ELFTools::Constants::DT_RELA
|
735
|
+
# return nil if not found, patchelf claims no problem in skipping
|
736
|
+
find_section('.rela.dyn')&.name
|
737
|
+
when ELFTools::Constants::DT_VERNEED
|
738
|
+
'.gnu.version_r'
|
739
|
+
when ELFTools::Constants::DT_VERSYM
|
740
|
+
'.gnu.version'
|
741
|
+
end
|
742
|
+
end
|
743
|
+
|
744
|
+
# updates dyn tags by syncing it with @section values
|
745
|
+
def sync_dyn_tags!
|
746
|
+
each_dynamic_tags do |dyn, buf_off|
|
747
|
+
sec_name = dyn_tag_to_section_name(dyn.d_tag)
|
748
|
+
next unless sec_name
|
749
|
+
|
750
|
+
shdr = find_section(sec_name).header
|
751
|
+
dyn.d_val = dyn.d_tag == ELFTools::Constants::DT_STRSZ ? shdr.sh_size.to_i : shdr.sh_addr.to_i
|
752
|
+
|
753
|
+
with_buf_at(buf_off) { |wbuf| dyn.write(wbuf) }
|
754
|
+
end
|
755
|
+
end
|
756
|
+
|
757
|
+
def update_section_idx!
|
758
|
+
@section_idx_by_name = @sections.map.with_index { |sec, idx| [sec.name, idx] }.to_h
|
759
|
+
end
|
760
|
+
|
761
|
+
def with_buf_at(pos)
|
762
|
+
return unless block_given?
|
763
|
+
|
764
|
+
opos = @buffer.tell
|
765
|
+
@buffer.seek pos
|
766
|
+
yield @buffer
|
767
|
+
@buffer.seek opos
|
768
|
+
nil
|
769
|
+
end
|
770
|
+
|
771
|
+
def sync_sec_to_seg(shdr, phdr)
|
772
|
+
phdr.p_offset = shdr.sh_offset.to_i
|
773
|
+
phdr.p_vaddr = phdr.p_paddr = shdr.sh_addr.to_i
|
774
|
+
phdr.p_filesz = phdr.p_memsz = shdr.sh_size.to_i
|
775
|
+
end
|
776
|
+
|
777
|
+
def phdrs_by_type(seg_type)
|
778
|
+
return unless seg_type
|
779
|
+
|
780
|
+
@segments.each_with_index do |seg, idx|
|
781
|
+
next unless (phdr = seg.header).p_type == seg_type
|
782
|
+
|
783
|
+
yield phdr, idx
|
784
|
+
end
|
785
|
+
end
|
786
|
+
|
787
|
+
def write_replaced_sections(cur_off, start_addr, start_offset)
|
788
|
+
sht_no_bits = ELFTools::Constants::SHT_NOBITS
|
789
|
+
|
790
|
+
# the original source says this has to be done seperately to
|
791
|
+
# prevent clobbering the previously written section contents.
|
792
|
+
@replaced_sections.each do |rsec_name, _|
|
793
|
+
shdr = find_section(rsec_name).header
|
794
|
+
with_buf_at(shdr.sh_offset) { |b| b.fill('X', shdr.sh_size) } if shdr.sh_type != sht_no_bits
|
795
|
+
end
|
796
|
+
|
797
|
+
# the sort is necessary, the strategy in ruby and Cpp to iterate map/hash
|
798
|
+
# is different, patchelf v0.10 iterates the replaced_sections sorted by
|
799
|
+
# keys.
|
800
|
+
@replaced_sections.sort.each do |rsec_name, rsec_data|
|
801
|
+
section = find_section(rsec_name)
|
802
|
+
shdr = section.header
|
803
|
+
|
804
|
+
Logger.debug <<~DEBUG
|
805
|
+
rewriting section '#{rsec_name}'
|
806
|
+
from offset 0x#{shdr.sh_offset.to_i.to_s 16}(size #{shdr.sh_size})
|
807
|
+
to offset 0x#{cur_off.to_i.to_s 16}(size #{rsec_data.size})
|
808
|
+
DEBUG
|
809
|
+
|
810
|
+
with_buf_at(cur_off) { |b| b.write rsec_data }
|
811
|
+
|
812
|
+
shdr.sh_offset = cur_off
|
813
|
+
shdr.sh_addr = start_addr + (cur_off - start_offset)
|
814
|
+
shdr.sh_size = rsec_data.size
|
815
|
+
shdr.sh_addralign = @section_alignment
|
816
|
+
|
817
|
+
seg_type = {
|
818
|
+
'.interp' => ELFTools::Constants::PT_INTERP,
|
819
|
+
'.dynamic' => ELFTools::Constants::PT_DYNAMIC
|
820
|
+
}[section.name]
|
821
|
+
|
822
|
+
phdrs_by_type(seg_type) { |phdr| sync_sec_to_seg(shdr, phdr) }
|
823
|
+
|
824
|
+
cur_off += Helper.alignup(rsec_data.size, @section_alignment)
|
825
|
+
end
|
826
|
+
@replaced_sections.clear
|
827
|
+
|
828
|
+
cur_off
|
829
|
+
end
|
830
|
+
end
|
831
|
+
end
|
data/lib/patchelf/cli.rb
CHANGED
@@ -34,9 +34,24 @@ module PatchELF
|
|
34
34
|
return $stdout.puts option_parser unless parse(argv)
|
35
35
|
|
36
36
|
# Now the options are (hopefully) valid, let's process the ELF file.
|
37
|
-
|
37
|
+
begin
|
38
|
+
@patcher = PatchELF::Patcher.new(@options[:in_file])
|
39
|
+
rescue ELFTools::ELFError, Errno::ENOENT => e
|
40
|
+
return PatchELF::Logger.error(e.message)
|
41
|
+
end
|
38
42
|
patcher.use_rpath! if @options[:force_rpath]
|
39
|
-
|
43
|
+
readonly
|
44
|
+
patch_requests
|
45
|
+
patcher.save(@options[:out_file])
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def patcher
|
51
|
+
@patcher
|
52
|
+
end
|
53
|
+
|
54
|
+
def readonly
|
40
55
|
@options[:print].uniq.each do |s|
|
41
56
|
content = patcher.__send__(s)
|
42
57
|
next if content.nil?
|
@@ -44,7 +59,9 @@ module PatchELF
|
|
44
59
|
s = :rpath if @options[:force_rpath] && s == :runpath
|
45
60
|
$stdout.puts "#{s}: #{Array(content).join(' ')}"
|
46
61
|
end
|
62
|
+
end
|
47
63
|
|
64
|
+
def patch_requests
|
48
65
|
@options[:set].each do |sym, val|
|
49
66
|
patcher.__send__("#{sym}=".to_sym, val)
|
50
67
|
end
|
@@ -52,12 +69,8 @@ module PatchELF
|
|
52
69
|
@options[:needed].each do |type, val|
|
53
70
|
patcher.__send__("#{type}_needed".to_sym, *val)
|
54
71
|
end
|
55
|
-
|
56
|
-
patcher.save(@options[:out_file])
|
57
72
|
end
|
58
73
|
|
59
|
-
private
|
60
|
-
|
61
74
|
def parse(argv)
|
62
75
|
remain = option_parser.permute(argv)
|
63
76
|
return false if remain.first.nil?
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# encoding: ascii-8bit
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'elftools/exceptions'
|
5
|
+
|
6
|
+
module PatchELF
|
7
|
+
# Raised on an error during ELF modification.
|
8
|
+
class PatchError < ELFTools::ELFError; end
|
9
|
+
# Raised when Dynamic Tag is missing
|
10
|
+
class MissingTagError < PatchError; end
|
11
|
+
# Raised on missing Program Header(segment)
|
12
|
+
class MissingSegmentError < PatchError; end
|
13
|
+
end
|
data/lib/patchelf/logger.rb
CHANGED
data/lib/patchelf/mm.rb
CHANGED
@@ -25,7 +25,8 @@ module PatchELF
|
|
25
25
|
# 1. Set ELF headers' attributes (with ELFTools)
|
26
26
|
# 2. Invoke {Saver#inline_patch}
|
27
27
|
def malloc(size, &block)
|
28
|
-
|
28
|
+
raise ArgumentError, 'malloc\'s size most be positive.' if size <= 0
|
29
|
+
|
29
30
|
@request << [size, block]
|
30
31
|
end
|
31
32
|
|
@@ -133,6 +134,7 @@ module PatchELF
|
|
133
134
|
nil
|
134
135
|
end
|
135
136
|
|
137
|
+
# TODO
|
136
138
|
def new_load_method
|
137
139
|
raise NotImplementedError
|
138
140
|
end
|
data/lib/patchelf/patcher.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
|
4
4
|
require 'elftools/elf_file'
|
5
5
|
|
6
|
+
require 'patchelf/exceptions'
|
6
7
|
require 'patchelf/logger'
|
7
8
|
require 'patchelf/saver'
|
8
9
|
|
@@ -12,14 +13,27 @@ module PatchELF
|
|
12
13
|
# @!macro [new] note_apply
|
13
14
|
# @note This setting will be saved after {#save} being invoked.
|
14
15
|
|
16
|
+
attr_reader :elf # @return [ELFTools::ELFFile] ELF parser object.
|
17
|
+
|
15
18
|
# Instantiate a {Patcher} object.
|
16
19
|
# @param [String] filename
|
17
20
|
# Filename of input ELF.
|
18
|
-
|
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)
|
19
29
|
@in_file = filename
|
20
30
|
@elf = ELFTools::ELFFile.new(File.open(filename))
|
21
31
|
@set = {}
|
22
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)
|
23
37
|
end
|
24
38
|
|
25
39
|
# @return [String?]
|
@@ -120,7 +134,23 @@ module PatchELF
|
|
120
134
|
# Get runpath.
|
121
135
|
# @return [String?]
|
122
136
|
def runpath
|
123
|
-
@set[@rpath_sym] || 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
|
147
|
+
#
|
148
|
+
# Modify / set DT_RPATH of the given ELF.
|
149
|
+
# similar to runpath= except DT_RPATH is modifed/created in DYNAMIC segment.
|
150
|
+
# @param [String] rpath
|
151
|
+
# @macro note_apply
|
152
|
+
def rpath=(rpath)
|
153
|
+
@set[:rpath] = rpath
|
124
154
|
end
|
125
155
|
|
126
156
|
# Set runpath.
|
@@ -143,22 +173,35 @@ module PatchELF
|
|
143
173
|
# Save the patched ELF as +out_file+.
|
144
174
|
# @param [String?] out_file
|
145
175
|
# If +out_file+ is +nil+, the original input file will be modified.
|
176
|
+
# @param [Boolean] patchelf_compatible
|
177
|
+
# When +patchelf_compatible+ is true, tries to produce same ELF as the one produced by NixOS/patchelf.
|
146
178
|
# @return [void]
|
147
|
-
def save(out_file = nil)
|
179
|
+
def save(out_file = nil, patchelf_compatible: false)
|
148
180
|
# If nothing is modified, return directly.
|
149
181
|
return if out_file.nil? && !dirty?
|
150
182
|
|
151
183
|
out_file ||= @in_file
|
152
|
-
saver =
|
184
|
+
saver = if patchelf_compatible
|
185
|
+
require 'patchelf/alt_saver'
|
186
|
+
PatchELF::AltSaver.new(@in_file, out_file, @set)
|
187
|
+
else
|
188
|
+
PatchELF::Saver.new(@in_file, out_file, @set)
|
189
|
+
end
|
153
190
|
|
154
191
|
saver.save!
|
155
192
|
end
|
156
193
|
|
157
194
|
private
|
158
195
|
|
196
|
+
def log_or_raise(msg, exception = PatchELF::PatchError)
|
197
|
+
raise exception, msg if @on_error == :exception
|
198
|
+
|
199
|
+
PatchELF::Logger.warn(msg) if @on_error == :log
|
200
|
+
end
|
201
|
+
|
159
202
|
def interpreter_
|
160
203
|
segment = @elf.segment_by_type(:interp)
|
161
|
-
return
|
204
|
+
return log_or_raise 'No interpreter found.', PatchELF::MissingSegmentError if segment.nil?
|
162
205
|
|
163
206
|
segment.interp_name
|
164
207
|
end
|
@@ -172,8 +215,8 @@ module PatchELF
|
|
172
215
|
end
|
173
216
|
|
174
217
|
# @return [String?]
|
175
|
-
def runpath_
|
176
|
-
tag_name_or_log(
|
218
|
+
def runpath_(rpath_sym = :runpath)
|
219
|
+
tag_name_or_log(rpath_sym, "Entry DT_#{rpath_sym.to_s.upcase} not found.")
|
177
220
|
end
|
178
221
|
|
179
222
|
# @return [String?]
|
@@ -191,14 +234,16 @@ module PatchELF
|
|
191
234
|
return if segment.nil?
|
192
235
|
|
193
236
|
tag = segment.tag_by_type(type)
|
194
|
-
return PatchELF::
|
237
|
+
return log_or_raise log_msg, PatchELF::MissingTagError if tag.nil?
|
195
238
|
|
196
239
|
tag.name
|
197
240
|
end
|
198
241
|
|
199
242
|
def dynamic_or_log
|
200
243
|
@elf.segment_by_type(:dynamic).tap do |s|
|
201
|
-
|
244
|
+
if s.nil?
|
245
|
+
log_or_raise 'DYNAMIC segment not found, might be a statically-linked ELF?', PatchELF::MissingSegmentError
|
246
|
+
end
|
202
247
|
end
|
203
248
|
end
|
204
249
|
end
|
data/lib/patchelf/saver.rb
CHANGED
@@ -121,30 +121,27 @@ module PatchELF
|
|
121
121
|
def patch_needed
|
122
122
|
original_needs = dynamic.tags_by_type(:needed)
|
123
123
|
@set[:needed].uniq!
|
124
|
+
|
125
|
+
original = original_needs.map(&:name)
|
126
|
+
replace = @set[:needed]
|
127
|
+
|
124
128
|
# 3 sets:
|
125
129
|
# 1. in original and in needs - remain unchanged
|
126
130
|
# 2. in original but not in needs - remove
|
127
131
|
# 3. not in original and in needs - append
|
128
|
-
|
129
|
-
|
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
|
132
|
+
append = replace - original
|
133
|
+
remove = original - replace
|
138
134
|
|
139
|
-
|
140
|
-
|
135
|
+
ignored_dyns = remove.each_with_object([]) do |name, ignored|
|
136
|
+
dyn = original_needs.find { |n| n.name == name }.header
|
137
|
+
dyn.d_tag = IGNORE
|
138
|
+
ignored << dyn
|
141
139
|
end
|
142
|
-
return if extra.empty?
|
143
140
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
reg_str_table(name) { |idx|
|
141
|
+
append.zip(ignored_dyns) do |name, ignored_dyn|
|
142
|
+
dyn = ignored_dyn || lazy_dyn(:needed)
|
143
|
+
dyn.d_tag = ELFTools::Constants::DT_NEEDED
|
144
|
+
reg_str_table(name) { |idx| dyn.d_val = idx }
|
148
145
|
end
|
149
146
|
end
|
150
147
|
|
@@ -189,9 +186,10 @@ module PatchELF
|
|
189
186
|
@mm.malloc(need_size) do |off, vaddr|
|
190
187
|
new_str = strtab_string + @strtab_extend_requests.map(&:first).join("\x00") + "\x00"
|
191
188
|
inline_patch(off, new_str)
|
189
|
+
cur = strtab_string.size
|
192
190
|
@strtab_extend_requests.each do |str, block|
|
193
|
-
|
194
|
-
|
191
|
+
block.call(cur)
|
192
|
+
cur += str.size + 1
|
195
193
|
end
|
196
194
|
# Now patching strtab header
|
197
195
|
strtab.header.d_val = vaddr
|
data/lib/patchelf/version.rb
CHANGED
metadata
CHANGED
@@ -1,43 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: patchelf
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- david942j
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-08-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: elftools
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 1.1.3
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 1.1.3
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '13.0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '13.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -72,14 +72,14 @@ dependencies:
|
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: 0.
|
75
|
+
version: '0.17'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: 0.
|
82
|
+
version: '0.17'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: tty-platform
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -121,7 +121,9 @@ files:
|
|
121
121
|
- README.md
|
122
122
|
- bin/patchelf.rb
|
123
123
|
- lib/patchelf.rb
|
124
|
+
- lib/patchelf/alt_saver.rb
|
124
125
|
- lib/patchelf/cli.rb
|
126
|
+
- lib/patchelf/exceptions.rb
|
125
127
|
- lib/patchelf/helper.rb
|
126
128
|
- lib/patchelf/logger.rb
|
127
129
|
- lib/patchelf/mm.rb
|
@@ -140,14 +142,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
140
142
|
requirements:
|
141
143
|
- - ">="
|
142
144
|
- !ruby/object:Gem::Version
|
143
|
-
version: 2.
|
145
|
+
version: '2.3'
|
144
146
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
145
147
|
requirements:
|
146
148
|
- - ">="
|
147
149
|
- !ruby/object:Gem::Version
|
148
150
|
version: '0'
|
149
151
|
requirements: []
|
150
|
-
rubygems_version: 3.
|
152
|
+
rubygems_version: 3.1.2
|
151
153
|
signing_key:
|
152
154
|
specification_version: 4
|
153
155
|
summary: patchelf
|