Skip to content

Commit 8caf28b

Browse files
committed
fix #38: better thumb mode handling
1 parent 5d12b49 commit 8caf28b

5 files changed

Lines changed: 307 additions & 47 deletions

File tree

src/patcherex2/components/binary_analyzers/angr.py

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
import logging
44
import traceback
5+
from bisect import bisect_right
56

67
import angr
78
from archinfo import ArchARM
89

9-
from .binary_analyzer import BinaryAnalyzer
10+
from .binary_analyzer import BinaryAnalyzer, UnknownInstructionModeError
1011

1112
logger = logging.getLogger(__name__)
1213

@@ -20,6 +21,7 @@ def __init__(self, binary_path: str, **kwargs) -> None:
2021
self._p = None
2122
self._cfg = None
2223
self._load_base = None
24+
self._mapping_symbols = None
2325

2426
@property
2527
def load_base(self) -> int:
@@ -124,6 +126,8 @@ def _flatten(n):
124126
for instr_addr in instr_addrs
125127
],
126128
}
129+
except UnknownInstructionModeError:
130+
raise
127131
except Exception: # noqa: BLE001
128132
logger.error(
129133
f"angr RegionIdentifier failed for function containing {hex(addr)}, falling back to use cfg nodes\n{traceback.format_exc()}"
@@ -208,16 +212,60 @@ def get_function(self, name_or_addr: int | str) -> dict[str, int] | None:
208212
else:
209213
raise TypeError(f"Invalid type for name_or_addr: {type(name_or_addr)}")
210214

211-
def is_thumb(self, addr: int) -> bool:
215+
@property
216+
def mapping_symbols(self) -> list[tuple[int, str]]:
217+
if self._mapping_symbols is None:
218+
self._mapping_symbols = sorted(
219+
(symbol.rebased_addr, symbol.name[:2])
220+
for symbol in self.p.loader.main_object.symbols
221+
if symbol.name and symbol.name[:2] in {"$a", "$d", "$t"}
222+
)
223+
return self._mapping_symbols
224+
225+
def _mapping_thumb_mode(self, addr: int) -> tuple[bool, bool | None]:
226+
index = bisect_right(self.mapping_symbols, (addr, "\uffff")) - 1
227+
if index < 0:
228+
return False, None
229+
_, kind = self.mapping_symbols[index]
230+
if kind == "$t":
231+
return True, True
232+
if kind == "$a":
233+
return True, False
234+
return True, None
235+
236+
def _cfg_thumb_mode(self, addr: int) -> bool | None:
237+
candidates = (addr, addr + 1) if addr % 2 == 0 else (addr,)
238+
modes = {
239+
node.thumb
240+
for node in self.cfg.model.nodes()
241+
if any(candidate in node.instruction_addrs for candidate in candidates)
242+
}
243+
if len(modes) != 1:
244+
return None
245+
return modes.pop()
246+
247+
def thumb_mode(self, addr: int) -> bool | None:
248+
"""Return the ARM instruction mode, or None when it is unknown."""
212249
if not isinstance(self.p.arch, ArchARM):
213250
return False
251+
214252
addr = self.denormalize_addr(addr)
253+
mapping_found, mapping_mode = self._mapping_thumb_mode(addr)
254+
cfg_mode = self._cfg_thumb_mode(addr)
215255

216-
for node in self.cfg.model.nodes():
217-
if addr in node.instruction_addrs:
218-
return node.thumb
219-
if addr % 2 == 0:
220-
return self.is_thumb(self.normalize_addr(addr + 1))
221-
else:
222-
logger.error(f"Cannot find a block containing address {hex(addr)}")
223-
return False
256+
if not mapping_found:
257+
return cfg_mode
258+
if cfg_mode is None:
259+
return mapping_mode
260+
if mapping_mode is not None and mapping_mode == cfg_mode:
261+
return mapping_mode
262+
return None
263+
264+
def is_thumb(self, addr: int) -> bool:
265+
mode = self.thumb_mode(addr)
266+
if mode is None:
267+
addr = self.denormalize_addr(addr)
268+
raise UnknownInstructionModeError(
269+
f"Cannot determine ARM instruction mode at {hex(addr)}"
270+
)
271+
return mode
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
1+
class UnknownInstructionModeError(RuntimeError):
2+
"""Raised when an instruction's architecture mode cannot be determined."""
3+
4+
15
class BinaryAnalyzer:
26
pass

src/patcherex2/components/binary_analyzers/ghidra.py

Lines changed: 65 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44
import tempfile
55

6-
from .binary_analyzer import BinaryAnalyzer
6+
from .binary_analyzer import BinaryAnalyzer, UnknownInstructionModeError
77

88
logger = logging.getLogger(__name__)
99

@@ -41,11 +41,10 @@ def __init__(self, binary_path: str, language: str | None = None, **kwargs):
4141
self.flatapi = FlatProgramAPI(self.currentProgram)
4242

4343
import ghidra
44+
import ghidra.program.model.block
4445

4546
self.ghidra = ghidra
46-
self.bbm = self.ghidra.program.model.block.BasicBlockModel(
47-
self.currentProgram
48-
)
47+
self.bbm = ghidra.program.model.block.BasicBlockModel(self.currentProgram)
4948
except BaseException:
5049
self.shutdown()
5150
raise
@@ -78,59 +77,70 @@ def shutdown(self):
7877
if temp_proj_dir_ctx is not None:
7978
temp_proj_dir_ctx.cleanup()
8079

81-
def normalize_addr(self, addr):
82-
addr = addr.getOffset()
80+
@property
81+
def load_base(self) -> int:
82+
return self.currentProgram.getImageBase().getOffset()
83+
84+
def normalize_addr(self, addr: int) -> int:
8385
if self.currentProgram.getRelocationTable().isRelocatable():
84-
addr -= self.currentProgram.getImageBase().getOffset()
86+
addr -= self.load_base
8587
return addr
8688

87-
def denormalize_addr(self, addr):
89+
def denormalize_addr(self, addr: int) -> int:
8890
if self.currentProgram.getRelocationTable().isRelocatable():
89-
addr += self.currentProgram.getImageBase().getOffset()
90-
return self.flatapi.toAddr(hex(addr))
91+
addr += self.load_base
92+
return addr
93+
94+
def _normalize_ghidra_addr(self, addr) -> int:
95+
return self.normalize_addr(addr.getOffset())
96+
97+
def _to_ghidra_addr(self, addr: int):
98+
return self.flatapi.toAddr(hex(self.denormalize_addr(addr)))
9199

92100
def mem_addr_to_file_offset(self, addr: int) -> int:
93-
addr = self.denormalize_addr(addr)
101+
ghidra_addr = self._to_ghidra_addr(addr)
94102
try:
95103
return (
96104
self.currentProgram.getMemory()
97-
.getAddressSourceInfo(addr)
105+
.getAddressSourceInfo(ghidra_addr)
98106
.getFileOffset()
99107
)
100108
except Exception: # noqa: BLE001
101109
raise ValueError("Can't get file offset for addr") from None
102110

103111
def get_basic_block(self, addr: int) -> dict[str, int | list[int]]:
104-
logger.info(f"getting basic block at 0x{addr} with ghidra")
105-
addr = self.denormalize_addr(addr)
112+
logger.info(f"getting basic block at {hex(addr)} with ghidra")
113+
ghidra_addr = self._to_ghidra_addr(addr)
106114

107115
block = self.bbm.getFirstCodeBlockContaining(
108-
addr, self.ghidra.util.task.TaskMonitor.DUMMY
116+
ghidra_addr, self.ghidra.util.task.TaskMonitor.DUMMY
109117
)
110118
if block is None:
111-
raise ValueError(f"Cannot find block containing address 0x{addr}")
119+
raise ValueError(f"Cannot find block containing address {hex(addr)}")
112120
instrs = []
113121
ii = self.currentProgram.getListing().getInstructions(block, True)
114122
for i in ii:
115-
instrs.append(self.normalize_addr(i.getAddress()))
123+
instrs.append(self._normalize_ghidra_addr(i.getAddress()))
116124
return {
117-
"start": self.normalize_addr(block.getMinAddress()),
118-
"end": self.normalize_addr(block.getMinAddress()) + block.getNumAddresses(),
125+
"start": self._normalize_ghidra_addr(block.getMinAddress()),
126+
"end": self._normalize_ghidra_addr(block.getMinAddress())
127+
+ block.getNumAddresses(),
119128
"size": block.getNumAddresses(),
120129
"instruction_addrs": instrs,
121130
}
122131

123132
def get_instr_bytes_at(self, addr: int, num_instr=1):
124-
addr = self.denormalize_addr(addr)
125-
instr = self.currentProgram.getListing().getInstructionContaining(addr)
133+
ghidra_addr = self._to_ghidra_addr(addr)
134+
instr = self.currentProgram.getListing().getInstructionContaining(ghidra_addr)
126135
if instr is None:
127136
return None
128-
b = instr.getBytes()
137+
b = bytes(instr.getBytes())
129138
for _i in range(1, num_instr):
130139
instr = instr.getNext()
131-
b = b"".join([b, instr.getBytes()])
140+
b = b"".join([b, bytes(instr.getBytes())])
132141
logger.info(
133-
f"got instr bytes of length {len(b)} for {num_instr} instrs at 0x{addr} with ghidra"
142+
f"got instr bytes of length {len(b)} for {num_instr} instrs at "
143+
f"{hex(addr)} with ghidra"
134144
)
135145
return b
136146

@@ -143,7 +153,7 @@ def get_unused_funcs(self) -> list[dict[str, int]]:
143153
b = f.getBody()
144154
unused_funcs.append(
145155
{
146-
"addr": self.normalize_addr(b.getMinAddress()),
156+
"addr": self._normalize_ghidra_addr(b.getMinAddress()),
147157
"size": b.getNumAddresses(),
148158
}
149159
)
@@ -162,15 +172,16 @@ def get_all_symbols(self) -> dict[str, int]:
162172
for f in fi:
163173
if f.getName() in symbols:
164174
continue
165-
symbols[f.getName()] = self.normalize_addr(f.getEntryPoint())
175+
symbols[f.getName()] = self._normalize_ghidra_addr(f.getEntryPoint())
166176
if self.is_thumb(symbols[f.getName()]):
167177
symbols[f.getName()] += 1
168178
return symbols
169179

170180
def get_function(self, name_or_addr: int | str) -> dict[str, int] | None:
171181
if isinstance(name_or_addr, int):
172-
name_or_addr = self.denormalize_addr(name_or_addr)
173-
func = self.currentProgram.getListing().getFunctionContaining(name_or_addr)
182+
func = self.currentProgram.getListing().getFunctionContaining(
183+
self._to_ghidra_addr(name_or_addr)
184+
)
174185
if func is None:
175186
return None
176187
elif isinstance(name_or_addr, str):
@@ -183,16 +194,33 @@ def get_function(self, name_or_addr: int | str) -> dict[str, int] | None:
183194

184195
b = func.getBody()
185196
return {
186-
"addr": self.normalize_addr(b.getMinAddress()),
197+
"addr": self._normalize_ghidra_addr(b.getMinAddress()),
187198
"size": b.getNumAddresses(),
188199
}
189200

190-
def is_thumb(self, addr: int) -> bool:
191-
addr = self.denormalize_addr(addr)
192-
r = self.currentProgram.getRegister("TMode")
193-
if r is None:
201+
def thumb_mode(self, addr: int) -> bool | None:
202+
"""Return the Ghidra ARM mode, or None when it is unknown."""
203+
register = self.currentProgram.getRegister("TMode")
204+
if register is None:
194205
return False
195-
v = self.currentProgram.getProgramContext().getRegisterValue(r, addr)
196-
t = v.unsignedValueIgnoreMask.intValue() == 1
197-
logger.info(f"address 0x{addr} {'is' if t else 'is not'} thumb from ghidra")
198-
return t
206+
207+
value = self.currentProgram.getProgramContext().getRegisterValue(
208+
register, self._to_ghidra_addr(addr)
209+
)
210+
if value is None or not value.hasValue():
211+
logger.info(f"address {hex(addr)} has no TMode value in ghidra")
212+
return None
213+
214+
is_thumb = value.unsignedValueIgnoreMask.intValue() == 1
215+
logger.info(
216+
f"address {hex(addr)} {'is' if is_thumb else 'is not'} thumb from ghidra"
217+
)
218+
return is_thumb
219+
220+
def is_thumb(self, addr: int) -> bool:
221+
mode = self.thumb_mode(addr)
222+
if mode is None:
223+
raise UnknownInstructionModeError(
224+
f"Cannot determine ARM instruction mode at {hex(addr)}"
225+
)
226+
return mode

tests/test_angr_thumb.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from types import SimpleNamespace
2+
3+
import pytest
4+
from archinfo import ArchARM, Endness
5+
6+
from patcherex2.components.binary_analyzers.angr import Angr
7+
from patcherex2.components.binary_analyzers.binary_analyzer import (
8+
UnknownInstructionModeError,
9+
)
10+
11+
12+
class FakeSymbol:
13+
def __init__(self, name: str, address: int) -> None:
14+
self.name = name
15+
self.rebased_addr = address
16+
17+
18+
class FakeNode:
19+
def __init__(self, address: int, thumb: bool) -> None:
20+
self.instruction_addrs = {address}
21+
self.thumb = thumb
22+
23+
24+
def make_analyzer(
25+
mapping_symbols: list[tuple[str, int]], cfg_nodes: list[tuple[int, bool]]
26+
) -> Angr:
27+
main_object = SimpleNamespace(
28+
mapped_base=0,
29+
pic=False,
30+
symbols=[FakeSymbol(name, address) for name, address in mapping_symbols],
31+
)
32+
analyzer = Angr.__new__(Angr)
33+
analyzer._p = SimpleNamespace(
34+
arch=ArchARM(Endness.LE),
35+
loader=SimpleNamespace(main_object=main_object),
36+
)
37+
analyzer._cfg = SimpleNamespace(
38+
model=SimpleNamespace(
39+
nodes=lambda: [FakeNode(address, thumb) for address, thumb in cfg_nodes]
40+
)
41+
)
42+
analyzer._load_base = 0
43+
analyzer._mapping_symbols = None
44+
return analyzer
45+
46+
47+
@pytest.mark.parametrize(
48+
("mapping_symbols", "cfg_nodes", "expected"),
49+
[
50+
(["$t"], [(0x101, True)], True),
51+
(["$a"], [(0x101, False)], False),
52+
(["$t"], [], True),
53+
(["$a"], [], False),
54+
([], [(0x101, True)], True),
55+
([], [(0x101, False)], False),
56+
(["$t"], [(0x101, False)], None),
57+
(["$a"], [(0x101, True)], None),
58+
(["$d"], [(0x101, True)], None),
59+
(["$d"], [], None),
60+
([], [], None),
61+
],
62+
)
63+
def test_thumb_mode_combines_mapping_symbols_and_cfg(
64+
mapping_symbols, cfg_nodes, expected
65+
):
66+
analyzer = make_analyzer([(name, 0x100) for name in mapping_symbols], cfg_nodes)
67+
68+
assert analyzer.thumb_mode(0x100) is expected
69+
70+
71+
def test_thumb_mode_uses_mapping_symbol_ranges():
72+
analyzer = make_analyzer([("$t", 0x100), ("$a", 0x200)], [])
73+
74+
assert analyzer.thumb_mode(0x1FF) is True
75+
assert analyzer.thumb_mode(0x200) is False
76+
77+
78+
def test_is_thumb_rejects_unknown_mode():
79+
analyzer = make_analyzer([("$t", 0x100)], [(0x101, False)])
80+
81+
with pytest.raises(UnknownInstructionModeError, match="0x100"):
82+
analyzer.is_thumb(0x100)
83+
84+
85+
def test_get_instr_bytes_at_rejects_unknown_mode():
86+
analyzer = make_analyzer([], [])
87+
88+
with pytest.raises(UnknownInstructionModeError, match="0x100"):
89+
analyzer.get_instr_bytes_at(0x100)

0 commit comments

Comments
 (0)