Android APK安全漏洞的些许问题

引言

很久没有写博客了,最近公司的一些老项目在接受安全检查的时候发现了很多的漏洞,说实话真的是被坑的挺难受的,在此记录一下。希望可以帮到碰到同样问题的猿友们。

1.Janus签名漏洞

为了提升安卓系统的安全性,Google发布了新的签名认证体系signature scheme V2。由于,signature scheme V2需要对App进行重新发布,而大量的已经存在的App APK无法使用V2校验机制,所以为了保证向前兼容性,V1的校验方式的还被保留,这就导致了“Janus”漏洞的出现。
分析显示,安卓5.0到8.0系统以及基于signature scheme V1签名机制的App均受“Janus”漏洞影响;基于signature scheme V2签名的App则不受影响。
知道问题产生的原因了那就很好解决了,打包apk的时候使用v2签名就好了。但实际上用过之后你才会发现,直接用v2签名在Android7.0一下版本不一定能用,如果碰到需要对签名后的信息做处理修改,7.0及以上的手机则安装不了apk。其实解决问题的方法很简单:v1和v2同时勾选,两个签名一起用。
Android APK安全漏洞的些许问题_第1张图片
因为google官方最后也说了:一个APK可以同时由v1和v2签名同时签署,所以它仍然可以向后兼容以前的Android版本。
最后引用一下网上的一个总结:

一定可行的方案: 只使用 v1 方案       

不一定可行的方案:同时使用 v1 和 v2 方案       

对 7.0 以下一定不行的方案:只使用 v2 方案

1, 如果要支持 Android 7.0 以下版本,那么尽量同时选择两种签名方式,但是一旦遇到签名问题,可以只使用 v1 签名方案   

2,如果需要对签名后的信息做处理修改,那就使用v1签名方案   

3,如果最后遇到各种不同的问题,可以不勾选v1和v2,直接打包签名
(对于这句话我不是太能理解,因为不勾选根本打包不了)

2. 剪切板敏感信息泄漏

对于这个问题说实话我并没有找的什么很好的方法来解决,网上这方面资料也比较少。先说一下为什么剪切板存在安全漏洞。

Android剪切板是可以暂存数据,剪切板在后台起作用,存放在内存中。如果把隐私数据,特别是密码,存放在剪切板中是不安全的,因为任何的应用程序都可以访问剪切板中的数据。
如果一个恶意应用,注册了系统剪切板的监听器事件,当剪切板数据发生变化的时候,就能获取到剪切板的数据。

然后说一下我的处理方式:
首先,我在用户登录、注册、修改密码等一些涉及敏感信息的界面把 EditText 的长点事件和复制黏贴功能给禁了。

