Package rekall :: Package plugins :: Package renderers :: Module visual_aides
[frames] | no frames]

Source Code for Module rekall.plugins.renderers.visual_aides

  1  # -*- coding: utf-8 -*- 
  2   
  3  # Rekall Memory Forensics 
  4  # Copyright 2014 Google Inc. All Rights Reserved. 
  5  # 
  6  # This program is free software; you can redistribute it and/or modify 
  7  # it under the terms of the GNU General Public License as published by 
  8  # the Free Software Foundation; either version 2 of the License, or (at 
  9  # your option) any later version. 
 10  # 
 11  # This program is distributed in the hope that it will be useful, but 
 12  # WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
 14  # General Public License for more details. 
 15  # 
 16  # You should have received a copy of the GNU General Public License 
 17  # along with this program; if not, write to the Free Software 
 18  # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
 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 
26 27 28 -class DepthIndicator(int):
29 pass
30
31 32 -class DepthIndicatorRenderer(text.TextObjectRenderer):
33 renders_type = "DepthIndicator" 34
35 - def render_row(self, target, **_):
36 return text.Cell("." * int(target))
37
38 39 -class MemoryMap(object):
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, *_, **__):
88 self.session = session
89 90 @staticmethod
91 - def _make_memorymap_headers(offset, limit, column_count, resolution):
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 # Template for column headers. We pad to the length that's necessary 104 # to format on the order of resolution as a hexadecimal number. 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
120 - def cells(self):
121 for row in self.rows: 122 for cell in row: 123 yield cell
124
125 126 -class RunBasedMap(MemoryMap):
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 # This is a monster of a constructor, but it wouldn't actually be 160 # more readable as separate functions. In short, this will: 161 # 1. Figure out how big the map needs to be. 162 # 2. Chunk up the runs into preallocated cells and blend the colors. 163 # 3. Do another pass to decide what sigils to display, based on 164 # relative weights of runs represented in each cell. 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 # Determine how many cells we need and preallocate them. 176 # Sort the runs by start. 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 # How many cells are we going to need to represent this thing? 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 # Prefabricate the required cells. They contain mutable members, hence 194 # this, somewhat awkward, construct. 195 cells = [self._make_cell() for _ in xrange(cell_count)] 196 197 # Chunk up the runs and populate cells, blending RGB as needed. 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 # Chunks need to align to resolution increments. 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 # Loop over cells and set up their string contents with sigils. 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
273 - def _make_cell():
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
280 281 -class Heatmap(MemoryMap):
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):
287 super(Heatmap, self).__init__(*args, **kwargs) 288 289 rows = [] 290 column_count = len(column_headers) 291 for i, cell in enumerate(cells): 292 if i % column_count == 0: 293 row = [] 294 rows.append(row) 295 row.append(cell) 296 297 self.rows = rows 298 self.row_headers = row_headers 299 self.column_headers = column_headers 300 self.caption = caption 301 self.legend = legend or HeatmapLegend() 302 self.greyscale = greyscale
303 304 @classmethod
305 - def from_hitcount(cls, hits, bucket_size, ceiling=None):
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 # Do we need to allocate more buckets? 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
336 337 -class MapLegend(object):
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
361 362 -def HeatmapLegend():
363 return MapLegend( 364 [(None, "%.1f" % (heat / 10.0), colors.HeatToRGB(heat / 10.0)) 365 for heat in xrange(11)])
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 # If we're prepending row headers we need an extra column on the left. 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
425 426 -class MapLegendRenderer(text.TextObjectRenderer):
427 renders_type = "MapLegend" 428
429 - def render_full(self, target, **options):
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
461 - def render_address(self, *_, **__):
462 raise NotImplementedError()
463
464 - def render_value(self, *_, **__):
465 raise NotImplementedError()
466
467 - def render_compact(self, target, **_):
468 return text.Cell(repr(target))
469