module documentation

Module related to stack. See the Stack object.

Stack

The Stack object represents a call stack in the trace. It exposes a list of stack frames that can be used to rebuild a calltree and a backtrace from its associated reven2.trace.Context.

The Stack object offers a view associated to a context that lists all StackFrames from the current top of the stack to the first known frame in that stack. The call stack evolves over time, so from a context to another inside the same stack, call frames may be added or removed.

Active stack, previous/next stacks

There are several call stacks depending on the process, on whether we are in kernel land or user land. For a given context, the active stack is defined as the stack that is accessible from the stack pointer (e.g. rsp in x64 architecture).

During the trace, the stack pointer may change to move inside the same stack or move to a different stack (for instance when changing process or switching from ring 3 to ring 0):

               Stack change   Stack change
                    +              +
                    |              |
  +----+---------------+---------------+------------->  Trace
       |            |  |           |   |
       |            +  |           +   |
       |rsp            |rsp            |
       |               |               |
       |               |               |
       |               |               |
       |    Stack 1    |  Stack 2      |
       |    +-----+    |  +-----+      |
       |    |     |    |  |     |      |
       +---->     |    +-->     |      |
            |     |       |     |      |
            +--^--+       +-----+      |
               |                       |
               +-----------------------+

From a given stack, the previous stack refers to the stack that was accessible just before the active stack in the trace, that is the stack that was accessible just before the last value change of the stack pointer to move to a different stack. Similarly, the next stack refers to the stack that will be accessible just after the active stack in the trace:

                     Current context
                            +
               Stack change | Stack change
                    +       |      +
                    |       |      |
  +--+---------------------------------+------------->  Trace
     |              |       |      |   |
     |              +  +----+      +   |
     |rsp              |rsp            |rsp
     |                 |               |
     |                 |               |
     |                 |               |
     |  Stack 1        |  Stack 2      |  Stack 3
     |  +------+       |  +------+     |  +------+
     |  |      |       |  |      |     |  |      |
     +--> Prev |       +-->Active|     +--> Next |
        |      |          |      |        |      |
        +------+          +------+        +------+

Stack event API

Starting with Reven 2.12, StackFrame provide methods to access:

  • their direct parent and children, as well as their ancestors
  • their descendant stack events, that comprise recursive children as well as stack change events
  • the beginning/end of the execution of the frame, as well as the OSSI location associated with the frame

Getting the stack frame associated with the current context

Your main entry point to a StackFrame will be, given a reven2.trace.Context ctx:

>>> frame = ctx.stack.frame()
>>> print(frame)
#199862 - usbhub!UsbhDecHubBusy

Getting to the beginning of the current function

Similarly to reven2.trace.Transition.step_out(forward=False), StackFrame.function_start will produce the transition that started the current frame.

>>> call_transition = frame.function_start()
>>> print(call_transition)
#199862 call 0xfffff800fe4ac010

However, not all calls begin in the trace, and some frames are not even the result of an explicit call instruction (but could be setup from another stack). So, StackFrame.function_start does not always return a value.

If you want to reach the first recorded context where the current stack frame or one of its children was executed, reach for StackFrame.first_context:

>>> print(frame.first_context)
Context before #199863

Note that the frame itself is not necessarily being executed at this context, especially for frames that started before the trace, where a child frame might be initially executing. If you want the precise frame to be executed, reach for StackFrame.first_executed_context.

Getting the name of the current function

A StackFrame object provides a way to get the OSSI location at the start of the frame:

>>> loc = frame.function_location()
>>> print(loc)
Location(binary='usbhub', symbol='UsbhDecHubBusy', ..., rva=0xc010)

Note that this is not merely a symbol name, because some frames are created in the middle of a symbol (for various reasons, such as missing symbols). The caller should check if the location is exactly at the symbol, or at a large offset (in which case the symbol name might be irrelevant).

>>> is_exact_symbol = loc is not None and loc.symbol_offset == 0
>>> print(is_exact_symbol)
True

Displaying a call tree

The API provides several ways of displaying a call tree that offer various degrees of control. The simplest way is to print the direct children of a frame with StackFrame.children:

>>> def print_children(ctx: reven2.trace.Context) -> None:
...     active_frame: reven2.stack.StackFrame = ctx.stack.frame()
...     print(active_frame)
...     for child in active_frame.children():
...         print(f"|- {child}")
...
>>> print_children(server.trace.first_context + 200_000)
#199862 - usbhub!UsbhDecHubBusy
|- #199886 - ntoskrnl!KeWaitForSingleObject
|- #200032 - ntoskrnl!ExFreePoolWithTag
|- #200239 - ntoskrnl!KiAcquireKobjectLockSafe
|- #200269 - ntoskrnl!KiExitDispatcher

