分类 默认分类 下的文章

大家都晓得四大组件是不能混淆的 因为混淆工具只会搞dex文件的混淆 xml就不行了 强行混淆会导致activity文件名改变了 注册清单里面的没变从而导致无法运行APP

下面介绍一个能混淆四大组件 自定义view的办法 和其他的组件
效果图如下
效果图
可以看到四大组件名字都改变了 谁也认不出咋是啥玩意 大大提高解读难度

这个gradle插件是饿了么开源的组件 开源项目我也copy了一份 点我直达
copy的版本是本文发布的时间 2019年01月20日14:51:38
如果有更新请自己跟踪copy

使用很简单 但是官方并没有太多的使用文档和注意事项说

我这里根据使用经验来补充介绍一下

  1. 插件的最新版本是2.0.0 也就是classpath 'me.ele:mess-plugin:2.0.0'这样 可以自己改成classpath 'me.ele:mess-plugin:+'来获取最新版本 获取完毕之后在改成写死的最新版本
  2. 2.0.0版本插件只支持gradle 3.0.x版本 如果你使用的gradle版本比较高 请自行降低 否则会报错找不到清单文件
  3. 混淆规则请添加指令-dontshrink 否则会导致插件失效
  4. 混淆如果使用之前文章的自定义字典 请注释classobfuscationdictionary这个类名字典 否则会导致编译失败 因为xml不识别特殊符号
  5. 使用时请吧混淆规则的排除四大组件 自定义view等规则注释 否则就没有使用这个插件的意义了
  6. 在测试混淆的时候 请不要使用 run的方法直接运行到手机 有可能导致插件失效 我是build debug apk 文件出来发送到手机来测试的

下面附上我的混淆配置文件

#必须要开 否则ele的activity混淆会失效
-dontshrink
-optimizationpasses 5
-dontusemixedcaseclassnames
#-overloadaggressively
#-useuniqueclassmembernames
-dontskipnonpubliclibraryclasses
-dontskipnonpubliclibraryclassmembers
-allowaccessmodification
-dontoptimize
-dontpreverify
-ignorewarnings
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
#-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes Exceptions
-renamesourcefileattribute SourceFile
-keepattributes SourceFile,LineNumberTable
#自定义输出到指定包名 捉迷藏
-repackageclasses 'android.support.v4'
-verbose

#自定义字典 类名的混淆字典注释 为什么上面说了
-obfuscationdictionary dict.txt
#-classobfuscationdictionary dict.txt
-packageobfuscationdictionary dict.txt

#关闭警告
-dontwarn android.support.**
-dontwarn androidx.**
#webview 使用了要加上
-keepclassmembers class fqcn.of.javascript.interface.for.webview {
   public *;
}

-keepclassmembers,allowoptimization enum * {
      public static **[] values();
      public static ** valueOf(java.lang.String);
}
-keepclassmembers class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator CREATOR;
}

-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    !static !transient <fields>;
    !private <fields>;
    !private <methods>;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}
#移除日志
-assumenosideeffects class android.util.Log {
    public static boolean isLoggable(java.lang.String, int);
    public static int v(...);
    public static int i(...);
    public static int w(...);
    public static int d(...);
    public static int e(...);
}
-assumenosideeffects class java.io.PrintStream {
    public *** println(...);
    public *** print(...);
}
#-----------------------------不动-----------------------------
#okhttp的混淆
-dontwarn javax.annotation.**
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
-dontwarn org.codehaus.mojo.animal_sniffer.*
-dontwarn okhttp3.internal.platform.ConscryptPlatform

#腾讯mta统计
-keep class com.tencent.stat.*{*;}
-keep class com.tencent.mid.*{*;}
#广点通广告
-keep class com.qq.e.** {
    public protected *;
}


#ndk类排除
-keep class 包名.Utils{
*;
}
#webview js 交互
-keep class 包名.JavaScriptLocal{
*;
}

配置好了就可以build apk出来测试了 可以使用jadx-gui来反编译项目查看效果 jadx-gui工具自己网上找

