Example #1
0
    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
Example #2
0
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()
Example #3
0
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()
Example #4
0
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)
Example #5
0
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()