public void disableCopyAndPaste(final EditText editText) {
        try {
            if (editText == null) {
                return;
            }

            editText.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    return true;
                }
            });
            editText.setLongClickable(false);
            editText.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    if (event.getAction() == MotionEvent.ACTION_DOWN) {
                        // setInsertionDisabled when user touches the view
                        setInsertionDisabled(editText);
                    }

                    return false;
                }
            });
            editText.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
                @Override
                public boolean onCreateActionMode(ActionMode mode, Menu menu) {
                    return false;
                }

                @Override
                public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
                    return false;
                }

                @Override
                public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
                    return false;
                }

                @Override
                public void onDestroyActionMode(ActionMode mode) {

                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void setInsertionDisabled(EditText editText) {
        try {
            Field editorField = TextView.class.getDeclaredField("mEditor");
            editorField.setAccessible(true);
            Object editorObject = editorField.get(editText);

            // if this view supports insertion handles
            Class editorClass = Class.forName("android.widget.Editor");
            Field mInsertionControllerEnabledField = editorClass.getDeclaredField("mInsertionControllerEnabled");
            mInsertionControllerEnabledField.setAccessible(true);
            mInsertionControllerEnabledField.set(editorObject, false);

            // if this view supports selection handles
            Field mSelectionControllerEnabledField = editorClass.getDeclaredField("mSelectionControllerEnabled");
            mSelectionControllerEnabledField.setAccessible(true);
            mSelectionControllerEnabledField.set(editorObject, false);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

这样用户就不能通过人为的方式对EditText中的数据进行操作了。其次就是在这些敏感界面监听系统的剪切板,当检测到系统剪切板中的数据发生变化就立刻把剪切板中的数据清空。(实在是想不到什么好的办法了)

 public void ClipboardListener() {
        // 获取系统剪贴板
       if (clipboard==null){
           clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
           // 添加剪贴板数据改变监听器
           clipboard.addPrimaryClipChangedListener(this);
       }
    }
    public void removeClipboard(){
        if (clipboard!=null) {
            clipboard.removePrimaryClipChangedListener(this);
        }
    }

    @Override
    public void onPrimaryClipChanged() {
        // 剪贴板中的数据被改变,此方法将被回调
        try {
            if (!TextUtils.isEmpty(clipboard.getPrimaryClip().getItemAt(0).getText())) {
                clipboard.setText("");
            }
        } catch (Exception e) {
        }
    }

这样做比较蠢但多少还是有点用的,既然我做不到禁用剪切板那我只能在你做剪切的时候把数据清空。但实际上这么做有点 “自欺欺人”。如果有什么好的处理方式希望各位告之。
在activity中调用:

//在onResume()中启动监听
 @Override
    protected void onResume() {
        super.onResume();
        disEditText = new DisEditText(this);
        disEditText.disableCopyAndPaste(etAlterPWD);
        disEditText.disableCopyAndPaste(etAlterNPWD);
        disEditText.disableCopyAndPaste(etAlterRENPWD);
        disEditText.ClipboardListener();
    }
    //在onPause()中移除监听,防止内存泄漏
     @Override
    protected void onPause() {
        super.onPause();
        if (disEditText!=null) {
            disEditText.removeClipboard();
        }
    }

3.App可以在Root手机中运行

刚看到这个漏洞问题时,我真的是想骂人。现在有几个程序是不能在root手机中运行的?至于为什么app在root的手机上运行会存在风险想必大家都明白,我就不说废话了。直接上解决方法:

//检查手机是否被root
public class SecurityUtil {
    private SecurityUtil() {
    }

    public static SecurityUtil getInstance() {
        return SecurityBuilder.instance;
    }

    private static class SecurityBuilder {
        private static SecurityUtil instance = new SecurityUtil();
    }

    public boolean isRoot() {
        if (CheckRootPathSU() || checkRootWhichSU()) {
            return true;
        } else {
            return false;
        }
    }

    private boolean CheckRootPathSU() {
        File f = null;
        final String kSuSearchPaths[] = {"/system/bin/", "/system/xbin/", "/system/sbin/", "/sbin/", "/vendor/bin/"};
        try {
            for (int i = 0; i < kSuSearchPaths.length; i++) {
                f = new File(kSuSearchPaths[i] + "su");
                if (f != null && f.exists()) {
                    return true;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }
    //有些手机并不包含which文件,所以单用此方法并不可靠应与checkRootPathSU()方法放在一起使用。
    private boolean checkRootWhichSU() {
        String[] strCmd = new String[]{"/system/xbin/which", "su"};
        ArrayList execResult = executeCommand(strCmd);
        if (execResult != null) {
            return true;
        } else {
            return false;
        }
    }
    private ArrayList executeCommand(String[] shellCmd){
        String line = null;
        ArrayList fullResponse = new ArrayList();
        Process localProcess = null;
        try {
            localProcess = Runtime.getRuntime().exec(shellCmd);
        } catch (Exception e) {
            return null;
        }
        BufferedWriter out = new BufferedWriter(new OutputStreamWriter(localProcess.getOutputStream()));
        BufferedReader in = new BufferedReader(new InputStreamReader(localProcess.getInputStream()));
        try {
            while ((line = in.readLine()) != null) {
                fullResponse.add(line);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return fullResponse;
    }
}

//在activity中进行调用,检查手机是否root。如果roo则跳出弹框t给出提示告诉用户存在风险并询问是否继续
if (SecurityUtil.getInstance().isRoot()) {
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setMessage("检查到您的手机已ROOT,会存在安全隐患及个人信息泄漏的风险,不建议您在此情况下*******。").setTitle("提示")
                    .setPositiveButton("继续", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.dismiss();
                        }
                    })
                    .setNegativeButton("退出", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.dismiss();
                            MainActivity.this.finish();
                            System.exit(0);
                        }
                    });
            builder.setCancelable(false);
            builder.show();

        }

上面的那些操作说到底也就只能起到一个提示和免责的作用。总不能说检测到用户的手机root了,就不让人家使用了吧。

4.Activity组件导出风险

这块没什么好说的,就是你的Apk 中的Activity组件开启了导出权限,存在组件导出风险。把权限关了便是。举个例子

//把exported这个属性设置为false即可。exported默认值为false,
//但当actvity中包含这个标签时exported默认值就变成了true

            
                
                

                
            
        

5.应用签名未校验风险

引用一下检测之后的评估 “签名证书是对App开发者身份的唯一标识,开发者可利用签名证书有效降低App的盗版率。未进行签名证书的App,可能被反编译后进行二次打包。重新打包签名的应用,可能导致App被仿冒盗版,影响其合法收入,甚至可能被添加钓鱼代码、病毒代码、恶意代码,导致用户敏感信息泄露或者恶意攻击。该App程序未对签名证书进行校验,被其他证书重新签名可正常启动。”
下面直接给出解决方法:
首先使用 keytool 获取签名的 sha-1 值,命令为 keytool -list -v -keystore xxx.jks 。然后输入keystore的密码。在这里插入图片描述
这是我的keystore文件的sha-1
在这里插入图片描述
下面是验证的工具类(网上找别人的,有人说它的apk加固后就用不了了。但是我没遇到这种问题)

public class SignCheck {
    private Context context;
    private String cer = null;
    private String realCer = null;

    public SignCheck(Context context) {
        this.context = context;
        this.cer = getCertificateSHA1Fingerprint();
    }

    public SignCheck(Context context, String realCer) {
        this.context = context;
        this.realCer = realCer;
        this.cer = getCertificateSHA1Fingerprint();
    }

    public String getRealCer() {
        return realCer;
    }

    /**
     * 设置正确的签名
     *
     * @param realCer
     */
    public void setRealCer(String realCer) {
        this.realCer = realCer;
    }

    /**
     * 获取应用的签名
     *
     * @return
     */
    private String getCertificateSHA1Fingerprint() {
        //获取包管理器
        PackageManager pm = context.getPackageManager();

        //获取当前要获取 SHA1 值的包名,也可以用其他的包名,但需要注意,
        //在用其他包名的前提是,此方法传递的参数 Context 应该是对应包的上下文。
        String packageName = context.getPackageName();

        //返回包括在包中的签名信息
        int flags = PackageManager.GET_SIGNATURES;

        PackageInfo packageInfo = null;

        try {
            //获得包的所有内容信息类
            packageInfo = pm.getPackageInfo(packageName, flags);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }

        //签名信息
        Signature[] signatures = packageInfo.signatures;
        byte[] cert = signatures[0].toByteArray();

        //将签名转换为字节数组流
        InputStream input = new ByteArrayInputStream(cert);

        //证书工厂类,这个类实现了出厂合格证算法的功能
        CertificateFactory cf = null;

        try {
            cf = CertificateFactory.getInstance("X509");
        } catch (Exception e) {
            e.printStackTrace();
        }

        //X509 证书,X.509 是一种非常通用的证书格式
        X509Certificate c = null;

        try {
            c = (X509Certificate) cf.generateCertificate(input);
        } catch (Exception e) {
            e.printStackTrace();
        }

        String hexString = null;

        try {
            //加密算法的类,这里的参数可以使 MD4,MD5 等加密算法
            MessageDigest md = MessageDigest.getInstance("SHA1");

            //获得公钥
            byte[] publicKey = md.digest(c.getEncoded());

            //字节到十六进制的格式转换
            hexString = byte2HexFormatted(publicKey);

        } catch (NoSuchAlgorithmException e1) {
            e1.printStackTrace();
        } catch (CertificateEncodingException e) {
            e.printStackTrace();
        }
		//在这里做了加密操作,你可以直接return
        String key = "shxh_hsby";
        return AES.encrypt(hexString, key);
    }

    //这里是将获取到得编码进行16 进制转换
    private String byte2HexFormatted(byte[] arr) {

        StringBuilder str = new StringBuilder(arr.length * 2);

        for (int i = 0; i  2)
                h = h.substring(l - 2, l);
            str.append(h.toUpperCase());
            if (i < (arr.length - 1))
                str.append(':');
        }
        return str.toString();
    }

    /**
     * 检测签名是否正确
     * @return true 签名正常 false 签名不正常
     */
    public boolean check() {
        if (this.realCer != null) {
            cer = cer.trim();
            realCer = realCer.trim();
            if (this.cer.equals(this.realCer)) {
                return true;
            }
        }
        return false;
    }

}

在activity中调用,判断签名是否一致如果不同则跳出提示弹框同时退出app

 //使用AES对rsa1进行加密
        String apkKey = "你的rsa1";
        //我在这里对rsa1进行了加密操作。
        String encrypt = AES.encrypt(apkKey, key);
        //apk签名认证
        SignCheck signCheck = new SignCheck(this, encrypt);
        if (!signCheck.check()) {
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setMessage("请前往官方渠道下载正版 App").setTitle("提示")
                    .setPositiveButton("确认", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            dialog.dismiss();
                            MainActivity.this.finish();
                            System.exit(0);
                        }
                    });
            builder.setCancelable(false);
            builder.show();
        }

6.代码未混淆风险

老项目也不知道怎么想的,当初只做了加固没有做混淆。直接上方案
首先要先把build.gradle中混淆开关打开,minifyEnabled设置为true,注意shrinkResources的值要和minifyEnabled保持一致。否则会报错
Android APK安全漏洞的些许问题_第2张图片
其次在proguard-rules.pro加入混淆规则,下面给出一个比较通用的

#指定压缩级别
-optimizationpasses 5

#不跳过非公共的库的类成员
-dontskipnonpubliclibraryclassmembers

#混淆时采用的算法
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*

#把混淆类中的方法名也混淆了
-useuniqueclassmembernames

#优化时允许访问并修改有修饰符的类和类的成员
-allowaccessmodification

#将文件来源重命名为“SourceFile”字符串
-renamesourcefileattribute SourceFile
#保留行号
-keepattributes SourceFile,LineNumberTable
#保持泛型
-keepattributes Signature

#保持所有实现 Serializable 接口的类成员
-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}
//----------------------这里是我项目用到的依赖,请自行去除
-keepattributes *Annotation*
-keepclassmembers class * {
    @org.greenrobot.eventbus.Subscribe ;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
//------------------------------------
# And if you use AsyncExecutor:
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
    (java.lang.Throwable);
}
//----------------------这里是我项目用到的jar包,请自行去除
-keep class com.hp.hpl.sparta.** {*;}
-keep class demo.** {*;}
-keep class net.sourceforge.pinyin4j.** {*;}
-keep class pinyidb.** {*;}

