heapinfo 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ module HeapInfo
2
+ # Self-defined array for collecting chunk(s)
3
+ class Chunks
4
+ # Instantiate a <tt>HeapInfo::Chunks</tt> object
5
+ def initialize
6
+ @chunks = []
7
+ end
8
+
9
+ def method_missing(method_sym, *arguments, &block)
10
+ return super unless @chunks.respond_to? method_sym
11
+ @chunks.send(method_sym, *arguments, &block)
12
+ end
13
+
14
+ # Hook <tt>#to_s</tt> for pretty printing.
15
+ # @return [String]
16
+ def to_s
17
+ @chunks.map(&:to_s).join("\n")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,204 @@
1
+ module HeapInfo
2
+ # Class for memory dump relation works
3
+ class Dumper
4
+ # Default dump length
5
+ DUMP_BYTES = 8
6
+
7
+ # Instantiate a <tt>HeapInfo::Dumper</tt> object
8
+ #
9
+ # @param [Hash] segments With values <tt>HeapInfo::Segment</tt>
10
+ # @param [String] mem_filename The filename that can be access for dump. Should be <tt>/proc/[pid]/mem</tt>.
11
+ def initialize(segments, mem_filename)
12
+ @segments, @filename = segments, mem_filename
13
+ need_permission unless dumpable?
14
+ end
15
+
16
+ # A helper for <tt>HeapInfo::Process</tt> to dump memory.
17
+ # @param [Mixed] args The use input commands, see examples of <tt>HeapInfo::Process#dump</tt>.
18
+ # @return [String, NilClass] Dump results. If error happend, <tt>nil</tt> is returned.
19
+ # @example
20
+ # p dump(:elf, 4)
21
+ # # => "\x7fELF"
22
+ def dump(*args)
23
+ return need_permission unless dumpable?
24
+ base, offset, len = Dumper.parse_cmd(args)
25
+ if base.instance_of?(Symbol) and @segments[base].instance_of?(Segment)
26
+ addr = @segments[base].base
27
+ elsif base.is_a? Integer
28
+ addr = base
29
+ else
30
+ fail # invalid usage
31
+ end
32
+ file = mem_f
33
+ file.pos = addr + offset
34
+ mem = file.read len
35
+ file.close
36
+ mem
37
+ rescue
38
+ nil
39
+ end
40
+
41
+ # Return the dump result as chunks.
42
+ # see <tt>HeapInfo::Chunks</tt> and <tt>HeapInfo::Chunk</tt> for more information.
43
+ #
44
+ # Note: Same as <tt>dump</tt>, need permission of attaching another process.
45
+ # @return [HeapInfo::Chunks] An array of chunk(s).
46
+ # @param [Mixed] args Same as arguments of <tt>#dump</tt>
47
+ def dump_chunks(*args)
48
+ base = base_of(*args)
49
+ dump(*args).to_chunks(bits: @segments[:bits], base: base)
50
+ end
51
+
52
+ # Show dump results like in gdb's command <tt>x</tt>.
53
+ #
54
+ # Details are in <tt>HeapInfo:Process#x</tt>
55
+ # @param [Integer] count The number of result need to dump.
56
+ # @param [Mixed] commands Same format as <tt>#dump(*args)</tt>.
57
+ # @param [IO] io <tt>IO</tt> that use for printing.
58
+ # @return [NilClass] The return value of <tt>io.puts</tt>.
59
+ # @example
60
+ # x 3, 0x400000
61
+ # # 0x400000: 0x00010102464c457f 0x0000000000000000
62
+ # # 0x400010: 0x00000001003e0002
63
+ def x(count, *commands, io: $stdout)
64
+ commands = commands + [count * size_t]
65
+ base = base_of(*commands)
66
+ res = dump(*commands).unpack(size_t == 4 ? "L*" : "Q*")
67
+ str = res.group_by.with_index{|_, i| i / (16 / size_t) }.map do |round, values|
68
+ "%#x:\t" % (base + round * 16) + values.map{|v| Helper.color "0x%0#{size_t * 2}x" % v}.join("\t")
69
+ end.join("\n")
70
+ io.puts str
71
+ end
72
+
73
+ # Search a specific value/string/regexp in memory.
74
+ # <tt>#find</tt> only return the first matched address.
75
+ # @param [Integer, String, Regexp] pattern The desired search pattern, can be value(<tt>Integer</tt>), string, or regular expression.
76
+ # @param [Integer, String, Symbol] from Start address for searching, can be segment(<tt>Symbol</tt>) or segments with offset. See examples for more information.
77
+ # @param [Integer] length The length limit for searching.
78
+ # @return [Integer, NilClass] The first matched address, <tt>nil</tt> is returned when no such pattern found.
79
+ # @example
80
+ # find(/E.F/, :elf)
81
+ # # => 4194305
82
+ # find(0x4141414141414141, 'heap+0x10', 0x1000)
83
+ # # => 6291472
84
+ # find('/bin/sh', :libc)
85
+ # # => 140662379588827
86
+ def find(pattern, from, length)
87
+ from = base_of(from)
88
+ length = 1 << 40 if length.is_a? Symbol
89
+ case pattern
90
+ when Integer; find_integer(pattern, from, length)
91
+ when String; find_string(pattern, from, length)
92
+ when Regexp; find_regexp(pattern, from, length)
93
+ else; nil
94
+ end
95
+ end
96
+
97
+
98
+ # Parse the dump command into <tt>[base, offset, length]</tt>
99
+ # @param [Array] args The command, see examples for more information
100
+ # @return [Array<Symbol, Integer>] <tt>[base, offset, length]</tt>, while <tt>base</tt> can be a [Symbol] or an [Integer]. <tt>length</tt> has default value equal to <tt>8</tt>.
101
+ # @example
102
+ # HeapInfo::Dumper.parse_cmd([:heap, 32, 10])
103
+ # # [:heap, 32, 10]
104
+ # HeapInfo::Dumper.parse_cmd(['heap+0x10, 10'])
105
+ # # [:heap, 16, 10]
106
+ # HeapInfo::Dumper.parse_cmd(['heap+0x10'])
107
+ # # [:heap, 16, 8]
108
+ # HeapInfo::Dumper.parse_cmd([0x400000, 4])
109
+ # # [0x400000, 0, 4]
110
+ def self.parse_cmd(args)
111
+ args = split_cmd args
112
+ return :fail unless args.size == 3
113
+ len = args[2].nil? ? DUMP_BYTES : Integer(args[2])
114
+ offset = Integer(args[1])
115
+ base = args[0]
116
+ base = Helper.integer?(base) ? Integer(base) : base.delete(':').to_sym
117
+ [base, offset, len]
118
+ end
119
+
120
+ # Helper for <tt>#parse_cmd</tt>.
121
+ #
122
+ # Split commands to exactly three parts: <tt>[base, offset, length]</tt>
123
+ # <tt>length</tt> is <tt>nil</tt> if not present.
124
+ # @param [Array] args
125
+ # @return [Array<String>] <tt>[base, offset, length]</tt> in string expression.
126
+ # @example
127
+ # HeapInfo::Dumper.split_cmd([:heap, 32, 10])
128
+ # # ['heap', '32', '10']
129
+ # HeapInfo::Dumper.split_cmd([':heap+0x10, 10'])
130
+ # # [':heap', '0x10', '10']
131
+ # HeapInfo::Dumper.split_cmd([':heap+0x10'])
132
+ # # [':heap', '0x10', nil]
133
+ # HeapInfo::Dumper.split_cmd([0x400000, 4])
134
+ # # ['4194304', 0, '4']
135
+ def self.split_cmd(args)
136
+ args = args.join(',').delete(' ').split(',').reject(&:empty?) # 'heap, 100', 32 => 'heap', '100', '32'
137
+ return [] if args.empty?
138
+ if args[0].include? '+' # 'heap+0x1'
139
+ args.unshift(*args.shift.split('+', 2))
140
+ elsif args.size <= 2 # no offset given
141
+ args.insert(1, 0)
142
+ end
143
+ args << nil if args.size <= 2 # no length given
144
+ args[0, 3]
145
+ end
146
+
147
+ private
148
+ def need_permission
149
+ puts Helper.color(%q(Could not attach to process. Check the setting of /proc/sys/kernel/yama/ptrace_scope, or try again as the root user. For more details, see /etc/sysctl.d/10-ptrace.conf), sev: :fatal)
150
+ end
151
+
152
+ # use /proc/[pid]/mem for memory dump, must sure have permission
153
+ def dumpable?
154
+ mem_f.close
155
+ true
156
+ rescue => e
157
+ if e.is_a? Errno::EACCES
158
+ false
159
+ else
160
+ throw e
161
+ end
162
+ end
163
+
164
+ def mem_f
165
+ File.open(@filename)
166
+ end
167
+
168
+ def base_of(*args)
169
+ base, offset, _ = Dumper.parse_cmd(args)
170
+ base = @segments[base].base if @segments[base].is_a? Segment
171
+ base + offset
172
+ end
173
+
174
+ def find_integer(value, from, length)
175
+ find_string([value].pack(size_t == 4 ? "L*" : "Q*"), from, length)
176
+ end
177
+
178
+ def find_string(string, from ,length)
179
+ batch_dumper(from, length) {|str| str.index string}
180
+ end
181
+
182
+ def find_regexp(pattern, from ,length)
183
+ batch_dumper(from, length) {|str| str =~ pattern}
184
+ end
185
+
186
+ def batch_dumper(from, remain_size)
187
+ page_size = 0x1000
188
+ while remain_size > 0
189
+ dump_size = [remain_size, page_size].min
190
+ str = dump(from, dump_size)
191
+ break if str.nil? # unreadable
192
+ break unless (idx = yield(str)).nil?
193
+ break if str.length < dump_size # remain is unreadable
194
+ remain_size -= str.length
195
+ from += str.length
196
+ end
197
+ return if idx.nil?
198
+ from + idx
199
+ end
200
+ def size_t
201
+ @segments[:bits] / 8
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,29 @@
1
+ module HeapInfo
2
+ module Ext
3
+ module String
4
+ # Methods to be mixed into String
5
+ module InstanceMethods
6
+ def to_chunk(bits: 64, base: 0)
7
+ size_t = bits / 8
8
+ dumper = lambda{|addr, len| self[addr-base, len]}
9
+ Chunk.new(size_t, base, dumper)
10
+ end
11
+
12
+ def to_chunks(bits: 64, base: 0)
13
+ size_t = bits / 8
14
+ chunks = Chunks.new
15
+ cur = 0
16
+ while cur + size_t * 2 <= self.length
17
+ now_chunk = self[cur, size_t * 2].to_chunk
18
+ sz = now_chunk.size
19
+ chunks << self[cur, sz + 1].to_chunk(bits: bits, base: base + cur) # +1 for dump prev_inuse
20
+ cur += sz
21
+ end
22
+ chunks
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ ::String.send(:include, HeapInfo::Ext::String::InstanceMethods)
@@ -0,0 +1,123 @@
1
+ module HeapInfo
2
+ # Some helper functions
3
+ module Helper
4
+ # Get the process id of a process
5
+ # @param [String] prog The request process name
6
+ # @return [Fixnum] process id
7
+ def self.pidof(prog)
8
+ # plz, don't cmd injection your self :p
9
+ pid = `pidof #{prog}`.strip.to_i
10
+ return nil if pid == 0 # process not exists yet
11
+ throw "pidof #{prog} fail" unless pid.between?(2, 65535)
12
+ pid
13
+ #TODO: handle when multi processes exists
14
+ end
15
+
16
+ # Create read <tt>/proc/[pid]/*</tt> methods
17
+ %w(exe maps).each do |method|
18
+ self.define_singleton_method("#{method}_of".to_sym) do |pid|
19
+ begin
20
+ IO.binread("/proc/#{pid}/#{method}")
21
+ rescue
22
+ throw "reading /proc/#{pid}/#{method} error"
23
+ end
24
+ end
25
+ end
26
+
27
+ # Parse the contents of <tt>/proc/[pid]/maps</tt>.
28
+ #
29
+ # @param [String] content The file content of <tt>/proc/[pid]/maps</tt>
30
+ # @return [Array] In form of <tt>[[start, end, permission, name], ...]</tt>. See examples.
31
+ # @example
32
+ # HeapInfo::Helper.parse_maps(<<EOS
33
+ # 00400000-0040b000 r-xp 00000000 ca:01 271708 /bin/cat
34
+ # 00bc4000-00be5000 rw-p 00000000 00:00 0 [heap]
35
+ # 7f2788315000-7f2788316000 r--p 00022000 ca:01 402319 /lib/x86_64-linux-gnu/ld-2.19.so
36
+ # EOS
37
+ # )
38
+ # # [[0x400000, 0x40b000, 'r-xp', '/bin/cat'],
39
+ # # [0xbc4000, 0xbe5000, 'rw-p', '[heap]'],
40
+ # # [0x7f2788315000, 0x7f2788316000, 'r--p', '/lib/x86_64-linux-gnu/ld-2.19.so']]
41
+ def self.parse_maps(content)
42
+ lines = content.split("\n")
43
+ lines.map do |line|
44
+ s = line.scan(/^([0-9a-f]+)-([0-9a-f]+)\s([rwxp-]{4})[^\/|\[]*([\/|\[].+)$/)[0]
45
+ next nil if s.nil?
46
+ s[0],s[1] = s[0,2].map{|h|h.to_i(16)}
47
+ s
48
+ end.compact
49
+ end
50
+
51
+ # Color codes for pretty print
52
+ COLOR_CODE = {
53
+ esc_m: "\e[0m",
54
+ normal_s: "\e[38;5;1m", # red
55
+ integer: "\e[38;5;12m", # light blue
56
+ fatal: "\e[38;5;197m", # dark red
57
+ bin: "\e[38;5;120m", # light green
58
+ klass: "\e[38;5;155m", # pry like
59
+ sym: "\e[38;5;229m", # pry like
60
+ }
61
+ # Wrapper color codes for for pretty inspect
62
+ # @param [String] s Contents for wrapper
63
+ # @param [Symbol?] sev Specific which kind of color want to use, valid symbols are defined in <tt>#COLOR_CODE</tt>.
64
+ # If this argument is not present, will detect according to the content of <tt>s</tt>
65
+ # @return [String] wrapper with color codes.
66
+ def self.color(s, sev: nil)
67
+ s = s.to_s
68
+ color = ''
69
+ cc = COLOR_CODE
70
+ if cc.keys.include?(sev)
71
+ color = cc[sev]
72
+ elsif s =~ /^(0x)?[0-9a-f]+$/ # integers
73
+ color = cc[:integer]
74
+ else #normal string
75
+ color = cc[:normal_s]
76
+ end
77
+ "#{color}#{s.sub(cc[:esc_m], color)}#{cc[:esc_m]}"
78
+ end
79
+
80
+ # Unpack strings to integer.
81
+ #
82
+ # Like the <tt>p32</tt> and <tt>p64</tt> in pwntools.
83
+ #
84
+ # @param [Integer] size_t Either 4 or 8.
85
+ # @param [String] data String to be unpack.
86
+ # @return [Integer] Unpacked result.
87
+ # @example
88
+ # HeapInfo::Helper.unpack(4, "\x12\x34\x56\x78")
89
+ # # 0x78563412
90
+ # HeapInfo::Helper.unpack(8, "\x12\x34\x56\x78\xfe\xeb\x90\x90")
91
+ # # 0x9090ebfe78563412
92
+ def self.unpack(size_t, data)
93
+ data.unpack(size_t == 4 ? 'L*' : 'Q*')[0]
94
+ end
95
+
96
+ # Retrieve pure class name(without module) of an object
97
+ # @param [Object] obj Any instance
98
+ # @return [String] Class name of <tt>obj</tt>
99
+ # @example
100
+ # # suppose obj is an instance of HeapInfo::Chunk
101
+ # Helper.class_name(obj)
102
+ # # => 'Chunk'
103
+ def self.class_name(obj)
104
+ obj.class.name.split('::').last || obj.class.name
105
+ end
106
+
107
+ # For checking a string is actually an integer
108
+ # @param [String] str String to be checked
109
+ # @return [Boolean] If <tt>str</tt> can be converted into integer
110
+ # @example
111
+ # Helper.integer? '1234'
112
+ # # => true
113
+ # Helper.integer? '0x1234'
114
+ # # => true
115
+ # Helper.integer? '0xheapoverflow'
116
+ # # => false
117
+ def self.integer?(str)
118
+ !!Integer(str)
119
+ rescue ArgumentError, TypeError
120
+ false
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,46 @@
1
+ module HeapInfo
2
+ class Libc < Segment
3
+ def main_arena_offset
4
+ return @_main_arena_offset if @_main_arena_offset
5
+ return nil unless exhaust_search :main_arena
6
+ @_main_arena_offset
7
+ end
8
+
9
+ def main_arena
10
+ return @_main_arena.reload if @_main_arena
11
+ off = main_arena_offset
12
+ return if off.nil?
13
+ @_main_arena = Arena.new(off + self.base, process.bits, lambda{|*args|process.send(:dumper).dump(*args)})
14
+ end
15
+
16
+ def self.find(maps, name, process)
17
+ obj = super(maps, name)
18
+ obj.send(:process=, process)
19
+ obj
20
+ end
21
+
22
+
23
+ private
24
+ attr_accessor :process
25
+ # only for searching offset of main_arena now
26
+ def exhaust_search(symbol)
27
+ return false if symbol != :main_arena
28
+ # TODO: read from cache
29
+ @_main_arena_offset = resolve_main_arena_offset
30
+ true
31
+ end
32
+
33
+ def resolve_main_arena_offset
34
+ tmp_elf = HeapInfo::TMP_DIR + "/get_arena"
35
+ libc_file = HeapInfo::TMP_DIR + "/libc.so.6"
36
+ ld_file = HeapInfo::TMP_DIR + "/ld.so"
37
+ flags = "-w #{@process.bits == 32 ? '-m32' : ''}"
38
+ %x(cp #{self.name} #{libc_file} && \
39
+ cp #{@process.ld.name} #{ld_file} && \
40
+ chdir #{File.expand_path('../tools', __FILE__)} && \
41
+ gcc #{flags} get_arena.c -o #{tmp_elf} 2>&1 > /dev/null && \
42
+ #{ld_file} --library-path #{HeapInfo::TMP_DIR} #{tmp_elf} && \
43
+ rm #{tmp_elf} #{libc_file} #{ld_file}).to_i(16)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,27 @@
1
+ module HeapInfo
2
+ # Self defined <tt>nil</tt> like class.
3
+ #
4
+ # Be the return values of <tt>#dump</tt> or <tt>#dump_chunks</tt>, to prevent use the return value for calculating accidentally while exploiting remote.
5
+ class Nil
6
+ %i(nil? inspect to_s).each do |method_sym|
7
+ define_method(method_sym){|*args, &block| nil.send(method_sym, *args, &block)}
8
+ end
9
+
10
+ # Hook all missing methods
11
+ # @return [HeapInfo::Nil] return <tt>self</tt> so that it can be a <tt>nil</tt> chain.
12
+ # @example
13
+ # # h.dump would return Nil when process not found
14
+ # p h.dump(:heap)[8,8].unpack("Q*)
15
+ # # => nil
16
+ def method_missing(method_sym, *args, &block)
17
+ return nil.send(method_sym, *args, &block) if nil.respond_to? method_sym
18
+ self
19
+ end
20
+
21
+ # To prevent error raised when using <tt>puts Nil.new</tt>
22
+ # @return [Array] Empty array
23
+ def to_ary
24
+ []
25
+ end
26
+ end
27
+ end