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 StackFrame
s 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 usingStackFrame.descendant_events
, only the events of the stack of the current frame are reported, so aStackLeave
event will always be followed by aStackEnter
event on the stack that was just exited.StackEnter
: The stack associated to the current frame was re-entered afterStackLeave
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"{' ' * depth}|- ...<br/>" ... return res ... res += f"{' ' * 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 |
|
Iterator over stack events. |
Class |
|
Representation of the event ending a frame of a stack. |
Class |
|
Enum describing the various type of stack frame end. |
Class |
|
Representation of the event starting a frame of a stack. |
Class |
|
Enum describing the various type of stack frame start. |
Class |
|
Representation of a stack. |
Class |
|
Representation of the event entering a stack. |
Class |
|
Representation of a part (frame) of a stack. |
Class |
|
Representation of the event leaving a stack. |
Class |
|
Representation of a stack. |
Class | _ |
Undocumented |
Function | _unpack |
Undocumented |
Function | _unpack |
Undocumented |
Function | _unpack |
Undocumented |
Undocumented
Parameters | |
data_DataSource | Undocumented |
trace:_Trace | Undocumented |
frame_LruCache[ | Undocumented |
ll_reven_api.StackEvent | Undocumented |
Returns | |
_Union[ | Undocumented |
Undocumented
Parameters | |
frame_reven_api.StackFrameEndType | Undocumented |
Returns | |
FrameEndType | Undocumented |
Undocumented
Parameters | |
frame_reven_api.StackFrameStartType | Undocumented |
Returns | |
FrameStartType | Undocumented |