def getRC4Key(apk): a, d, dx = AnalyzeAPK(apk) classes = dx.get_classes() for clazz in classes: methods = clazz.get_methods() for method in methods: ''' First step is to find the generator function ''' caller_method = method.get_method() if caller_method.get_descriptor() != "([B [B)V": continue source = caller_method.get_source() if "length" not in source: continue ''' Then we can find the key instantiation method by looking at the XRef method form the generator ''' caller_xrefs = method.get_xref_from() if len(list(caller_xrefs)) != 1: print("Error : No XRefs found for PRNG") sys.exit(-1) key_method = list(caller_xrefs)[0][1] key_method_source = key_method.get_source() ''' Quick regex to find the key ''' keys = re.findall(r"= {(?P<key>(\s*-?\d+,?)+)};", key_method_source) if (keys == None): print("Error: No key found") sys.exit(-1) for key in keys: key = list(map(lambda x: int(x) & 0xff, key[0].split(","))) # print(f"Key : {key}") key = bytes(key) ''' Second step : we decrypt the good asset containing the payload ''' resources = a.get_files() for res in resources: if (res.startswith("res/") or res.startswith("META-INF/") or res == "resource.arsc" or res == "classes.dex"): continue else: content = a.get_file(res) rc4 = ARC4.new(key) unciphered_file = rc4.decrypt(content[4:]) magic_number = unciphered_file[:2] if (magic_number == b'PK'): payload_size = int.from_bytes(content[0:4], byteorder='little') # print(f"Filesize : {payload_size}") extract_name = apk + ".payload.apk" f = open(extract_name, "wb") f.write(unciphered_file[:payload_size]) f.close() print(f"Saved to : {extract_name}") return extract_name
class AndroHelper: def __init__(self, apk_path, output_dir): self.apk_path = apk_path # output directory self.output_dir = output_dir + "/" self.packed_files = dict() self.a, self.d, self.dx = AnalyzeAPK(self.apk_path) self.detected_malware = dict() def analyse(self): self.packed_files = dict() self.malware_detect() for file in self.a.get_files(): file_type = check_header(self.a.get_file(file)[0:4].hex()) if file_type == "JAR": if not os.path.isdir(self.output_dir): os.makedirs(self.output_dir) f = open(self.output_dir + file.split("/")[-1], 'wb') f.write(self.a.get_file(file)) f.close() try: a, d, dx = AnalyzeAPK(self.output_dir + file.split("/")[-1]) if a.get_package(): self.packed_files[self.a.get_package()] = {file: {}} else: continue except Exception as e: # not an APK file continue with open(PERMISSIONS_FILE) as json_file: permissions = json.load(json_file) perms_desc = {} dangerous_perms = {} if a.get_permissions(): for perm in a.get_permissions(): try: mapped = list(filter(lambda x: x["permission"] == perm, permissions)) perms_desc[perm] = {"desc": mapped[0]["desc"], "level": mapped[0]["protection_lvl"]} if any(re.findall(r'dangerous', mapped[0]["protection_lvl"], re.IGNORECASE)): # Permission is flagged as dangerous dangerous_perms[mapped[0]["permission"]] = mapped[0]["desc"] except Exception as e: continue self.packed_files[self.a.get_package()][file] = dangerous_perms return {"packed_file": self.packed_files, "detected_malware": self.detected_malware} def malware_detect(self): action_spy = ActionSpy(apk_path=self.apk_path, output_dir=self.output_dir) succeeded_test = action_spy.check() self.detected_malware["actionspy"] = succeeded_test wolf_rat = WolfRat(apk_path=self.apk_path, output_dir=self.output_dir) succeeded_test = wolf_rat.check() self.detected_malware["wolfrat"] = succeeded_test anubis = Anubis(apk_path=self.apk_path, output_dir=self.output_dir) succeeded_test = anubis.check() self.detected_malware["anubis"] = succeeded_test
class SmartInput(object): default = '123456' type_class = { 'TYPE_NULL': '', 'TYPE_CLASS_TEXT': 'example', 'TYPE_CLASS_NUMBER': '1', 'TYPE_CLASS_PHONE': '3453453456', 'TYPE_CLASS_DATETIME': '03032015' } type_variation = { 'TYPE_TEXT_VARIATION_NORMAL': 'example', 'TYPE_TEXT_VARIATION_URI': 'https://www.example.com', 'TYPE_TEXT_VARIATION_EMAIL_ADDRESS': '*****@*****.**', 'TYPE_TEXT_VARIATION_EMAIL_SUBJECT': 'Example Email Subject', 'TYPE_TEXT_VARIATION_SHORT_MESSAGE': 'Example Short Message', 'TYPE_TEXT_VARIATION_LONG_MESSAGE': 'This is an example of a very long message for an input text.', 'TYPE_TEXT_VARIATION_PERSON_NAME': 'John Smith', 'TYPE_TEXT_VARIATION_POSTAL_ADDRESS': '16100', 'TYPE_TEXT_VARIATION_PASSWORD': '******', 'TYPE_TEXT_VARIATION_VISIBLE_PASSWORD': '******', 'TYPE_TEXT_VARIATION_WEB_EDIT_TEXT': '', 'TYPE_TEXT_VARIATION_FILTER': '', 'TYPE_TEXT_VARIATION_PHONETIC': '', 'TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS': '*****@*****.**', 'TYPE_TEXT_VARIATION_WEB_PASSWORD': '******', 'TYPE_NUMBER_VARIATION_NORMAL': '3453453456', 'TYPE_NUMBER_VARIATION_PASSWORD': '******', 'TYPE_DATETIME_VARIATION_NORMAL': '03032015', 'TYPE_DATETIME_VARIATION_DATE': '03032015', 'TYPE_DATETIME_VARIATION_TIME': '000000' } def __init__(self, apk_path: str): self.logger = logging.getLogger('{0}.{1}'.format( __name__, self.__class__.__name__)) self.logger.info('Smart input generation') self.smart_inputs = {} self.apk: APK = None self.dx: Analysis = None self.apk, _, self.dx = AnalyzeAPK(apk_path) tmp_edit_text_classes = self.get_subclass_names( 'Landroid/widget/EditText;') # Convert EditText classes to dot notation ('EditText' is a built-in class so the prefix is not needed). # This notation will be used when looking for text inputs in the xml layout files. self.edit_text_classes = {'EditText'} for clazz in tmp_edit_text_classes: self.edit_text_classes.add( re.search('L(.*);', clazz).group(1).replace('/', '.')) try: self.class_object_list = [ clazz.get_vm_class() for clazz in self.dx.get_internal_classes() ] self.classes_dict = self.get_class_dict() # Find the R$id classes. self.resource_ids = self.get_resource_ids(self.class_object_list) # Find the R$layout classes. self.resource_layouts = self.get_resource_layouts( self.class_object_list) self.field_refs = get_field_refs(self.resource_ids) self.find_text_fields() except Exception as e: self.logger.error( 'Error during smart input generation: {0}'.format(e)) raise def get_subclass_names(self, class_name: str): subclass_names = set() edit_text_class = self.dx.get_class_analysis(class_name) if edit_text_class: for clazz in edit_text_class.get_xref_from(): if clazz.get_vm_class().get_superclassname() == class_name: subclass_name = clazz.get_vm_class().get_name() subclass_names.add(subclass_name) subclass_names.update( self.get_subclass_names(subclass_name)) return subclass_names # Return a dict with the class names and the class objects. def get_class_dict(self): classes = {} for clazz in self.class_object_list: # Get the name of the class using the dot notation. clazz_name = re.search('L(.*);', clazz.get_name()).group(1).replace( '/', '.') classes[clazz_name] = clazz return classes # Get R$id classes. def get_resource_ids(self, classes): resource_ids = [] for clazz in classes: if clazz.get_name().endswith('R$id;'): self.logger.debug('Found R$id class at {0}'.format( clazz.get_name())) resource_ids.append(clazz) return resource_ids # Get R$layout classes. def get_resource_layouts(self, classes): resource_layouts = [] for clazz in classes: if clazz.get_name().endswith('R$layout;'): self.logger.debug('Found R$layout class at {0}'.format( clazz.get_name())) resource_layouts.append(clazz) return resource_layouts def get_xml_from_file(self, xml_file): ap = AXMLPrinter(self.apk.get_file(xml_file)) return minidom.parseString(ap.get_buff()) # Return every instance of an EditText field and their inputType in the XML. # Not all EditText fields will have an inputType specified in the XML. def get_input_fields_with_input_types_from_xml(self, xml_file): input_fields = {} xml_content = self.get_xml_from_file(xml_file) for edit_text_tag in self.edit_text_classes: for item in xml_content.getElementsByTagName(edit_text_tag): android_id = None input_type = {'type': None, 'is_password': False} for k, v in item.attributes.itemsNS(): if k[1] == 'id': android_id = v[1:] if k[1] == 'inputType': input_type['type'] = v if k[1] == 'password': # Deprecated, only inputType should be used, but some apps still use this. input_type['is_password'] = True if v.lower( ) == 'true' else False if android_id: input_fields[hex(int(android_id, 16))] = input_type return input_fields def parse_move(self, bc, index): i = bc.get_instruction(index) register = i.get_output().split(',')[1].strip() for x in range(index - 1, -1, -1): i = bc.get_instruction(x) if 'const' in i.get_name() and register in i.get_output(): return parse_const(bc.get_instruction(x)) def get_activity_xml(self, activity_class): # Build a list of every layout hex value referenced in activity's bytecode. hex_codes = [] for method in activity_class.get_methods(): if method.get_name() == 'onCreate': try: for index, instruction in enumerate( method.get_instructions()): # Find setContentView, then parse the passed value from the previous # const or const/high16 instruction. if 'setContentView' in instruction.show_buff(0): instruction = method.get_code().get_bc( ).get_instruction(index - 1) if 'const' in instruction.get_name(): hex_codes.append(parse_const(instruction)) elif 'move' in instruction.get_name(): hex_codes.append( self.parse_move(method.get_code().get_bc(), index - 1)) except Exception: pass # Cross check the list of hex codes with R$layout to retrieve XML layout file name. for layout in self.resource_layouts: for field in layout.get_fields(): if hex(field.get_init_value().get_value()) in hex_codes: return 'res/layout/{0}.xml'.format(field.get_name()) return None def get_input_field_from_code(self, class_object: ClassDefItem, field_id: str): self.logger.debug('Analyzing field {0}'.format(field_id)) for method in class_object.get_methods(): instructions = iter(method.get_instructions()) for instruction in instructions: if ('const' == instruction.get_name() or 'const/high16' == instruction.get_name()) \ and field_id == parse_const(instruction): # Get the register in which the constant is assigned. register = instruction.get_output().split(',')[0].strip() while True: try: last_instruction = instruction instruction = next(instructions) except StopIteration: self.logger.debug( 'Could not get input field {0} from code'. format(field_id)) return None # Follow the register to the next invoke-virtual of findViewById... if (register in instruction.get_output() and 'findViewById' in instruction.get_output()) \ and 'invoke-virtual' in instruction.get_name(): # ...and get the register of that output. register = instruction.get_output().split( ',')[1].strip() elif instruction.get_name() == 'move-result-object' and \ 'invoke-virtual' in last_instruction.get_name(): register = instruction.get_output().strip() elif (instruction.get_name() == 'iput-object' or instruction.get_name() == 'sput-object') and \ register in instruction.get_output().split(',')[0].strip(): out_sp = re.search( r'.*, (.*)->(\b[\w]*\b) (.*)', instruction.get_output()).groups() try: field_analysis = list( self.dx.find_fields( out_sp[0], out_sp[1], out_sp[2])) if field_analysis: return field_analysis[0] else: for field in self.dx.get_class_analysis( out_sp[0]).get_vm_class( ).get_fields(): if field.get_name() == out_sp[ 1] and field.get_descriptor( ) == out_sp[2]: return FieldClassAnalysis(field) except Exception: return None return None def find_text_fields(self): try: # Get all the input fields from the xml layout files. input_fields = {} for xml_layout_file in filter(lambda x: x.startswith('res/layout'), self.apk.get_files()): try: input_fields.update( self.get_input_fields_with_input_types_from_xml( xml_layout_file)) except Exception: pass # Combine all information into a TextField dict. text_fields = {} for field_id in input_fields: text_fields[field_id] = TextField( field_id, self.field_refs[field_id].get_name(), input_fields[field_id]['type'], self.field_refs[field_id], is_password=input_fields[field_id]['is_password']) self.smart_inputs['all'] = list(text_fields.values()) # Group input fields by activity (if possible). for activity_name in self.apk.get_activities(): self.logger.debug( 'Analyzing activity {0}'.format(activity_name)) if activity_name in self.classes_dict: # Include also the internal classes of the activity. class_objects = [ self.classes_dict[dot_class_name] for dot_class_name in self.classes_dict if dot_class_name == activity_name or dot_class_name.startswith('{0}$'.format(activity_name)) ] input_types_for_fields = {} for class_object in class_objects: # Find all XML layouts referenced in setContentView in activity bytecode. activity_xml_file = self.get_activity_xml(class_object) if not activity_xml_file: continue try: input_types_for_fields.update( self. get_input_fields_with_input_types_from_xml( activity_xml_file)) except Exception: pass if not input_types_for_fields: self.logger.debug( 'No XMLs found for activity {0}'.format( activity_name)) continue # Combine all information into a TextField dict. text_fields = {} for field_id in input_types_for_fields: for class_object in class_objects: field = self.get_input_field_from_code( class_object, field_id) if field: tf = TextField( field_id, self.field_refs[field_id].get_name(), input_types_for_fields[field_id]['type'], self.field_refs[field_id], field, is_password=input_types_for_fields[ field_id]['is_password']) text_fields[field_id] = tf else: tf = TextField( field_id, self.field_refs[field_id].get_name(), input_types_for_fields[field_id]['type'], self.field_refs[field_id], is_password=input_types_for_fields[ field_id]['is_password']) if field_id not in text_fields: text_fields[field_id] = tf if not text_fields: self.logger.debug( 'No text fields found for activity {0}'.format( activity_name)) else: self.smart_inputs[activity_name] = list( text_fields.values()) except Exception as e: self.logger.warning( 'There was a problem during the search for text fields: {0}'. format(e)) finally: if len(self.smart_inputs) > 0: self.logger.debug('{0} text fields identified'.format( len(self.smart_inputs))) return self.smart_inputs def get_smart_input_for_id(self, input_id: str): # No id was provided, return the default text. if not input_id: return self.default to_return = None item = None if 'all' in self.smart_inputs: for item in self.smart_inputs['all']: if item.name == input_id: if item.type_variation in self.type_variation: to_return = self.type_variation[item.type_variation] break if item.type_class in self.type_class: to_return = self.type_class[item.type_class] break if to_return and item: # This field requires a specific input. self.logger.info('Possible input for Editable({0}): "{1}"'.format( item, to_return)) elif 'username' in input_id.lower() or 'email' in input_id.lower( ) or 'user' in input_id.lower(): # Maybe this is a username field. to_return = self.type_variation[ 'TYPE_TEXT_VARIATION_EMAIL_ADDRESS'] self.logger.info( 'Using username input for Editable(id={0}): "{1}"'.format( input_id, to_return)) elif 'password' in input_id.lower() or 'pwd' in input_id.lower( ) or 'secret' in input_id.lower(): # Maybe this is a password field. to_return = self.type_variation['TYPE_TEXT_VARIATION_PASSWORD'] self.logger.info( 'Using password input for Editable(id={0}): "{1}"'.format( input_id, to_return)) else: # No hint for this field, using the default text. to_return = self.default self.logger.info( 'Using default input for Editable(id={0}): "{1}"'.format( input_id, to_return)) return to_return