patchelf 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/david942j/patchelf.rb.svg?branch=master)](https://travis-ci.org/david942j/patchelf.rb)
|
2
|
+
[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=david942j/patchelf.rb)](https://dependabot.com)
|
3
|
+
[![Code Climate](https://codeclimate.com/github/david942j/patchelf.rb/badges/gpa.svg)](https://codeclimate.com/github/david942j/patchelf.rb)
|
4
|
+
[![Issue Count](https://codeclimate.com/github/david942j/patchelf.rb/badges/issue_count.svg)](https://codeclimate.com/github/david942j/patchelf.rb)
|
5
|
+
[![Test Coverage](https://codeclimate.com/github/david942j/patchelf.rb/badges/coverage.svg)](https://codeclimate.com/github/david942j/patchelf.rb/coverage)
|
6
|
+
[![Inline docs](https://inch-ci.org/github/david942j/patchelf.rb.svg?branch=master)](https://inch-ci.org/github/david942j/patchelf.rb)
|
7
|
+
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](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: []
|