1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 """This module implements various visual aides and their renderers."""
22
23 from rekall.ui import colors
24 from rekall.ui import text
25 from rekall_lib import utils
30
37
40 """Represents a map of memory regions with various highlighting.
41
42 Memory maps are divided into rows with constant number of cells, each
43 with individual highlighting rules (mostly coloring).
44
45 Attributes:
46 ==========
47 column_headers: As table headers.
48 row_headers: (Optional) First column with row headers.
49 caption: (Optional) Should describe relationship between headers
50 and row headers. Rendering is up to the renderer.
51 greyscale: If False (default) heatmap intensity values will be translated
52 into colors with increasing hue. If True, shades of grey will
53 be used instead with varying luminosity.
54
55 Some subclasses may enforce this to be True or False.
56 legend: Instance of MapLegend explaining the map.
57 rows: Rows of constant number of cells each.
58 cells (read-only): All the cells.
59
60 Cell format:
61 ============
62 Each cell is a dict with the following public keys:
63
64 heat (optional): Number between 0 and 1.0 signifying the relative heat.
65 Will be converted to color at rendering.
66 If not given, rgb must be given.
67 bg (optional): The actual desired color of the cell, given as tuple of
68 (red, green, blue) with values of each in the 0-255 range.
69 If not given, heat must be given.
70 fg (optional): Foreground color. Better not specified, will be derived
71 from background.
72 value (optional): String contents of the cell. Usually something like
73 a number of hits in that part of the heatmap.
74
75 Cells may also end up containing non-public keys that are implementation
76 specific; they'll always be prefixed with an underscore, in the great
77 Pythonic tradition.
78 """
79
80 rows = None
81 legend = None
82 row_headers = None
83 column_headers = None
84 caption = "Offset"
85 greyscale = False
86
87 - def __init__(self, session=None, *_, **__):
89
90 @staticmethod
92 """Will make row and column headers suitable for maps of memory.
93
94 The mapped region starts at 'offset' and ends at 'limit'. Each row
95 represents a region of memory subdivided into columns, so that rows
96 are labeled with the absolute offset from 0, and columns are labeled
97 with relative offsets to the row they're in.
98
99 Returns tuple of (row_headers, column_headers).
100 """
101 size = limit - offset
102
103
104
105 column_tpl = "+%%0.%dx" % len("%x" % resolution)
106 columns = [column_tpl % c for c
107 in xrange(offset, resolution * column_count, resolution)]
108
109 row_count, r = divmod(size, column_count * resolution)
110 if r:
111 row_count += 1
112
113 row_tpl = "0x%x"
114 rows = [row_tpl % (r * resolution * column_count)
115 for r in xrange(row_count)]
116
117 return rows, columns
118
119 @utils.safe_property
121 for row in self.rows:
122 for cell in row:
123 yield cell
124
127 """MemoryMap representing discrete ranges in memory.
128
129 Colors, names and sigils representing the ranges will be read from the
130 legend.
131
132 Arguments:
133 ==========
134 runs: dict of "start" (int), "end" (int) and "value" (str).
135 The value will be used to look up colors and sigils in the legend, so
136 it has to match an entry in the legend.
137 legend: Instance of MapLegend - see doc there.
138 limit: The highest address in the map. Optional - if not supplied,
139 the map will show up to the end of the highest range.
140 offset: The lowest address in the map. Optional - if not supplied, map
141 will start at zero.
142 caption: Explanation of what the map is showing. Default is 'Offset'
143 and is typically overriden to something like 'Offset (v)'.
144 resolution: How many bytes one cell in the map represents.
145 cell_width: How long of a string is permitted in the cells themselves.
146 This value is important because the cells show sigils
147 (see MapLegend) in order of representation.
148 blend: Should the map attempt to blend the color of overlapping ranges?
149 If False the map basically becomes a painter's algorithm.
150 column_count: How many columns wide should the map be? Lowering this
151 value will result in more rows.
152 cell_count: Alternative to providing resolution, caller may request a map
153 of constant size, with variable resolution.
154 """
155
156 - def __init__(self, runs, legend, offset=None, limit=None, caption="Offset",
157 resolution=0x100000, cell_width=6, blend=True,
158 cell_count=None, column_count=8, *args, **kwargs):
159
160
161
162
163
164
165 super(RunBasedMap, self).__init__(*args, **kwargs)
166
167 if cell_count and resolution or not (cell_count or resolution):
168 raise ValueError(
169 ("Must specify EITHER resolution (got %s) OR cell count "
170 "(got %s).") % (repr(cell_count), repr(resolution)))
171
172 if not runs:
173 raise ValueError("Must provide runs.")
174
175
176
177 runs = sorted(runs, key=lambda run: run["start"])
178
179 if not offset:
180 offset = runs[0]["start"]
181
182 if not limit:
183 limit = runs[-1]["end"]
184
185
186 if cell_count:
187 resolution = (limit - offset) / cell_count
188 else:
189 cell_count, r = divmod(limit - offset, resolution)
190 if r:
191 cell_count += 1
192
193
194
195 cells = [self._make_cell() for _ in xrange(cell_count)]
196
197
198 for run in runs:
199 start = run["start"]
200 if start < offset:
201 start = offset
202
203 end = run["end"]
204 if end > limit:
205 end = limit
206
207 value = run["value"]
208 rgb = legend.colors.get(value, (0, 0, 0))
209 sigil = legend.sigils.get(value)
210 if not sigil:
211 sigil = "?"
212 self.session.logging.warning("Unknown memory region %s!", value)
213
214
215 mod = start % resolution
216 for chunk in xrange(start - mod, end, resolution):
217 cell_idx = chunk / resolution
218 chunk_start = max(chunk, start)
219 chunk_end = min(chunk + resolution, end)
220 chunk_size = chunk_end - chunk_start
221 chunk_weight = resolution / float(chunk_size)
222 chunk_rgb = rgb
223
224 cell = cells[cell_idx]
225 prev_weight = cell["_weight"]
226
227 if blend and prev_weight:
228 chunk_rgb = colors.BlendRGB(x=cell["bg"],
229 y=rgb,
230 wx=prev_weight,
231 wy=chunk_weight)
232
233 prev_sigils = cell["_sigils"]
234 prev_sigils.setdefault(sigil, 0.0)
235 prev_sigils[sigil] += chunk_weight
236
237 cell["_weight"] = chunk_weight + prev_weight
238 cell["bg"] = chunk_rgb
239 cell["_idx"] = cell_idx
240
241
242 rows = []
243 row = None
244 for i, cell in enumerate(cells):
245 if i % column_count == 0:
246 row = []
247 rows.append(row)
248
249 room = cell_width
250 string = ""
251 for sigil, _ in sorted(cell["_sigils"].iteritems(),
252 key=lambda x: x[1], reverse=True):
253 if len(sigil) < room:
254 string += sigil
255 room -= len(sigil)
256
257 if not room:
258 break
259
260 cell["value"] = string or "-"
261 row.append(cell)
262
263 self.runs = runs
264 self.rows = rows
265 self.legend = legend
266 self.caption = caption
267 self.row_headers, self.column_headers = self._make_memorymap_headers(
268 limit=limit, offset=offset, resolution=resolution,
269 column_count=column_count)
270 self.greyscale = False
271
272 @staticmethod
274 """Prefab cell with some default values already set."""
275 return dict(_weight=0.0,
276 _sigils=dict(),
277 bg=(0, 0, 0),
278 value="-")
279
282 """MemoryMap with colors assigned based on heat."""
283
284 - def __init__(self, cells, caption=None, row_headers=None,
285 column_headers=None, legend=None, greyscale=False, *args,
286 **kwargs):
303
304 @classmethod
306 """Build a heatmap from a collection of hits falling into buckets.
307
308 Returns instance of HeatMap with only rows set. Caller should set
309 row_headers, column_headers, caption, legend and greyscale as desired.
310
311 Arguments:
312 ==========
313 hits: List of addresses where something hit.
314 bucket_size: Size of each bucket to divide hits up between.
315 ceiling (optional): Max number of hits per bucket. If not given
316 will be determined. The ceiling isn't enforced -
317 exceeding cells will appear as such.
318 """
319 buckets = []
320 for hit in hits:
321 bucket_idx = hit / bucket_size
322
323
324 for _ in xrange(bucket_idx - len(buckets) + 1):
325 buckets.append(0)
326
327 buckets[bucket_idx] += 1
328
329 ceiling = ceiling or max(*buckets)
330 tpl = "%%d/%d" % ceiling
331 cells = [dict(heat=float(x) / ceiling, value=tpl % x)
332 for x in buckets]
333
334 return cls(cells=cells)
335
338 """Describes a (heat) map using colors, sigils and optional description.
339
340 Attributes:
341 notes: Optional text to display next to the legend (depends on renderer.)
342 legend: List of tuples of ((str) sigil, (str) name, (r,g,b) color).
343
344 Sigils, names and colors:
345 A name is a long, descriptive title of each range. E.g. "ACPI Memory"
346 A sigil is a short (len 1-2) symbol which will be displayed within each
347 cell for more clarity (by some renderers). E.g. "Ac"
348 A color is a tuple of (red, green, blue) and is exactly what it sounds
349 like.
350 """
351
352 - def __init__(self, legend, notes=None):
353 self.notes = notes
354 self.legend = legend
355 self.colors = {}
356 self.sigils = {}
357 for sigil, title, rgb in legend:
358 self.colors[title] = rgb
359 self.sigils[title] = sigil
360
366
367
368 -class MemoryMapTextRenderer(text.TextObjectRenderer):
369 renders_type = "MemoryMap"
370
371 - def render_address(self, *_, **__):
372 raise NotImplementedError()
373
374 - def render_full(self, target, **options):
375 column_headers = []
376 row_headers = []
377
378 for row_header in target.row_headers or ():
379 row_headers.append(text.Cell(
380 row_header, align="r", padding=1))
381
382
383 if row_headers:
384 column_headers.append(text.Cell(
385 target.caption or "-", padding=1))
386 for column_header in target.column_headers:
387 column_headers.append(text.Cell(
388 column_header, align="c", padding=1))
389
390 rows = [text.JoinedCell(*column_headers, tablesep="")]
391 for idx, row in enumerate(target.rows):
392 cells = []
393 if row_headers:
394 cells.append(row_headers[idx])
395
396 for cell in row:
397 fg = cell.get("fg")
398 bg = cell.get("bg")
399 heat = cell.get("heat")
400 if heat and not bg:
401 bg = colors.HeatToRGB(heat, greyscale=target.greyscale)
402
403 bg = colors.RGBToXTerm(*bg) if bg else None
404
405 if bg and not fg:
406 fg = colors.XTermTextForBackground(bg)
407
408 cells.append(text.Cell(
409 value=unicode(cell.get("value", "-")),
410 highlights=[dict(
411 bg=bg, fg=fg, start=0, end=-1, bold=True)],
412 colorizer=self.renderer.colorizer,
413 padding=1))
414
415 rows.append(text.JoinedCell(*cells, tablesep="", align="l"))
416
417 return text.StackedCell(*rows, align="l")
418
419 - def render_value(self, *_, **__):
420 raise NotImplementedError
421
422 - def render_compact(self, target, **_):
423 return text.Cell(repr(target))
424
427 renders_type = "MapLegend"
428
430 orientation = options.pop("orientation", "vertical")
431
432 cells = []
433 for sigil, description, bg in target.legend:
434 bg = colors.RGBToXTerm(*bg)
435 fg = colors.XTermTextForBackground(bg)
436 if sigil:
437 title = "%s (%s)" % (description, sigil)
438 else:
439 title = description
440 cell = text.Cell(
441 value=title,
442 highlights=[dict(bg=bg, fg=fg, start=0, end=-1)],
443 colorizer=self.renderer.colorizer,
444 padding=2,
445 align="c")
446 cells.append(cell)
447
448 if orientation == "vertical":
449 legend = text.StackedCell(*cells, table_align=False)
450 elif orientation == "horizontal":
451 legend = text.JoinedCell(*cells)
452 else:
453 raise ValueError("Invalid orientation %s." % orientation)
454
455 if target.notes:
456 cell = text.Cell(target.notes)
457 legend = text.StackedCell(cell, legend, table_align=False)
458
459 return legend
460
462 raise NotImplementedError()
463
465 raise NotImplementedError()
466
469