Skip to content

Commit beea471

Browse files
committed
feat(r2): CallStack and fuzzy backtrace hook
1 parent 5d7a8f0 commit beea471

File tree

3 files changed

+111
-0
lines changed

3 files changed

+111
-0
lines changed

examples/extensions/r2/hello_r2.py

Lines changed: 1 addition & 0 deletions
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

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

qiling/extensions/r2/r2.py

Lines changed: 37 additions & 0 deletions
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,42 @@ 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+
oldframe = 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+
frame = CallStack(addr=addr, sp=cursp, bp=oldsp, name=self.at(addr), next=oldframe)
293+
oldframe = frame
294+
oldsp = cursp
295+
cursp += wordsize
296+
return oldframe
297+
298+
def bt(self, target: Union[int, str]):
299+
'''Backtrace when reaching target'''
300+
def bt_hook(ql: 'Qiling', addr: int, size: int, target):
301+
if isinstance(target, str):
302+
target = self.where(target)
303+
if addr <= target and target <= addr + size:
304+
callstack = self._backtrace_fuzzy()
305+
print(callstack)
306+
self.ql.hook_code(bt_hook, target)
307+
271308
def disassembler(self, ql: 'Qiling', addr: int, size: int, filt: Pattern[str]=None) -> int:
272309
'''A human-friendly monkey patch of QlArchUtils.disassembler powered by r2, can be used for hook_code
273310
:param ql: Qiling instance

0 commit comments

Comments
 (0)