patchelf 0.0.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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