patchelf 0.0.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 +7 -0
- data/README.md +79 -0
- data/bin/patchelf.rb +5 -0
- data/lib/patchelf.rb +8 -0
- data/lib/patchelf/cli.rb +91 -0
- data/lib/patchelf/helper.rb +68 -0
- data/lib/patchelf/interval.rb +30 -0
- data/lib/patchelf/logger.rb +23 -0
- data/lib/patchelf/mm.rb +167 -0
- data/lib/patchelf/patcher.rb +238 -0
- data/lib/patchelf/version.rb +4 -0
- metadata +155 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e432d8ac7a3e75e8c9e80d433bc756e635d6b4d057b7e02ed952be113c7edbf5
|
4
|
+
data.tar.gz: 463cee3d6e780346706752f2a537e67fde649db82b206a8d79f15e60dd4cb4ae
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bebe4cca89632d3660045d7582c19cf356562d2cbd389cc910377e88ea293c4fb812bb8917e89670cad05224254497c361f608e7429dcfe2c09ed94d31834a9f
|
7
|
+
data.tar.gz: 849a0a0b5b8c5bc1c1e52c4f65c9452917f58cf881bca89de4a4986df0556fd677724fe1b17466ff469eed8bcada2320d69169951d9ea0ac4aed1d9be6e4d5aa
|
data/README.md
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
[](https://travis-ci.org/david942j/patchelf.rb)
|
2
|
+
[](https://dependabot.com)
|
3
|
+
[](https://codeclimate.com/github/david942j/patchelf.rb)
|
4
|
+
[](https://codeclimate.com/github/david942j/patchelf.rb)
|
5
|
+
[](https://codeclimate.com/github/david942j/patchelf.rb/coverage)
|
6
|
+
[](https://inch-ci.org/github/david942j/patchelf.rb)
|
7
|
+
[](http://choosealicense.com/licenses/mit/)
|
8
|
+
|
9
|
+
# patchelf.rb
|
10
|
+
|
11
|
+
Implements features of NixOS/patchelf in pure Ruby.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
WIP.
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
```
|
20
|
+
$ patchelf.rb
|
21
|
+
# 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
|
26
|
+
# Set interpreter's name.
|
27
|
+
# --so, --set-soname SONAME Set name of a shared library.
|
28
|
+
# --version Show current gem's version.
|
29
|
+
|
30
|
+
```
|
31
|
+
|
32
|
+
### Display information
|
33
|
+
```
|
34
|
+
$ 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
|
37
|
+
|
38
|
+
```
|
39
|
+
|
40
|
+
### Change the dynamic loader (interpreter)
|
41
|
+
```
|
42
|
+
# $ patchelf.rb --interp NEW_INTERP input.elf output.elf
|
43
|
+
$ patchelf.rb --interp /lib/AAAA.so /bin/ls ls.patch
|
44
|
+
|
45
|
+
$ file ls.patch
|
46
|
+
# ls.patch: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/AAAA.so, for GNU/Linux 3.2.0, BuildID[sha1]=9567f9a28e66f4d7ec4baf31cfbf68d0410f0ae6, stripped
|
47
|
+
|
48
|
+
```
|
49
|
+
|
50
|
+
### Change SONAME of a shared library
|
51
|
+
```
|
52
|
+
$ patchelf.rb --soname libnewname.so.217 /lib/x86_64-linux-gnu/libc.so.6 ./libc.patched
|
53
|
+
|
54
|
+
$ readelf -d libc.patched | grep SONAME
|
55
|
+
|
56
|
+
```
|
57
|
+
|
58
|
+
### As Ruby library
|
59
|
+
```rb
|
60
|
+
require 'patchelf'
|
61
|
+
|
62
|
+
patcher = PatchELF::Patcher.new('/bin/ls')
|
63
|
+
patcher.get(:interpreter)
|
64
|
+
#=> "/lib64/ld-linux-x86-64.so.2"
|
65
|
+
|
66
|
+
patcher.interpreter = '/lib/AAAA.so.2'
|
67
|
+
patcher.get(:interpreter)
|
68
|
+
#=> "/lib/AAAA.so.2"
|
69
|
+
|
70
|
+
patcher.save('ls.patch')
|
71
|
+
|
72
|
+
# $ file ls.patch
|
73
|
+
# ls.patch: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/AAAA.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=9567f9a28e66f4d7ec4baf31cfbf68d0410f0ae6, stripped
|
74
|
+
|
75
|
+
```
|
76
|
+
|
77
|
+
## Environment
|
78
|
+
|
79
|
+
patchelf.rb is implemented in pure Ruby, so it should work in all environments include Linux, maxOS, and Windows!
|
data/bin/patchelf.rb
ADDED
data/lib/patchelf.rb
ADDED
data/lib/patchelf/cli.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
require 'patchelf/patcher'
|
4
|
+
require 'patchelf/version'
|
5
|
+
|
6
|
+
module PatchELF
|
7
|
+
# For command line interface to parsing arguments.
|
8
|
+
module CLI
|
9
|
+
# Name of binary.
|
10
|
+
SCRIPT_NAME = 'patchelf.rb'.freeze
|
11
|
+
# CLI usage string.
|
12
|
+
USAGE = format('Usage: %s <commands> FILENAME [OUTPUT_FILE]', SCRIPT_NAME).freeze
|
13
|
+
|
14
|
+
module_function
|
15
|
+
|
16
|
+
# Main method of CLI.
|
17
|
+
# @param [Array<String>] argv
|
18
|
+
# Command line arguments.
|
19
|
+
# @return [void]
|
20
|
+
# @example
|
21
|
+
# PatchELF::CLI.work(%w[--help])
|
22
|
+
# # usage message to stdout
|
23
|
+
# PatchELF::CLI.work(%w[--version])
|
24
|
+
# # version message to stdout
|
25
|
+
def work(argv)
|
26
|
+
@options = {
|
27
|
+
set: {},
|
28
|
+
print: []
|
29
|
+
}
|
30
|
+
return $stdout.puts "PatchELF Version #{PatchELF::VERSION}" if argv.include?('--version')
|
31
|
+
return $stdout.puts option_parser unless parse(argv)
|
32
|
+
|
33
|
+
# Now the options are (hopefully) valid, let's process the ELF file.
|
34
|
+
patcher = PatchELF::Patcher.new(@options[:in_file])
|
35
|
+
# TODO: Handle ELFTools::ELFError
|
36
|
+
@options[:print].uniq.each do |s|
|
37
|
+
content = patcher.get(s)
|
38
|
+
next if content.nil?
|
39
|
+
|
40
|
+
$stdout.puts "#{s.to_s.capitalize}: #{Array(content).join(' ')}"
|
41
|
+
end
|
42
|
+
|
43
|
+
@options[:set].each do |sym, val|
|
44
|
+
patcher.__send__("#{sym}=".to_sym, val)
|
45
|
+
end
|
46
|
+
|
47
|
+
patcher.save(@options[:out_file])
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def parse(argv)
|
53
|
+
remain = option_parser.permute(argv)
|
54
|
+
return false if remain.first.nil?
|
55
|
+
|
56
|
+
@options[:in_file] = remain.first
|
57
|
+
@options[:out_file] = remain[1] # can be nil
|
58
|
+
true
|
59
|
+
end
|
60
|
+
|
61
|
+
def option_parser
|
62
|
+
@option_parser ||= OptionParser.new do |opts|
|
63
|
+
opts.banner = USAGE
|
64
|
+
|
65
|
+
opts.on('--pi', '--print-interpreter', 'Show interpreter\'s name.') do
|
66
|
+
@options[:print] << :interpreter
|
67
|
+
end
|
68
|
+
|
69
|
+
opts.on('--pn', '--print-needed', 'Show needed libraries specified in DT_NEEDED.') do
|
70
|
+
@options[:print] << :needed
|
71
|
+
end
|
72
|
+
|
73
|
+
opts.on('--ps', '--print-soname', 'Show soname specified in DT_SONAME.') do
|
74
|
+
@options[:print] << :soname
|
75
|
+
end
|
76
|
+
|
77
|
+
opts.on('--interp INTERP', '--set-interpreter INTERP', 'Set interpreter\'s name.') do |interp|
|
78
|
+
@options[:set][:interpreter] = interp
|
79
|
+
end
|
80
|
+
|
81
|
+
opts.on('--so SONAME', '--set-soname SONAME', 'Set name of a shared library.') do |soname|
|
82
|
+
@options[:set][:soname] = soname
|
83
|
+
end
|
84
|
+
|
85
|
+
opts.on('--version', 'Show current gem\'s version.') {}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
extend self
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module PatchELF
|
2
|
+
# Helper methods for internal usage.
|
3
|
+
module Helper
|
4
|
+
# The size of one page.
|
5
|
+
PAGE_SIZE = 0x1000
|
6
|
+
|
7
|
+
module_function
|
8
|
+
|
9
|
+
# Color codes for pretty print.
|
10
|
+
COLOR_CODE = {
|
11
|
+
esc_m: "\e[0m",
|
12
|
+
info: "\e[38;5;82m", # light green
|
13
|
+
warn: "\e[38;5;230m", # light yellow
|
14
|
+
error: "\e[38;5;196m" # heavy red
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
# For wrapping string with color codes for prettier inspect.
|
18
|
+
# @param [String] str
|
19
|
+
# Content to colorize.
|
20
|
+
# @param [Symbol] type
|
21
|
+
# Specify which kind of color to use, valid symbols are defined in {.COLOR_CODE}.
|
22
|
+
# @return [String]
|
23
|
+
# String that wrapped with color codes.
|
24
|
+
def colorize(str, type)
|
25
|
+
return str unless color_enabled?
|
26
|
+
|
27
|
+
cc = COLOR_CODE
|
28
|
+
color = cc.key?(type) ? cc[type] : ''
|
29
|
+
"#{color}#{str.sub(COLOR_CODE[:esc_m], color)}#{cc[:esc_m]}"
|
30
|
+
end
|
31
|
+
|
32
|
+
# For {#colorize} to decide if need add color codes.
|
33
|
+
# @return [Boolean]
|
34
|
+
def color_enabled?
|
35
|
+
$stderr.tty?
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param [Integer] val
|
39
|
+
# @param [Integer] align
|
40
|
+
# @return [Integer]
|
41
|
+
# Aligned result.
|
42
|
+
# @example
|
43
|
+
# Helper.aligndown(0x1234)
|
44
|
+
# #=> 4096
|
45
|
+
# Helper.aligndown(0x33, 0x20)
|
46
|
+
# #=> 32
|
47
|
+
# Helper.aligndown(0x10, 0x8)
|
48
|
+
# #=> 16
|
49
|
+
def aligndown(val, align = PAGE_SIZE)
|
50
|
+
val - (val & (align - 1))
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param [Integer] val
|
54
|
+
# @param [Integer] align
|
55
|
+
# @return [Integer]
|
56
|
+
# Aligned result.
|
57
|
+
# @example
|
58
|
+
# Helper.alignup(0x1234)
|
59
|
+
# #=> 8192
|
60
|
+
# Helper.alignup(0x33, 0x20)
|
61
|
+
# #=> 64
|
62
|
+
# Helper.alignup(0x10, 0x8)
|
63
|
+
# #=> 16
|
64
|
+
def alignup(val, align = PAGE_SIZE)
|
65
|
+
(val & (align - 1)).zero? ? val : (aligndown(val, align) + align)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,30 @@
|
|
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
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
require 'patchelf/helper'
|
4
|
+
|
5
|
+
module PatchELF
|
6
|
+
# A logger for internal usage.
|
7
|
+
module Logger
|
8
|
+
module_function
|
9
|
+
|
10
|
+
@logger = ::Logger.new($stderr).tap do |log|
|
11
|
+
log.formatter = proc do |severity, _datetime, _progname, msg|
|
12
|
+
"[#{PatchELF::Helper.colorize(severity, severity.downcase.to_sym)}] #{msg}\n"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
%i[info warn error].each do |sym|
|
17
|
+
define_method(sym) do |msg|
|
18
|
+
@logger.__send__(sym, msg)
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/patchelf/mm.rb
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'patchelf/helper'
|
2
|
+
require 'patchelf/interval'
|
3
|
+
|
4
|
+
module PatchELF
|
5
|
+
# Memory management, provides malloc/free to allocate LOAD segments.
|
6
|
+
class MM
|
7
|
+
attr_reader :extend_size # @return [Integer]
|
8
|
+
attr_reader :threshold # @return [Integer]
|
9
|
+
|
10
|
+
# Instantiate a {MM} object.
|
11
|
+
# @param [ELFTools::ELFFile] elf
|
12
|
+
def initialize(elf)
|
13
|
+
@elf = elf
|
14
|
+
@request = []
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param [Integer] size
|
18
|
+
# @return [void]
|
19
|
+
# @yieldparam [Integer] off
|
20
|
+
# @yieldparam [Integer] vaddr
|
21
|
+
# @yieldreturn [void]
|
22
|
+
# One can only do the following things in the block:
|
23
|
+
# 1. Set ELF headers' attributes (with ELFTools)
|
24
|
+
# 2. Invoke {Patcher#inline_patch}
|
25
|
+
def malloc(size, &block)
|
26
|
+
# TODO: check size > 0
|
27
|
+
@request << [size, block]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Let the malloc / free requests be effective.
|
31
|
+
# @return [void]
|
32
|
+
def dispatch!
|
33
|
+
return if @request.empty?
|
34
|
+
|
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
|
68
|
+
end
|
69
|
+
|
70
|
+
# Query if extended.
|
71
|
+
# @return [Boolean]
|
72
|
+
def extended?
|
73
|
+
defined?(@threshold)
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [Integer]
|
77
|
+
def extended_offset(off)
|
78
|
+
return off unless defined?(@threshold)
|
79
|
+
return off if off < @threshold
|
80
|
+
|
81
|
+
off + @extend_size
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
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
|
91
|
+
|
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?
|
97
|
+
|
98
|
+
true
|
99
|
+
end
|
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)
|
105
|
+
|
106
|
+
loads = load_segments.map do |seg|
|
107
|
+
PatchELF::Interval.new(seg.file_head, seg.size)
|
108
|
+
end
|
109
|
+
# TODO: raise if loads.min != loads.first
|
110
|
+
|
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)
|
117
|
+
end
|
118
|
+
|
119
|
+
# For all attributes >= threshold, += offset
|
120
|
+
def shift_attributes
|
121
|
+
# ELFHeader->section_header
|
122
|
+
# Sections:
|
123
|
+
# all
|
124
|
+
# Segments:
|
125
|
+
# all
|
126
|
+
# XXX: will be buggy if one day the number of segments might be changed.
|
127
|
+
|
128
|
+
# Bottom-up
|
129
|
+
@elf.each_sections do |sec|
|
130
|
+
sec.header.sh_offset += extend_size if sec.header.sh_offset >= threshold
|
131
|
+
end
|
132
|
+
@elf.each_segments do |seg|
|
133
|
+
seg.header.p_offset += extend_size if seg.header.p_offset >= threshold
|
134
|
+
end
|
135
|
+
|
136
|
+
@elf.header.e_shoff += extend_size if @elf.header.e_shoff >= threshold
|
137
|
+
end
|
138
|
+
|
139
|
+
def load_segments
|
140
|
+
@elf.segments_by_type(:load)
|
141
|
+
end
|
142
|
+
|
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
|
155
|
+
@request.each do |sz, block|
|
156
|
+
block.call(cur, seg.offset_to_vma(cur))
|
157
|
+
cur += sz
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def grow_first_load(size)
|
162
|
+
seg = load_segments.first
|
163
|
+
seg.header.p_filesz += size
|
164
|
+
seg.header.p_memsz += size
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
require 'elftools'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
require 'patchelf/logger'
|
5
|
+
require 'patchelf/mm'
|
6
|
+
|
7
|
+
module PatchELF
|
8
|
+
# Class to handle all patching things.
|
9
|
+
class Patcher
|
10
|
+
# @!macro [new] note_apply
|
11
|
+
# @note This setting will be saved after {#save} being invoked.
|
12
|
+
|
13
|
+
# Instantiate a {Patcher} object.
|
14
|
+
# @param [String] filename
|
15
|
+
# Filename of input ELF.
|
16
|
+
def initialize(filename)
|
17
|
+
@in_file = filename
|
18
|
+
@elf = ELFTools::ELFFile.new(File.open(filename))
|
19
|
+
@set = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
# Set interpreter's name.
|
23
|
+
#
|
24
|
+
# If the input ELF has no existent interpreter,
|
25
|
+
# this method will show a warning and has no effect.
|
26
|
+
# @param [String] interp
|
27
|
+
# @macro note_apply
|
28
|
+
def interpreter=(interp)
|
29
|
+
return if interpreter.nil? # will also show warning if there's no interp segment.
|
30
|
+
|
31
|
+
@set[:interpreter] = interp
|
32
|
+
end
|
33
|
+
|
34
|
+
# Set soname.
|
35
|
+
#
|
36
|
+
# If the input ELF is not a shared library with a soname,
|
37
|
+
# this method will show a warning and has no effect.
|
38
|
+
# @param [String] name
|
39
|
+
# @macro note_apply
|
40
|
+
def soname=(name)
|
41
|
+
return if soname.nil?
|
42
|
+
|
43
|
+
@set[:soname] = name
|
44
|
+
end
|
45
|
+
|
46
|
+
# Set rpath.
|
47
|
+
#
|
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
|
51
|
+
# @macro note_apply
|
52
|
+
def rpath=(rpath)
|
53
|
+
@set[:rpath] = rpath
|
54
|
+
end
|
55
|
+
|
56
|
+
# Save the patched ELF as +out_file+.
|
57
|
+
# @param [String?] out_file
|
58
|
+
# If +out_file+ is +nil+, the original input file will be modified.
|
59
|
+
# @return [void]
|
60
|
+
def save(out_file = nil)
|
61
|
+
# TODO: Test if we can save twice, and the output files are exactly same.
|
62
|
+
# If nothing is modified, return directly.
|
63
|
+
return if out_file.nil? && !dirty?
|
64
|
+
|
65
|
+
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]
|
130
|
+
|
131
|
+
__send__(name)
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
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
|
142
|
+
segment = @elf.segment_by_type(:interp)
|
143
|
+
return PatchELF::Logger.warn('No interpreter found.') if segment.nil?
|
144
|
+
|
145
|
+
segment.interp_name
|
146
|
+
end
|
147
|
+
|
148
|
+
# @return [Array<String>]
|
149
|
+
def needed
|
150
|
+
segment = dynamic_or_log
|
151
|
+
return if segment.nil?
|
152
|
+
|
153
|
+
segment.tags_by_type(:needed).map(&:name)
|
154
|
+
end
|
155
|
+
|
156
|
+
# @return [String?]
|
157
|
+
def rpath
|
158
|
+
# TODO: consider both rpath and runpath
|
159
|
+
tag_name_or_log(:rpath, 'Entry DT_RPATH not found.')
|
160
|
+
end
|
161
|
+
|
162
|
+
# @return [String?]
|
163
|
+
def soname
|
164
|
+
tag_name_or_log(:soname, 'Entry DT_SONAME not found, not a shared library?')
|
165
|
+
end
|
166
|
+
|
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
|
+
# @return [Boolean]
|
203
|
+
def dirty?
|
204
|
+
@set.any?
|
205
|
+
end
|
206
|
+
|
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
|
+
def tag_name_or_log(type, log_msg)
|
216
|
+
segment = dynamic_or_log
|
217
|
+
return if segment.nil?
|
218
|
+
|
219
|
+
tag = segment.tag_by_type(type)
|
220
|
+
return PatchELF::Logger.warn(log_msg) if tag.nil?
|
221
|
+
|
222
|
+
tag.name
|
223
|
+
end
|
224
|
+
|
225
|
+
def dynamic_or_log
|
226
|
+
@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?
|
228
|
+
end
|
229
|
+
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
|
+
end
|
238
|
+
end
|
metadata
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: patchelf
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- david942j
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-01-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: elftools
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '12.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '12.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.8'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.8'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.62'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.62'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: simplecov
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.16.1
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.16.1
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: tty-platform
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.1'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.1'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: yard
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.9'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.9'
|
111
|
+
description: |2
|
112
|
+
A simple utility for modifying existing ELF executables and
|
113
|
+
libraries.
|
114
|
+
email:
|
115
|
+
- david942j@gmail.com
|
116
|
+
executables:
|
117
|
+
- patchelf.rb
|
118
|
+
extensions: []
|
119
|
+
extra_rdoc_files: []
|
120
|
+
files:
|
121
|
+
- README.md
|
122
|
+
- bin/patchelf.rb
|
123
|
+
- lib/patchelf.rb
|
124
|
+
- lib/patchelf/cli.rb
|
125
|
+
- lib/patchelf/helper.rb
|
126
|
+
- lib/patchelf/interval.rb
|
127
|
+
- lib/patchelf/logger.rb
|
128
|
+
- lib/patchelf/mm.rb
|
129
|
+
- lib/patchelf/patcher.rb
|
130
|
+
- lib/patchelf/version.rb
|
131
|
+
homepage: https://github.com/david942j/patchelf.rb
|
132
|
+
licenses:
|
133
|
+
- MIT
|
134
|
+
metadata: {}
|
135
|
+
post_install_message:
|
136
|
+
rdoc_options: []
|
137
|
+
require_paths:
|
138
|
+
- lib
|
139
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
140
|
+
requirements:
|
141
|
+
- - ">="
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: 2.1.0
|
144
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
145
|
+
requirements:
|
146
|
+
- - ">="
|
147
|
+
- !ruby/object:Gem::Version
|
148
|
+
version: '0'
|
149
|
+
requirements: []
|
150
|
+
rubyforge_project:
|
151
|
+
rubygems_version: 2.7.6
|
152
|
+
signing_key:
|
153
|
+
specification_version: 4
|
154
|
+
summary: patchelf
|
155
|
+
test_files: []
|