附上使用方法

项目根目的gradle依赖导入

dependencies {
        //noinspection GradleDependency
        classpath 'com.android.tools.build:gradle:3.0.0'
        classpath 'me.ele:mess-plugin:2.0.0'
    }

app模块的使用插件

apply plugin: 'com.android.application'
apply plugin: 'me.ele.mess'

这样就行

Android应用安全防护实践一辣敌方眼睛之代码混淆成空白字符和乱码(四)
37/100
发布文章
weixin_44515491

代码混淆应该很多人听过 但是很少使用 因为他会修改编译后的最终代码 可能会导致软件闪退啊 什么乱七八糟的 对此敬而远之 不想去碰他

下面来简单说下我的使用经验

混淆 不能混淆四大组件名字和路径、自定义view的名字和部分方法名字(什么动画要用到)、ndk的类 不能混淆名字 方法名字、以及之前说过的ndk调java的 那个java方法名字、还有webview的一些东西也和被反射的类不能混淆
以上的网上都有说明和每个混淆参数的简洁 我这里就不多讲了 自己先把基础了解下
例如这个博客的文章或者其他的文章 去了解下各个指令的用途

其他的就无所谓了

一般的配置按照你学习的办法搞就行了 然后我这里只重点说明一下

  1. 不要使用网上的那些什么狗劳子 第三方库通用混淆配置大全 没篮子用ojbk
  2. 使用的开源库 如果没有说明混淆要配置什么 就不要去配置他的排除
  3. 如果因为第二条出现运行时错误 先去仔细找一下官方有没有混淆说明 是不是之前没注意到 然后再通过-keep等相关的之类去排除 写代码这些都行怎么搞都懂 这里懒得讲
  4. 一定要了解项目 和自己使用的第三方库(低优先级) 其中代码可能用到反射等操作 这时候一定要排除被反射的类名 和方法 也尽量避免使用反射 第三方库用到反射一般会说明混淆排除的 除非那个开源十分不负责任
  5. 项目使用了webview的要注意混淆配置 以及使用了h5 js交互的 要排除

Json实体类混淆参照我之前的文章Json混淆办法

记得我说的重点 然后开始你的混淆配置 编译混淆包后 测试一遍检查是否存在问题 没有其他问题就可以开始了解一下下面几个指令

    #混淆时重打包 把你包名里面的代码抽取到你指定的包名底下 没有会创建
    #比如我这里就比较皮了 伪装到v4包里面 玩几秒钟的捉迷藏
    -repackageclasses 'android.support.v4'

    #自定义混淆字典 配和名字一样 成员的名字 类名 包名 各种都可以设置字典
    -obfuscationdictionary dict.txt
    -classobfuscationdictionary dict.txt
    -packageobfuscationdictionary dict.txt

这个字典比较重要了 我们默认的规则就是一些字母 混淆后也不算很难阅读

