Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a77adac

Browse files
committedAug 25, 2022
feat(r2): CallStack and fuzzy backtrace hook
1 parent 5d7a8f0 commit a77adac

File tree

3 files changed

+111
-0
lines changed

3 files changed

+111
-0
lines changed
 

‎examples/extensions/r2/hello_r2.py

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def my_sandbox(path, rootfs):
3535
ql.hook_address(func, r2.functions['main'].offset)
3636
# enable trace powered by r2 symsmap
3737
# r2.enable_trace()
38+
r2.bt(0x401906)
3839
ql.run()
3940

4041
if __name__ == "__main__":

‎qiling/extensions/r2/callstack.py

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from dataclasses import dataclass
2+
from typing import Iterator, Optional
3+
4+
5+
@dataclass
6+
class CallStack:
7+
"""Linked Frames
8+
See https://github.com/angr/angr/blob/master/angr/state_plugins/callstack.py
9+
"""
10+
addr: int
11+
sp: int
12+
bp: int
13+
name: str = None # 'name + offset'
14+
next: Optional['CallStack'] = None
15+
16+
def __iter__(self) -> Iterator['CallStack']:
17+
"""
18+
Iterate through the callstack, from top to bottom
19+
(most recent first).
20+
"""
21+
i = self
22+
while i is not None:
23+
yield i
24+
i = i.next
25+
26+
def __getitem__(self, k):
27+
"""
28+
Returns the CallStack at index k, indexing from the top of the stack.
29+
"""
30+
orig_k = k
31+
for i in self:
32+
if k == 0:
33+
return i
34+
k -= 1
35+
raise IndexError(orig_k)
36+
37+
def __len__(self):
38+
"""
39+
Get how many frames there are in the current call stack.
40+
41+
:return: Number of frames
42+
:rtype: int
43+
"""
44+
45+
o = 0
46+
for _ in self:
47+
o += 1
48+
return o
49+
50+
def __repr__(self):
51+
"""
52+
Get a string representation.
53+
54+
:return: A printable representation of the CallStack object
55+
:rtype: str
56+
"""
57+
return "<CallStack (depth %d)>" % len(self)
58+
59+
def __str__(self):
60+
return "Backtrace:\n" + "\n".join(f"Frame {i}: [{f.name}] {f.addr:#x} sp={f.sp:#x}, bp={f.bp:#x}" for i, f in enumerate(self))
61+
62+
def __eq__(self, other):
63+
if not isinstance(other, CallStack):
64+
return False
65+
66+
if self.addr != other.addr or self.sp != other.sp or self.bp != other.bp:
67+
return False
68+
69+
return self.next == other.next
70+
71+
def __ne__(self, other):
72+
return not (self == other)
73+
74+
def __hash__(self):
75+
return hash(tuple((c.addr, c.sp, c.bp) for c in self))

‎qiling/extensions/r2/r2.py

+35
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from qiling.const import QL_ARCH
1414
from qiling.extensions import trace
1515
from unicorn import UC_PROT_NONE, UC_PROT_READ, UC_PROT_WRITE, UC_PROT_EXEC, UC_PROT_ALL
16+
from .callstack import CallStack
1617

1718
if TYPE_CHECKING:
1819
from qiling.core import Qiling
@@ -268,6 +269,40 @@ def dis_nbytes(self, addr: int, size: int) -> List[Instruction]:
268269
insts = [Instruction(**dic) for dic in self._cmdj(f"pDj {size} @ {addr}")]
269270
return insts
270271

272+
def dis_ninsts(self, addr: int, n: int=1) -> List[Instruction]:
273+
insts = [Instruction(**dic) for dic in self._cmdj(f"pdj {n} @ {addr}")]
274+
return insts
275+
276+
def _backtrace_fuzzy(self, at: int = None, depth: int = 128) -> Optional[CallStack]:
277+
'''Fuzzy backtrace, see https://github.com/radareorg/radare2/blob/master/libr/debug/p/native/bt/fuzzy_all.c#L38
278+
Args:
279+
at: address to start walking stack, default to current SP
280+
depth: limit of stack walking
281+
Returns:
282+
List of Frame
283+
'''
284+
sp = at or self.ql.arch.regs.arch_sp
285+
wordsize = self.ql.arch.bits // 8
286+
frame = None
287+
cursp = oldsp = sp
288+
for i in range(depth):
289+
addr = self.ql.stack_read(i * wordsize)
290+
inst = self.dis_ninsts(addr)[0]
291+
if inst.type.lower() == 'call':
292+
newframe = CallStack(addr=addr, sp=cursp, bp=oldsp, name=self.at(addr), next=frame)
293+
frame = newframe
294+
oldsp = cursp
295+
cursp += wordsize
296+
return frame
297+
298+
def set_backtrace(self, target: Union[int, str]):
299+
'''Set backtrace at target address before executing'''
300+
if isinstance(target, str):
301+
target = self.where(target)
302+
def bt_hook(__ql: "Qiling", *args):
303+
print(self._backtrace_fuzzy())
304+
self.ql.hook_address(bt_hook, target)
305+
271306
def disassembler(self, ql: 'Qiling', addr: int, size: int, filt: Pattern[str]=None) -> int:
272307
'''A human-friendly monkey patch of QlArchUtils.disassembler powered by r2, can be used for hook_code
273308
:param ql: Qiling instance

0 commit comments

Comments
 (0)
Please sign in to comment.