测试目标apk是否能被反编译
目前绝大多数的Android软件都是由java编写,由于java的本质属于解释型语言(即java编译之后需要依赖JVM虚拟机执行,类似于C#编译之后会生成IL),故java代码编译之后,从理论上来讲是可以还原的。若源代码未混淆,源代码反编译之后就可以看到源代码,对Android应用程序来讲,是极大的隐患。
将apk反编译通常是安全测试的第一步,目前有很多工具可以实现apk的反编译。其中大多数依赖于apktool,所以首先以Apktool为例,介绍一下Apktool的配置和使用
windows平台下有许多apk反编译工具,例举几个常用的如下:
apktool
JEB
Android killer
ApkToolBox
dex2jar
....
apktool官方下载地址:https://ibotpeaches.github.io/Apktool/install/
apktool提供了三个平台的下载以及安装教程
apktool使用方法:
或者在地址栏直接输入cmd或者powershell,然后回车运行,即可在当前目录打开powershell/cmd
如下所示:
成功执行后,会在当前目录生成一个和apk同名的文件夹,里面即包含了apk解包后的数据
一个简单的apk,解包后的文件夹结构如下:
其中
反编译是否成功,可以通过查看反编译之后的AndroidManifest.xml 来判断。
AndroidManifest.xml文件中存放了APK的大量配置信息,比如软件的名称、包名、图标、组件配置等。
如果反编译之后的AndroidManifest.xml 以明文显示则说明反编译成功,比如在本例子中该文件如下:
我们可以从Application标签的Activity标签的Android:name字段中知道,该apk在开发的时候,程序入口点在com.example.myapplication.MainActivity。
一般来说,AndroidManifest文件被反编译成功即说明apk未做反编译保护,我们也可以接着对res目录下的资源文件以及smali目录下的smali文件进行查看,如果明文显示则说明反编译成功。如下所示
由于Apktool依赖于命令行,有些参数和操作不太方便,所以后面有了许多带UI界面的反编译工具,但其实基本上都是依赖于Apktool。以ApktoolBox为例。ApktoolBox界面如图所示,是一个工具集:
通过查看ApktoolBox的路径可以得知,反编译所使用的工具还是Apktool.jar
可以看到,这里的Apktool.jar是17年的,比较老,我们直接替换为刚从官网下载的最新版。(其他工具的替换方式同理,直接下载新版本替换即可)
现在直接用ApktoolBox打开我们想要反编译的apk,然后选择<反编译apk>即可
这里的<是否需要忽略res资源文件>需要选<取消>,如果选择了<确定>则会在反编译的时候不对AndroidManifest以及资源文件进行反编译,没有AndroidManifest,我们将不能直观的了解到程序的入口Activity在哪里。
反编译完成之后,则会在指定目录下释放生成对应的文件夹,和Apktool d 包名 的方式的结果一样。
可以针对AndroidManifest做简单的处理,起到入门级的反编译作用。
首先介绍一下AndroidManifest的相关知识。Android studio在编译APK的时候会把AndroidManifest.xml处理为一个二进制文件。我们必须对APK反编译之后,才能看到原本的AndroidManifest.xml内容,否则只能看到一个全是乱码的文件,这种文件格式称为"AXML"
根据之前大佬们发现的AXML解析漏洞,我们可以直接尝试修改AXML文件,使得反编译工具无法正常识别该文件,从而起到防反编译的作用。
我们通过ApktoolBox打开目标apk,然后选择<反编译apk>,在反编译的时候会弹出选项<是否需要忽略res资源文件> 选择<确定>,忽略了资源文件之后,将只会反编译smali文件,不会反编译AndroidManifest以及res目录下的各种资源文件。
然后打开AndroidManifest,此时的AndroidManifest是一个AXML文件格式的二进制数据流。
根据AndroidManifest的解析漏洞,我们可以使用一个简单的方法起到反编译的作用,即更改编译之后的AndroidManifest文件头,然后直接重新打包,签名。这样可以在一定程度上防止程序被直接反编译。
所以我们把的AndroidManifest的文件头 03 00 08 00 更改为00 00 08 00
然后将后面的数据跟改一下再保存。
再重新打开该文件,可以看到010edit已经没有正常识别该文件为AndroidManifest
此时我们直接使用ApktoolBox打开我们修改过后的文件夹,选择<回编译apk>。
回编译成功之后将会在当前文件夹下生成指定的apk,我们将<demo1_Mod.apk> 安装到手机上是可以正常运行的。
但是此时的<demo1_Mod.apk>已经不能正常反编译了。
我们通过Apktool d 包名的方式对<demo1_Mod.apk>会得到如下结果,反编译之后的文件夹中只有一个空的AndroidManifest文件。
使用ApktoolBox在反编译资源文件的情况下也是同样的结果
这是因为我们更改了AndroidManifest文件,导致Apktool在反编译的时候不认识这个文件了,从而解析失败。
要想绕过这样的"保护"也很简单,使用Apktool的 -no -res 参数即可,这样Apktool在反编译的时候就不会尝试解析资源文件和AndroidManifest文件。
就可以正常反编译出smali文件
第二个解决方案也是一样,就是在是使用ApktoolBox反编译的时候,<是否需要忽略res资源文件>选择确定,这样也会跳过资源文件去反编译class文件。
但是如果想要看到原本的AndroidManifest文件,我们就需要尝试修复一下被更改的文件头。
我们可以找几个正常的apk,查看AndroidManifest的二进制文件,通过对比的方法将被更改的AndroidManifest文件改成正常的格式,然后重复上面的步骤,打包之后再反编译,应该就可以正常反编译出AndroidManifest文件了。
测试目标apk是否能对签名有验证。
Android的签名机制是一种比较邮件的身份标识,若开发人员没有对Android的签名,那么攻击者可以对apk反编译之后,插入恶意代码再二次打包运行。
关于如何在编写Android程序的时候自定义签名,网上有很多教程,这里就不赘述。
还是使用ApktoolBox进行测试,我们将有签名的APK反编译,然后直接使用ApktoolBox进行重签名,如果重前面之后还可以正常运行,则说明程序没有做签名验证。
首先我们可以查看一下APK的签名信息,随便找一个APK的安装包,我们可以直接解压该APK或者反编译。如果是直接解压,签名的文件存放在META-INF文件夹下
如果是反编译,签名的文件在original的META-INF文件夹下
在本例中签名文件为CERT.RSA
我们可以直接通过keytool对该签名文件进行解析
解析语法为:
keytool -printcert -file CERT.RSA
我们可以在这里看到签名的所有者 Henry
签名的序列号,有效日期,证书的指纹等信息。
我们使用apktoolbox直接对反编译出来的文件夹进行重打包
回编译完成之后,会自动重新签名并在当前目录下生成一个原始文件名_Mod.apk的文件
我们再查看Naver Mail_Mod.apk的签名信息
此时的签名文件已经变成了我们自定义的签名文件NETEASE.RSA
具体信息如下:
然后我们安装自己签名的Naver Mail_Mod.apk 依旧可以正常运行,说明该APK并没有进行签名验证。
我们可以先确定好我们所使用的签名文件,然后通过keytool计算出该文件的MD5值,然后在代码中写一个验证,获取当前apk前面的MD5和预定义的MD5进行对比,如果不相同,则说明签名文件不是我们所指定的,说明文件被重新签名,这个时候可以终止运行。
为了测试方便,新建一个Android studio项目,然后通过代码获取当前的包名和签名文件的MD5,获取到的信息如下。我们可以看到,如果使用Android studio 默认的签名MD5为:90f2ba57841b2e30d56581f8d5c47c33
于是我们将这个MD5硬编码到代码中进行比较。
然后我们将生成的apk拿去反编译,然后再重新打包和签名。
然后我们安装app_Mod.apk,会发现运行直接闪退,因为检测到签名信息不一致。
这里更好的做法是将预定义的MD5值写到JNI层,这个后面再详细说说。
获取并判断签名的代码如下
package com.example.imageviewer.tool.myapplication; import androidx.appcompat.app.AppCompatActivity; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; import android.os.Bundle; import android.util.Log; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); try { PackageInfo packageInfo = getPackageManager().getPackageInfo(BuildConfig.APPLICATION_ID, PackageManager.GET_SIGNATURES); String signValidString = getSignValidString(packageInfo.signatures[0].toByteArray()); Log.e("get PackageName", BuildConfig.APPLICATION_ID ); Log.e("get PackageSign", signValidString); if(!signValidString.equals("90f2ba57841b2e30d56581f8d5c47c33")) finish(); } catch (Exception e) { Log.e("get PackageName", "error:" + e); } } private String getSignValidString(byte[] paramArrayOfByte) throws NoSuchAlgorithmException { MessageDigest localMessageDigest = MessageDigest.getInstance("MD5"); localMessageDigest.update(paramArrayOfByte); return toHexString(localMessageDigest.digest()); } public String toHexString(byte[] paramArrayOfByte) { if (paramArrayOfByte == null) { return null; } StringBuilder localStringBuilder = new StringBuilder(2 * paramArrayOfByte.length); for (int i = 0; ; i++) { if (i >= paramArrayOfByte.length) { return localStringBuilder.toString(); } String str = Integer.toString(0xFF & paramArrayOfByte[i], 16); if (str.length() == 1) { str = "0" + str; } localStringBuilder.append(str); } } }
先占个坑
0x02 应用权限测试
0x03 系统备份测试
0x04 调试检测
0x05 四大组件暴露测试
0x06 本地拒绝服务漏洞
0x07 WebView密码明文存储
0x08 日志泄露
0x09 SharePreference数据安全测试
0x0A 截屏测试
......