但是这个字典就比较骚了 空白字符+特殊字符 看起来完全分不清啥是啥看下图 手段极其残忍
效果图
这里是字典下载地址 我找了很久才找到这个开源的字典生 我copy了一份点我为飞机直达
简单混淆就到这里了
下面再补充一发 字符串混淆 防止别人通过文本搜索定位到某个功能代码段
下面是混淆后的字符串(来呀 你搜啊 哇咔咔
字符串全部变成byte了 完全卡不出是个啥
在这里插入图片描述
混淆工具github地址 这个也不是我的 我只是copy了一份点我飞机直达
按照github上面的说明使用就行 这里不说了
Markdown 已选中 1746 字数 46 行数 当前行 46, 当前列 24HTML 1108 字数 30 段落

这个炒鸡简单

demo撸上

    //获取你重新自身的安装包位置 一般在/data/app/包名/xxx.apk
    public static String getApkPath(Context context) {
        try {
            PackageInfo packageInfo = context.getPackageManager().getPackageInfo(BuildConfig.APPLICATION_ID, PackageManager.GET_META_DATA);
            ApplicationInfo applicationInfo = packageInfo.applicationInfo;
            return applicationInfo.publicSourceDir; // 获取当前apk包的绝对路径
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return "";
    }

    //获取hash值 整个apk的 注意 这里代码不太严谨 demo随便敲的 跑通就行了
    public static String getHash(Context context) {
        MessageDigest msgDigest;
        String apkPath = getApkPath(context);

        FileInputStream fis = null;
        try {
            msgDigest = MessageDigest.getInstance("SHA-1");
            byte[] bytes = new byte[1024];
            int byteCount;
            fis = new FileInputStream(new File(apkPath));
            while ((byteCount = fis.read(bytes)) > 0) {
                msgDigest.update(bytes, 0, byteCount);
            }
            BigInteger bi = new BigInteger(1, msgDigest.digest());
            return bi.toString(16).toUpperCase();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return "";
    }

代码就这些了 每次发布软件 签名后 获取apk的hash保存到服务器 然后
app通过getHash方法获取hash和服务器保存的比对 如果不正确就自动退出什么的 爱咋地咋地

这个校验可以放到ndk里面写 免得被反编译删除校验就没用了
注意 获取hash之后不要动apk文件了 不要做任何修改(一键打渠道包工具什么的) 否则会导致apk文件hash改变

如果需要渠道包 可以后台存多个hash 对应不同的渠道包

这个东西就比较简单了 先上个小demo

public static HttpParams sign(Map<String, String> paramValues, List<String> ignoreParamNames) {
        try {
            paramValues.put("timestamp", String.valueOf(System.currentTimeMillis()));
            StringBuilder sb = new StringBuilder();
            List<String> paramNames = new ArrayList<>(paramValues.size());
            paramNames.addAll(paramValues.keySet());
            if (ignoreParamNames != null && ignoreParamNames.size() > 0) {
                paramNames.removeAll(ignoreParamNames);
            }
            Collections.sort(paramNames);
            for (String paramName : paramNames) {
               sb.append(paramName).append(paramValues.get(paramName));
            }
            //重点 这个是一个ndk开放的一个签名+加盐工具类
            String sign = Utils.getSign(sb.toString());
            HttpParams httpParams = new HttpParams();
            for (String key : paramValues.keySet()) {
                httpParams.put(key, paramValues.get(key));
            }
            httpParams.put("sign", sign);
            return httpParams;
        } catch (Exception e) {
            throw new RuntimeException("加密签名计算错误", e);
        }

    }

如上就完成了参数的签名 并且为参数里面增加了一个时间戳字段timestamp和一个签名值字段sign 其中sign不参与签名 参数要做排序

然后是ndk那边

#include <jni.h>
#include <string>
#include <string.h>
#include <malloc.h>
#include <iostream>
#include <sstream>
#include <algorithm>
#include <iterator>
#include <cctype>

jstring str2jstring(JNIEnv *env, const char *pat) {
    //定义java String类 strClass
    jclass strClass = (env)->FindClass("Ljava/lang/String;");
    //获取String(byte[],String)的构造器,用于将本地byte[]数组转换为一个新String
    jmethodID ctorID = (env)->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V");
    //建立byte数组
    jbyteArray bytes = (env)->NewByteArray(strlen(pat));
    //将char* 转换为byte数组
    (env)->SetByteArrayRegion(bytes, 0, strlen(pat), (jbyte *) pat);
    // 设置String, 保存语言类型,用于byte数组转换至String时的参数
    jstring encoding = (env)->NewStringUTF("UTF-8");
    //将byte数组转换为java String,并输出
    return (jstring) (env)->NewObject(strClass, ctorID, bytes, encoding);
}


std::string jstring2str(JNIEnv *env, jstring jstr) {
    char *rtn = NULL;
    jclass clsstring = env->FindClass("java/lang/String");
    jstring strencode = env->NewStringUTF("UTF-8");
    jmethodID mid = env->GetMethodID(clsstring, "getBytes", "(Ljava/lang/String;)[B");
    jbyteArray barr = (jbyteArray) env->CallObjectMethod(jstr, mid, strencode);
    jsize alen = env->GetArrayLength(barr);
    jbyte *ba = env->GetByteArrayElements(barr, JNI_FALSE);
    if (alen > 0) {
        rtn = (char *) malloc(alen + 1);
        memcpy(rtn, ba, alen);
        rtn[alen] = 0;
    }
    env->ReleaseByteArrayElements(barr, ba, 0);
    std::string stemp(rtn);
    free(rtn);
    return stemp;
}


//xxx是你的包名 点好换成下划线
extern "C"
JNIEXPORT jstring JNICALL Java_com_xxx_xxx_Utils_getSign(
        JNIEnv *env,
        jclass type, jstring arg0) {
    std::string content = jstring2str(env, arg0);
    content="我是盐值"+content+"我是味精值";
    std::string val = A1 + "com/xxx/xxx/Utils"

//这里是ndk调用java方法 因为懒 直接调用java的MD5方法了 注意路径和参数
    jclass clazz = env->FindClass(val.c_str());
    jmethodID mid = env->GetStaticMethodID(clazz, "getMD5",
                                           "(Ljava/lang/String;)Ljava/lang/Object;");
    jstring byte = (jstring) env->CallStaticObjectMethod(clazz, mid,
                                                         env->NewStringUTF(content.c_str()));

    content = jstring2str(env, byte);

    std::transform(content.begin(), content.end(), content.begin(), toupper);

    return env->NewStringUTF(content.c_str());
}

对应的java类

public class Utils {
    static {
    //ndk编译后的名字 
        System.loadLibrary("sign");
    }
    //Keep注解是为了防止混淆的时候混淆掉名字
    @Keep 
    //对应ndk调用的方法名字
    public native static String getSign(String arg0);

    @Keep
    //这个就是ndk调用的java方法 注意名字 参数返回值 
    public static Object getMD5(String data) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            return byte2hex(md.digest(data.getBytes("UTF-8")));
        } catch (Exception gse) {
            gse.printStackTrace();
        }
        return "";
    }

    private static String byte2hex(byte[] bytes) {
        StringBuilder sign = new StringBuilder();
        for (byte aByte : bytes) {
            String hex = Integer.toHexString(aByte & 0xFF);
            if (hex.length() == 1) {
                sign.append("0");
            }
            sign.append(hex.toUpperCase());
        }
        return sign.toString();
    }
}

上面就完成了一套安卓端的签名了 签名办法也放在了ndk 大大提高了安全性

然后是服务端的签名校验

    public static Boolean sign(Map<String, String> paramValues) throws CommonException {
        String sign = paramValues.get("sign");
        //先提前并移除sign字段 因为他不参与签名计算
        paramValues.remove("sign");
        long currentTimeMillis = System.currentTimeMillis();
        Long timestamp = Long.valueOf(paramValues.get("timestamp"));

        long difference = getDifference(timestamp, currentTimeMillis, 0);
        if (difference > 60) {
        //抛出自定义异常 没做异常return也行 无所谓 不是重点 这里的if是个时间戳校验 免得api重放工具(别人抓包你的借口模拟请求 重复请求)
            throw new CommonException(HttpStatus.FORBIDDEN.value(), "数据校验失败 请重试或检查手机时间是否准确");
        }
        StringBuilder sb = new StringBuilder();

        List<String> paramNames = new ArrayList<>(paramValues.size());
        paramNames.addAll(paramValues.keySet());

//记得排序 不然结构和安卓签名时的顺序不一致 结果也会不同
        Collections.sort(paramNames);

        sb.append("我是盐值");
        for (String paramName : paramNames) {
            sb.append(paramName).append(paramValues.get(paramName));
        }
        sb.append("我是味精值");

        String md5 = DigestUtils.md5Hex(sb.toString()).toUpperCase();
        //然后判断是否与安卓端传过来的签名一致 不一致就返回错误信息就行 
        return md5.equalsIgnoreCase(sign);
    }

注意 时间戳校验 如果用户手机时间不正确 或者时区和服务器不一致 要做处理 提示用户校准时间 并且获取时间时要获取和服务器一致的时区时间 否则校验不通过

使用时可以自定义个注解+拦截器 给要进行校验的接口添加校验注解就OK 十分方便

下面给出拦截器示例代码

@Component
public class SafetyCheckInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        SafetyCheck annotation = method.getAnnotation(SafetyCheck.class);
        if (annotation == null) {
            return true;
        }

        String requestMethod = request.getMethod();
        if (requestMethod.equalsIgnoreCase("GET")) {
            Map<String, String> map = new LinkedHashMap<>();
            Enumeration<String> parameterNames = request.getParameterNames();
            while (parameterNames.hasMoreElements()) {
                String nextElement = parameterNames.nextElement();
                map.put(nextElement, request.getParameter(nextElement));
            }
            //这里判断是否存在sign参数 代码走到这里了 肯定是接口加了校验 但是安卓端没有提供这个sign字段 那估计是有点问题了 怎么处理看你自己咯
            if (map.containsKey("sign")) {
            //最终比对 如果匹配就玩下执行 如果不匹配 就返回异常
                if (VerifyUtils.sign(map)) {
                    return true;
                }
            }
        }

//这里是校验post的请求 因为post 的body不能被重复读取 所以只能勉强校验一下parameter 如果你的请求没用body传数据 那就下面的代码就可以的 如果body传参 那就需要单独在Controller校验了

//        if (requestMethod.equalsIgnoreCase("POST")) {
//            Map<String, String> map = new LinkedHashMap<>();
//            Enumeration<String> parameterNames = request.getParameterNames();
//            while (parameterNames.hasMoreElements()) {
//                String nextElement = parameterNames.nextElement();
//                map.put(nextElement, request.getParameter(nextElement));
//            }
//            if (map.isEmpty()) {
//                return true;
//            }
//            if (map.containsKey("sign")) {
//                if (VerifyUtils.sign(map)) {
//                    return true;
//                }
//            }
//        }
        System.out.println("-----------------------------------------");
        System.out.println("数据校验失败 请重试并检查手机时间是否准确");
        System.out.println(request.getRequestURL().toString());
        System.out.println("Version:" + request.getHeader("version"));
        System.out.println("-----------------------------------------");
        //校验不通过的 抛出异常提现客户端
        throw new CommonException(HttpStatus.FORBIDDEN.value(), "数据校验失败 请重试并检查手机时间是否准确");
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {

    }
}

