def get_arguments_from_clinit(self, field): reg = '([\w\W]+?)sput-object (v\d+), %s' % re.escape(field) sput_obj_ptn = re.compile(reg) from smaliemu.emulator import Emulator emu = Emulator() array_data_ptn = re.compile(r':array_[\w\d]+\s*.array-data[\w\W\s]+.end array-data') class_name = field.split('->')[0] for sf in self.smali_files: if sf.class_name == class_name: for mtd in sf.methods: arr = [] if mtd.name == '<clinit>': matchs = sput_obj_ptn.search(mtd.body).groups() snippet = matchs[0] code_content = matchs[0] array_data_context = re.split(r'\n+', array_data_ptn.search(mtd.body).group()) # print(array_data_context) return_register_name = matchs[1] arr = re.split(r'\n+', snippet)[:-1] arr.append('return-object %s' % return_register_name) arr.extend(array_data_context) # print(arr) # raise Exception try: # TODO 默认异常停止,这种情况可以考虑,全部跑一遍。 # 因为有可能参数声明的时候,位置错位,还有可能是寄存器复用。 arr_data = emu.call(arr, thrown=True) if len(emu.vm.exceptions) > 0: break arguments = [] byte_arr = [] for item in arr_data: if item == '': item = 0 byte_arr.append(item) arguments.append('[B:' + str(byte_arr)) return arguments except Exception as e: print(e) pass break
class Plugin(object): """ 解密插件基类 """ name = 'Plugin' description = '' version = '' enabled = True index = 0 # 插件执行顺序;最小值为0,数值越大,执行越靠后。 # TODO 这个得放到库中 # const/16 v2, 0x1a CONST_NUMBER = r'const(?:\/\d+) [vp]\d+, (-?0x[a-f\d]+)\s+' # ESCAPE_STRING = '''"(.*?)(?<!\\\\)"''' ESCAPE_STRING = '"(.*?)"' # const-string v3, "encode string" CONST_STRING = r'const-string [vp]\d+, ' + ESCAPE_STRING + '.*' # move-result-object v0 MOVE_RESULT_OBJECT = r'move-result-object ([vp]\d+)' # new-array v1, v1, [B NEW_BYTE_ARRAY = r'new-array [vp]\d+, [vp]\d+, \[B\s+' # new-array v1, v1, [B NEW_INT_ARRAY = r'new-array [vp]\d+, [vp]\d+, \[I\s+' # new-array v1, v1, [B NEW_CHAR_ARRAY = r'new-array [vp]\d+, [vp]\d+, \[C\s+' # fill-array-data v1, :array_4e FILL_ARRAY_DATA = r'fill-array-data [vp]\d+, :array_[\w\d]+\s+' ARRAY_DATA_PATTERN = r':array_[\w\d]+\s*.array-data[\w\W\s]+.end array-data' # [{'className':'', 'methodName':'', 'arguments':'', 'id':''}, ..., ] json_list = [] # [(mtd, old_content, new_content), ..., ] target_contexts = {} # data_arraies = {} # smali methods witch have been update smali_mtd_updated_set = set() # 存放field的内容,各个插件通用。 fields = {} def __init__(self, driver, smalidir, mfilters=None): self.make_changes = False self.driver = driver self.smalidir = smalidir # method filters self.mfilters = mfilters # self.smali_files = smali_files self.emu = Emulator() self.emu2 = Emulator() # def get_return_variable_name(self, line): # mro_statement = re.search(self.MOVE_RESULT_OBJECT, line).group() # return mro_statement[mro_statement.rindex(' ') + 1:] @timeout(1) def pre_process(self, snippet): """ smaliemu 处理sget等获取类的变量时,是直接从变量池中取等。 所以,对于这些指令,可以预先初始化。 先执行Feild Value插件,对类的成员变量进行解密,再进行处理。 TODO 其他指令,其他参数 FIXME 注意,这种方法不一定能获取到参数,改用其他方式吧。 """ args = {} # sget-object v0, clz_name;->field_name:Ljava/util/List; # 能够作为参数的值,数字、字符串、数组等;而不是其他 for line in snippet: if 'sget' not in line: continue field_desc = line.split()[-1] try: field = self.smalidir.get_field(field_desc) if not field: continue except TypeError as ex: logger.warning(ex) logger(field_desc) continue value = field.get_value() if not value: continue args.update({field_desc: value}) return args @staticmethod def convert_args(typ8, value): """ 根据参数类型,把参数转换为适合Json保存的格式。 """ if value is None: return None if typ8 == 'I': if not isinstance(value, int): return None return 'I:' + str(value) if typ8 == 'B': if not isinstance(value, int): return None return 'B:' + str(value) if typ8 == 'S': if not isinstance(value, int): return None return 'S:' + str(value) if typ8 == 'C': # don't convert to char, avoid some unreadable chars. return 'C:' + str(value) if typ8 == 'Ljava/lang/String;': if not isinstance(value, str): return None # smali 会把非ascii字符串转换为unicode字符串 # java 可以直接处理unicode字符串 return "java.lang.String:" + value if typ8 == '[B': if not isinstance(value, list): return None byte_arr = [] for item in value: if item == '': item = 0 byte_arr.append(item) return '[B:' + str(byte_arr) if typ8 == '[C': if not isinstance(value, list): return None byte_arr = [] for item in value: if item == '': item = 0 byte_arr.append(item) return '[C:' + str(byte_arr) logger.warning('不支持该类型 %s %s', typ8, value) @timeout(3) def get_vm_variables(self, snippet, args, rnames): """ snippet : smali 代码 args :方法参数 rnames :寄存器 获取当前vm的变量 """ # 原本想法是,行数太多,执行过慢;而参数一般在前几行 # 可能执行5句得倒的结果,跟全部执行的不一样 # TODO 有一定的几率,得到奇怪的参数,导致解密结果异常 self.emu2.call(snippet[-5:], args=args, thrown=False) result = self.varify_argments(self.emu2.vm.variables, rnames) if result: return self.emu2.vm.variables self.emu2.call(snippet, args=args, thrown=False) result = self.varify_argments(self.emu2.vm.variables, rnames) if result: return self.emu2.vm.variables @staticmethod def varify_argments(variables, arguments): """ variables :vm存放的变量 arguments :smali方法的参数 验证smali方法的参数 """ for k in arguments: value = variables.get(k, None) if value is None: return False return True @staticmethod def get_json_item(cls_name, mtd_name, args): """ json item 为一个json格式的解密对象。 包含id、className、methodName、arguments。 模拟器/手机会通过解析这个对象进行解密。 """ item = { 'className': cls_name, 'methodName': mtd_name, 'arguments': args } item['id'] = hashlib.sha256( JSONEncoder().encode(item).encode('utf-8')).hexdigest() return item def append_json_item(self, json_item, mtd, old_content, rtn_name): """ 往json list添加json解密对象 json list 存放了所有的json格式解密对象。 """ mid = json_item['id'] if rtn_name: new_content = 'const-string ' + rtn_name + ', "{}"\n' else: new_content = ('const-string v0, "Dexsim"\n' 'const-string v1, "{}"\n' 'invoke-static {{v0, v1}}, Landroid/util/Log;->d' '(Ljava/lang/String;Ljava/lang/String;)I\n') if mid not in self.target_contexts: self.target_contexts[mid] = [(mtd, old_content, new_content)] else: self.target_contexts[mid].append((mtd, old_content, new_content)) if json_item not in self.json_list: self.json_list.append(json_item) @abstractmethod def run(self): """ 插件执行逻辑 插件必须实现该方法 """ pass def optimize(self): """ smali 通用优化代码 一般情况下,可以使用这个,插件也可以实现自己的优化方式。 """ if not self.json_list or not self.target_contexts: return jsons = JSONEncoder().encode(self.json_list) if DEBUG: print("\nJSON内容(解密类、方法、参数):") print(jsons) outputs = {} with tempfile.NamedTemporaryFile(mode='w+', delete=False) as tfile: tfile.write(jsons) outputs = self.driver.decode(tfile.name) os.unlink(tfile.name) if DEBUG: print("解密结果:") print(outputs) if not outputs: return if isinstance(outputs, str): return for key, value in outputs.items(): if key not in self.target_contexts: logger.warning('not found %s', key) continue if not value[0] or value[0] == 'null': continue if not value[0].isprintable(): print("解密结果不可读:", key, value) continue # json_item, mtd, old_content, rtn_name for item in self.target_contexts[key]: old_body = item[0].get_body() old_content = item[1] if DEBUG: print(item[2], value[0]) new_content = item[2].format(value[0]) item[0].set_body(old_body.replace(old_content, new_content)) item[0].set_modified(True) self.make_changes = True self.smali_files_update() self.clear() def clear(self): """ 每次解密完毕后,都需要清理。 """ self.json_list.clear() self.target_contexts.clear() def smali_files_update(self): ''' write changes to smali files ''' if self.make_changes: for sf in self.smalidir: sf.update()
class NEW_STRING(Plugin): name = "NEW_STRING" version = '0.0.3' enabled = True def __init__(self, driver, methods, smali_files): self.emu = Emulator() Plugin.__init__(self, driver, methods, smali_files) def run(self): print('run Plugin: %s' % self.name, end=' -> ') self.__process_new_str() self.__process_to_string() def __process_new_str(self): ''' 这里有2种情况: 1. 只有1个数值常量 2. 有1个以上的数值常量,会使用fill-array-data 这个都无所谓,直接执行代码片段即可 ''' for mtd in self.methods: if 'Ljava/lang/String;-><init>([B)V' not in mtd.body: continue # TODO 初始化 array-data 所有的数组 fill_array_datas = {} array_re = r'(array_[\w\d]+)\s*\.array-data[\w\s]+.end array-data$' ptn1 = re.compile(array_re) flag = False new_body = [] array_key = None new_string_re = r'invoke-direct {(v\d+), v\d+}, Ljava/lang/String;-><init>\([\[BCI]+\)V' ptn2 = re.compile(new_string_re) for line in re.split(r'\n+', mtd.body): new_line = None if 'Ljava/lang/String;-><init>' in line: result = ptn2.search(line) if not result: new_body.append(line) continue return_register_name = result.groups()[0] tmp = new_body.copy() tmp.append(line) try: tmp.append('return-object %s' % return_register_name) decoded_string = (self.emu.call(tmp)) if decoded_string: new_line = 'const-string %s, "%s"' % (return_register_name, decoded_string) except Exception as e: # TODO log2file pass if new_line: flag = True new_body.append(new_line) else: new_body.append(line) if flag: mtd.body = '\n'.join(new_body) mtd.modified = True self.make_changes = True self.smali_files_update() def __process_to_string(self): to_string_re = (r'new-instance v\d+, Ljava/lang/StringBuilder;[\w\W\s]+?{(v\d+)[.\sv\d]*}, Ljava/lang/StringBuilder;->toString\(\)Ljava/lang/String;') ptn2 = re.compile(to_string_re) for mtd in self.methods: if 'const-string' not in mtd.body: continue if 'Ljava/lang/StringBuilder;-><init>' not in mtd.body: continue if 'Ljava/lang/StringBuilder;->toString()Ljava/lang/String;' not in mtd.body: continue flag = False new_content = None result = ptn2.finditer(mtd.body) for item in result: return_register_name = item.groups()[0] old_content = item.group() arr = re.split(r'\n+', old_content) arr.append('return-object %s' % return_register_name) try: decoded_string = self.emu.call(arr) if len(self.emu.vm.exceptions) > 0: continue if decoded_string: new_content = 'const-string %s, "%s"' % (return_register_name, decoded_string) except Exception as e: # print(e) continue if new_content: flag = True mtd.body = mtd.body.replace(old_content, new_content) if flag: mtd.modified = True self.make_changes = True self.smali_files_update()
filename = os.path.join(os.path.dirname(__file__), 'test.smali') ret = emu2.run(filename, trace=True) print(ret) print(emu2.vm.variables) exit() snippet = [ 'const/16 v5, 0x29', 'new-array v0, v5, [B', 'fill-array-data v0, :array_66', 'sput-object v0, xbd:[B', 'const/16 v0, 0xde', 'sput v0, xba:I', 'new-instance v0, Ljava/lang/StringBuilder;', 'sget-object v1, xbd:[B', 'const/4 v2, 0x6', 'aget-byte v1, v1, v2', 'int-to-byte v1, v1', 'or-int/lit8 v2, v1, 0x50', 'int-to-byte v2, v2', 'sget-object v3, xbd:[B', 'const/16 v4, 0x13', 'aget-byte v3, v3, v4', 'int-to-byte v3, v3', 'return-object v0', ':array_66', ' .array-data 1', ' 0x79t', ' -0x52t', ' 0x16t', ' 0x47t', ' 0xet', ' 0x2t', ' 0x5t', ' 0xct', ' 0x7t', ' 0x8t', ' 0x4t', ' 0x5t', ' 0x16t', ' 0x8t', ' 0x4bt', ' -0x46t', ' 0xft', ' -0x7t', ' 0x7t', ' 0x19t', ' 0x1t', ' 0x9t', ' 0x4ct', ' -0x4dt', ' 0x2t', ' 0x10t', ' 0x12t', ' 0x32t', ' 0x21t', ' 0x13t', ' -0x1t', ' -0x36t', ' -0xct', ' 0xft', ' 0x12t', ' 0x9t', ' -0xat', ' 0x16t', ' 0x8t', ' 0x31t', ' 0x21t', ' .end array-data' ] ret = emu2.call(snippet, trace=True) print("'%s'" % ret)
class Plugin(object): """ 解密插件基类 """ name = 'Plugin' description = '' version = '' enabled = True # const/16 v2, 0x1a CONST_NUMBER = r'const(?:\/\d+) [vp]\d+, (-?0x[a-f\d]+)\s+' # ESCAPE_STRING = '''"(.*?)(?<!\\\\)"''' ESCAPE_STRING = '"(.*?)"' # const-string v3, "encode string" CONST_STRING = r'const-string [vp]\d+, ' + ESCAPE_STRING + '.*' # move-result-object v0 MOVE_RESULT_OBJECT = r'move-result-object ([vp]\d+)' # new-array v1, v1, [B NEW_BYTE_ARRAY = r'new-array [vp]\d+, [vp]\d+, \[B\s+' # new-array v1, v1, [B NEW_INT_ARRAY = r'new-array [vp]\d+, [vp]\d+, \[I\s+' # new-array v1, v1, [B NEW_CHAR_ARRAY = r'new-array [vp]\d+, [vp]\d+, \[C\s+' # fill-array-data v1, :array_4e FILL_ARRAY_DATA = r'fill-array-data [vp]\d+, :array_[\w\d]+\s+' ARRAY_DATA_PATTERN = r':array_[\w\d]+\s*.array-data[\w\W\s]+.end array-data' # [{'className':'', 'methodName':'', 'arguments':'', 'id':''}, ..., ] json_list = [] # [(mtd, old_content, new_content), ..., ] target_contexts = {} # data_arraies = {} # smali methods witch have been update smali_mtd_updated_set = set() def __init__(self, driver, smalidir): self.make_changes = False self.driver = driver self.smalidir = smalidir # self.smali_files = smali_files self.emu = Emulator() self.emu2 = Emulator() # def get_return_variable_name(self, line): # mro_statement = re.search(self.MOVE_RESULT_OBJECT, line).group() # return mro_statement[mro_statement.rindex(' ') + 1:] @timeout(1) def pre_process(self, snippet): """ 预处理 sget指令 """ # emu2 = Emulator() args = {} clz_sigs = set() field_desc_prog = re.compile(r'^.*, (.*?->.*)$') for line in snippet: if 'sget' not in line: continue field_desc = field_desc_prog.match(line).groups()[0] try: field = self.smalidir.get_field(field_desc) except TypeError as ex: logger.warning(ex) logger(field_desc) continue if field: value = field.get_value() if value: args.update({field_desc: value}) continue clz_sigs.add(field_desc.split('->')[0]) for clz_sig in clz_sigs: mtd = self.smalidir.get_method(clz_sig, '<clinit>()V') if mtd: body = mtd.get_body() self.emu2.call(re.split(r'\n\s*', body), thrown=False) self.emu2.call(re.split(r'\n\s*', body), thrown=False) args.update(self.emu2.vm.variables) for (key, value) in self.emu2.vm.variables.items(): if clz_sig in key: field = self.smalidir.get_field(key) field.set_value(value) # print(__name__, 'pre_process, emu2', sys.getsizeof(self.emu2)) return args @staticmethod def convert_args(typ8, value): """ 根据参数类型,把参数转换为适合Json保存的格式。 """ if value is None: return None if typ8 == 'I': if not isinstance(value, int): return None return 'I:' + str(value) if typ8 == 'B': if not isinstance(value, int): return None return 'B:' + str(value) if typ8 == 'S': if not isinstance(value, int): return None return 'S:' + str(value) if typ8 == 'C': # don't convert to char, avoid some unreadable chars. return 'C:' + str(value) if typ8 == 'Ljava/lang/String;': if not isinstance(value, str): return None import codecs item = codecs.getdecoder('unicode_escape')(value)[0] args = [] for i in item.encode("UTF-8"): args.append(i) return "java.lang.String:" + str(args) if typ8 == '[B': if not isinstance(value, list): return None byte_arr = [] for item in value: if item == '': item = 0 byte_arr.append(item) return '[B:' + str(byte_arr) if typ8 == '[C': if not isinstance(value, list): return None byte_arr = [] for item in value: if item == '': item = 0 byte_arr.append(item) return '[C:' + str(byte_arr) logger.warning('不支持该类型 %s %s', typ8, value) @timeout(3) def get_vm_variables(self, snippet, args, rnames): """ snippet : smali 代码 args :方法藏书 rnames :寄存器 获取当前vm的变量 """ self.emu2.call(snippet[-5:], args=args, thrown=False) # 注意: 寄存器的值,如果是跨方法的话,可能存在问题 —— 导致解密乱码 # A方法的寄存器v1,与B方法的寄存器v1,保存的内容不一定一样 # TODO 下一个方法,则进行清理 # 方法成员变量,可以考虑初始化到smalifile中 # 其他临时变量,则用smali执行 result = self.varify_argments(self.emu2.vm.variables, rnames) if result: return self.emu2.vm.variables self.emu2.call(snippet, args=args, thrown=False) result = self.varify_argments(self.emu2.vm.variables, rnames) if result: return self.emu2.vm.variables @staticmethod def varify_argments(variables, arguments): """ variables :vm存放的变量 arguments :smali方法的参数 验证smali方法的参数 """ for k in arguments: value = variables.get(k, None) if value is None: return False return True @staticmethod def get_json_item(cls_name, mtd_name, args): """ json item 为一个json格式的解密对象。 包含id、className、methodName、arguments。 模拟器/手机会通过解析这个对象进行解密。 """ item = { 'className': cls_name, 'methodName': mtd_name, 'arguments': args } item['id'] = hashlib.sha256( JSONEncoder().encode(item).encode('utf-8')).hexdigest() return item def append_json_item(self, json_item, mtd, old_content, rtn_name): """ 往json list添加json解密对象 json list 存放了所有的json格式解密对象。 """ mid = json_item['id'] if rtn_name: new_content = 'const-string %s, ' % rtn_name + '%s' else: # TODO XX 也许有更好的方式 # const-string v0, "Dexsim" # const-string v1, "Decode String" # invoke-static {v0, v1}, Landroid/util/Log;->d( # Ljava/lang/String;Ljava/lang/String;)I new_content = ('const-string v0, "Dexsim"\n' 'const-string v1, %s\n' 'invoke-static {v0, v1}, Landroid/util/Log;->d' '(Ljava/lang/String;Ljava/lang/String;)I\n') if mid not in self.target_contexts: self.target_contexts[mid] = [(mtd, old_content, new_content)] else: self.target_contexts[mid].append((mtd, old_content, new_content)) if json_item not in self.json_list: self.json_list.append(json_item) @abstractmethod def run(self): """ 插件执行逻辑 插件必须实现该方法 """ pass def optimize(self): """ smali 通用优化代码 一般情况下,可以使用这个,插件也可以实现自己的优化方式。 """ if not self.json_list or not self.target_contexts: return jsons = JSONEncoder().encode(self.json_list) outputs = {} with tempfile.NamedTemporaryFile(mode='w+', delete=False) as tfile: tfile.write(jsons) outputs = self.driver.decode(tfile.name) os.unlink(tfile.name) if not outputs: return if isinstance(outputs, str): return for key, value in outputs.items(): if 'success' not in value: continue if key not in self.target_contexts: logger.warning('not found %s', key) continue if value[1] == 'null': continue # json_item, mtd, old_content, rtn_name for item in self.target_contexts[key]: old_body = item[0].get_body() old_content = item[1] new_content = item[2] % value[1] # It's not a string. if outputs[key][1] == 'null': continue item[0].set_body(old_body.replace(old_content, new_content)) item[0].set_modified(True) self.make_changes = True self.smali_files_update() def clear(self): """ 每次解密完毕后,都需要清理。 """ self.json_list.clear() self.target_contexts.clear() def smali_files_update(self): ''' write changes to smali files ''' if self.make_changes: for sf in self.smalidir: sf.update()