1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
"""Signedness / integer-wrap bounds-check scanner (a bug class not previously audited).
Pattern hunted:
x = *(host_buffer + off); // attacker-controlled 32-bit value
if (x <= LIMIT) { ... } // SIGNED compare (ble/blt/bge/bgt)
memcpy(dst, src, x); // used as an UNSIGNED length
A negative x (0x80000000..0xFFFFFFFF) passes a signed "<= LIMIT" check but is a
huge unsigned length -> overflow. This is orthogonal to every previously audited
class (double-fetch, session state, multiply-overflow, uninit disclosure).
Also flags:
* sign-extending loads (ldrsb/ldrsh) of data later used as a size/index
* add-then-compare without an overflow guard (a+b wraps past the limit)
Heuristic, so every hit needs triage; the point is to surface candidates that the
double-fetch tooling structurally could not see.
Usage: python3 signscan.py <elf> <symbols.json> [--all]
"""
import json, struct, sys
import capstone as cs
from capstone import arm
SIGNED_BR = {'lt','le','gt','ge'} # signed conditions
UNSIGNED_BR = {'lo','ls','hi','hs','cc','cs'}
LOADS = {'ldr','ldrb','ldrh','ldrsb','ldrsh'}
SEXT_LOADS = {'ldrsb','ldrsh'}
COPY_HINTS = ('memcpy','memmove','copy','bzero','memset')
def cond_of(mn):
# capstone puts the condition in the mnemonic, e.g. 'ble', 'blt.w', 'it le'
m = mn.split('.')[0]
if m.startswith('b') and len(m) >= 3:
return m[1:3]
return None
def scan(body, base_va, fva, fend, name, md, callnames):
start, n = fva - base_va, fend - fva
if start < 0 or n <= 0 or start + n > len(body):
return []
try:
ins = list(md.disasm(body[start:start+n], fva))
except Exception:
return []
if len(ins) < 4:
return []
hits = []
# index -> (reg, is_sext, addr) for recent loads
recent = []
for i, x in enumerate(ins):
m = x.mnemonic.split('.')[0]
try: ops = x.operands
except Exception: ops = []
if m in LOADS and ops and ops[0].type == arm.ARM_OP_REG:
# only track loads from a register base (not pc-relative literals)
if len(ops) >= 2 and ops[-1].type == arm.ARM_OP_MEM \
and ops[-1].mem.base not in (0, arm.ARM_REG_PC):
recent.append((i, ops[0].reg, m in SEXT_LOADS, x.address, x.mnemonic, x.op_str))
if len(recent) > 8: recent.pop(0)
continue
if m == 'cmp' and ops and ops[0].type == arm.ARM_OP_REG:
creg = ops[0].reg
imm = ops[1].imm if len(ops) > 1 and ops[1].type == arm.ARM_OP_IMM else None
# find the branch that consumes these flags
cond = None; baddr = None
for j in range(i+1, min(i+4, len(ins))):
c = cond_of(ins[j].mnemonic)
if c:
cond = c; baddr = ins[j].address; break
if ins[j].mnemonic.split('.')[0] == 'it':
cond = ins[j].op_str.strip(); baddr = ins[j].address; break
if cond in SIGNED_BR:
for (li, lreg, sext, laddr, lmn, lop) in recent:
if lreg == creg:
# is the value later used near a copy-ish call?
near_copy = False
for k in range(i, min(i+40, len(ins))):
if ins[k].mnemonic.startswith('bl'):
try: t = int(ins[k].op_str.replace('#','').strip(), 16)
except Exception: continue
nm = callnames.get(t, '')
if any(h in nm.lower() for h in COPY_HINTS):
near_copy = True; break
hits.append((name, fva, laddr, lmn, lop, x.address, imm, cond,
baddr, sext, near_copy))
break
recent = []
return hits
def main():
elf, symf = sys.argv[1], sys.argv[2]
show_all = '--all' in sys.argv
d = json.load(open(symf))
raw = open(elf, 'rb').read()
phoff = struct.unpack('<I', raw[28:32])[0]
off, vaddr, paddr, fsz = struct.unpack('<IIII', raw[phoff+4:phoff+20])
body = raw[off:off+fsz]
md = cs.Cs(cs.CS_ARCH_ARM, cs.CS_MODE_THUMB); md.detail = True
callnames = {int(k,16): v.get('name','') for k, v in d['functions'].items()}
hits = []
for k, v in d['functions'].items():
fva = int(k,16); end = v.get('end')
if not end: continue
hits += scan(body, vaddr, fva, end, v.get('name','?'), md, callnames)
strong = [h for h in hits if h[10] or h[9]] # near a copy, or sign-extending load
print("functions: %d signed-bounds-check candidates: %d (strong: %d)"
% (len(d['functions']), len(hits), len(strong)))
for h in (hits if show_all else strong):
(name, fva, laddr, lmn, lop, caddr, imm, cond, baddr, sext, near_copy) = h
tag = []
if sext: tag.append('SEXT-LOAD')
if near_copy: tag.append('NEAR-COPY')
print(" %-34s load@%#08x %-8s %-18s cmp@%#08x #%s b%s %s"
% (name, laddr, lmn, lop, caddr,
('%#x' % imm) if imm is not None else '?', cond, ','.join(tag)))
if __name__ == '__main__':
main()
|