下面是 单独处理body

    @Authentication
    @RequestMapping(value = "/setCachePool", method = RequestMethod.POST)
    public ServerResponse setCachePool(@RequestAttribute @ApiIgnore String from, @RequestParam String sign, @RequestBody String cachePool) {
    //这里单独校验body的内容签名
        VerifyUtils.sign(cachePool, sign);
        likePoolService.setCachePool(from, GsonUtils.create().fromJson(cachePool, CachePool.class));
        return new ServerResponse();
    }

校验工具类

    public static void sign(String json, String sign) {
        String sb = "我是盐值" + json + "我是味精值";
        String md5 = DigestUtils.md5Hex(sb).toUpperCase();
        if (!md5.equalsIgnoreCase(sign)) {
        //不通过就甩异常
            throw new CommonException(HttpStatus.FORBIDDEN.value(), "数据校验失败");
        }
    }

客户端单独处理body用Utils.getSign(body); 然后参数拼接 xxx.com/getinfo?sign="+ sign

这样就行

最后附上客户端普通处理的用法

 Map<String, String> paramMap = new TreeMap<>();
        paramMap.put("size", String.valueOf(size));
        HTTP.<String>get(HOST.concat("/api/v3/pool/getPool"))
                .params(ParamUtils.sign(paramMap))
                .execute();

ParamUtils返回的是一个参数类 你们改成返回map什么的都行 自己安排就好

最后附上另一篇文章 证书双向认证防抓包/模拟请求
防止APP请求被人抓包