aboutsummaryrefslogtreecommitdiffstats
path: root/psp_out/signscan.py
blob: dbdab52ebe9e4eea12b63307024b0bbfa28867ac (plain) (blame)
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()