某片语音sign算法分析
此文并不是一天写完的,以至于验证结果之前的内容 用的是上一个版本17(1.6.0) ,验证结果之后内容是18(1.6.1);但思路都是一样的,两个版本只是更换了类的包名、方法名(可能是编译发版的问题?)等,hook的时候我重新反编译了一次,获取新版本的hook点,所以代理里会和上面分析出来的类不大一样。sign算法的代码也是一样的。
前言
他们家的应用,对IOS的支持一直不友好;这个语音跟安卓查了也给大版本,而且IOS的还被AppStore下架了;就想着要不要自己抓下安卓的接口搞个网页来用。
测试了安卓的,有防抓包SSL Pinning;按照以前的经验,使用Xposed模块JustTrustMe解决;发现并没有效果,打开代理还是断网,网上搜索了下发现双端验证的SSL Pinning使用这个方式是无效的,但并没有找到合适的解决方案。
但是在使用wireshark的时候,发现这个App会把接口打印到日志里。而且我刚好用IOS测试过抓包,是可以正常抓包的;只是IOS里面有发现请求头包含sign,那他肯定就对参数进行签名了呀,就算有能拿到接口不拿到sign算法也是无济于事。遂反编译代码,开始获取接口、参数、sign等。
准备
- App加壳了,用于脱壳。
- 安卓真机一台:vivo Xplay6 - 用夜神模拟器安装`BlackDex`脱壳失败,真机成功。
- 用于从 Android Dex 和 Apk 文件生成 Java 源代码的命令行和 GUI 工具 Command line and GUI tools for producing Java source code from Android Dex and Apk files
- Xposed - 用于帮助分析和验证结果。
- Android Studio - 写Xposed模块代码、连接ADB查看日志
- PostMan - 随便,能发送请求就行
关键词定位
我准备先从sign入手,因为我觉得其他比如获取接口会简单一点,事实也是如此。

枚举、常量、注解,我认为这三个是比较重要的,所以就挨个往下看了下。
枚举

这个字符串很像是个盐啊,但点进去看,里面的一直值是接口里没见过的,下一个

#### 常量
有个包名包含taobao的就不看了,应该是第三方的。
KeyProvider.SIGN也没有被哪里用到,GG
注解
从图一可以看出来,注解是Header,sign只是value。点进去这个注解看, 就一个value
 
 
搜索了一下这个注解retrofit2.http.value,看到了一个文章:https://blog.csdn.net/SilenceOO/article/details/77460607
这个也确实是用来设置请求头的,那就顺着往下看就好了。

其实这个有注解的地方,@POST注解里的值,点进去就能看到是接口。有的地方看起像是接口参数,而且有的接口是有@Header("sign")有些是没有的,猜测是不是有些接口不需要签名。
看到这种统用注解,第一想法是会不会使用aop做处理,所以搜索了下Header.class,无果。
但直接搜索这个api类e.l.d.g.a.d.a.class,发现了重点

找个第一个参数是sign的方法看一下,能看出来变量i2是请求的参数,并且通过c.o(i2)处理了下。
追踪进去,这怎么看都是一个sign算法

通过这些文件又对比了接口接口文件,发现只有POST请求才使用了sign。
验证结果
通过上面的排查,找到了疑似sign算法;接下来就是验证结果是不是对的了。
具体怎么验证?
我是Xposed进行hook,因为我还想看到都要哪些参数。
具体Xposed的介绍可以看我上一篇博文。
XposedHelpers.findAndHookMethod(
                "e.w.f.c",
                utils.getClassLoader(),
                "w",
                Map.class,
                new XC_MethodHook() {
                    @Override
                    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                        Log.d("shupian", "hook 后");
                        int length = param.args.length;
                        Log.d("shupian", String.valueOf(length));
                        Log.d("shupian", JSON.toJSONString(param.args[0]));
                        if(param.args[0] instanceof Map){
                            Map<String, String> map = (Map<String, String>) param.args[0];
                            for(String key : map.keySet()){
                                Log.d("shupian", "key【"+key+"】" + "value【"+ map.get(key) +"】" );
                            }
                        }
                        String result = (String) param.getResult();
                        Log.d("shupian", "返回值:【"+ result +"】" );
                    }
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                        Log.d("shupian", "hook 前");
                        int length = param.args.length;
                        Log.d("shupian", String.valueOf(length));
                        if(param.args[0] instanceof Map){
                            Map<String, String> map = (Map<String, String>) param.args[0];
                            for(String key : map.keySet()){
                                Log.d("shupian", "key【"+key+"】" + "value【"+ map.get(key) +"】" );
                            }
                        }
                        String result = (String) param.getResult();
                        Log.d("shupian", "返回值:【"+ result +"】" );
                    }
                }
        );
        XposedHelpers.findAndHookMethod(
                "e.w.f.c",
                utils.getClassLoader(),
                "v",
                byte[].class,
                new XC_MethodHook() {
                    @Override
                    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                        Log.d("shupian", "wwwwwwwwwwwwwwwwww后");
                        Log.d("shupian", param.args[0].toString());
                        String result = (String) param.getResult();
                        Log.d("shupian", "返回值:【"+ result +"】" );
                    }
                    @Override
                    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                        Log.d("shupian", "wwwwwwwwwwwwwwwwww前");
                        Log.d("shupian", param.args[0].toString());
                        String result = (String) param.getResult();
                        Log.d("shupian", "返回值:【"+ result +"】" );
                    }
                }
        );一共hook了两个方法,第一个是签名算法的入口,只能拿到一些参与签名的参数;第二个是签名算法retrue前调用的一个方法,就一个md5操作,这个拿到了签名后的结果。

那这个参数先登录下看看

因为短信验证码用过了,所以会提示错误,随便改一个参数都会提示{"msg":"签名错误","code":103}
可以证明我们hook的方法就是sign算法
提取出来算法测试一下,用同样的参数看看能不能输出同样的值。

OK。完美提取出sign算法。
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。