来自i春秋作者:penguin_wwy

一、理论基础(我们先讲道理)

上回说到我们找到了dex中的加密字符串 提取加密字符串。 观众老爷们问:那么找到这些加密字符串有什么作用呢?该看不懂的还是看不懂啊。。。

那么今天我就来告诉大家,找到的这些加密字符串我们该怎么利用。 首先来观察一下加密字符串出现时的场景,一般情况下是这样

paramContext.getSharedPreferences(Fegli.a("SjUIVhhB:&Zi2}3mo@i"), 0);

对于动态调用,或者反射等等之类的行为来说,加密的字符串肯定是需要解密之后才能用的。也就是说加密字符串一般会作为解密函数的输入,而解密函数的输出则会成为目标函数如Class.forName之类的函数输入。 看完Java代码我们再来看看smali代码

[color=#000000]const-string v0, "SjUIVhhB:&Zi2}3mo@i"
 
invoke-static {v0}, Lcom/molniya/free/Fegli;->a(Ljava/lang/String;)Ljava/lang/String;
 
move-result-object v0
 
invoke-virtual {p1, v0, v1}, Landroid/content/Context;->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;[/color]

由于加密字符串是直接写到函数中的,没有用变量保存,所以在smali中必然是const-string指令,之后的下一条指令必然是调用解密函数,也就是说是invoke指令。换句话说,我们找到一条const-string指令,它的值又恰好是加密字符串,并且它的下一条指令是invoke类型的指令,那么调用的这个函数就极大的可能(99.99999999...%)是解密函数了。我们还可以进行函数检查,比如这个函数的输入是不是一个Ljava/lang/String类型,输出是不是Ljava/lang/String类型,如果都是,那我们可以断定,这个就是解密函数(此处应有掌声,啪、啪、啪)。

二、实践过程(弟兄们,抄家伙动手)

下面我们就可以在Androguard的基础上来实现了。 首先,我们先看看Androguard为我们提供了哪些东西。如果大家读过源码的话(没读过也没关系,反正我读过)应该可以发现这样一句

vmx = analysis.VMAnalysis( vm )

这个vm就是我们之前讲的DalvikVMFormat类,它保存了dex文件的全部结构。这个VMAnalysis,从名字就可以看出来和分析有关。在这个类的初始化当中有这样一段

for i in self.vm.get_methods():
    x = MethodAnalysis( self.vm, i, self )
    self.methods.append( x )
    self.hmethods[ i ] = x
    self.__nmethods[ i.get_name() ] = x

从vm中获得所有method,然后调用MethodAnalysis进行分析。在MethodAnalysis中我发现了这个

code = self.method.get_code()

还有这个

bc = code.get_bc()

以及这个

instructions = [i for i in bc.get_instructions()]

不知道instructions 是什么意思的童鞋可以查一下英文字典,这个在计算机中表示指令的意思。也就是说这个instructions列表,保存了函数中的所有指令 这里我们需要要简单了解一下Dalvik的指令集,详细内容可以看这里http://www.netmite.com/android/mydroid/dalvik/docs/dalvik-bytecode.html 。

具体的内容很难说清楚(反正我是很难说清楚,掌握的还不透彻),为了不误人子弟,我就简单说说我们用到的内容。正常情况下我们反编译出来的smali代码和指令集的字节码是对应的也就是instructions的每一个元素,代表一行samli代码(不是Java代码)。每一行smali代码由4或者6字节组成,第一个字节表示op值,也就是代表一个操作。比如const-string的op值为0x1a,invoke-static的op值为0x71。而其他字节根据op值决定的操作类型分别代表寄存器编号啊,寄存器数量啊等等等等。还是举例说明,比如我们看到的const-string v0, "Hello"这句代码会由4字节指令构成。第一个字节为0x1a,表示const-string操作符。第二个字节表示寄存器下标,0就是v0,1就指v1。三四字节会表示操作的字符串在字符串池中的id(注意!!!)。

再举个例子,比如invoke-static {v0}, Lcom/molniya/free/Fegli;->a(Ljava/lang/String;)Ljava/lang/String;这句。会有6个字节指令。第一个字节0x71表示invoke-static操作符。第二个字节的高四位,指调用这个函数需要的寄存器个数(注意,如果是静态函数,那么寄存器个数和参数个数相等。如果不是,那么要增加一个p0寄存器,保存this指针)。第三和第四字节保存被调用method在method_id,每个methon_id为一个MethodIdsItem结构,该结构三个元素

public short class_idx;
public short proto_idx;
public int name_idx;

第一个指向它所属的class,第二个是函数原型,第三个是函数名称。 第五和第六个字节每四位代表一个寄存器。等等,第二个字节的低四位呢。嗯,保存的是第五个寄存器。。。(思索脸(´・ω・`))其实看到这里我挺惊讶的,并不是因为它保存的是第五个寄存器的值,而是在以往我看的arm体系中,会用四个寄存器保存参数,不够的话再通过栈保存。这里我也不知道为什么会是奇数个(也有可能是我想多了),不够了怎么办。。。还是学的不够深入。哪位表哥了解,还请指教一下。扯远了。

简单的介绍一下指令集,我们继续。现在可以获得每个函数的指令,我们就可以遍历这些指令,op值为0x1a的就检查它操作的字符串是不是加密字符串,如果是就看它下一行指令,op值在不在0x6e到0x72之间(invoke-virtual、super、direct、static、interface的op值),如果在就获取可以它的method_id,然后检查参数类型返回类型,都符合那这个method就是解密函数了。 总结一下过程:

获取指令 ——> 遍历指令 ——> 如果是const-string ——> 检查字符串 ——> 符合则检查下一条指令 ——> 符合则获取method,再检查类型。

看起来步骤也不是很多,但必须对dex文件结构有清醒的认识,还需要一点点指令集的知识。

下面是我写的核心代码

class decryptMethonA:
    def __init__(self, encrypt, vm):
        self.encrypt = encrypt
        self.vm = vm
        self.methons = self.vm.get_methods()
        #self.register = 0
        self.methon_dict = {}
        self.methon_info = []
 
    def analyze(self):
        for methon in self.methons:
            code = methon.get_code()
            if code == None:
                continue
 
            bc = code.get_bc()
            instructions = [i for i in bc.get_instructions()] #获取指令
            flag = 0
            for i in instructions:
                if flag == 1:
                    self.add_methon(i)        #如果是检查下一条指令
                    flag = 0
                if self.searchFor(i): #op是否为0x1a
                    flag = 1
 
    def searchFor(self, ins):
        op_value = ins.get_op_value()
        if op_value == 0x1a:
            string_name = self.vm.get_cm_string(ins.get_ref_kind())
            return string_name in self.encrypt
        return False
 
    def add_methon(self, ins):
        op_value = ins.get_op_value()
        if (op_value >= 0x6e and op_value <= 0x72) or (op_value >= 0x74 and op_value <= 0x78):
            idx_meth = ins.get_ref_kind()
            meth_info = self.vm.get_cm_method(idx_meth)
            if meth_info[2][1] == 'Ljava/lang/String;':
                if meth_info not in self.methon_info:
                    self.methon_info.append(meth_info)
                    self.methon_dict[self.methon_info.index(meth_info)] = 1
                else:
                    self.methon_dict[self.methon_info.index(meth_info)] += 1
 
    def get_meth_dict(self):
        return self.methon_dict
 
    def get_meth_info(self):
        return self.methon_info

三、测试结果(激动人心的时刻)

图片有点小,观众老爷们将就一下,但还是可以看到,我们成功输出了这个函数。

四、总结性发言

说几点问题。

第一:上次说到我们判断随机字符串,也就是判断哪个字符串是加密字符串的算法还有误差,正确率不高。那么对于我们判断解密函数会不会有影响呢?其实我觉得没有。我们大可对每个加密字符串(其中包含了误判)进行搜索,然后统计我们找到的解密函数每个的次数,次数最多的一定是解密函数。找到解密函数后可以再回头看它的参数,一定是加密字符串,又可以将加密字符串中误判的过滤。

第二:找到解密函数之后,怎么办。最简单的可以写个apk,不干别的。就加载这个dex,然后通过反射,找到解密函数,将加密字符串传入,然后调用,就可以获得正确的字符串了。我已经通过代码实现了,大家有兴趣也可以试试,核心代码其实就三句

DexClassLoder classLoder = new DexClassLoder(目标dex,..., ..., ...);
Class clazz = classLoder.loadClass(目标类);
clazz.getMethod(目标函数,object [] {...}).invoke(null, 加密字符串);

就完成了。这样我们就可以实现完全自动化解密dex中的加密字符串。

本文由i春秋学院提供:http://bbs.ichunqiu.com/thread-11730-1-1.html?from=paper


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/43/