-ignorewarnings
-keep class org.kobjects.** { *; }
-keep class org.ksoap2.** { *; }
-keep class org.kxml2.** { *; }
-keep class org.xmlpull.** { *; }

-keep class org.jdom.** { *; }

-keep class com.alibaba.sdk.android.oss.** { *; }
-dontwarn okio.**
-dontwarn org.apache.commons.codec.binary.**
//-------------------------------------------------------------------------------------------
#Fragment不需要在AndroidManifest.xml中注册,需要额外保护下
-keep public class * extends android.support.v4.app.Fragment
-keep public class * extends android.app.Fragment

# 保持测试相关的代码
-dontnote junit.framework.**
-dontnote junit.runner.**
-dontwarn android.test.**
-dontwarn android.support.test.**
-dontwarn org.junit.**

7.截屏攻击风险

在不想被截屏的界面加上(在oncreate()中在setContentView()方法之上)

getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);

8.界面劫持风险

什么是Activity 劫持呢?

简单来说就是当用户在运行一个 App 的时候,这时候有另一个恶意的程序在监听用户使用情况,当发现了想要劫持的 App 正在运行的时候,恶意程序就会替换了当前 App 的页面,从而让用户将个人的信息输入到恶意程序中去,被不法利用。

用 Android 术语来讲就是,后台运行了一个 Service ,在这个 Service 中,定时循环的查询当前正在运行的进程,当查询到了想要劫持的进程正在运行在前台的时候,这时候用FLAG_ACTIVITY_NEW_TASK启动自己的钓鱼页面覆盖原本的页面,从而获取信息。