You can also recursively print all descendants with StackFrame.descendant_events. This API is lower level as it will return the events occurring on the stack:

  • FrameStart: A frame started, generally a new call.

  • FrameEnd: A frame ended, generally via a return instruction.

  • StackLeave: The stack associated to the current frame was exited and a new stack is now active. When using StackFrame.descendant_events, only the events of the stack of the current frame are reported, so a StackLeave event will always be followed by a StackEnter event on the stack that was just exited.

  • StackEnter: The stack associated to the current frame was re-entered after StackLeave event.

    >>> def print_descendants(ctx: reven2.trace.Context) -> None:
    ...     active_frame = ctx.stack.frame()
    ...     print(active_frame)
    ...     depth = 0
    ...     for event in active_frame.descendant_events():
    ...         if isinstance(event, reven2.stack.FrameStart):
    ...             print(f"{'  ' * depth}|- {event.frame}")
    ...             depth += 1
    ...         elif isinstance(event, reven2.stack.FrameEnd):
    ...             depth -= 1
    ...         elif isinstance(event, reven2.stack.StackEnter):
    ...             print(event)
    ...         elif isinstance(event, reven2.stack.StackLeave):
    ...             print(event)
    ...
    >>> print_descendants(server.trace.first_context + 3_000_000)
    #2999024 - win32kfull!xxxDCEWindowHitTestInternal
      |- #2999033 - ntoskrnl!PsGetCurrentProcessWin32Process
      |- #2999065 - win32kfull!xxxDCEWindowHitTest2Internal
        |- #2999114 - win32kfull!__security_check_cookie
      |- #2999151 - win32kfull!xxxDCEWindowHitTest2Internal
        |- #2999188 - win32kfull!__security_check_cookie
      |- #2999225 - win32kfull!xxxDCEWindowHitTest2Internal
        |- #2999262 - win32kfull!__security_check_cookie
      |- #2999299 - win32kfull!xxxDCEWindowHitTest2Internal
        |- #2999336 - win32kfull!__security_check_cookie
      |- #2999373 - win32kfull!xxxDCEWindowHitTest2Internal
        |- #2999442 - win32kfull!__security_check_cookie
      |- #2999479 - win32kfull!xxxDCEWindowHitTest2Internal
        |- #2999657 - win32kfull!__security_check_cookie
      |- #2999694 - win32kfull!xxxDCEWindowHitTest2Internal
        |- #2999763 - win32kfull!__security_check_cookie
      |- #2999800 - win32kfull!xxxDCEWindowHitTest2Internal
        |- #2999869 - win32kfull!__security_check_cookie
      |- #2999906 - win32kfull!xxxDCEWindowHitTest2Internal
        |- #2999975 - win32kfull!__security_check_cookie
      |- #3000012 - win32kfull!xxxDCEWindowHitTest2Internal
        |- #3000081 - win32kfull!__security_check_cookie
      |- #3000118 - win32kfull!xxxDCEWindowHitTest2Internal
        |- #3000187 - win32kfull!__security_check_cookie
      |- #3000224 - win32kfull!xxxDCEWindowHitTest2Internal
      [... Output elided for concision]
          |- #3016734 - win32kbase!ThreadUnlock1
        |- #3016755 - win32kbase!ThreadUnlock1
        |- #3016776 - win32kfull!__security_check_cookie
      |- #3016796 - win32kbase!ThreadUnlock1
    

Lastly, here's an example of using StackFrame.ancestors and StackFrame.children to produce an HTML representation of a call tree with some context around the current call:

>>> from typing import Tuple
>>>
>>> def ancestor_frame(
...     frame: reven2.stack.StackFrame,
...     max_ancestors: int = 0,
... ) -> Tuple[int, reven2.stack.StackFrame]:
...     current_frame = frame
...     index = 0
...     for index, ancestor in enumerate(current_frame.ancestors()):
...         if index == max_ancestors:
...             break
...         current_frame = ancestor
...     return (index, current_frame)
...
>>>
>>> def frame_html(current_frame: reven2.stack.StackFrame, active_frame: reven2.stack.StackFrame):
...     if current_frame == active_frame:
...         return f"<strong>{current_frame.format_as_html()}</strong><br/>"
...     else:
...         return current_frame.format_as_html()
...
>>>
>>> def call_tree_one_level_html(
...     current_frame: reven2.stack.StackFrame,
...     active_frame: reven2.stack.StackFrame,
...     depth: int,
...     max_depth: int,
...     max_children: int,
... ) -> str:
...     if depth >= max_depth:
...         return ""
...     res = ""
...     for index, child in enumerate(current_frame.children()):
...         if index == max_children:
...             res += f"{'&emsp;' * depth}|- ...<br/>"
...             return res
...         res += f"{'&emsp;' * depth}|- {frame_html(child, active_frame)}<br/>"
...         res += call_tree_one_level_html(child, active_frame, depth + 1, max_depth, max_children)
...     return res
...
>>>
>>> def call_tree_html(
...     ctx: reven2.trace.Context, max_ancestors: int = 0, max_depth: int = 5, max_children: int = 10
... ) -> str:
...     res = ""
...     active_frame = ctx.stack.frame()
...     index, current_frame = ancestor_frame(active_frame, max_ancestors)
...     max_depth += index
...     res += f"{frame_html(current_frame, active_frame)}<br/>"
...     res += call_tree_one_level_html(current_frame, active_frame, 0, max_depth, max_children)
...     return res
...
>>> from IPython.display import HTML
>>>
>>> display(
... HTML(call_tree_html(server.trace.first_context + 3_000_000, max_ancestors=2, max_children=5, max_depth=2))
... )
...

Building execution ranges for a call

When trying to find the span of execution of a call, it is tempting to compute it from the frame's first and last context:

>>> # WARNING: semantically incorrect
>>> reven2.trace.ContextRange(frame.first_context, frame.last_context)
[Context before #199863, Context before #200320]

This is incorrect in general though, because Reven captures the entire system, so an interrupt can cause the execution of a call to be disrupted. An interrupt can even result in a different stack taking over completely.

This means that the correct representation of the span of execution of a call is a list of context ranges rather than a single one.

>>> from typing import Iterator
>>> def execution_ranges(frame: reven2.stack.StackFrame) -> Iterator[reven2.trace.ContextRange]:
...     first_context = frame.first_context
...     for event in frame.descendant_events():
...         if isinstance(event, reven2.stack.StackLeave):
...             if first_context is not None:
...                 yield reven2.trace.ContextRange(first_context, event.transition.context_before())
...             first_context = None
...         elif isinstance(event, reven2.stack.StackEnter):
...             first_context = event.transition.context_after()
...     if first_context is not None:
...         yield reven2.trace.ContextRange(first_context, frame.last_context)
...
>>> list(execution_ranges(frame.parent.parent.parent))
[ContextRange(begin=Context before #198206, last=Context before #209497),
 ContextRange(begin=Context before #211889, last=Context before #212379)]

Lastly, one might want the ability to exclude some subcalls from the execution ranges of a symbol, for various reasons (calls to other binaries, standard library calls, ...). To do so, a good technique can be to split the execution ranges per subcall, and then associate each range to its callstack, so that consumers of this iterator can filter depending on whether they want to handle the call.

>>> from dataclasses import dataclass
>>> from typing import Iterator, List
>>> from reven2.trace import ContextRange
>>> @dataclass
>>> class ExecutionRange:
...     path: List[reven2.stack.StackFrame]
...     range: ContextRange
...     def __str__(self) -> str:
...         return f"[{len(self.path) - 1}]{self.path[-1]} {self.range}"
...
>>> def execution_ranges(frame: reven2.stack.StackFrame) -> Iterator[ExecutionRange]:
...     first_context = frame.first_context
...     frames = [frame]
...     for event in frame.descendant_events():
...         if isinstance(event, reven2.stack.StackLeave):
...             if first_context is not None:
...                 yield ExecutionRange(frames, ContextRange(first_context, event.transition.context_before()))
...             first_context = None
...         elif isinstance(event, reven2.stack.StackEnter):
...             first_context = event.transition.context_after()
...         elif isinstance(event, reven2.stack.FrameStart):
...             if first_context is not None:
...                 yield ExecutionRange(frames, ContextRange(first_context, event.frame.first_context - 1))
...             first_context = event.frame.first_context
...             frames.append(event.frame)
...         elif isinstance(event, reven2.stack.FrameEnd):
...             if first_context is not None:
...                 yield ExecutionRange(frames, ContextRange(first_context, event.frame.last_context))
...             first_context = event.frame.last_context + 1
...             if len(frames) > 1:
...                 frames.pop()
...     if first_context is not None:
...         yield ExecutionRange(frames, ContextRange(first_context, frame.last_context))
...
>>> for execution_range in execution_ranges(frame):
...     print(execution_range)
[0]#199862 - usbhub!UsbhDecHubBusy [Context before #199863, Context before #199886]
[1]#199886 - ntoskrnl!KeWaitForSingleObject [Context before #199887, Context before #199992]
[0]#199862 - usbhub!UsbhDecHubBusy [Context before #199993, Context before #200032]
[1]#200032 - ntoskrnl!ExFreePoolWithTag [Context before #200033, Context before #200183]
[2]#200183 - ntoskrnl!ExpInterlockedPushEntrySList [Context before #200184, Context before #200199]
[1]#200032 - ntoskrnl!ExFreePoolWithTag [Context before #200200, Context before #200209]
[0]#199862 - usbhub!UsbhDecHubBusy [Context before #200210, Context before #200239]
[1]#200239 - ntoskrnl!KiAcquireKobjectLockSafe [Context before #200240, Context before #200250]
[0]#199862 - usbhub!UsbhDecHubBusy [Context before #200251, Context before #200269]
[1]#200269 - ntoskrnl!KiExitDispatcher [Context before #200270, Context before #200309]
[0]#199862 - usbhub!UsbhDecHubBusy [Context before #200310, Context before #200320]

One can then filter on the path:

>>> for execution_range in execution_ranges(frame):
...     loc = execution_range.path[-1].function_location()
...     if loc is not None and loc.binary.name == "ntoskrnl":  # ignore symbols in ntoskrnl
...         continue # ignore this range
...     print(execution_range)
...
[0]#199862 - usbhub!UsbhDecHubBusy [Context before #199863, Context before #199886]
[0]#199862 - usbhub!UsbhDecHubBusy [Context before #199993, Context before #200032]
[0]#199862 - usbhub!UsbhDecHubBusy [Context before #200210, Context before #200239]
[0]#199862 - usbhub!UsbhDecHubBusy [Context before #200251, Context before #200269]
[0]#199862 - usbhub!UsbhDecHubBusy [Context before #200310, Context before #200320]

Ignoring some descendant events

The drawback of filtering on the frames produced by the execution_range iterator from the previous section is that the server needs to produce all of the events, even if they are ultimately irrelevant.

As an optimization, when iterating on child frames, one can decide to ignore remaining descendant events for a subframe if one detected that the subframe is not of interest:

>>> events = frame.descendant_events()
>>> for event in events:
...     if isinstance(event, reven2.stack.FrameStart):  # only FrameStart events can be used to skip descendants
...         loc = event.frame.function_location()
...         if loc.binary.name == "ntoskrnl":
...             events.skip_children()  # instructs the server to skip the descendants of this event
...         print(event)  # still skip the event, its descendants won't be
...
Class EventsIterator Iterator over stack events.
Class FrameEnd Representation of the event ending a frame of a stack.
Class FrameEndType Enum describing the various type of stack frame end.
Class FrameStart Representation of the event starting a frame of a stack.
Class FrameStartType Enum describing the various type of stack frame start.
Class Stack Representation of a stack.
Class StackEnter Representation of the event entering a stack.
Class StackFrame Representation of a part (frame) of a stack.
Class StackLeave Representation of the event leaving a stack.
Class StackSpace Representation of a stack.
Class _DataSource Undocumented
Function _unpack_event Undocumented
Function _unpack_frame_end_type Undocumented
Function _unpack_frame_start_type Undocumented
def _unpack_event(data_source, trace, frame_cache, ll_event):

Undocumented

Parameters
data_source:_DataSourceUndocumented
trace:_TraceUndocumented
frame_cache:_LruCache[StackFrame]Undocumented
ll_event:_reven_api.StackEventUndocumented
Returns
_Union[FrameStart, FrameEnd, StackEnter, StackLeave]Undocumented
def _unpack_frame_end_type(frame_end_type):

Undocumented

Parameters
frame_end_type:_reven_api.StackFrameEndTypeUndocumented
Returns
FrameEndTypeUndocumented
def _unpack_frame_start_type(frame_start_type):

Undocumented

Parameters
frame_start_type:_reven_api.StackFrameStartTypeUndocumented
Returns
FrameStartTypeUndocumented