patchelf 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e432d8ac7a3e75e8c9e80d433bc756e635d6b4d057b7e02ed952be113c7edbf5
4
- data.tar.gz: 463cee3d6e780346706752f2a537e67fde649db82b206a8d79f15e60dd4cb4ae
3
+ metadata.gz: d0268e11ae1f743826ab85b81245876d13ec987f4bf9b530f399260447b939aa
4
+ data.tar.gz: f8fca7603e33bdea3d44c328076a64a37172d61c49d1fd7a7915fe5b4d9ddba3
5
5
  SHA512:
6
- metadata.gz: bebe4cca89632d3660045d7582c19cf356562d2cbd389cc910377e88ea293c4fb812bb8917e89670cad05224254497c361f608e7429dcfe2c09ed94d31834a9f
7
- data.tar.gz: 849a0a0b5b8c5bc1c1e52c4f65c9452917f58cf881bca89de4a4986df0556fd677724fe1b17466ff469eed8bcada2320d69169951d9ea0ac4aed1d9be6e4d5aa
6
+ metadata.gz: 462df528984a4f51efb4f9b0f589b0378ff4b28a1008357c0f7b7b4cdaa4a41739402c3821cc6b06a1dccd7292e02c1f2e1d9513a7926e9708b56e0abc66ba84
7
+ data.tar.gz: acddd987c408cf26dc323a6873d5db81aba3635ffba5e9eb6a3d4ca76f2d0d5cfe395739073f0594307274be756e5f270b162cbc952ac5d4797672b321ddeb5f
data/README.md CHANGED
@@ -12,19 +12,34 @@ Implements features of NixOS/patchelf in pure Ruby.
12
12
 
13
13
  ## Installation
14
14
 
15
- WIP.
15
+ Available on RubyGems.org!
16
+ ```
17
+ $ gem install patchelf
18
+ ```
16
19
 
17
20
  ## Usage
18
21
 
19
22
  ```
20
23
  $ patchelf.rb
21
24
  # Usage: patchelf.rb <commands> FILENAME [OUTPUT_FILE]
22
- # --pi, --print-interpreter Show interpreter's name.
23
- # --pn, --print-needed Show needed libraries specified in DT_NEEDED.
24
- # --ps, --print-soname Show soname specified in DT_SONAME.
25
- # --interp, --set-interpreter INTERP
25
+ # --print-interpreter, --pi Show interpreter's name.
26
+ # --print-needed, --pn Show needed libraries specified in DT_NEEDED.
27
+ # --print-runpath, --pr Show the path specified in DT_RUNPATH.
28
+ # --print-soname, --ps Show soname specified in DT_SONAME.
29
+ # --set-interpreter, --interp INTERP
26
30
  # Set interpreter's name.
27
- # --so, --set-soname SONAME Set name of a shared library.
31
+ # --set-needed, --needed LIB1,LIB2,LIB3
32
+ # Set needed libraries, this will remove all existent needed libraries.
33
+ # --add-needed LIB Append a new needed library.
34
+ # --remove-needed LIB Remove a needed library.
35
+ # --replace-needed LIB1,LIB2 Replace needed library LIB1 as LIB2.
36
+ # --set-runpath, --runpath PATH
37
+ # Set the path of runpath.
38
+ # --force-rpath According to the ld.so docs, DT_RPATH is obsolete,
39
+ # patchelf.rb will always try to get/set DT_RUNPATH first.
40
+ # Use this option to force every operations related to runpath (e.g. --runpath)
41
+ # to consider 'DT_RPATH' instead of 'DT_RUNPATH'.
42
+ # --set-soname, --so SONAME Set name of a shared library.
28
43
  # --version Show current gem's version.
29
44
 
30
45
  ```
@@ -32,8 +47,8 @@ $ patchelf.rb
32
47
  ### Display information
33
48
  ```
34
49
  $ patchelf.rb --print-interpreter --print-needed /bin/ls
35
- # Interpreter: /lib64/ld-linux-x86-64.so.2
36
- # Needed: libselinux.so.1 libc.so.6
50
+ # interpreter: /lib64/ld-linux-x86-64.so.2
51
+ # needed: libselinux.so.1 libc.so.6
37
52
 
38
53
  ```
39
54
 
@@ -47,11 +62,54 @@ $ file ls.patch
47
62
 