这一块我同样没有找到什么好的处理方式,网上的处理方式是写一个后台服务用来监听你的程序,当你的程序运行到后台之时,就发射一个广播告诉用户当前程序在后台运行。(个人感觉没啥用,如果你没注意到那个提示呢。有人说可以把提示用notification展示出来,好像也是方法)

更新一下解决的方法
之前说是写一个后台服务用来监听你的程序,当你的程序运行到后台之时,就发射一个广播告诉用户当前程序在后台运行。虽然说可行但是难免有些麻烦,现在有了更好的处理方式:就是用lifecycleJetpack中的lifecycle,不了解的可以先去了解一下。下面直接给出解决方法。

首先引用
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"

其次在你的Application中对整个程序进行监听具体代码如下

public class MyApp extends Application {
    @SuppressLint("StaticFieldLeak")
    public static Context context;
    @Override
    public void onCreate() {
        super.onCreate();
        context = getApplicationContext();//这里我定义了一个全局的context你可以不用这么写
        //这里用的是ProcessLifecycleOwner,不了解的可以百度一下。难度不大,看一下就懂了。
        ProcessLifecycleOwner.get().getLifecycle().addObserver(new ApplicationObservable());
    }
    private static class ApplicationObservable implements LifecycleObserver {
        @OnLifecycleEvent(Lifecycle.Event.ON_START)//对应的生命周期,下面的方法名随便写
        private void onAppForeground(){
        //做你想做的事情
            Toast.makeText(context, "在前台", Toast.LENGTH_SHORT).show();
        }
        @OnLifecycleEvent(Lifecycle.Event.ON_STOP)//对应的生命周期
        private void onAppBackground(){
        //做你想做的事情,这里只做了简单的提示
            Toast.makeText(context, "程序运行在后台", Toast.LENGTH_SHORT).show();
        //    Intent intent = new Intent(context, LoginActivity.class);
       //     intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        //    context.startActivity(intent);
        }
    }
}

最后记得在AndroidManifest.xml中注册

总结

目前就这么多吧,后续应该会继续更新。(上述问题的解决方法说实话并不怎么靠谱,如果各位有好的提议和解决办法请给我留言。在此先行谢过!)

你可能感兴趣的:(android)