一、理论基础(我们先讲道理)
上回说到我们找到了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
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/43/