48
63
  ```
49
64
 
65
+ ### Modify dependency libraries
66
+
67
+ #### Add
68
+ ```
69
+ $ patchelf.rb --add-needed libnew.so /bin/ls ls.patch
70
+ ```
71
+
72
+ #### Remove
73
+ ```
74
+ $ patchelf.rb --remove-needed libc.so.6 /bin/ls ls.patch
75
+ ```
76
+
77
+ #### Replace
78
+ ```
79
+ $ patchelf.rb --replace-needed libc.so.6,libcnew.so.6 /bin/ls ls.patch
80
+
81
+ $ readelf -d ls.patch | grep NEEDED
82
+ # 0x0000000000000001 (NEEDED) Shared library: [libselinux.so.1]
83
+ # 0x0000000000000001 (NEEDED) Shared library: [libcnew.so.6]
84
+
85
+ ```
86
+
87
+ #### Set directly
88
+ ```
89
+ $ patchelf.rb --needed a.so,b.so,c.so /bin/ls ls.patch
90
+
91
+ $ readelf -d ls.patch | grep NEEDED
92
+ # 0x0000000000000001 (NEEDED) Shared library: [a.so]
93
+ # 0x0000000000000001 (NEEDED) Shared library: [b.so]
94
+ # 0x0000000000000001 (NEEDED) Shared library: [c.so]
95
+
96
+ ```
97
+
98
+ ### Set RUNPATH of an executable
99
+ ```
100
+ $ patchelf.rb --runpath . /bin/ls ls.patch
101
+
102
+ $ readelf -d ls.patch | grep RUNPATH
103
+ # 0x000000000000001d (RUNPATH) Library runpath: [.]
104
+
105
+ ```
106
+
50
107
  ### Change SONAME of a shared library
51
108
  ```
52
- $ patchelf.rb --soname libnewname.so.217 /lib/x86_64-linux-gnu/libc.so.6 ./libc.patched
109
+ $ patchelf.rb --so libc.so.217 /lib/x86_64-linux-gnu/libc.so.6 libc.patch
53
110
 
54
- $ readelf -d libc.patched | grep SONAME
111
+ $ readelf -d libc.patch | grep SONAME
112
+ # 0x000000000000000e (SONAME) Library soname: [libc.so.217]
55
113
 
