def do_detect(self): policy = ComponentNamePolicy() ns = dict(android='http://schemas.android.com/apk/res/android') for name in set(itertools.chain( self.context.parsed_manifest().getroot().xpath('//activity[not(@android:permission)]/intent-filter/../@android:name', namespaces=ns), self.context.parsed_manifest().getroot().xpath('//activity[not(@android:permission) and (@android:exported="true")]/@android:name', namespaces=ns), )): filter_ = [name for name in self.context.parsed_manifest().getroot().xpath('//activity[@android:name="%s"]/intent-filter/action/@android:name' % name, namespaces=ns) if not policy.looks_public(name)] if not filter_: yield Issue( detector_id=self.option, confidence=IssueConfidence.CERTAIN, cvss3_vector=self.cvss1, summary='manipulatable Activity', info1=name, source='AndroidManifest.xml', synopsis="Application is exporting one or more activities.", description="Application is exporting one or more activities. Activities are entrypoints to the application. Exporting enables them to be invoked from other applications or system. Unnecessary export increases attack surfaces. Please note that Android automatically exports ones with IntentFilter defined in the manifest. This issue is just an observation; exporting activities alone does not constitute an security issue.", solution="Review them, and restrict access with application-specific permissions if necessary." ) else: yield Issue( detector_id=self.option, confidence=IssueConfidence.CERTAIN, cvss3_vector=self.cvss2, summary='manipulatable Activity with private action names', info1=name, info2=', '.join(filter_), source='AndroidManifest.xml', synopsis="Application is exporting one or more activities using seemingly private action names, suggesting inadvent export.", description="Application is exporting one or more activities using seemingly private action names, suggesting inadvent export. Activities are entrypoints to the application. Exporting enables them to be invoked from other applications or system. Inadvent exporting enables malwares or malicious users to manipulate the application. Please note that Android automatically exports ones with IntentFilter defined in the manifest.", solution="Review them, and restrict access with application-specific permissions if necessary." )
def do_detect(self): with open(pkg_resources.resource_filename( __name__, os.path.join('..', 'libs', 'tlds.txt')), 'r', encoding='utf-8') as f: self.re_tlds = re.compile('^(?:%s)$' % '|'.join( re.escape(l.strip()) for l in f if l and not l.startswith('#')), flags=re.IGNORECASE) with self.context.store() as store: for cl in store.query().consts( InvocationPattern( 'const-string', r'://|^/[{}$%a-zA-Z0-9_-]+(/[{}$%a-zA-Z0-9_-]+)+|^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+(:[0-9]+)?$' )): for match in self.analyzed(cl.p[1].v): for v in match['value']: yield Issue(detector_id=self.option, confidence=IssueConfidence.FIRM, cvss3_vector=self.cvss, summary='detected %s' % match['type_'], info1=v, source=store.query().qualname_of(cl)) for name, val in self.context.string_resources(): for match in self.analyzed(val): for v in match['value']: yield Issue(detector_id=self.option, confidence=IssueConfidence.FIRM, cvss3_vector=self.cvss, summary='detected %s' % match['type_'], info1=v, source='R.string.%s' % name)
def do_detect(self): with self.context.store() as store: targets = {'WebView', 'XWalkView', 'GeckoView'} more = True while more: more = False for cl in store.query().related_classes('|'.join(targets)): name = store.query().class_name_of(cl) if name not in targets: targets.add(name) more = True for fn in (n for n in self.context.disassembled_resources() if 'layout' in n): with open(fn, 'rb') as f: r = ET.parse(f, parser=ET.XMLParser(recover=True)).getroot() for t in functools.reduce( lambda x, y: x + y, (r.xpath('//%s' % self.context.class_name_of_dalvik_class_type( c).replace('$', '_')) for c in targets)): size = LayoutSizeGuesser().guessed_size(t, fn) if size > 0.5: yield Issue( detector_id=self.option, confidence=IssueConfidence.TENTATIVE, cvss3_vector=self.cvss1, summary='tamperable webview', info1='{0} (score: {1:.02f})'.format( t.attrib['{0}id'.format( self.xmlns_android)], size), source=self.context. source_name_of_disassembled_resource(fn)) # XXX: crude detection for op in store.query().invocations( InvocationPattern('invoke-', ';->loadUrl')): try: v = DataFlows.solved_constant_data_in_invocation( store, op, 0) if v.startswith('http://'): yield Issue(detector_id=self.option, confidence=IssueConfidence.FIRM, cvss3_vector=self.cvss2, summary='tamperable webview with URL', info1=v, source=store.query().qualname_of(op)) except DataFlows.NoSuchValueError: pass
def do_detect(self): with self.context.store() as store: for cl in store.query().invocations( InvocationPattern( 'invoke-', 'L.*->([dwie]|debug|error|exception|warning|info|notice|wtf)\(Ljava/lang/String;Ljava/lang/String;.*?Ljava/lang/(Throwable|.*?Exception);|L.*;->print(ln)?\(Ljava/lang/String;|LException;->printStackTrace\(' )): if 'print' not in cl.p[1].v: try: yield Issue( detector_id=self.option, confidence=IssueConfidence.TENTATIVE, cvss3_vector=self.cvss, summary='detected logging', info1=cl.p[1].v, info2=DataFlows.solved_constant_data_in_invocation( store, cl, 1), source=store.query().qualname_of(cl)) except (DataFlows.NoSuchValueError): yield Issue(detector_id=self.option, confidence=IssueConfidence.TENTATIVE, cvss3_vector=self.cvss, summary='detected logging', info1=cl.p[1].v, source=store.query().qualname_of(cl)) elif 'Exception;->' not in cl.p[1].v: try: yield Issue( detector_id=self.option, confidence=IssueConfidence.TENTATIVE, cvss3_vector=self.cvss, summary='detected logging', info1=cl.p[1].v, info2=DataFlows.solved_constant_data_in_invocation( store, cl, 0), source=store.query().qualname_of(cl)) except (DataFlows.NoSuchValueError): yield Issue(detector_id=self.option, confidence=IssueConfidence.TENTATIVE, cvss3_vector=self.cvss, summary='detected logging', info1=cl.p[1].v, source=store.query().qualname_of(cl)) else: yield Issue(detector_id=self.option, confidence=IssueConfidence.TENTATIVE, cvss3_vector=self.cvss, summary='detected logging', info1=cl.p[1].v, source=store.query().qualname_of(cl))
def do_detect_case2(self): # XXX: Crude detection def should_be_secret(store, k, val): return any( x in store.query().qualname_of(k).lower() for x in ['inapp', 'billing', 'iab', 'sku', 'store', 'key']) pat = '^MI[IG][0-9A-Za-z+/=-]{32,}AQAB' with self.context.store() as store: for cl in store.query().consts( InvocationPattern('const-string', pat)): val = cl.p[1].v yield Issue( detector_id=self.option, cvss3_vector=self.cvss, confidence={ True: IssueConfidence.FIRM, False: IssueConfidence.TENTATIVE }[should_be_secret(store, cl, val)], summary='insecure cryptography: static keys (2)', info1='"%(target_val)s" [%(target_val_len)d] (X.509)' % dict(target_val=val, target_val_len=len(val)), source=store.query().qualname_of(cl), synopsis= 'Traces of X.509 certificates has been found the application binary.', description='''\ Traces of X.509 certificates has been found in the application binary. X.509 ceritificates describe public key materials. Their notable uses include Google Play in-app billing identity. If is hardcoded, attackers can extract or replace them. ''', solution='''\ Use a device or installation specific information, or obfuscate them. Especially, do not use the stock implementation of in-app billing logic. ''') for name, val in self.context.string_resources(): if re.match(pat, val): yield Issue( detector_id=self.option, cvss3_vector=self.cvss, confidence=IssueConfidence.TENTATIVE, summary='insecure cryptography: static keys (2)', info1='"%(target_val)s" [%(target_val_len)d] (X.509)' % dict(target_val=val, target_val_len=len(val)), source='R.string.%s' % name, synopsis= 'Traces of X.509 certificates has been found the application binary.', description='''\ Traces of X.509 certificates has been found in the application binary. X.509 ceritificates describe public key materials. Their notable uses include Google Play in-app billing identity. If is hardcoded, attackers can extract or replace them. ''', solution='''\ Use a device or installation specific information, or obfuscate them. Especially, do not use the stock implementation of in-app billing logic. ''')
def do_detect(self): with self.context.store() as store: for cl in store.query().invocations( InvocationPattern( 'invoke-static', 'Ljavax/crypto/Cipher;->getInstance\(Ljava/lang/String;.*?\)' )): try: target_val = DataFlows.solved_possible_constant_data_in_invocation( store, cl, 0) if any((('ECB' in x or '/' not in x) and 'RSA' not in x) for x in target_val): yield Issue( detector_id=self.option, cvss3_vector=self.cvss, confidence=IssueConfidence.CERTAIN, summary= 'insecure cryptography: cipher might be operating in ECB mode', info1=','.join(target_val), source=store.query().qualname_of(cl), synopsis= 'The application might be using ciphers in ECB mode.', description='''\ The application might be using symmetric ciphers in ECB mode. ECB mode is the most basic operating mode that independently transform data blocks. Indepent transformation leaks information about distribution in plaintext. ''', solution='''\ Use CBC or CTR mode. ''') except (DataFlows.NoSuchValueError): pass
def detect(self): for c in (self.class_name_of( self.context.source_name_of_disassembled_class(r)) for r in self.context.disassembled_classes()): if re.search('(?:^|\.)a$', c): yield Issue(detector_id=self.option, confidence=IssueConfidence.CERTAIN, cvss3_vector=self.cvss_true, summary='detected obfuscator', info1='ProGuard') break else: yield Issue(detector_id=self.option, confidence=IssueConfidence.FIRM, cvss3_vector=self.cvss_false, summary='lack of obfuscation')
def do_detect(self): package = self.context.parsed_manifest().getroot().xpath( '/manifest/@package', namespaces=dict( android='http://schemas.android.com/apk/res/android'))[0] packages = dict() for fn in (self.context.source_name_of_disassembled_class(r) for r in self.context.disassembled_classes()): family = self.package_family_of(self.package_name_of(fn)) if family is not None: try: packages[family].append(fn) except KeyError: packages[family] = [fn] else: pass packages = { k: v for k, v in packages.items() if not self.is_kind_of(k, package) and re.search(r'\.[a-zA-Z0-9]{4,}(?:\.|$)', k) } yield from (Issue( detector_id=self.option, confidence=IssueConfidence.FIRM, cvss3_vector=self.cvss, summary='detected library', info1='%s (score: %d)' % (p, len(packages[p])), ) for p in sorted(packages.keys()))
def do_detect(self): if not self.do_detect_plain_pins_x509(): if not self.do_detect_plain_pins_hostnameverifier(): yield Issue(detector_id=self.option, confidence=IssueConfidence.CERTAIN, cvss3_vector=self.cvss, summary='insecure TLS connection', info1='no pinning detected')
def do_detect(self): with self.context.store() as store: for cl in store.query().consts( InvocationPattern('const-string', r'%s')): for t in self.analyzed(cl.p[1].v): yield Issue(detector_id=self.option, confidence=t['confidence'], cvss3_vector=self.cvss, summary='detected format string', info1=t['value'], source=store.query().qualname_of(cl)) for name, val in self.context.string_resources(): for t in self.analyzed(val): yield Issue(detector_id=self.option, confidence=t['confidence'], cvss3_vector=self.cvss, summary='detected format string', info1=t['value'], source='R.string.%s' % name)
def do_detect(self): policy = ComponentNamePolicy() ns = dict(android='http://schemas.android.com/apk/res/android') for name in set(itertools.chain( self.context.parsed_manifest().getroot().xpath('//provider[not(@android:permission)]/intent-filter/../@android:name', namespaces=dict(android='http://schemas.android.com/apk/res/android')), self.context.parsed_manifest().getroot().xpath('//provider[not(@android:permission) and (@android:exported="true")]/@android:name', namespaces=dict(android='http://schemas.android.com/apk/res/android')), )): filter_ = [name for name in self.context.parsed_manifest().getroot().xpath('//receiver[@android:name="%s"]/intent-filter/action/@android:name' % name, namespaces=ns) if not policy.looks_public(name)] if not filter_: yield Issue( detector_id=self.option, confidence=IssueConfidence.CERTAIN, cvss3_vector=self.cvss1, summary='manipulatable ContentProvider', info1=name, source='AndroidManifest.xml', synopsis="Application is exporting one or more content providers.", description="Application is exporting one or more content providers. Content providers defines REST/RDBMS-like IPC mechanism for the application. Exporting enables them to be invoked from other applications or system. Unnecessary export increases attack surfaces. Please note that Android automatically exports them (API 8 or ealier) or ones with IntentFilter defined in the manifest (API level 9 or later). This issue is just an observation; exporting content providers alone does not constitute an security issue.", solution='''\ Review them and explicitly unexport or restrict access with application-specific permissions if necessary. To explicitly unexporting an content provider, define the following attribute to the <provider> tag in the manifest: android:export="false" ''' ) else: yield Issue( detector_id=self.option, confidence=IssueConfidence.CERTAIN, cvss3_vector=self.cvss2, summary='manipulatable ContentProvider with private action names', info1=name, info2=', '.join(filter_), source='AndroidManifest.xml', synopsis="Application is exporting one or more content providers using seemingly private action names, suggesting inadvent export.", description="Application is exporting one or more content providers using seemingly private action names, suggesting inadvent export. Content providers defines REST/RDBMS-like IPC mechanism for the application. Exporting enables them to be invoked from other applications or system. Inadvent exporting enables malwares or malicious users to manipulate the application. Please note that Android automatically exports them (API 8 or ealier) or ones with IntentFilter defined in the manifest (API level 9 or later).", solution='''\ Review them and explicitly unexport or restrict access with application-specific permissions if necessary. To explicitly unexporting an content provider, define the following attribute to the <provider> tag in the manifest: android:export="false" ''' )
def do_detect(self): with self.context.store() as store: for cl in store.query().ops_of('xor-int/lit8'): target_val = int(cl.p[2].v, 16) if (cl.p[0].v == cl.p[1].v) and target_val > 1: yield Issue( detector_id=self.option, cvss3_vector=self.cvss, confidence=IssueConfidence.FIRM, summary='insecure cryptography: non-random XOR cipher', info1='0x%02x' % target_val, source=store.query().qualname_of(cl))
def do_detect(self): # TBD: compare with actual permission needs yield from ( Issue( detector_id=self.option, confidence=IssueConfidence.CERTAIN, cvss3_vector=self.cvss, summary='open permissions', info1=p, source='AndroidManifest.xml', synopsis="Application is requesting one or more permissions.", description="Application is requesting one or more permissions. Permissions are an important security system of Android. They control accesses to sensitive information (e.g. GPS, IMEI/IMSI, process stats, accounts, contacts, SMSs) or possibly dangerous/costly operation (e.g. SMSs, internet access, controlling system services, obstructing screens.) Requesting ones are vital for proper functioning of application, though abusage leads to hurt privacy or device availability. This issue is just an observation; requesting permissions alone does not constitute an security issue.", ) for p in self.context.permissions_declared())
def do_detect(self): with self.context.store() as store: for op in store.query().invocations( InvocationPattern( 'invoke-', 'Landroid/net/Uri;->parse\(Ljava/lang/String;\)Landroid/net/Uri;' )): try: if DataFlows.solved_constant_data_in_invocation( store, op, 0).startswith('content://sms/'): yield Issue(detector_id=self.option, confidence=IssueConfidence.CERTAIN, cvss3_vector=self.cvss, summary='privacy concerns', info1='accessing SMS', source=store.query().qualname_of(op)) except DataFlows.NoSuchValueError: pass for op in store.query().invocations( InvocationPattern('invoke-', 'Landroid/telephony/SmsManager;->send')): yield Issue(detector_id=self.option, confidence=IssueConfidence.CERTAIN, cvss3_vector=self.cvss, summary='privacy concerns', info1='sending SMS', source=store.query().qualname_of(op)) for op in store.query().invocations( InvocationPattern( 'invoke-', 'Landroid/telephony/SmsMessage;->createFromPdu\(')): yield Issue(detector_id=self.option, confidence=IssueConfidence.FIRM, cvss3_vector=self.cvss, summary='privacy concerns', info1='intercepting incoming SMS', source=store.query().qualname_of(op))
def do_detect(self): with self.context.store() as store: for op in store.query().invocations( InvocationPattern( 'invoke-', 'Landroid/provider/Settings\$Secure;->getString\(Landroid/content/ContentResolver;Ljava/lang/String;\)Ljava/lang/String;|Landroid/telephony/TelephonyManager;->getDeviceId\(\)Ljava/lang/String;|Landroid/telephony/TelephonyManager;->getSubscriberId\(\)Ljava/lang/String;|Landroid/telephony/TelephonyManager;->getLine1Number\(\)Ljava/lang/String;|Landroid/bluetooth/BluetoothAdapter;->getAddress\(\)Ljava/lang/String;|Landroid/net/wifi/WifiInfo;->getMacAddress\(\)Ljava/lang/String;|Ljava/net/NetworkInterface;->getHardwareAddress\(\)' )): val_type = self.analyzed(store, op) if val_type is not None: yield Issue(detector_id=self.option, confidence=IssueConfidence.CERTAIN, cvss3_vector=self.cvss, summary='privacy concerns', info1='getting %s' % val_type, source=store.query().qualname_of(op))
def do_detect(self): if self.context.parsed_manifest().getroot().xpath('//application[not(@android:allowBackup="false")]', namespaces=dict(android='http://schemas.android.com/apk/res/android')): yield Issue( detector_id=self.option, confidence=IssueConfidence.CERTAIN, cvss3_vector=self.cvss, summary='manipulatable backups', source='AndroidManifest.xml', synopsis="Application data can be backed up and restored with the Full Backup feature.", description="Application data can be backed up and restored with the Full Backup feature, thusly making it subjectible to the backup attack.", solution='''\ Review them and opt-out from the Full Backup feature if necessary. To opt-out, define the following attribute to the <application> tag in the manifest: android:allowBackup="false" ''' )
def do_detect(self): if self.context.parsed_manifest().getroot().xpath('//application[@android:debuggable="true"]', namespaces=dict(android='http://schemas.android.com/apk/res/android')): yield Issue( detector_id=self.option, confidence=IssueConfidence.CERTAIN, cvss3_vector=self.cvss, summary='app is debuggable', source='AndroidManifest.xml', synopsis="Application can be debugged.", description="Application can be debugged (the debuggable bit is set.) Debugging it gives attackers complete control of its process memory and control flow.", solution='''\ Disable the debuggable bit. To disable it, define the following attribute to the <application> tag in the manifest: android:debuggable="false" ''' )
def generate(self): super().generate() with self._context.store().db as db: issues = [] for row, no in zip(db.execute('select distinct detector, summary, synopsis, description, seealso, solution, cvss3_score, cvss3_vector from analysis_issues order by cvss3_score desc'), range(1, 2**32)): instances = [] issues.append(dict(no=no, detector=row[0], summary=row[1].title(), synopsis=row[2], description=row[3], seealso=row[4], solution=row[5], cvss3_score=row[6], cvss3_vector=row[7], severity=CVSS3Scoring.severity_of(row[6]).title(), instances=instances, severity_panel_style={'critical':'panel-danger', 'high':'panel-warning', 'medium':'panel-warning', 'low':'panel-success', 'info':'panel-info'}[CVSS3Scoring.severity_of(row[6])])) for m in db.execute('select * from analysis_issues where detector=:detector and summary=:summary and cvss3_score=:cvss3_score', {v:row[k] for k,v in {0:'detector', 1:'summary', 6:'cvss3_score'}.items()}): issue = Issue.from_analysis_issues_row(m) instances.append(dict(info=issue.brief_info(), source=issue.source, row=issue.row, col=issue.col)) app = dict( package=self._context.parsed_manifest().getroot().xpath('/manifest/@package', namespaces=dict(android='http://schemas.android.com/apk/res/android'))[0], issues=len(issues), issues_critical=len([_ for _ in issues if _['severity'] == 'Critical']), issues_high=len([_ for _ in issues if _['severity'] == 'High']), issues_medium=len([_ for _ in issues if _['severity'] == 'Medium']), issues_low=len([_ for _ in issues if _['severity'] == 'Low']), issues_info=len([_ for _ in issues if _['severity'] == 'Info']) ) self._write(self._template.render(app=app, issues=issues))
def do_detect(self): with self.context.store() as store: for cl in store.query().invocations( InvocationPattern( 'invoke-virtual', 'Landroid/content/Context;->openFileOutput\(Ljava/lang/String;I\)' )): try: target_val = int( DataFlows.solved_constant_data_in_invocation( store, cl, 1), 16) if target_val & 3: yield Issue(detector_id=self.option, confidence=IssueConfidence.CERTAIN, cvss3_vector=self.cvss, summary='insecure file permission', info1={ 1: 'MODE_WORLD_READABLE', 2: 'MODE_WORLD_WRITABLE' }[target_val], source=store.query().qualname_of(cl)) except (DataFlows.NoSuchValueError): pass
def do_detect_case1(self): def looks_like_real_key(k): # XXX: silly return len(k) >= 8 and not any( x in k for x in ('Padding', 'SHA1', 'PBKDF2', 'Hmac', 'emulator')) with self.context.store() as store: for cl in store.query().invocations( InvocationPattern( 'invoke-', '^Ljavax?.*/(SecretKey|(Iv|GCM)Parameter|(PKCS8|X509)EncodedKey)Spec|^Ljavax?.*/MessageDigest;->(update|digest)' )): try: for nr in self.important_args_on_invocation(cl): for found in DataFlows.solved_possible_constant_data_in_invocation( store, cl, nr): try: decoded = base64.b64decode(found) info1 = '"%(target_val)s" [%(target_val_len)d] (base64; "%(decoded_val)s" [%(decoded_val_len)d])' % dict( target_val=found, target_val_len=len(found), decoded_val=binascii.hexlify( decoded).decode('ascii'), decoded_val_len=len(decoded)) except (ValueError, binascii.Error): info1 = '"%(target_val)s" [%(target_val_len)d]' % dict( target_val=found, target_val_len=len(found)) if looks_like_real_key(found): yield Issue( detector_id=self.option, cvss3_vector=self.cvss, confidence=IssueConfidence.FIRM, summary= 'insecure cryptography: static keys', info1=info1, source=store.query().qualname_of(cl), synopsis= 'Traces of cryptographic material has been found the application binary.', description='''\ Traces of cryptographic material has been found in the application binary. If cryptographic material is hardcoded, attackers can extract or replace them. ''', solution='''\ Use a device or installation specific information, or obfuscate them. ''') else: yield Issue( detector_id=self.option, cvss3_vector=self.cvss_nonkey, confidence=IssueConfidence.TENTATIVE, summary='Cryptographic constants detected', info1=info1, source=store.query().qualname_of(cl), synopsis= 'Possible cryptographic constants have been found.', description='''\ Possible cryptographic constants has been found in the application binary. ''') except IndexError: pass
def do_detect(self): with self.context.store() as store: targets = set() seeds = {'WebView', 'XWalkView', 'GeckoView'} more = True while more: more = False for cl in store.query().related_classes('|'.join(seeds)): name = store.query().class_name_of(cl) if name not in targets: targets.add(name) more = True for seed in seeds: targets.add('L.*%s;' % seed) # XXX: Crude detection for p in store.query().invocations( InvocationPattern( 'invoke-virtual', 'Landroid/webkit/WebSettings;->setJavaScriptEnabled')): try: if DataFlows.solved_constant_data_in_invocation( store, p, 0): for target in targets: for q in store.query().invocations_in_class( p, InvocationPattern( 'invoke-virtual', '%s->addJavascriptInterface' % target)): try: if DataFlows.solved_constant_data_in_invocation( store, q, 0): yield Issue( detector_id=self.option, confidence=IssueConfidence.FIRM, cvss3_vector=self.cvss, summary= 'insecure Javascript interface', source=store.query().qualname_of( q)) except (DataFlows.NoSuchValueError): yield Issue( detector_id=self.option, confidence=IssueConfidence.TENTATIVE, cvss3_vector=self.cvss, summary='insecure Javascript interface', source=store.query().qualname_of(q)) except (DataFlows.NoSuchValueError): pass for q in store.query().invocations_in_class( p, InvocationPattern( 'invoke-virtual', 'Landroid/webkit/WebSettings;->setMixedContentMode' )): try: val = int( DataFlows.solved_constant_data_in_invocation( store, q, 0), 16) if val == 0: yield Issue(detector_id=self.option, confidence=IssueConfidence.FIRM, cvss3_vector=self.cvss, summary='insecure mixed content mode', info1='MIXED_CONTENT_ALWAYS_ALLOW', source=store.query().qualname_of(q)) except (DataFlows.NoSuchValueError): pass