patchelf 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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