"""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 [--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('