memory_io 0.2.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/LICENSE +21 -0
- data/README.md +38 -12
- data/lib/memory_io/context.rb +95 -0
- data/lib/memory_io/error.rb +38 -0
- data/lib/memory_io/io.rb +83 -11
- data/lib/memory_io/logger.rb +32 -0
- data/lib/memory_io/process.rb +76 -15
- data/lib/memory_io/stream.rb +54 -0
- data/lib/memory_io/types/basic/number.rb +105 -23
- data/lib/memory_io/types/clang/c_str.rb +17 -3
- data/lib/memory_io/types/cpp/string.rb +21 -14
- data/lib/memory_io/types/record.rb +9 -7
- data/lib/memory_io/types/type.rb +38 -18
- data/lib/memory_io/types/types.rb +6 -6
- data/lib/memory_io/util.rb +79 -29
- data/lib/memory_io/version.rb +1 -1
- data/lib/memory_io.rb +27 -3
- metadata +66 -15
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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 david942j
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
[](https://travis-ci.org/david942j/memory_io)
|
|
2
1
|
[](https://badge.fury.io/rb/memory_io)
|
|
3
|
-
[](https://github.com/david942j/memory_io/actions)
|
|
3
|
+
[](https://qlty.sh/gh/david942j/projects/memory_io)
|
|
4
|
+
[](https://qlty.sh/gh/david942j/projects/memory_io)
|
|
5
|
+
[](https://www.rubydoc.info/github/david942j/memory_io/)
|
|
6
6
|
[](http://choosealicense.com/licenses/mit/)
|
|
7
7
|
|
|
8
8
|
# MemoryIO
|
|
@@ -14,8 +14,8 @@ Read/Write complicated structures in memory easily.
|
|
|
14
14
|
I usually need to dump a structure, say `string` in C++, from memory for debugging.
|
|
15
15
|
This is not hard if using gdb.
|
|
16
16
|
However, gdb doesn't support writing Ruby scripts
|
|
17
|
-
(unless you use [gdb-ruby](https://github.com/david942j/gdb-ruby), which has
|
|
18
|
-
So I
|
|
17
|
+
(unless you use [gdb-ruby](https://github.com/david942j/gdb-ruby), which has **MemoryIO** as its dependency).
|
|
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
|
|
|
@@ -25,16 +25,14 @@ This repository has two main goals:
|
|
|
25
25
|
## Why
|
|
26
26
|
|
|
27
27
|
It's not hard to read/write a process's memory (simply open the file `/proc/$PID/mem`),
|
|
28
|
-
but it still
|
|
28
|
+
but it's still worthy to make a utility.
|
|
29
29
|
|
|
30
|
-
This
|
|
30
|
+
This project also targets to collect all common structures, such as how to parse a C++/Rust/Python object from memory.
|
|
31
31
|
Therefore, **Pull Requests of adding new structures** are welcome :D
|
|
32
32
|
|
|
33
33
|
## Supported Platform
|
|
34
34
|
|
|
35
35
|
- Linux
|
|
36
|
-
- (TODO) Windows
|
|
37
|
-
- (TODO) MacOS
|
|
38
36
|
|
|
39
37
|
## Implemented Structures
|
|
40
38
|
|
|
@@ -122,7 +120,7 @@ string
|
|
|
122
120
|
require 'memory_io'
|
|
123
121
|
process = MemoryIO.attach(`pidof victim`.to_i)
|
|
124
122
|
|
|
125
|
-
# An example that
|
|
123
|
+
# An example that reads a chunk of pt-malloc.
|
|
126
124
|
read_chunk = lambda do |stream|
|
|
127
125
|
_prev_size = stream.read(8)
|
|
128
126
|
size = (stream.read(8).unpack('Q').first & -16) - 8
|
|
@@ -169,6 +167,34 @@ process.read('libc', 4)
|
|
|
169
167
|
|
|
170
168
|
## Developing
|
|
171
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
|
+
|
|
172
179
|
### To Add a New Structure
|
|
173
180
|
|
|
174
|
-
|
|
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,10 +216,46 @@ 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
|
|
186
|
-
ret =
|
|
258
|
+
ret = MemoryIO::Types.get_proc(ret, rw) unless ret.respond_to?(:call)
|
|
187
259
|
raise ArgumentError, <<-EOERR.strip unless ret.respond_to?(:call)
|
|
188
260
|
|
|
189
261
|
Invalid argument `as`: #{as.inspect}. It should be either a Proc or a supported type of MemoryIO::Types.
|
|
@@ -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,30 +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
|
-
|
|
20
|
-
# @todo
|
|
21
|
-
# Support MacOS
|
|
22
|
-
# @todo
|
|
23
|
-
# Support Windows
|
|
24
|
-
def initialize(pid)
|
|
40
|
+
def initialize(pid, endian: nil, pointer_size: nil)
|
|
25
41
|
@pid = pid
|
|
26
42
|
@mem = "/proc/#{pid}/mem"
|
|
27
43
|
# check permission of '/proc/pid/mem'
|
|
28
44
|
@perm = MemoryIO::Util.file_permission(@mem)
|
|
29
|
-
#
|
|
30
|
-
|
|
45
|
+
raise MemoryIO::ProcessNotFoundError, "#{@mem} does not exist" if perm.nil?
|
|
46
|
+
|
|
47
|
+
@context = build_context(endian, pointer_size)
|
|
31
48
|
|
|
32
|
-
|
|
33
|
-
warn(<<-EOS.strip) unless perm.readable? || perm.writable?
|
|
49
|
+
MemoryIO.logger.warn(<<-EOS.strip) unless perm.readable? || perm.writable?
|
|
34
50
|
You have no permission to read/write this process.
|
|
35
51
|
|
|
36
52
|
Check the setting of /proc/sys/kernel/yama/ptrace_scope, or try
|
|
@@ -79,9 +95,9 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
|
|
|
79
95
|
|
|
80
96
|
# Read from process's memory.
|
|
81
97
|
#
|
|
82
|
-
# This method has *almost* same
|
|
98
|
+
# This method has *almost* same arguments and return types as {IO#read}.
|
|
83
99
|
# The only difference is this method needs parameter +addr+ (which
|
|
84
|
-
# will be passed to
|
|
100
|
+
# will be passed to parameter +from+ in {IO#read}).
|
|
85
101
|
#
|
|
86
102
|
# @param [Integer, String] addr
|
|
87
103
|
# The address start to read.
|
|
@@ -94,6 +110,9 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
|
|
|
94
110
|
# @return [String, Object, Array<Object>]
|
|
95
111
|
# See {IO#read}.
|
|
96
112
|
#
|
|
113
|
+
# @raise [MemoryIO::InvalidAddressError]
|
|
114
|
+
# +addr+ is an expression that cannot be evaluated.
|
|
115
|
+
#
|
|
97
116
|
# @example
|
|
98
117
|
# process = MemoryIO.attach(`pidof victim`.to_i)
|
|
99
118
|
# puts process.read('heap', 4, as: :u64).map { |c| '0x%016x' % c }
|
|
@@ -109,7 +128,7 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
|
|
|
109
128
|
# #=> "\x7fELF"
|
|
110
129
|
# @see IO#read
|
|
111
130
|
def read(addr, num_elements, **options)
|
|
112
|
-
mem_io(:read) { |io| io.read(num_elements, from:
|
|
131
|
+
mem_io(:read) { |io| io.read(num_elements, from: resolve_address(addr), **options) }
|
|
113
132
|
end
|
|
114
133
|
|
|
115
134
|
# Write objects at +addr+.
|
|
@@ -125,6 +144,9 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
|
|
|
125
144
|
#
|
|
126
145
|
# @return [void]
|
|
127
146
|
#
|
|
147
|
+
# @raise [MemoryIO::InvalidAddressError]
|
|
148
|
+
# +addr+ is an expression that cannot be evaluated.
|
|
149
|
+
#
|
|
128
150
|
# @example
|
|
129
151
|
# process = MemoryIO.attach('self')
|
|
130
152
|
# s = 'A' * 16
|
|
@@ -133,14 +155,53 @@ $ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
|
|
|
133
155
|
# #=> 'BBBBCCCCAAAAAAAA'
|
|
134
156
|
# @see IO#write
|
|
135
157
|
def write(addr, objects, **options)
|
|
136
|
-
mem_io(:write) { |io| io.write(objects, from:
|
|
158
|
+
mem_io(:write) { |io| io.write(objects, from: resolve_address(addr), **options) }
|
|
137
159
|
end
|
|
138
160
|
|
|
139
161
|
private
|
|
140
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
|
+
|
|
141
202
|
def mem_io(perm)
|
|
142
203
|
flags = perm == :write ? 'wb' : 'rb'
|
|
143
|
-
File.open(@mem, flags) { |f| yield MemoryIO::IO.new(f) }
|
|
204
|
+
File.open(@mem, flags) { |f| yield MemoryIO::IO.new(f, **@context.to_h) }
|
|
144
205
|
end
|
|
145
206
|
end
|
|
146
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
|