memory_io 0.3.0 → 1.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 +4 -4
- data/README.md +30 -2
- data/lib/memory_io/context.rb +95 -0
- data/lib/memory_io/error.rb +38 -0
- data/lib/memory_io/io.rb +82 -10
- data/lib/memory_io/logger.rb +32 -0
- data/lib/memory_io/process.rb +76 -10
- data/lib/memory_io/stream.rb +54 -0
- data/lib/memory_io/types/basic/number.rb +104 -22
- data/lib/memory_io/types/clang/c_str.rb +17 -3
- data/lib/memory_io/types/cpp/string.rb +17 -8
- data/lib/memory_io/types/record.rb +6 -4
- data/lib/memory_io/types/type.rb +36 -15
- data/lib/memory_io/types/types.rb +4 -4
- data/lib/memory_io/util.rb +48 -12
- data/lib/memory_io/version.rb +1 -1
- data/lib/memory_io.rb +27 -3
- metadata +46 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae436fb71c1c77ce99b26657dc3e9497e8730ee578f0fe412186a67ee4881d12
|
|
4
|
+
data.tar.gz: 822612ae59c6c669a6136724dc9636349a3f6b3b314b20d3e024bc1481faf9e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1fc69554905f1fbd661045c65fd0570e0baea78191d531a7faa39157f9a5faa6618f3693a2cabd7f2117a7a849cd1fc5925123868103b8a414359ed49843068e
|
|
7
|
+
data.tar.gz: f5449be7a6af8e443e857492aa73533735fdc639d4215a79c4b7b0a7effae9db34d5511ec46fec11aba545185f0d6b9f9947ab02f9de88d17a10b78901a96c19
|
data/README.md
CHANGED
|
@@ -15,7 +15,7 @@ I usually need to dump a structure, say `string` in C++, from memory for debuggi
|
|
|
15
15
|
This is not hard if using gdb.
|
|
16
16
|
However, gdb doesn't support writing Ruby scripts
|
|
17
17
|
(unless you use [gdb-ruby](https://github.com/david942j/gdb-ruby), which has **MemoryIO** as its dependency).
|
|
18
|
-
So I created this
|
|
18
|
+
So I created this project to make the debug procedure much easier.
|
|
19
19
|
|
|
20
20
|
This repository has two main goals:
|
|
21
21
|
|
|
@@ -167,6 +167,34 @@ process.read('libc', 4)
|
|
|
167
167
|
|
|
168
168
|
## Developing
|
|
169
169
|
|
|
170
|
+
```bash
|
|
171
|
+
$ git clone https://github.com/david942j/memory_io
|
|
172
|
+
$ cd memory_io
|
|
173
|
+
$ bundle install
|
|
174
|
+
$ bundle exec rake
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The default rake task regenerates README.md, runs RuboCop, and runs all specs.
|
|
178
|
+
|
|
170
179
|
### To Add a New Structure
|
|
171
180
|
|
|
172
|
-
|
|
181
|
+
Pull Requests of new structures are welcome! Say you want to add the structure `Vec` of Rust:
|
|
182
|
+
|
|
183
|
+
1. Create a file `lib/memory_io/types/rust/vec.rb`.
|
|
184
|
+
2. Define class `MemoryIO::Types::Rust::Vec` and make it inherit from `MemoryIO::Types::Type`.
|
|
185
|
+
Types are registered automatically when the class is defined;
|
|
186
|
+
the symbols to access it are derived from the class name.
|
|
187
|
+
`MemoryIO::Types::Rust::Vec` gets the full-name `:'rust/vec'` and the alias `:vec`.
|
|
188
|
+
3. Implement class method `read(stream)`, which reads bytes from `stream`
|
|
189
|
+
and returns an instance of your class.
|
|
190
|
+
Implement class method `write(stream, obj)` as well if the structure supports writing.
|
|
191
|
+
Some helper methods, such as `read_size_t` and `keep_pos`, are defined
|
|
192
|
+
in [Types::Type](https://www.rubydoc.info/github/david942j/memory_io/MemoryIO/Types/Type) for you.
|
|
193
|
+
4. Write the doc-comment right above the class definition.
|
|
194
|
+
The first line of it will be shown in the section [Implemented Structures](#implemented-structures),
|
|
195
|
+
which is auto-generated by `rake readme`.
|
|
196
|
+
5. Add specs in `spec/types/rust/vec_spec.rb`.
|
|
197
|
+
6. Run `bundle exec rake` and make sure everything is green.
|
|
198
|
+
|
|
199
|
+
See [lib/memory_io/types/cpp/string.rb](lib/memory_io/types/cpp/string.rb)
|
|
200
|
+
([spec](spec/types/cpp/string_spec.rb)) as a complete example.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'elftools'
|
|
4
|
+
|
|
5
|
+
require 'memory_io/stream'
|
|
6
|
+
|
|
7
|
+
module MemoryIO
|
|
8
|
+
# Describes how the memory being accessed lays out its data.
|
|
9
|
+
#
|
|
10
|
+
# The context belongs to the memory, not to the machine running this library.
|
|
11
|
+
# They only coincide when the memory belongs to a process on the same host.
|
|
12
|
+
class Context
|
|
13
|
+
# Byte orders that can be asked for. +:native+ resolves to the byte order
|
|
14
|
+
# of the host, which is the right answer whenever the memory belongs to a
|
|
15
|
+
# process running on it.
|
|
16
|
+
ENDIANS = %i[little big native].freeze
|
|
17
|
+
|
|
18
|
+
# The byte order of the host.
|
|
19
|
+
NATIVE_ENDIAN = [1].pack('S') == "\x01\x00".b ? :little : :big
|
|
20
|
+
|
|
21
|
+
# Assumed when nothing more specific is known.
|
|
22
|
+
DEFAULT_POINTER_SIZE = 8
|
|
23
|
+
|
|
24
|
+
# @return [:little, :big]
|
|
25
|
+
# Byte order of the memory. +:native+ has already been resolved.
|
|
26
|
+
attr_reader :endian
|
|
27
|
+
|
|
28
|
+
# @return [Integer]
|
|
29
|
+
# Size of a pointer, in bytes.
|
|
30
|
+
attr_reader :pointer_size
|
|
31
|
+
|
|
32
|
+
# @return [Hash]
|
|
33
|
+
# The attributes, in the form {#initialize} accepts.
|
|
34
|
+
def to_h
|
|
35
|
+
{ endian: endian, pointer_size: pointer_size }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param [:little, :big, :native] endian
|
|
39
|
+
# Byte order of the memory.
|
|
40
|
+
# @param [Integer] pointer_size
|
|
41
|
+
# Size of a pointer, in bytes.
|
|
42
|
+
#
|
|
43
|
+
# @raise [ArgumentError]
|
|
44
|
+
# +endian+ is not one of {ENDIANS}.
|
|
45
|
+
#
|
|
46
|
+
# @example
|
|
47
|
+
# Context.new(endian: :big).endian
|
|
48
|
+
# #=> :big
|
|
49
|
+
def initialize(endian: :native, pointer_size: DEFAULT_POINTER_SIZE)
|
|
50
|
+
raise ArgumentError, "endian must be one of #{ENDIANS.inspect}, got #{endian.inspect}" \
|
|
51
|
+
unless ENDIANS.include?(endian)
|
|
52
|
+
|
|
53
|
+
@endian = endian == :native ? NATIVE_ENDIAN : endian
|
|
54
|
+
@pointer_size = pointer_size
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class << self
|
|
58
|
+
# @return [MemoryIO::Context]
|
|
59
|
+
# Used when a stream carries no context of its own.
|
|
60
|
+
def default
|
|
61
|
+
@default ||= new
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param [Object] stream
|
|
65
|
+
# The stream a type is reading from.
|
|
66
|
+
#
|
|
67
|
+
# @return [MemoryIO::Context]
|
|
68
|
+
# The context +stream+ was tagged with, or {.default} when it carries none.
|
|
69
|
+
def of(stream)
|
|
70
|
+
stream.is_a?(MemoryIO::Stream) ? stream.context : default
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Derive a context from an ELF file, which describes the memory it is
|
|
74
|
+
# loaded into.
|
|
75
|
+
#
|
|
76
|
+
# @param [String] path
|
|
77
|
+
# Path of the ELF file.
|
|
78
|
+
#
|
|
79
|
+
# @return [MemoryIO::Context?]
|
|
80
|
+
# +nil+ if +path+ is unreadable or is not an ELF file.
|
|
81
|
+
#
|
|
82
|
+
# @example
|
|
83
|
+
# Context.from_elf('/proc/self/exe')
|
|
84
|
+
# #=> #<MemoryIO::Context @endian=:little, @pointer_size=8>
|
|
85
|
+
def from_elf(path)
|
|
86
|
+
::File.open(path, 'rb') do |file|
|
|
87
|
+
elf = ELFTools::ELFFile.new(file)
|
|
88
|
+
new(endian: elf.endian, pointer_size: elf.elf_class / 8)
|
|
89
|
+
end
|
|
90
|
+
rescue SystemCallError, ELFTools::ELFError
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MemoryIO
|
|
4
|
+
# The base class of all errors raised by {MemoryIO}.
|
|
5
|
+
#
|
|
6
|
+
# Rescue this class to catch every error this library raises on its own.
|
|
7
|
+
# Errors that propagate from Ruby itself are not covered.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# begin
|
|
11
|
+
# MemoryIO.attach(0)
|
|
12
|
+
# rescue MemoryIO::Error => e
|
|
13
|
+
# puts e.message
|
|
14
|
+
# end
|
|
15
|
+
# # /proc/0/mem does not exist
|
|
16
|
+
class Error < StandardError; end
|
|
17
|
+
|
|
18
|
+
# Raised when the memory of the target process is not accessible.
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# MemoryIO.attach(0)
|
|
22
|
+
# # MemoryIO::ProcessNotFoundError: /proc/0/mem does not exist
|
|
23
|
+
class ProcessNotFoundError < Error; end
|
|
24
|
+
|
|
25
|
+
# Raised when an address expression cannot be evaluated.
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# MemoryIO.attach('self').read('heep + 0x10', 4)
|
|
29
|
+
# # MemoryIO::InvalidAddressError: Failed to evaluate address: "heep + 0x10"
|
|
30
|
+
class InvalidAddressError < Error; end
|
|
31
|
+
|
|
32
|
+
# Raised when a value doesn't fit in the type it is written as.
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# MemoryIO::IO.new(stream).write(0x100000041, as: :u32)
|
|
36
|
+
# # MemoryIO::ValueOutOfRangeError: 0x100000041 is out of range for 32-bit unsigned integer (0x0..0xffffffff)
|
|
37
|
+
class ValueOutOfRangeError < Error; end
|
|
38
|
+
end
|
data/lib/memory_io/io.rb
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
# encoding: ascii-8bit
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require 'memory_io/context'
|
|
5
|
+
require 'memory_io/stream'
|
|
4
6
|
require 'memory_io/types/types'
|
|
5
7
|
|
|
6
8
|
module MemoryIO
|
|
7
9
|
# Main class to use {MemoryIO}.
|
|
8
10
|
class IO
|
|
9
|
-
|
|
11
|
+
# @!attribute [r] stream
|
|
12
|
+
# @return [#pos, #pos=, #read, #write] The stream given at instantiation.
|
|
13
|
+
# @!attribute [r] context
|
|
14
|
+
# @return [MemoryIO::Context] The context of the memory reached through {#stream}.
|
|
15
|
+
attr_reader :stream, :context
|
|
10
16
|
|
|
11
17
|
# Instantiate an {IO} object.
|
|
12
18
|
#
|
|
@@ -15,17 +21,29 @@ module MemoryIO
|
|
|
15
21
|
# +file+ can be un-writable if you will not invoke any write-related method.
|
|
16
22
|
#
|
|
17
23
|
# If +stream.read(*)+ returns empty string or +nil+, it would be seen as reaching EOF.
|
|
18
|
-
|
|
24
|
+
# @param [:little, :big, :native] endian
|
|
25
|
+
# Byte order of the memory reached through +stream+.
|
|
26
|
+
# The default is right whenever that memory belongs to a process on this host,
|
|
27
|
+
# and should be given when it does not, such as a dump taken elsewhere.
|
|
28
|
+
# @param [Integer] pointer_size
|
|
29
|
+
# Size of a pointer in that memory, in bytes.
|
|
30
|
+
#
|
|
31
|
+
# @example
|
|
32
|
+
# # a dump captured on a 32-bit big endian machine
|
|
33
|
+
# MemoryIO::IO.new(File.open('core.dump', 'rb'), endian: :big, pointer_size: 4)
|
|
34
|
+
def initialize(stream, endian: :native, pointer_size: MemoryIO::Context::DEFAULT_POINTER_SIZE)
|
|
19
35
|
@stream = stream
|
|
36
|
+
@context = MemoryIO::Context.new(endian: endian, pointer_size: pointer_size)
|
|
37
|
+
@tagged = MemoryIO::Stream.new(stream, @context)
|
|
20
38
|
end
|
|
21
39
|
|
|
22
40
|
# Read and convert result into custom type/structure.
|
|
23
41
|
#
|
|
24
42
|
# @param [Integer] num_elements
|
|
25
43
|
# Number of elements to be read.
|
|
26
|
-
#
|
|
44
|
+
# Zero reads nothing, as it does for +::IO#read+.
|
|
27
45
|
#
|
|
28
|
-
# This parameter may
|
|
46
|
+
# This parameter may affect the return type,
|
|
29
47
|
# see documents of return value.
|
|
30
48
|
# @param [Integer?] from
|
|
31
49
|
# Invoke +stream.pos = from+ before starting to read.
|
|
@@ -57,7 +75,11 @@ module MemoryIO
|
|
|
57
75
|
# * +as != nil+ and +num_elements > 1+:
|
|
58
76
|
# An array with length +num_elements+ is returned.
|
|
59
77
|
#
|
|
60
|
-
# If EOF
|
|
78
|
+
# If EOF occurred, only the objects that could be read in full are returned,
|
|
79
|
+
# so the result may be shorter than +num_elements+ (possibly empty).
|
|
80
|
+
#
|
|
81
|
+
# @raise [ArgumentError]
|
|
82
|
+
# +num_elements+ is negative or is not an Integer.
|
|
61
83
|
#
|
|
62
84
|
# @example
|
|
63
85
|
# stream = StringIO.new('A' * 8 + 'B' * 8)
|
|
@@ -67,7 +89,7 @@ module MemoryIO
|
|
|
67
89
|
# io.read(100)
|
|
68
90
|
# #=> "BBBBBBB"
|
|
69
91
|
#
|
|
70
|
-
# # read two unsigned 32-bit integers
|
|
92
|
+
# # read two unsigned 32-bit integers starting from position 4
|
|
71
93
|
# io.read(2, from: 4, as: :u32)
|
|
72
94
|
# #=> [1094795585, 1111638594] # [0x41414141, 0x42424242]
|
|
73
95
|
#
|
|
@@ -89,6 +111,17 @@ module MemoryIO
|
|
|
89
111
|
# io.read(2, as: :c_str)
|
|
90
112
|
# #=> ["123", "45678"]
|
|
91
113
|
# @example
|
|
114
|
+
# # reading beyond the end of stream returns what was read
|
|
115
|
+
# stream = StringIO.new("\x01\x02\x03\x04")
|
|
116
|
+
# io = MemoryIO::IO.new(stream)
|
|
117
|
+
# io.read(3, as: :u32)
|
|
118
|
+
# #=> [67305985]
|
|
119
|
+
#
|
|
120
|
+
# # an object that can't be read in full is not returned
|
|
121
|
+
# io.rewind
|
|
122
|
+
# io.read(1, as: :u64)
|
|
123
|
+
# #=> nil
|
|
124
|
+
# @example
|
|
92
125
|
# # pass lambda to `as`
|
|
93
126
|
# stream = StringIO.new("\x03123\x044567")
|
|
94
127
|
# io = MemoryIO::IO.new(stream)
|
|
@@ -101,12 +134,15 @@ module MemoryIO
|
|
|
101
134
|
#
|
|
102
135
|
# @see Types
|
|
103
136
|
def read(num_elements, from: nil, as: nil, force_array: false)
|
|
137
|
+
unless num_elements.is_a?(Integer) && !num_elements.negative?
|
|
138
|
+
raise ArgumentError, "num_elements must be a non-negative Integer, got #{num_elements.inspect}"
|
|
139
|
+
end
|
|
140
|
+
|
|
104
141
|
stream.pos = from if from
|
|
105
142
|
return stream.read(num_elements) if as.nil?
|
|
106
143
|
|
|
107
144
|
conv = to_proc(as, :read)
|
|
108
|
-
|
|
109
|
-
ret = Array.new(num_elements) { conv.call(stream) }
|
|
145
|
+
ret = read_elements(num_elements, conv)
|
|
110
146
|
ret = ret.first if num_elements == 1 && !force_array
|
|
111
147
|
ret
|
|
112
148
|
end
|
|
@@ -125,7 +161,7 @@ module MemoryIO
|
|
|
125
161
|
#
|
|
126
162
|
# A +Proc+ is allowed, which should accept +stream+ and one object as arguments.
|
|
127
163
|
#
|
|
128
|
-
# If +objects+ is a descendant instance of {Types::Type} and +as+ is +nil
|
|
164
|
+
# If +objects+ is a descendant instance of {Types::Type} and +as+ is +nil+,
|
|
129
165
|
# +objects.class+ will be used for +as+.
|
|
130
166
|
# Otherwise, when +as = nil+, this method will simply call +stream.write(objects)+.
|
|
131
167
|
#
|
|
@@ -167,7 +203,7 @@ module MemoryIO
|
|
|
167
203
|
return stream.write(objects) if as.nil?
|
|
168
204
|
|
|
169
205
|
conv = to_proc(as, :write)
|
|
170
|
-
Array(objects).map { |o| conv.call(
|
|
206
|
+
Array(objects).map { |o| conv.call(@tagged, o) }
|
|
171
207
|
end
|
|
172
208
|
|
|
173
209
|
# Set +stream+ to the beginning.
|
|
@@ -180,6 +216,42 @@ module MemoryIO
|
|
|
180
216
|
|
|
181
217
|
private
|
|
182
218
|
|
|
219
|
+
# @api private
|
|
220
|
+
#
|
|
221
|
+
# Read up to +num_elements+ objects, stopping early at the end of stream.
|
|
222
|
+
#
|
|
223
|
+
# An object is only collected if it could be read in full, so a stream
|
|
224
|
+
# that ends mid-object yields the objects preceding it rather than a
|
|
225
|
+
# truncated one.
|
|
226
|
+
#
|
|
227
|
+
# @return [Array<Object>]
|
|
228
|
+
def read_elements(num_elements, conv)
|
|
229
|
+
ret = []
|
|
230
|
+
num_elements.times do
|
|
231
|
+
break if eof?
|
|
232
|
+
|
|
233
|
+
begin
|
|
234
|
+
ret << conv.call(@tagged)
|
|
235
|
+
rescue ::EOFError
|
|
236
|
+
break
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
ret
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# @api private
|
|
243
|
+
#
|
|
244
|
+
# @return [Boolean]
|
|
245
|
+
# Whether +stream+ has no more data to be read.
|
|
246
|
+
def eof?
|
|
247
|
+
return stream.eof? if stream.respond_to?(:eof?)
|
|
248
|
+
|
|
249
|
+
pos = stream.pos
|
|
250
|
+
byte = stream.read(1)
|
|
251
|
+
stream.pos = pos
|
|
252
|
+
byte.nil? || byte.empty?
|
|
253
|
+
end
|
|
254
|
+
|
|
183
255
|
# @api private
|
|
184
256
|
def to_proc(as, rw)
|
|
185
257
|
ret = as.respond_to?(rw) ? as.method(rw) : as
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
|
|
5
|
+
# MemoryIO - Read/Write structures in memory.
|
|
6
|
+
module MemoryIO
|
|
7
|
+
class << self
|
|
8
|
+
# Diagnostics that are worth surfacing but don't stop the operation
|
|
9
|
+
# are written here, so they can be silenced or redirected.
|
|
10
|
+
#
|
|
11
|
+
# @return [Logger]
|
|
12
|
+
# Defaults to a logger writing to +$stderr+.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# MemoryIO.logger.level = Logger::ERROR
|
|
16
|
+
#
|
|
17
|
+
# MemoryIO.logger = Logger.new('memory_io.log')
|
|
18
|
+
def logger
|
|
19
|
+
@logger ||= ::Logger.new($stderr, progname: 'memory_io', formatter: FORMATTER)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr_writer :logger
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @api private
|
|
26
|
+
#
|
|
27
|
+
# Keeps a message readable when it is shown to a human.
|
|
28
|
+
#
|
|
29
|
+
# @example
|
|
30
|
+
# # [memory_io] WARN: something happened
|
|
31
|
+
FORMATTER = proc { |severity, _datetime, progname, msg| "[#{progname}] #{severity}: #{msg}\n" }
|
|
32
|
+
end
|
data/lib/memory_io/process.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'memory_io/error'
|
|
4
|
+
require 'memory_io/io'
|
|
5
|
+
require 'memory_io/logger'
|
|
6
|
+
require 'memory_io/util'
|
|
7
|
+
|
|
3
8
|
module MemoryIO
|
|
4
9
|
# Records information of a process.
|
|
5
10
|
class Process
|
|
@@ -7,25 +12,41 @@ module MemoryIO
|
|
|
7
12
|
# @return [#readable?, #writable?]
|
|
8
13
|
attr_reader :perm
|
|
9
14
|
|
|
15
|
+
# @return [MemoryIO::Context]
|
|
16
|
+
# The context of this process's memory.
|
|
17
|
+
attr_reader :context
|
|
18
|
+
|
|
10
19
|
# @api private
|
|
11
20
|
#
|
|
12
21
|
# Create process object from pid.
|
|
13
22
|
#
|
|
14
23
|
# @param [Integer] pid
|
|
15
24
|
# Process id.
|
|
25
|
+
# @param [:little, :big, :native, nil] endian
|
|
26
|
+
# Byte order of the process's memory.
|
|
27
|
+
# @param [Integer?] pointer_size
|
|
28
|
+
# Size of a pointer in the process, in bytes.
|
|
29
|
+
#
|
|
30
|
+
# Both default to what the process's executable declares, so a 32-bit
|
|
31
|
+
# process is read correctly without being told. Pass them to override
|
|
32
|
+
# a target whose executable can't be examined, or names an interpreter
|
|
33
|
+
# rather than the program itself.
|
|
34
|
+
#
|
|
35
|
+
# @raise [MemoryIO::ProcessNotFoundError]
|
|
36
|
+
# The memory of +pid+ is not accessible.
|
|
16
37
|
#
|
|
17
38
|
# @note
|
|
18
39
|
# This class only supports procfs-based system. i.e. /proc is mounted and readable.
|
|
19
|
-
def initialize(pid)
|
|
40
|
+
def initialize(pid, endian: nil, pointer_size: nil)
|
|
20
41
|
@pid = pid
|
|
21
42
|
@mem = "/proc/#{pid}/mem"
|
|
22
43
|
# check permission of '/proc/pid/mem'
|
|
23
44
|
@perm = MemoryIO::Util.file_permission(@mem)
|
|
24
|
-
#
|
|
25
|
-
|
|
45
|
+
raise MemoryIO::ProcessNotFoundError, "#{@mem} does not exist" if perm.nil?
|
|
46
|
+
|
|
47
|
+
@context = build_context(endian, pointer_size)
|
|
26
48
|
|
|
27
|
-
|
|
28
|
-
warn(<<-EOS.strip) unless perm.readable? || perm.writable?
|
|
49
|
+
MemoryIO.logger.warn(<<-EOS.strip) unless perm.readable? || perm.writable?
|
|
29
50
|
You have no permission to read/write this process.
|
|
30
51
|
|
|
31
52
|
Check the setting of /proc/sys/kernel/yama/ptrace_scope, or try
|
|
@@ -74,9 +95,9 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
|
|
|
74
95
|
|
|
75
96
|
# Read from process's memory.
|
|
76
97
|
#
|
|
77
|
-
# This method has *almost* same
|
|
98
|
+
# This method has *almost* same arguments and return types as {IO#read}.
|
|
78
99
|
# The only difference is this method needs parameter +addr+ (which
|
|
79
|
-
# will be passed to
|
|
100
|
+
# will be passed to parameter +from+ in {IO#read}).
|
|
80
101
|
#
|
|
81
102
|
# @param [Integer, String] addr
|
|
82
103
|
# The address start to read.
|
|
@@ -89,6 +110,9 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
|
|
|
89
110
|
# @return [String, Object, Array<Object>]
|
|
90
111
|
# See {IO#read}.
|
|
91
112
|
#
|
|
113
|
+
# @raise [MemoryIO::InvalidAddressError]
|
|
114
|
+
# +addr+ is an expression that cannot be evaluated.
|
|
115
|
+
#
|
|
92
116
|
# @example
|
|
93
117
|
# process = MemoryIO.attach(`pidof victim`.to_i)
|
|
94
118
|
# puts process.read('heap', 4, as: :u64).map { |c| '0x%016x' % c }
|
|
@@ -104,7 +128,7 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
|
|
|
104
128
|
# #=> "\x7fELF"
|
|
105
129
|
# @see IO#read
|
|
106
130
|
def read(addr, num_elements, **options)
|
|
107
|
-
mem_io(:read) { |io| io.read(num_elements, from:
|
|
131
|
+
mem_io(:read) { |io| io.read(num_elements, from: resolve_address(addr), **options) }
|
|
108
132
|
end
|
|
109
133
|
|
|
110
134
|
# Write objects at +addr+.
|
|
@@ -120,6 +144,9 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
|
|
|
120
144
|
#
|
|
121
145
|
# @return [void]
|
|
122
146
|
#
|
|
147
|
+
# @raise [MemoryIO::InvalidAddressError]
|
|
148
|
+
# +addr+ is an expression that cannot be evaluated.
|
|
149
|
+
#
|
|
123
150
|
# @example
|
|
124
151
|
# process = MemoryIO.attach('self')
|
|
125
152
|
# s = 'A' * 16
|
|
@@ -128,14 +155,53 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
|
|
|
128
155
|
# #=> 'BBBBCCCCAAAAAAAA'
|
|
129
156
|
# @see IO#write
|
|
130
157
|
def write(addr, objects, **options)
|
|
131
|
-
mem_io(:write) { |io| io.write(objects, from:
|
|
158
|
+
mem_io(:write) { |io| io.write(objects, from: resolve_address(addr), **options) }
|
|
132
159
|
end
|
|
133
160
|
|
|
134
161
|
private
|
|
135
162
|
|
|
163
|
+
# The executable of a process describes the memory it runs in, so prefer it
|
|
164
|
+
# over assuming this host's context. What the caller gave wins over both.
|
|
165
|
+
#
|
|
166
|
+
# A process started through an interpreter names the interpreter here, whose
|
|
167
|
+
# context can differ from the program's. Recovering the program's own context
|
|
168
|
+
# would mean picking it out of the mappings, which aren't populated yet when
|
|
169
|
+
# a process is attached to right after it starts, so leave that to the caller.
|
|
170
|
+
#
|
|
171
|
+
# @return [MemoryIO::Context]
|
|
172
|
+
def build_context(endian, pointer_size)
|
|
173
|
+
declared = MemoryIO::Context.from_elf("/proc/#{@pid}/exe") || MemoryIO::Context.new
|
|
174
|
+
MemoryIO::Context.new(endian: endian || declared.endian,
|
|
175
|
+
pointer_size: pointer_size || declared.pointer_size)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Resolve +addr+ into an absolute address.
|
|
179
|
+
#
|
|
180
|
+
# {#bases} is only consulted when +addr+ is an expression that can
|
|
181
|
+
# reference it, so an address that is already absolute costs no extra work.
|
|
182
|
+
#
|
|
183
|
+
# @param [Integer, String] addr
|
|
184
|
+
# The address to resolve.
|
|
185
|
+
#
|
|
186
|
+
# @return [Integer]
|
|
187
|
+
# The resolved address.
|
|
188
|
+
#
|
|
189
|
+
# @raise [MemoryIO::InvalidAddressError]
|
|
190
|
+
# +addr+ is an expression that cannot be evaluated,
|
|
191
|
+
# or evaluates to something that is not an address.
|
|
192
|
+
def resolve_address(addr)
|
|
193
|
+
return addr if addr.is_a?(Integer)
|
|
194
|
+
|
|
195
|
+
address = MemoryIO::Util.safe_eval(addr, **bases)
|
|
196
|
+
raise MemoryIO::InvalidAddressError, "Failed to evaluate address: #{addr.inspect}" unless address.is_a?(Numeric)
|
|
197
|
+
raise MemoryIO::InvalidAddressError, "Address is not an integer: #{addr.inspect}" unless address == address.to_i
|
|
198
|
+
|
|
199
|
+
address.to_i
|
|
200
|
+
end
|
|
201
|
+
|
|
136
202
|
def mem_io(perm)
|
|
137
203
|
flags = perm == :write ? 'wb' : 'rb'
|
|
138
|
-
File.open(@mem, flags) { |f| yield MemoryIO::IO.new(f) }
|
|
204
|
+
File.open(@mem, flags) { |f| yield MemoryIO::IO.new(f, **@context.to_h) }
|
|
139
205
|
end
|
|
140
206
|
end
|
|
141
207
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MemoryIO
|
|
4
|
+
# @api private
|
|
5
|
+
#
|
|
6
|
+
# A stream tagged with the {Context} of the memory it accesses.
|
|
7
|
+
#
|
|
8
|
+
# Types are handed one of these instead of the bare stream, so that a type
|
|
9
|
+
# can learn how to interpret the bytes it reads without the reading
|
|
10
|
+
# interface having to grow another argument.
|
|
11
|
+
#
|
|
12
|
+
# Every other message is forwarded, so a stream behaves as it did before.
|
|
13
|
+
class Stream
|
|
14
|
+
# @return [MemoryIO::Context]
|
|
15
|
+
attr_reader :context
|
|
16
|
+
|
|
17
|
+
# @param [#read, #write] stream
|
|
18
|
+
# The stream to be tagged.
|
|
19
|
+
# @param [MemoryIO::Context] context
|
|
20
|
+
# The context of the memory reached through +stream+.
|
|
21
|
+
def initialize(stream, context)
|
|
22
|
+
@stream = stream
|
|
23
|
+
@context = context
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def read(*)
|
|
27
|
+
@stream.read(*)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def write(*)
|
|
31
|
+
@stream.write(*)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def pos
|
|
35
|
+
@stream.pos
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def pos=(val)
|
|
39
|
+
@stream.pos = val
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def method_missing(name, *, &)
|
|
45
|
+
return super unless @stream.respond_to?(name)
|
|
46
|
+
|
|
47
|
+
@stream.public_send(name, *, &)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def respond_to_missing?(name, include_private = false)
|
|
51
|
+
@stream.respond_to?(name, include_private) || super
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'memory_io/context'
|
|
4
|
+
require 'memory_io/error'
|
|
3
5
|
require 'memory_io/types/type'
|
|
4
6
|
|
|
5
7
|
module MemoryIO
|
|
@@ -8,64 +10,144 @@ module MemoryIO
|
|
|
8
10
|
module Basic
|
|
9
11
|
# Register numbers to {Types}.
|
|
10
12
|
#
|
|
11
|
-
#
|
|
13
|
+
# The byte order is taken from the {MemoryIO::Context} of the stream being
|
|
14
|
+
# read, so the same type reads correctly from memory of either byte order.
|
|
12
15
|
#
|
|
13
|
-
# This class registered (un)signed
|
|
16
|
+
# This class registered (un)signed (8, 16, 32, 64)-bit integers and IEEE-754 floating numbers.
|
|
14
17
|
class Number
|
|
18
|
+
# Indicators of the integer widths, by size in bytes.
|
|
19
|
+
# A single byte has no byte order to indicate.
|
|
20
|
+
INTEGER_INDICATORS = { 1 => 'C', 2 => 'S', 4 => 'L', 8 => 'Q' }.freeze
|
|
21
|
+
|
|
22
|
+
# Indicators of the real widths, by size in bytes and then byte order.
|
|
23
|
+
#
|
|
24
|
+
# @example
|
|
25
|
+
# # 'e' and 'E' are IEEE-754 single and double precision, little endian.
|
|
26
|
+
REAL_INDICATORS = { 4 => { little: 'e', big: 'g' }, 8 => { little: 'E', big: 'G' } }.freeze
|
|
27
|
+
|
|
28
|
+
# Appended to an integer indicator to fix its byte order.
|
|
29
|
+
ENDIAN_INDICATORS = { little: '<', big: '>' }.freeze
|
|
30
|
+
|
|
15
31
|
# @param [Integer] bytes
|
|
16
32
|
# Bytes.
|
|
17
33
|
# @param [Boolean] signed
|
|
18
34
|
# Signed or unsigned.
|
|
19
|
-
# @param [
|
|
20
|
-
#
|
|
21
|
-
def initialize(bytes, signed,
|
|
35
|
+
# @param [Boolean] real
|
|
36
|
+
# Whether this type holds a real number rather than an integer.
|
|
37
|
+
def initialize(bytes, signed, real: false)
|
|
22
38
|
@bytes = bytes
|
|
23
39
|
@signed = signed
|
|
24
|
-
@
|
|
40
|
+
@real = real
|
|
41
|
+
@range = value_range unless real
|
|
42
|
+
@indicators = ENDIAN_INDICATORS.keys.to_h { |e| [e, indicator(e)] }
|
|
25
43
|
end
|
|
26
44
|
|
|
27
45
|
# @return [Integer]
|
|
46
|
+
#
|
|
47
|
+
# @raise [EOFError]
|
|
48
|
+
# Fewer than +bytes+ bytes remain in +stream+.
|
|
28
49
|
def read(stream)
|
|
29
|
-
|
|
50
|
+
endian = MemoryIO::Context.of(stream).endian
|
|
51
|
+
unpack(MemoryIO::Util.read_exactly(stream, @bytes), endian)
|
|
30
52
|
end
|
|
31
53
|
|
|
32
54
|
# @param [Integer] val
|
|
55
|
+
#
|
|
56
|
+
# @raise [MemoryIO::ValueOutOfRangeError]
|
|
57
|
+
# +val+ doesn't fit in this type, which would otherwise be written truncated.
|
|
33
58
|
def write(stream, val)
|
|
34
|
-
|
|
59
|
+
endian = MemoryIO::Context.of(stream).endian
|
|
60
|
+
raise MemoryIO::ValueOutOfRangeError, out_of_range_message(val) if out_of_range?(val, endian)
|
|
61
|
+
|
|
62
|
+
stream.write(pack(val, endian))
|
|
35
63
|
end
|
|
36
64
|
|
|
37
65
|
private
|
|
38
66
|
|
|
39
|
-
|
|
40
|
-
|
|
67
|
+
# Anything that isn't a number of the matching kind is left for
|
|
68
|
+
# +Array#pack+ to reject.
|
|
69
|
+
#
|
|
70
|
+
# @return [Boolean]
|
|
71
|
+
def out_of_range?(val, endian)
|
|
72
|
+
if @range
|
|
73
|
+
val.is_a?(Integer) && !@range.cover?(val)
|
|
74
|
+
else
|
|
75
|
+
val.is_a?(Numeric) && overflows?(val, endian)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# A finite value that packs to an infinity has exceeded what the type
|
|
80
|
+
# can represent. Losing precision is inherent to the type and allowed,
|
|
81
|
+
# as is writing an infinity that was asked for.
|
|
82
|
+
#
|
|
83
|
+
# @return [Boolean]
|
|
84
|
+
def overflows?(val, endian)
|
|
85
|
+
val.infinite?.nil? && pack(val, endian).unpack1(@indicators[endian]).infinite?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @return [Range]
|
|
89
|
+
def value_range
|
|
90
|
+
bits = @bytes * 8
|
|
91
|
+
@signed ? (-(2**(bits - 1))..((2**(bits - 1)) - 1)) : (0..((2**bits) - 1))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @return [String]
|
|
95
|
+
def out_of_range_message(val)
|
|
96
|
+
return format('%s exceeds the range of %d-bit floating number', val, @bytes * 8) unless @range
|
|
97
|
+
|
|
98
|
+
format('%s is out of range for %d-bit %s integer (%s..%s)',
|
|
99
|
+
hex(val), @bytes * 8, @signed ? 'signed' : 'unsigned',
|
|
100
|
+
hex(@range.first), hex(@range.last))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# +format+'s '%#x' renders a negative number in two's complement notation.
|
|
104
|
+
#
|
|
105
|
+
# @return [String]
|
|
106
|
+
#
|
|
107
|
+
# @example
|
|
108
|
+
# hex(-128)
|
|
109
|
+
# #=> '-0x80'
|
|
110
|
+
def hex(val)
|
|
111
|
+
format('%s0x%x', val.negative? ? '-' : '', val.abs)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# @return [String]
|
|
115
|
+
# The +Array#pack+ indicator of this type in +endian+ byte order.
|
|
116
|
+
def indicator(endian)
|
|
117
|
+
return REAL_INDICATORS[@bytes][endian] if @real
|
|
118
|
+
return INTEGER_INDICATORS[@bytes] if @bytes == 1
|
|
119
|
+
|
|
120
|
+
INTEGER_INDICATORS[@bytes] + ENDIAN_INDICATORS[endian]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def unpack(str, endian)
|
|
124
|
+
val = str.unpack1(@indicators[endian])
|
|
125
|
+
# a real is already signed by its representation
|
|
126
|
+
return val if @real
|
|
127
|
+
|
|
41
128
|
val -= (2**(@bytes * 8)) if @signed && val >= (2**((@bytes * 8) - 1))
|
|
42
129
|
val
|
|
43
130
|
end
|
|
44
131
|
|
|
45
|
-
def pack(val)
|
|
46
|
-
[val].pack(@
|
|
132
|
+
def pack(val, endian)
|
|
133
|
+
[val].pack(@indicators[endian])
|
|
47
134
|
end
|
|
48
135
|
|
|
49
136
|
# Register (un)signed n-bits integers.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
16 => 'S',
|
|
53
|
-
32 => 'I',
|
|
54
|
-
64 => 'Q'
|
|
55
|
-
}.each do |t, c|
|
|
56
|
-
Type.register(Number.new(t / 8, true, c),
|
|
137
|
+
[8, 16, 32, 64].each do |t|
|
|
138
|
+
Type.register(Number.new(t / 8, true),
|
|
57
139
|
alias: [:"basic/s#{t}", :"s#{t}"],
|
|
58
140
|
doc: "A signed #{t}-bit integer.")
|
|
59
|
-
Type.register(Number.new(t / 8, false
|
|
141
|
+
Type.register(Number.new(t / 8, false),
|
|
60
142
|
alias: [:"basic/u#{t}", :"u#{t}"],
|
|
61
143
|
doc: "An unsigned #{t}-bit integer.")
|
|
62
144
|
end
|
|
63
145
|
|
|
64
146
|
# Register floating numbers.
|
|
65
|
-
Type.register(Number.new(4,
|
|
147
|
+
Type.register(Number.new(4, true, real: true),
|
|
66
148
|
alias: %i[basic/float float],
|
|
67
149
|
doc: 'IEEE-754 32-bit floating number.')
|
|
68
|
-
Type.register(Number.new(8,
|
|
150
|
+
Type.register(Number.new(8, true, real: true),
|
|
69
151
|
alias: %i[basic/double double],
|
|
70
152
|
doc: 'IEEE-754 64-bit floating number.')
|
|
71
153
|
end
|
|
@@ -12,17 +12,31 @@ module MemoryIO
|
|
|
12
12
|
# A null-terminated string.
|
|
13
13
|
class CStr < Types::Type
|
|
14
14
|
|
|
15
|
+
# Number of bytes to fetch at a time while searching for the terminator.
|
|
16
|
+
CHUNK_SIZE = 1024
|
|
17
|
+
|
|
15
18
|
# @api private
|
|
16
19
|
#
|
|
20
|
+
# The terminator is searched for in blocks rather than byte by byte,
|
|
21
|
+
# and +stream+ is left just after it so the next read starts at the
|
|
22
|
+
# following string.
|
|
23
|
+
#
|
|
17
24
|
# @return [String]
|
|
18
25
|
# String without null byte.
|
|
19
26
|
def self.read(stream)
|
|
20
27
|
ret = +''
|
|
21
28
|
loop do
|
|
22
|
-
|
|
23
|
-
break if
|
|
29
|
+
chunk = stream.read(CHUNK_SIZE)
|
|
30
|
+
break if chunk.nil? || chunk.empty?
|
|
31
|
+
|
|
32
|
+
terminator = chunk.index("\x00")
|
|
33
|
+
if terminator
|
|
34
|
+
ret << chunk[0, terminator]
|
|
35
|
+
stream.pos -= chunk.size - terminator - 1
|
|
36
|
+
break
|
|
37
|
+
end
|
|
24
38
|
|
|
25
|
-
ret <<
|
|
39
|
+
ret << chunk
|
|
26
40
|
end
|
|
27
41
|
ret
|
|
28
42
|
end
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'memory_io/context'
|
|
4
|
+
require 'memory_io/logger'
|
|
3
5
|
require 'memory_io/types/type'
|
|
6
|
+
require 'memory_io/util'
|
|
4
7
|
|
|
5
8
|
module MemoryIO
|
|
6
9
|
module Types
|
|
@@ -50,20 +53,20 @@ module MemoryIO
|
|
|
50
53
|
# @param [String] str
|
|
51
54
|
def data=(str)
|
|
52
55
|
@data = str
|
|
53
|
-
|
|
56
|
+
return unless str.size > capacity
|
|
57
|
+
|
|
58
|
+
MemoryIO.logger.warn("Length of str (#{str.size}) is larger than capacity (#{capacity})")
|
|
54
59
|
end
|
|
55
60
|
|
|
56
61
|
# Custom inspect view.
|
|
57
62
|
#
|
|
58
63
|
# @return [String]
|
|
59
64
|
def inspect
|
|
60
|
-
|
|
61
|
-
format("#<%s @data=%s, @capacity=%d, @dataplus=0x%0#{SIZE_T * 2}x>",
|
|
65
|
+
format('#<%s @data=%s, @capacity=%d, @dataplus=0x%s>',
|
|
62
66
|
self.class.name,
|
|
63
67
|
data.inspect,
|
|
64
68
|
capacity,
|
|
65
|
-
dataplus)
|
|
66
|
-
# rubocop:enable Lint/FormatParameterMismatch
|
|
69
|
+
dataplus.to_s(16).rjust(SIZE_T * 2, '0'))
|
|
67
70
|
end
|
|
68
71
|
|
|
69
72
|
class << self
|
|
@@ -71,6 +74,11 @@ module MemoryIO
|
|
|
71
74
|
#
|
|
72
75
|
# @return [CPP::String]
|
|
73
76
|
#
|
|
77
|
+
# @raise [EOFError]
|
|
78
|
+
# The object is incomplete, or its characters cannot be read from
|
|
79
|
+
# where it points. {MemoryIO::IO#read} answers with the objects it
|
|
80
|
+
# read in full rather than one that is partly filled in.
|
|
81
|
+
#
|
|
74
82
|
# @example
|
|
75
83
|
# # echo '#include <string>\n#include <cstdio>\nint main() {' > a.cpp && \
|
|
76
84
|
# # echo 'std::string a="abcd"; printf("%p\\n", &a);' >> a.cpp && \
|
|
@@ -85,10 +93,11 @@ module MemoryIO
|
|
|
85
93
|
def read(stream)
|
|
86
94
|
dataplus = read_size_t(stream)
|
|
87
95
|
length = read_size_t(stream)
|
|
88
|
-
union =
|
|
96
|
+
union = MemoryIO::Util.read_exactly(stream, LOCAL_CAPACITY + 1)
|
|
89
97
|
if length > LOCAL_CAPACITY
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
context = MemoryIO::Context.of(stream)
|
|
99
|
+
capacity = MemoryIO::Util.unpack(union[0, context.pointer_size], context.endian)
|
|
100
|
+
data = keep_pos(stream, pos: dataplus) { |s| MemoryIO::Util.read_exactly(s, length) }
|
|
92
101
|
else
|
|
93
102
|
capacity = LOCAL_CAPACITY
|
|
94
103
|
data = union[0, length]
|
|
@@ -19,12 +19,14 @@ module MemoryIO
|
|
|
19
19
|
#
|
|
20
20
|
# @param [Object] object
|
|
21
21
|
# @param [Array<Symbol>] keys
|
|
22
|
+
# @param [Hash] option
|
|
23
|
+
# Extra options.
|
|
22
24
|
#
|
|
23
|
-
# @option [Thread::Backtrace::Location] caller
|
|
24
|
-
# This option should present if and only if +object+ is a subclass of {Types::Type}.
|
|
25
|
-
# @option [String] doc
|
|
25
|
+
# @option option [Thread::Backtrace::Location] :caller
|
|
26
|
+
# This option should be present if and only if +object+ is a subclass of {Types::Type}.
|
|
27
|
+
# @option option [String] :doc
|
|
26
28
|
# Doc-string.
|
|
27
|
-
# Automatically
|
|
29
|
+
# Automatically parsed from caller location if this option isn't present.
|
|
28
30
|
def initialize(object, keys, option = {})
|
|
29
31
|
@obj = object
|
|
30
32
|
@keys = keys
|
data/lib/memory_io/types/type.rb
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'memory_io/context'
|
|
3
4
|
require 'memory_io/types/record'
|
|
4
5
|
require 'memory_io/util'
|
|
5
6
|
|
|
6
7
|
module MemoryIO
|
|
7
8
|
module Types
|
|
8
|
-
# The base class, all descendants of this class would be
|
|
9
|
+
# The base class, all descendants of this class would be considered as a valid 'type'.
|
|
9
10
|
class Type
|
|
10
|
-
# The size of +size_t
|
|
11
|
-
|
|
11
|
+
# The size of +size_t+ assumed when the stream says nothing more specific.
|
|
12
|
+
#
|
|
13
|
+
# @see MemoryIO::Context#pointer_size
|
|
14
|
+
SIZE_T = MemoryIO::Context::DEFAULT_POINTER_SIZE
|
|
12
15
|
|
|
13
16
|
class << self
|
|
14
|
-
# Read
|
|
17
|
+
# Read one +size_t+ and cast it to an unsigned integer.
|
|
18
|
+
#
|
|
19
|
+
# Its width and byte order are taken from the {MemoryIO::Context} of +stream+.
|
|
15
20
|
#
|
|
16
21
|
# @param [#read] stream
|
|
17
22
|
# Stream to read.
|
|
@@ -19,15 +24,21 @@ module MemoryIO
|
|
|
19
24
|
# @return [Integer]
|
|
20
25
|
# Result.
|
|
21
26
|
#
|
|
27
|
+
# @raise [EOFError]
|
|
28
|
+
# Fewer bytes than a +size_t+ remain in +stream+.
|
|
29
|
+
#
|
|
22
30
|
# @example
|
|
23
31
|
# s = StringIO.new("\xEF\xBE\xAD\xDExV4\x00")
|
|
24
32
|
# Type.read_size_t(s).to_s(16)
|
|
25
33
|
# #=> '345678deadbeef'
|
|
26
34
|
def read_size_t(stream)
|
|
27
|
-
MemoryIO::
|
|
35
|
+
context = MemoryIO::Context.of(stream)
|
|
36
|
+
MemoryIO::Util.unpack(MemoryIO::Util.read_exactly(stream, context.pointer_size), context.endian)
|
|
28
37
|
end
|
|
29
38
|
|
|
30
|
-
# Pack +val+ into
|
|
39
|
+
# Pack +val+ into one +size_t+ and write it to +stream+.
|
|
40
|
+
#
|
|
41
|
+
# Its width and byte order are taken from the {MemoryIO::Context} of +stream+.
|
|
31
42
|
#
|
|
32
43
|
# @param [#write] stream
|
|
33
44
|
# Stream to write.
|
|
@@ -42,11 +53,16 @@ module MemoryIO
|
|
|
42
53
|
# s.string
|
|
43
54
|
# #=> "\x23\x01\x00\x00\x00\x00\x00\x00"
|
|
44
55
|
def write_size_t(stream, val)
|
|
45
|
-
|
|
56
|
+
context = MemoryIO::Context.of(stream)
|
|
57
|
+
stream.write(MemoryIO::Util.pack(val, context.pointer_size, context.endian))
|
|
46
58
|
end
|
|
47
59
|
|
|
48
60
|
# Yield a block and resume the position of stream.
|
|
49
61
|
#
|
|
62
|
+
# The position is resumed even if the block raises,
|
|
63
|
+
# so a failure while following an invalid pointer doesn't
|
|
64
|
+
# leave the stream at an unexpected position.
|
|
65
|
+
#
|
|
50
66
|
# @param [#pos, #pos=] stream
|
|
51
67
|
# Stream.
|
|
52
68
|
# @param [Integer] pos
|
|
@@ -69,9 +85,11 @@ module MemoryIO
|
|
|
69
85
|
def keep_pos(stream, pos: nil)
|
|
70
86
|
org = stream.pos
|
|
71
87
|
stream.pos = pos if pos
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
88
|
+
begin
|
|
89
|
+
yield stream
|
|
90
|
+
ensure
|
|
91
|
+
stream.pos = org
|
|
92
|
+
end
|
|
75
93
|
end
|
|
76
94
|
|
|
77
95
|
# @api private
|
|
@@ -81,8 +99,9 @@ module MemoryIO
|
|
|
81
99
|
# @param [Symbol] symbol
|
|
82
100
|
# Symbol that has been registered in {.register}.
|
|
83
101
|
#
|
|
84
|
-
# @return [
|
|
85
|
-
# The
|
|
102
|
+
# @return [MemoryIO::Types::Record?]
|
|
103
|
+
# The record that was registered in {.register},
|
|
104
|
+
# or +nil+ if +symbol+ has never been registered.
|
|
86
105
|
#
|
|
87
106
|
# @see .register
|
|
88
107
|
def find(symbol)
|
|
@@ -93,10 +112,12 @@ module MemoryIO
|
|
|
93
112
|
#
|
|
94
113
|
# @param [#read, #write] object
|
|
95
114
|
# Normally, +object+ is a descendant class of {Type}.
|
|
115
|
+
# @param [Hash] option
|
|
116
|
+
# Extra options.
|
|
96
117
|
#
|
|
97
|
-
# @option [Symbol, Array<Symbol>] alias
|
|
118
|
+
# @option option [Symbol, Array<Symbol>] :alias
|
|
98
119
|
# Custom symbol name(s) that can be used in {.find}.
|
|
99
|
-
# @option [String] doc
|
|
120
|
+
# @option option [String] :doc
|
|
100
121
|
# Doc string that will be shown in README.md.
|
|
101
122
|
#
|
|
102
123
|
# @return [Array<Symbol>]
|
|
@@ -117,7 +138,7 @@ module MemoryIO
|
|
|
117
138
|
#
|
|
118
139
|
# @note
|
|
119
140
|
# If all symbols in +alias+ have been registered, an ArgumentError will be raised.
|
|
120
|
-
# However, if at least one of aliases hasn't been used, registration will
|
|
141
|
+
# However, if at least one of aliases hasn't been used, registration will succeed.
|
|
121
142
|
#
|
|
122
143
|
# @see .find
|
|
123
144
|
def register(object, option = {})
|
|
@@ -18,16 +18,16 @@ module MemoryIO
|
|
|
18
18
|
#
|
|
19
19
|
# This method will search all descendants of {Types::Type}.
|
|
20
20
|
#
|
|
21
|
-
# @
|
|
21
|
+
# @param [Symbol] name
|
|
22
22
|
# Class name to be searched.
|
|
23
23
|
#
|
|
24
24
|
# @return [#read, #write]
|
|
25
25
|
# Any object that implemented method +read+ and +write+.
|
|
26
|
-
# Usually returns a class
|
|
26
|
+
# Usually returns a class inherited from {Types::Type}.
|
|
27
27
|
#
|
|
28
28
|
# @example
|
|
29
29
|
# Types.find(:c_str)
|
|
30
|
-
# #=> MemoryIO::Types::CStr
|
|
30
|
+
# #=> MemoryIO::Types::Clang::CStr
|
|
31
31
|
#
|
|
32
32
|
# Types.find(:u64)
|
|
33
33
|
# #=> #<MemoryIO::Types::Number:0x000055ecc017a310 @bytes=8, @pack_str="Q", @signed=false>
|
|
@@ -50,7 +50,7 @@ module MemoryIO
|
|
|
50
50
|
#
|
|
51
51
|
# @example
|
|
52
52
|
# Types.get_proc(:c_str, :write)
|
|
53
|
-
# #=> #<Method: MemoryIO::Types::CStr.write>
|
|
53
|
+
# #=> #<Method: MemoryIO::Types::Clang::CStr.write>
|
|
54
54
|
# Types.get_proc(:s32, :read)
|
|
55
55
|
# #=> #<Method: MemoryIO::Types::Number#read>
|
|
56
56
|
def get_proc(name, rw)
|
data/lib/memory_io/util.rb
CHANGED
|
@@ -18,12 +18,12 @@ module MemoryIO
|
|
|
18
18
|
@writable = stat.writable_real?
|
|
19
19
|
# we do a trick here because /proc/[pid]/mem might be marked as writeable but fails at sysopen.
|
|
20
20
|
begin
|
|
21
|
-
@readable && File.open(file, 'rb')
|
|
21
|
+
@readable && File.open(file, 'rb', &:close)
|
|
22
22
|
rescue Errno::EACCES
|
|
23
23
|
@readable = false
|
|
24
24
|
end
|
|
25
25
|
begin
|
|
26
|
-
@writable && File.open(file, 'wb')
|
|
26
|
+
@writable && File.open(file, 'wb', &:close)
|
|
27
27
|
rescue Errno::EACCES
|
|
28
28
|
@writable = false
|
|
29
29
|
end
|
|
@@ -78,25 +78,53 @@ module MemoryIO
|
|
|
78
78
|
# @param [{Symbol => Integer}] vars
|
|
79
79
|
# Predefined variables
|
|
80
80
|
#
|
|
81
|
-
# @return [Integer]
|
|
82
|
-
# Result.
|
|
81
|
+
# @return [Integer?]
|
|
82
|
+
# Result, or +nil+ if +str+ is not a valid expression.
|
|
83
83
|
#
|
|
84
84
|
# @example
|
|
85
85
|
# Util.safe_eval('heap + 0x10 * pp', heap: 0xde00, pp: 8)
|
|
86
86
|
# #=> 56960 # 0xde80
|
|
87
|
+
#
|
|
88
|
+
# Util.safe_eval('0xzz')
|
|
89
|
+
# #=> nil
|
|
87
90
|
def safe_eval(str, **vars)
|
|
88
91
|
return str if str.is_a?(Integer)
|
|
89
92
|
|
|
90
|
-
# dentaku 2 doesn't support hex
|
|
91
|
-
str = str.gsub(/0x[0-9a-zA-Z]+/) { |c| c.to_i(16) }
|
|
92
93
|
Dentaku::Calculator.new.store(vars).evaluate(str)
|
|
93
94
|
end
|
|
94
95
|
|
|
96
|
+
# @api private
|
|
97
|
+
#
|
|
98
|
+
# Read exactly +size+ bytes from +stream+.
|
|
99
|
+
#
|
|
100
|
+
# @param [#read] stream
|
|
101
|
+
# Stream to read.
|
|
102
|
+
# @param [Integer] size
|
|
103
|
+
# Number of bytes to read.
|
|
104
|
+
#
|
|
105
|
+
# @return [String]
|
|
106
|
+
# The bytes read.
|
|
107
|
+
#
|
|
108
|
+
# @raise [EOFError]
|
|
109
|
+
# Fewer than +size+ bytes remain in +stream+.
|
|
110
|
+
#
|
|
111
|
+
# @example
|
|
112
|
+
# Util.read_exactly(StringIO.new('1234'), 8)
|
|
113
|
+
# # EOFError: Requires 0x8 bytes, but only 0x4 bytes remain
|
|
114
|
+
def read_exactly(stream, size)
|
|
115
|
+
str = stream.read(size)
|
|
116
|
+
remain = str.nil? ? 0 : str.size
|
|
117
|
+
raise ::EOFError, format('Requires 0x%x bytes, but only 0x%x bytes remain', size, remain) if remain < size
|
|
118
|
+
|
|
119
|
+
str
|
|
120
|
+
end
|
|
121
|
+
|
|
95
122
|
# Unpack a string into an integer.
|
|
96
|
-
# Little endian is used.
|
|
97
123
|
#
|
|
98
124
|
# @param [String] str
|
|
99
125
|
# String.
|
|
126
|
+
# @param [:little, :big] endian
|
|
127
|
+
# Byte order of +str+.
|
|
100
128
|
#
|
|
101
129
|
# @return [Integer]
|
|
102
130
|
# Result.
|
|
@@ -106,12 +134,14 @@ module MemoryIO
|
|
|
106
134
|
# #=> 255
|
|
107
135
|
# Util.unpack("@\xE2\x01\x00")
|
|
108
136
|
# #=> 123456
|
|
109
|
-
|
|
110
|
-
|
|
137
|
+
# Util.unpack("\x00\x01\xE2@", :big)
|
|
138
|
+
# #=> 123456
|
|
139
|
+
def unpack(str, endian = :little)
|
|
140
|
+
bytes = endian == :little ? str.bytes.reverse : str.bytes
|
|
141
|
+
bytes.reduce(0) { |s, c| (s * 256) + c }
|
|
111
142
|
end
|
|
112
143
|
|
|
113
144
|
# Pack an integer into +b+ bytes.
|
|
114
|
-
# Little endian is used.
|
|
115
145
|
#
|
|
116
146
|
# @param [Integer] val
|
|
117
147
|
# The integer to pack.
|
|
@@ -119,6 +149,8 @@ module MemoryIO
|
|
|
119
149
|
# only lower +b+ bytes in +val+ will be packed.
|
|
120
150
|
#
|
|
121
151
|
# @param [Integer] b
|
|
152
|
+
# @param [:little, :big] endian
|
|
153
|
+
# Byte order to pack +val+ in.
|
|
122
154
|
#
|
|
123
155
|
# @return [String]
|
|
124
156
|
# Packing result with length +b+.
|
|
@@ -126,8 +158,12 @@ module MemoryIO
|
|
|
126
158
|
# @example
|
|
127
159
|
# Util.pack(0x123, 4)
|
|
128
160
|
# #=> "\x23\x01\x00\x00"
|
|
129
|
-
|
|
130
|
-
|
|
161
|
+
# Util.pack(0x123, 4, :big)
|
|
162
|
+
# #=> "\x00\x00\x01\x23"
|
|
163
|
+
def pack(val, b, endian = :little)
|
|
164
|
+
bytes = Array.new(b) { |i| (val >> (i * 8)) & 0xff }
|
|
165
|
+
bytes.reverse! if endian == :big
|
|
166
|
+
bytes.pack('C*')
|
|
131
167
|
end
|
|
132
168
|
|
|
133
169
|
# Remove extension name (.so) and version in library name.
|
data/lib/memory_io/version.rb
CHANGED
data/lib/memory_io.rb
CHANGED
|
@@ -9,12 +9,36 @@ module MemoryIO
|
|
|
9
9
|
# Get a process by process id.
|
|
10
10
|
#
|
|
11
11
|
# @param [Integer] pid
|
|
12
|
-
# Process
|
|
12
|
+
# Process id in Linux.
|
|
13
|
+
# @param [:little, :big, :native, nil] endian
|
|
14
|
+
# Byte order of the process's memory.
|
|
15
|
+
# @param [Integer?] pointer_size
|
|
16
|
+
# Size of a pointer in the process, in bytes.
|
|
17
|
+
#
|
|
18
|
+
# Both are taken from the process's executable when not given.
|
|
13
19
|
#
|
|
14
20
|
# @return [MemoryIO::Process]
|
|
15
21
|
# A process object for further usage.
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
#
|
|
23
|
+
# @raise [MemoryIO::ProcessNotFoundError]
|
|
24
|
+
# The memory of +pid+ is not accessible.
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# process = MemoryIO.attach(`pidof victim`.to_i)
|
|
28
|
+
# process.read('heap', 8)
|
|
29
|
+
# @example
|
|
30
|
+
# # a program started through an interpreter is described by the
|
|
31
|
+
# # interpreter, so state the context of the program instead
|
|
32
|
+
# MemoryIO.attach(`pidof victim32`.to_i, pointer_size: 4)
|
|
33
|
+
#
|
|
34
|
+
# @note
|
|
35
|
+
# The context is read from +/proc/[pid]/exe+, which names the interpreter
|
|
36
|
+
# when the process was started through one. Pass +endian+ and
|
|
37
|
+
# +pointer_size+ for such a process, whose context may differ from
|
|
38
|
+
# the interpreter running it.
|
|
39
|
+
# @see MemoryIO::Process#initialize
|
|
40
|
+
def attach(pid, endian: nil, pointer_size: nil)
|
|
41
|
+
MemoryIO::Process.new(pid, endian: endian, pointer_size: pointer_size)
|
|
18
42
|
end
|
|
19
43
|
end
|
|
20
44
|
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: memory_io
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- david942j
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: dentaku
|
|
@@ -15,14 +15,48 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '4'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
25
|
+
version: '4'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: elftools
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.3'
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '3.0'
|
|
36
|
+
type: :runtime
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '1.3'
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '3.0'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: logger
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '1.6'
|
|
53
|
+
type: :runtime
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '1.6'
|
|
26
60
|
- !ruby/object:Gem::Dependency
|
|
27
61
|
name: ostruct
|
|
28
62
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -85,14 +119,14 @@ dependencies:
|
|
|
85
119
|
requirements:
|
|
86
120
|
- - "~>"
|
|
87
121
|
- !ruby/object:Gem::Version
|
|
88
|
-
version: '0
|
|
122
|
+
version: '1.0'
|
|
89
123
|
type: :development
|
|
90
124
|
prerelease: false
|
|
91
125
|
version_requirements: !ruby/object:Gem::Requirement
|
|
92
126
|
requirements:
|
|
93
127
|
- - "~>"
|
|
94
128
|
- !ruby/object:Gem::Version
|
|
95
|
-
version: '0
|
|
129
|
+
version: '1.0'
|
|
96
130
|
- !ruby/object:Gem::Dependency
|
|
97
131
|
name: yard
|
|
98
132
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -119,8 +153,12 @@ files:
|
|
|
119
153
|
- LICENSE
|
|
120
154
|
- README.md
|
|
121
155
|
- lib/memory_io.rb
|
|
156
|
+
- lib/memory_io/context.rb
|
|
157
|
+
- lib/memory_io/error.rb
|
|
122
158
|
- lib/memory_io/io.rb
|
|
159
|
+
- lib/memory_io/logger.rb
|
|
123
160
|
- lib/memory_io/process.rb
|
|
161
|
+
- lib/memory_io/stream.rb
|
|
124
162
|
- lib/memory_io/types/basic/number.rb
|
|
125
163
|
- lib/memory_io/types/clang/c_str.rb
|
|
126
164
|
- lib/memory_io/types/cpp/string.rb
|
|
@@ -141,14 +179,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
141
179
|
requirements:
|
|
142
180
|
- - ">="
|
|
143
181
|
- !ruby/object:Gem::Version
|
|
144
|
-
version: '3.
|
|
182
|
+
version: '3.3'
|
|
145
183
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
146
184
|
requirements:
|
|
147
185
|
- - ">="
|
|
148
186
|
- !ruby/object:Gem::Version
|
|
149
187
|
version: '0'
|
|
150
188
|
requirements: []
|
|
151
|
-
rubygems_version: 3.6.
|
|
189
|
+
rubygems_version: 3.6.9
|
|
152
190
|
specification_version: 4
|
|
153
191
|
summary: memory_io
|
|
154
192
|
test_files: []
|