56
114
  ```
57
115
 
@@ -60,11 +118,11 @@ $ readelf -d libc.patched | grep SONAME
60
118
  require 'patchelf'
61
119
 
62
120
  patcher = PatchELF::Patcher.new('/bin/ls')
63
- patcher.get(:interpreter)
121
+ patcher.interpreter
64
122
  #=> "/lib64/ld-linux-x86-64.so.2"
65
123
 
66
124
  patcher.interpreter = '/lib/AAAA.so.2'
67
- patcher.get(:interpreter)
125
+ patcher.interpreter
68
126
  #=> "/lib/AAAA.so.2"
69
127
 
70
128
  patcher.save('ls.patch')
data/bin/patchelf.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'patchelf/cli'
4
5
 
data/lib/patchelf.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Main module of patchelf.
2
4
  #
3
5
  # @author david942j
data/lib/patchelf/cli.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'optparse'
2
4
 
3
5
  require 'patchelf/patcher'
@@ -25,25 +27,32 @@ module PatchELF
25
27
  def work(argv)
26
28
  @options = {
27
29
  set: {},
28
- print: []
30
+ print: [],
31
+ needed: []
29
32
  }
30
33
  return $stdout.puts "PatchELF Version #{PatchELF::VERSION}" if argv.include?('--version')
31
34
  return $stdout.puts option_parser unless parse(argv)
32
35
 
33
36
  # Now the options are (hopefully) valid, let's process the ELF file.
34
37
  patcher = PatchELF::Patcher.new(@options[:in_file])
38
+ patcher.use_rpath! if @options[:force_rpath]
35
39
  # TODO: Handle ELFTools::ELFError
36
40
  @options[:print].uniq.each do |s|
37
- content = patcher.get(s)
41
+ content = patcher.__send__(s)
38
42
  next if content.nil?
39
43
 
40
- $stdout.puts "#{s.to_s.capitalize}: #{Array(content).join(' ')}"
44
+ s = :rpath if @options[:force_rpath] && s == :runpath
45
+ $stdout.puts "#{s}: #{Array(content).join(' ')}"
41
46
  end
42
47
 
43
48
  @options[:set].each do |sym, val|
44
49
  patcher.__send__("#{sym}=".to_sym, val)
45
50
  end
46
51
 
52
+ @options[:needed].each do |type, val|
53
+ patcher.__send__("#{type}_needed".to_sym, *val)
54
+ end
55
+
47
56
  patcher.save(@options[:out_file])
48
57
  end
49
58
 
@@ -62,23 +71,58 @@ module PatchELF
62
71
  @option_parser ||= OptionParser.new do |opts|
63
72
  opts.banner = USAGE
64
73
 
65
- opts.on('--pi', '--print-interpreter', 'Show interpreter\'s name.') do
74
+ opts.on('--print-interpreter', '--pi', 'Show interpreter\'s name.') do
66
75
  @options[:print] << :interpreter
67
76
  end
68
77
 
69
- opts.on('--pn', '--print-needed', 'Show needed libraries specified in DT_NEEDED.') do
78
+ opts.on('--print-needed', '--pn', 'Show needed libraries specified in DT_NEEDED.') do
70
79
  @options[:print] << :needed
71
80
  end
72
81
 
73
- opts.on('--ps', '--print-soname', 'Show soname specified in DT_SONAME.') do
82
+ opts.on('--print-runpath', '--pr', 'Show the path specified in DT_RUNPATH.') do
83
+ @options[:print] << :runpath
84
+ end
85
+
86
+ opts.on('--print-soname', '--ps', 'Show soname specified in DT_SONAME.') do
74
87
  @options[:print] << :soname
75
88
  end
76
89
 
77
- opts.on('--interp INTERP', '--set-interpreter INTERP', 'Set interpreter\'s name.') do |interp|
90
+ opts.on('--set-interpreter INTERP', '--interp INTERP', 'Set interpreter\'s name.') do |interp|
78
91
  @options[:set][:interpreter] = interp
79
92
  end
80
93
 
81
- opts.on('--so SONAME', '--set-soname SONAME', 'Set name of a shared library.') do |soname|
94
+ opts.on('--set-needed LIB1,LIB2,LIB3', '--needed LIB1,LIB2,LIB3', Array,
95
+ 'Set needed libraries, this will remove all existent needed libraries.') do |needs|
96
+ @options[:set][:needed] = needs
97
+ end
98
+
99
+ opts.on('--add-needed LIB', 'Append a new needed library.') do |lib|
100
+ @options[:needed] << [:add, lib]
101
+ end
102
+
103
+ opts.on('--remove-needed LIB', 'Remove a needed library.') do |lib|
104
+ @options[:needed] << [:remove, lib]
105
+ end
106
+
107
+ opts.on('--replace-needed LIB1,LIB2', Array, 'Replace needed library LIB1 as LIB2.') do |libs|
108
+ @options[:needed] << [:replace, libs]
109
+ end
110
+
111
+ opts.on('--set-runpath PATH', '--runpath PATH', 'Set the path of runpath.') do |path|
112
+ @options[:set][:runpath] = path
113
+ end
114
+
115
+ opts.on(
116
+ '--force-rpath',
117
+ 'According to the ld.so docs, DT_RPATH is obsolete,',
118
+ "#{SCRIPT_NAME} will always try to get/set DT_RUNPATH first.",
119
+ 'Use this option to force every operations related to runpath (e.g. --runpath)',
120
+ 'to consider \'DT_RPATH\' instead of \'DT_RUNPATH\'.'
121
+ ) do
122
+ @options[:force_rpath] = true
123
+ end
124
+
125
+ opts.on('--set-soname SONAME', '--so SONAME', 'Set name of a shared library.') do |soname|
82
126
  @options[:set][:soname] = soname
83
127
  end
84
128
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PatchELF
2
4
  # Helper methods for internal usage.
3
5
  module Helper
@@ -40,11 +42,11 @@ module PatchELF
40
42
  # @return [Integer]
41
43
  # Aligned result.
42
44
  # @example
43
- # Helper.aligndown(0x1234)
45
+ # aligndown(0x1234)
44
46
  # #=> 4096
45
- # Helper.aligndown(0x33, 0x20)
47
+ # aligndown(0x33, 0x20)
46
48
  # #=> 32
47
- # Helper.aligndown(0x10, 0x8)
49
+ # aligndown(0x10, 0x8)
48
50
  # #=> 16
49
51
  def aligndown(val, align = PAGE_SIZE)
50
52
  val - (val & (align - 1))
@@ -55,11 +57,11 @@ module PatchELF
55
57
  # @return [Integer]
56
58
  # Aligned result.
57
59
  # @example
58
- # Helper.alignup(0x1234)
60
+ # alignup(0x1234)
59
61
  # #=> 8192
60
- # Helper.alignup(0x33, 0x20)
62
+ # alignup(0x33, 0x20)
61
63
  # #=> 64
62
- # Helper.alignup(0x10, 0x8)
64
+ # alignup(0x10, 0x8)
63
65
  # #=> 16
64
66
  def alignup(val, align = PAGE_SIZE)
65
67
  (val & (align - 1)).zero? ? val : (aligndown(val, align) + align)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'logger'
2
4
 
3
5
  require 'patchelf/helper'
data/lib/patchelf/mm.rb CHANGED
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'patchelf/helper'
2
- require 'patchelf/interval'
3
4
 
4
5
  module PatchELF
5
6
  # Memory management, provides malloc/free to allocate LOAD segments.
7
+ # @private
6
8
  class MM
7
- attr_reader :extend_size # @return [Integer]
8
- attr_reader :threshold # @return [Integer]
9
+ attr_reader :extend_size # @return [Integer] The size extended.
10
+ attr_reader :threshold # @return [Integer] Where the file start to be extended.
9
11
 
10
12
  # Instantiate a {MM} object.
11
13
  # @param [ELFTools::ELFFile] elf
@@ -21,7 +23,7 @@ module PatchELF
21
23
  # @yieldreturn [void]
22
24
  # One can only do the following things in the block:
23
25
  # 1. Set ELF headers' attributes (with ELFTools)
24
- # 2. Invoke {Patcher#inline_patch}
26
+ # 2. Invoke {Saver#inline_patch}
25
27
  def malloc(size, &block)
26
28
  # TODO: check size > 0
27
29
  @request << [size, block]
@@ -32,39 +34,21 @@ module PatchELF
32
34
  def dispatch!
33
35
  return if @request.empty?
34
36
 
35
- request_size = @request.map(&:first).inject(0, :+)
36
- # TODO: raise exception if no LOAD exists.
37
-
38
- # We're going to expand the first LOAD segment.
39
- # Sometimes there's a 'gap' between the first and the second LOAD segment,
40
- # in this case we only need to expand the first LOAD segment and remain all other things unchanged.
41
- if gap_useful?(request_size)
42
- invoke_callbacks
43
- grow_first_load(request_size)
44
- elsif extendable?(request_size)
45
- # After extended we should have large enough 'gap'.
46
-
47
- # | 1 | | 2 |
48
- # | 1 | | 2 |
49
- #=>
50
- # | 1 | | 2 |
51
- # | 1 | | 2 |
52
- # This is really dangerous..
53
- # We have to check all p_offset / sh_offset
54
- # 1. Use ELFTools to patch all headers
55
- # 2. Mark the extended size, inline_patch will behave different after this.
56
- # 3. Invoke block.call, which might copy tables and (not-allow-to-patch) strings into the gap
57
-
58
- @threshold = load_segments[1].file_head
59
- # 1.file_tail + request_size <= 2.file_head + 0x1000x
60
- @extend_size = PatchELF::Helper.alignup(request_size - gap_between_load.size)
61
- shift_attributes
62
-
63
- invoke_callbacks
64
- grow_first_load(request_size)
65
- # else
66
- # This can happen in 32bit
67
- end
37
+ @request_size = @request.map(&:first).inject(0, :+)
38
+ # The malloc-ed area must be 'rw-' since the dynamic table will be modified during runtime.
39
+ # Find all LOADs and calculate their f-gaps and m-gaps.
40
+ # We prefer f-gap since it doesn't need move the whole binaries.
41
+ # 1. Find if any f-gap has enough size, and one of the LOAD next to it is 'rw-'.
42
+ # - expand (forwardlly), only need to change the attribute of LOAD.
43
+ # 2. Do 1. again but consider m-gaps instead.
44
+ # - expand (forwardlly), need to modify all section headers.
45
+ # 3. We have to create a new LOAD, now we need to expand the first LOAD for putting new segment header.
46
+
47
+ # First of all we check if there're less than two LOADs.
48
+ abnormal_elf('No LOAD segment found, not an executable.') if load_segments.empty?
49
+ # TODO: Handle only one LOAD. (be careful if memsz > filesz)
50
+
51
+ fgap_method || mgap_method || new_load_method
68
52
  end
69
53
 
70
54
  # Query if extended.
@@ -73,7 +57,11 @@ module PatchELF
73
57
  defined?(@threshold)
74
58
  end
75
59
 
60
+ # Get correct offset after the extension.
61
+ #
62
+ # @param [Integer] off
76
63
  # @return [Integer]
64
+ # Shifted offset.
77
65
  def extended_offset(off)
78
66
  return off unless defined?(@threshold)
79
67
  return off if off < @threshold
@@ -83,37 +71,74 @@ module PatchELF
83
71
 
84
72
  private
85
73
 
86
- def gap_useful?(need_size)
87
- # Two conditions:
88
- # 1. gap is large enough
89
- gap = gap_between_load
90
- return false if gap.size < need_size
74
+ def fgap_method
75
+ idx = find_gap { |prv, nxt| nxt.file_head - prv.file_tail }
76
+ return false if idx.nil?
77
+
78
+ loads = load_segments
79
+ # prefer extend backwardly
80
+ return extend_backward(loads[idx - 1]) if writable?(loads[idx - 1])
91
81
 
92
- # XXX: Do we really need this..?
93
- # If gap is enough but not all zeros, we will fail on extension..
94
- # 2. gap is all zeroes.
95
- # @elf.stream.pos = gap.head
96
- # return false unless @elf.stream.read(gap.size).bytes.inject(0, :+).zero?
82
+ extend_forward(loads[idx])
83
+ end
84
+
85
+ def extend_backward(seg, size = @request_size)
86
+ invoke_callbacks(seg, seg.file_tail)
87
+ seg.header.p_filesz += size
88
+ seg.header.p_memsz += size
89
+ true
90
+ end
97
91
 
92
+ def extend_forward(seg, size = @request_size)
93
+ seg.header.p_offset -= size
94
+ seg.header.p_vaddr -= size
95
+ seg.header.p_filesz += size
96
+ seg.header.p_memsz += size
97
+ invoke_callbacks(seg, seg.file_head)
98
98
  true
99
99
  end
100
100
 
101
- # @return [PatchELF::Interval]
102
- def gap_between_load
103
- # We need this cache since the second LOAD might be changed
104
- return @gap_between_load if defined?(@gap_between_load)
101
+ def mgap_method
102
+ # | 1 | | 2 |
103
+ # | 1 | | 2 |
104
+ #=>
105
+ # | 1 | | 2 |
106
+ # | 1 | | 2 |
107
+ idx = find_gap(check_sz: false) { |prv, nxt| PatchELF::Helper.aligndown(nxt.mem_head) - prv.mem_tail }
108
+ return false if idx.nil?
105
109
 
106
- loads = load_segments.map do |seg|
107
- PatchELF::Interval.new(seg.file_head, seg.size)
110
+ loads = load_segments
111
+ @threshold = loads[idx].file_head
112
+ @extend_size = PatchELF::Helper.alignup(@request_size)
113
+ shift_attributes
114
+ # prefer backward than forward
115
+ return extend_backward(loads[idx - 1]) if writable?(loads[idx - 1])
116
+
117
+ # note: loads[idx].file_head has been changed in shift_attributes
118
+ extend_forward(loads[idx], @extend_size)
119
+ end
120
+
121
+ def find_gap(check_sz: true)
122
+ loads = load_segments
123
+ loads.each_with_index do |l, i|
124
+ next if i.zero?
125
+ next unless writable?(l) || writable?(loads[i - 1])
126
+
127
+ sz = yield(loads[i - 1], l)
128
+ abnormal_elf('LOAD segments are out of order.') if check_sz && sz.negative?
129
+ next unless sz >= @request_size
130
+
131
+ return i
108
132
  end
109
- # TODO: raise if loads.min != loads.first
133
+ nil
134
+ end
135
+
136
+ def new_load_method
137
+ raise NotImplementedError
138
+ end
110
139
 
111
- loads.sort!
112
- # Only one LOAD, the gap has infinity size!
113
- size = if loads.size == 1 then Float::INFINITY
114
- else loads[1].head - loads.first.tail
115
- end
116
- @gap_between_load = PatchELF::Interval.new(loads.first.tail, size)
140
+ def writable?(seg)
141
+ seg.readable? && seg.writable?
117
142
  end
118
143
 
119
144
  # For all attributes >= threshold, += offset
@@ -123,14 +148,18 @@ module PatchELF
123
148
  # all
124
149
  # Segments:
125
150
  # all
126
- # XXX: will be buggy if one day the number of segments might be changed.
151
+ # XXX: will be buggy if someday the number of segments can be changed.
127
152
 
128
153
  # Bottom-up
129
154
  @elf.each_sections do |sec|
130
155
  sec.header.sh_offset += extend_size if sec.header.sh_offset >= threshold
131
156
  end
132
157
  @elf.each_segments do |seg|
133
- seg.header.p_offset += extend_size if seg.header.p_offset >= threshold
158
+ next unless seg.header.p_offset >= threshold
159
+
160
+ seg.header.p_offset += extend_size
161
+ # We have to change align of LOAD segment since ld.so checks it.
162
+ seg.header.p_align = Helper::PAGE_SIZE if seg.is_a?(ELFTools::Segments::LoadSegment)
134
163
  end
135
164
 
136
165
  @elf.header.e_shoff += extend_size if @elf.header.e_shoff >= threshold
@@ -140,28 +169,16 @@ module PatchELF
140
169
  @elf.segments_by_type(:load)
141
170
  end
142
171
 
143
- def extendable?(request_size)
144
- loads = load_segments
145
- # We can assume loads.size >= 2 because
146
- # 0: has raised an exception before
147
- # 1: the gap must be used, nobody cares extendable size.
148
- # Calcluate the max size of the first LOAD segment can be.
149
- PatchELF::Helper.aligndown(loads[1].mem_head) - loads.first.mem_tail >= request_size
150
- end
151
-
152
- def invoke_callbacks
153
- seg = load_segments.first
154
- cur = gap_between_load.head
172
+ def invoke_callbacks(seg, start)
173
+ cur = start
155
174
  @request.each do |sz, block|
156
175
  block.call(cur, seg.offset_to_vma(cur))
157
176
  cur += sz
158
177
  end
159
178
  end
160
179
 
161
- def grow_first_load(size)
162
- seg = load_segments.first
163
- seg.header.p_filesz += size
164
- seg.header.p_memsz += size
180
+ def abnormal_elf(msg)
181
+ raise ArgumentError, msg
165
182
  end
166
183
  end
167
184
  end
@@ -1,8 +1,10 @@
1
- require 'elftools'
2
- require 'fileutils'
1
+ # encoding: ascii-8bit
2
+ # frozen_string_literal: true
3
+
4
+ require 'elftools/elf_file'
3
5
 
4
6
  require 'patchelf/logger'
5
- require 'patchelf/mm'
7
+ require 'patchelf/saver'
6
8
 
7
9
  module PatchELF
8
10
  # Class to handle all patching things.
@@ -17,6 +19,16 @@ module PatchELF
17
19
  @in_file = filename
18
20
  @elf = ELFTools::ELFFile.new(File.open(filename))
19
21
  @set = {}
22
+ @rpath_sym = :runpath
23
+ end
24
+
25
+ # @return [String?]
26
+ # Get interpreter's name.
27
+ # @example
28
+ # PatchELF::Patcher.new('/bin/ls').interpreter
29
+ # #=> "/lib64/ld-linux-x86-64.so.2"
30
+ def interpreter
31
+ @set[:interpreter] || interpreter_
20
32
  end
21
33
 
22
34
  # Set interpreter's name.
@@ -26,11 +38,73 @@ module PatchELF
26
38
  # @param [String] interp
27
39
  # @macro note_apply
28
40
  def interpreter=(interp)
29
- return if interpreter.nil? # will also show warning if there's no interp segment.
41
+ return if interpreter_.nil? # will also show warning if there's no interp segment.
30
42
 
31
43
  @set[:interpreter] = interp
32
44
  end
33
45
 
46
+ # Get needed libraries.
47
+ # @return [Array<String>]
48
+ # @example
49
+ # patcher = PatchELF::Patcher.new('/bin/ls')
50
+ # patcher.needed
51
+ # #=> ["libselinux.so.1", "libc.so.6"]
52
+ def needed
53
+ @set[:needed] || needed_
54
+ end
55
+
56
+ # Set needed libraries.
57
+ # @param [Array<String>] needs
58
+ # @macro note_apply
59
+ def needed=(needs)
60
+ @set[:needed] = needs
61
+ end
62
+
63
+ # Add the needed library.
64
+ # @param [String] need
65
+ # @return [void]
66
+ # @macro note_apply
67
+ def add_needed(need)
68
+ @set[:needed] ||= needed_
69
+ @set[:needed] << need
70
+ end
71
+
72
+ # Remove the needed library.
73
+ # @param [String] need
74
+ # @return [void]
75
+ # @macro note_apply
76
+ def remove_needed(need)
77
+ @set[:needed] ||= needed_
78
+ @set[:needed].delete(need)
79
+ end
80
+
81
+ # Replace needed library +src+ with +tar+.
82
+ #
83
+ # @param [String] src
84
+ # Library to be replaced.
85
+ # @param [String] tar
86
+ # Library replace with.
87
+ # @return [void]
88
+ # @macro note_apply
89
+ def replace_needed(src, tar)
90
+ @set[:needed] ||= needed_
91
+ @set[:needed].map! { |v| v == src ? tar : v }
92
+ end
93
+
94
+ # Get the soname of a shared library.
95
+ # @return [String?] The name.
96
+ # @example
97
+ # patcher = PatchELF::Patcher.new('/bin/ls')
98
+ # patcher.soname
99
+ # # [WARN] Entry DT_SONAME not found, not a shared library?
100
+ # #=> nil
101
+ # @example
102
+ # PatchELF::Patcher.new('/lib/x86_64-linux-gnu/libc.so.6').soname
103
+ # #=> "libc.so.6"
104
+ def soname
105
+ @set[:soname] || soname_
106
+ end
107
+
34
108
  # Set soname.
35
109
  #
36
110
  # If the input ELF is not a shared library with a soname,
@@ -38,19 +112,32 @@ module PatchELF
38
112
  # @param [String] name
39
113
  # @macro note_apply
40
114
  def soname=(name)
41
- return if soname.nil?
115
+ return if soname_.nil?
42
116
 
43
117
  @set[:soname] = name
44
118
  end
45
119
 
46
- # Set rpath.
120
+ # Get runpath.
121
+ # @return [String?]
122
+ def runpath
123
+ @set[@rpath_sym] || runpath_
124
+ end
125
+
126
+ # Set runpath.
47
127
  #
48
- # If DT_RPATH is not presented in the input ELF,
49
- # a new DT_RPATH attribute will be inserted into the DYNAMIC segment.
50
- # @param [String] rpath
128
+ # If DT_RUNPATH is not presented in the input ELF,
129
+ # a new DT_RUNPATH attribute will be inserted into the DYNAMIC segment.
130
+ # @param [String] runpath
51
131
  # @macro note_apply
52
- def rpath=(rpath)
53
- @set[:rpath] = rpath
132
+ def runpath=(runpath)
133
+ @set[@rpath_sym] = runpath
134
+ end
135
+
136
+ # Set all operations related to DT_RUNPATH to use DT_RPATH.
137
+ # @return [self]
138
+ def use_rpath!
139
+ @rpath_sym = :rpath
140
+ self
54
141
  end
55
142
 
56
143
  # Save the patched ELF as +out_file+.
@@ -58,87 +145,18 @@ module PatchELF
58
145
  # If +out_file+ is +nil+, the original input file will be modified.
59
146
  # @return [void]
60
147
  def save(out_file = nil)
61
- # TODO: Test if we can save twice, and the output files are exactly same.
62
148
  # If nothing is modified, return directly.
63
149
  return if out_file.nil? && !dirty?
64
150
 
65
151
  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
98
-
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)
107
- end
108
-
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]
152
+ saver = PatchELF::Saver.new(@in_file, out_file, @set)
130
153
 
131
- __send__(name)
154
+ saver.save!
132
155
  end
133
156
 
134
157
  private
135
158
 
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
159
+ def interpreter_
142
160
  segment = @elf.segment_by_type(:interp)
143
161
  return PatchELF::Logger.warn('No interpreter found.') if segment.nil?
144
162
 
@@ -146,7 +164,7 @@ module PatchELF
146
164
  end
147
165
 
148
166
  # @return [Array<String>]
149
- def needed
167
+ def needed_
150
168
  segment = dynamic_or_log
151
169
  return if segment.nil?
152
170
 
@@ -154,64 +172,20 @@ module PatchELF
154
172
  end
155
173
 
156
174
  # @return [String?]
157
- def rpath
158
- # TODO: consider both rpath and runpath
159
- tag_name_or_log(:rpath, 'Entry DT_RPATH not found.')
175
+ def runpath_
176
+ tag_name_or_log(@rpath_sym, "Entry DT_#{@rpath_sym.to_s.upcase} not found.")
160
177
  end
161
178
 
162
179
  # @return [String?]
163
- def soname
180
+ def soname_
164
181
  tag_name_or_log(:soname, 'Entry DT_SONAME not found, not a shared library?')
165
182
  end
166
183
 
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
184
  # @return [Boolean]
203
185
  def dirty?
204
186
  @set.any?
205
187
  end
206
188
 
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
189
  def tag_name_or_log(type, log_msg)
216
190
  segment = dynamic_or_log
217
191
  return if segment.nil?
@@ -227,12 +201,5 @@ module PatchELF
227
201
  PatchELF::Logger.warn('DYNAMIC segment not found, might be a statically-linked ELF?') if s.nil?
228
202
  end
229
203
  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
204
  end
238
205
  end
@@ -0,0 +1,284 @@
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
+ @strtab_extend_requests.each do |str, block|
193
+ # TODO: make here more efficient
194
+ block.call(new_str.index(str + "\x00"))
195
+ end
196
+ # Now patching strtab header
197
+ strtab.header.d_val = vaddr
198
+ # We also need to patch dynstr to let readelf have correct output.
199
+ if dynstr
200
+ dynstr.sh_size = new_str.size
201
+ dynstr.sh_offset = off
202
+ dynstr.sh_addr = vaddr
203
+ end
204
+ end
205
+ end
206
+
207
+ # @param [String] str
208
+ # @yieldparam [Integer] idx
209
+ # @yieldreturn [void]
210
+ def reg_str_table(str, &block)
211
+ idx = strtab_string.index(str + "\x00")
212
+ # Request string is already exist
213
+ return yield idx if idx
214
+
215
+ # Record the request
216
+ @strtab_extend_requests << [str, block]
217
+ end
218
+
219
+ def strtab_string
220
+ return @strtab_string if defined?(@strtab_string)
221
+
222
+ # TODO: handle no strtab exists..
223
+ offset = @elf.offset_from_vma(dynamic.tag_by_type(:strtab).value)
224
+ # This is a little tricky since no length information is stored in the tag.
225
+ # We first get the file offset of the string then 'guess' where the end is.
226
+ @elf.stream.pos = offset
227
+ @strtab_string = +''
228
+ loop do
229
+ c = @elf.stream.read(1)
230
+ break unless c =~ /\x00|[[:print:]]/
231
+
232
+ @strtab_string << c
233
+ end
234
+ @strtab_string
235
+ end
236
+
237
+ # This can only be used for patching interpreter's name
238
+ # or set strings in a malloc-ed area.
239
+ # i.e. NEVER intend to change the string defined in strtab
240
+ def inline_patch(off, str)
241
+ @inline_patch[off] = str
242
+ end
243
+
244
+ # Modify the out_file according to registered patches.
245
+ def patch_out(out_file)
246
+ File.open(out_file, 'r+') do |f|
247
+ if @mm.extended?
248
+ original_head = @mm.threshold
249
+ extra = {}
250
+ # Copy all data after the second load
251
+ @elf.stream.pos = original_head
252
+ extra[original_head + @mm.extend_size] = @elf.stream.read # read to end
253
+ # zero out the 'gap' we created
254
+ extra[original_head] = "\x00" * @mm.extend_size
255
+ extra.each do |pos, str|
256
+ f.pos = pos
257
+ f.write(str)
258
+ end
259
+ end
260
+ @elf.patches.each do |pos, str|
261
+ f.pos = @mm.extended_offset(pos)
262
+ f.write(str)
263
+ end
264
+
265
+ @inline_patch.each do |pos, str|
266
+ f.pos = pos
267
+ f.write(str)
268
+ end
269
+ end
270
+ end
271
+
272
+ # @return [ELFTools::Sections::Section?]
273
+ def section_header(name)
274
+ sec = @elf.section_by_name(name)
275
+ return if sec.nil?
276
+
277
+ sec.header
278
+ end
279
+
280
+ def dynamic
281
+ @dynamic ||= @elf.segment_by_type(:dynamic)
282
+ end
283
+ end
284
+ end
@@ -1,4 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module PatchELF
2
4
  # Current gem version.
3
- VERSION = '0.0.0'.freeze
5
+ VERSION = '0.1.0'.freeze
4
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: patchelf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - david942j
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-01-07 00:00:00.000000000 Z
11
+ date: 2019-01-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: elftools
@@ -123,10 +123,10 @@ files:
123
123
  - lib/patchelf.rb
124
124
  - lib/patchelf/cli.rb
125
125
  - lib/patchelf/helper.rb
126
- - lib/patchelf/interval.rb
127
126
  - lib/patchelf/logger.rb
128
127
  - lib/patchelf/mm.rb
129
128
  - lib/patchelf/patcher.rb
129
+ - lib/patchelf/saver.rb
130
130
  - lib/patchelf/version.rb
131
131
  homepage: https://github.com/david942j/patchelf.rb
132
132
  licenses:
@@ -147,8 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
147
  - !ruby/object:Gem::Version
148
148
  version: '0'
149
149
  requirements: []
150
- rubyforge_project:
151
- rubygems_version: 2.7.6
150
+ rubygems_version: 3.0.2
152
151
  signing_key:
153
152
  specification_version: 4
154
153
  summary: patchelf
@@ -1,30 +0,0 @@
1
- module PatchELF
2
- # Provides easier-to-use methods for manipulating LOAD segment.
3
- #
4
- # Internal use only.
5
- class Interval
6
- include Comparable
7
-
8
- attr_reader :head # @return [Integer] Head.
9
- attr_reader :size # @return [Integer] Length.
10
-
11
- # @param [Integer] head
12
- # @param [Integer] size
13
- def initialize(head, size)
14
- @head = head
15
- @size = size
16
- end
17
-
18
- # Comparator.
19
- # @param [Interval] other
20
- def <=>(other)
21
- head <=> other.head
22
- end
23
-
24
- # @return [Integer]
25
- # The end of this interval.
26
- def tail
27
- head + size
28
- end
29
- end
30
- end