抖音设备注册生成device_id与install_id hook分析记录

前言

了解到前一段时间,火山小视频升级为了抖音火山版,想必其相应的安全措施也升级了吧,笔者本着学术交流的目的研究了一下抖音火山版的设备注册方法,也就是生成device_id与iid(install_id)的过程。从而找到了如何能够模拟生成这两个id,生成出来的这两个id在访问抖音火山版网络接口的时候证明有效,但是能否用于其他头条系应用就不得而知了,可供参考。

逆向

静态分析

静态分析也就是反编译,反编译软件很多,从基本的Apktool工具,到强大的jadx,还有也比较强大的JEB,有时候Android studio也能帮忙分析,之前也试过GDA也不错,笔者比较常用的还是jadx和JEB。工欲善其事,必先利其器,这句话在逆向分析的研究中太适配了。废话不多说,下面就开始吧。

jadx打开抖音火山版的apk文件,可以先找到AndroidManifest.xml文件,记录一下它的包名package=”com.ss.android.ugc.live”,然后呢?一脸懵,要不是看了前辈的工作才不知道要看什么,可以得知有个api很关键:https://log.snssdk.com/servic... ,那么就先搜索这个字符串吧,可以尝试搜索_device_register_或者_service/2/device_register/_等等可能的字符串都行,看看能找到什么代码。使用jadx具体操作的时候需要选择_导航-》搜索文本_,然后要等一会儿,因为它正在反编译,如下图所示:

Alt pic
反编译完之后可以试试查找文本:_service/2/device_register/_,发现有个类很可疑:…deviceregister.b.a.URL_DEVICE_REGISTER(),如下图所示:

点进去仔细瞧瞧。发现这是个构造url列表的函数,肯定有什么地方调用它了。然后按着control键寻找该函数的用例,发现除了它自己就只有一个地方调用它了,进入调用它的函数发现是未能正常反编译的函数,如下图所示:

可以看到有个_DeviceRegisterThread字符串_映入眼帘,然后就能大概知道这个类比较重要了,上面能看到它继承的Thread类,那么这就应该是设备注册请求网络部分的子线程了。唉,其实一开始笔者找到别的地方去了,一开始专注于分析这个device_id所在的本地缓存了,因为本地有缓存的时候会优先加载本地缓存而不是发起这个网络请求,后来发现分析完了本地缓存之后并没有什么用,不过倒是把附近的几个类整明白了一点。扯远了,下面还是继续分析这个继承自Thread类的com.ss.android.deviceregister.b.c.a类吧,这个类其实可以从头分析,这里不再详细说了,先把jadx正常反编译的部分仔细过一遍,然后就能发现上图那个反编译失败的函数很重要,应该就是发起网络请求相关的部分。但是jadx反编译失败了,而且这个伪码看起来也不适合分析,那么就想着看smail代码呗,但是发现竟然找不到这个类对应的smail代码,只能说是jadx的bug了吧。那怎么办?这时候JEB就可以出场了。

笔者之前用的是JEB2,这次搞这个的时候搜了一下这个工具发现JEB3也出来了,上面提到的JEB超链接给的就是一个JEB3的地址,虽然好像是个beta版本,但是也不影响使用。之前用JEB2的时候就遇到过一个问题,那就是直接打开一个apk安装包的时候,特别是体积比较大的安装包的时候会提示内存溢出而无法打开的情况,解决方法是找到想要具体分析的dex文件,然后只打开该dex文件分析即可。所以这次笔者也是先找到了上面要分析的代码所在的dex文件,然后单独分析该dex文件即可。那么问题来了,怎么找到代码所在的dex文件呢?这时候想到jadx反编译的时候好像在shell上面输出了一些日志信息,可以看看这些信息能不能有所帮助。重新使用jadx打开apk文件,然后从源代码目录中找到com.ss.android.deviceregister.b.c.a类,进行反编译,如下图所示:

从上图中能够看到,日志中输出了该类所在的dex文件为classes4.dex。那么就用JEB打开classes4.dex吧,然后找到这个类,分析一下上面那个重要的函数,如下图所示:

这个看起来就不错了,jeb对匿名内部类的反编译效果的确比jadx好一些,那就仔细分析一下这个函数吧,jeb的改名功能还是很好用的,经过笔者手动改名之后的函数,如下图所示:

这样看起来就舒服一些了,但是有些跳转还是很奇怪,经过笔者的手动重写,代码提取如下:

private boolean is_successful_get_app_config_from_net(String args_json_string) {
    String response = null;
    Object args_bytes_clone;
    String url;
    int url_index;
    String default_response = null;
    boolean is_in_10min_from_last_request;
    long now_time;
    byte[] args_bytes;
    try {
        args_bytes = args_json_string.getBytes("UTF-8");
        now_time = System.currentTimeMillis();
        if(now_time - this.b.mLastGetAppConfigTime < 600000) {
            is_in_10min_from_last_request = true;
        }
        else {
            is_in_10min_from_last_request = false;
        }
    }
    catch(Throwable v0) {
        return false;
    }
 
    try {
        this.b.mLastGetAppConfigTime = now_time;
        String[] url_list = com.ss.android.deviceregister.b.a.URL_DEVICE_REGISTER();
        if(url_list == null) {
            throw new IllegalArgumentException("url is null");
        }
 
        int urls_length = url_list.length;
        url_index = 0;
        while(true) {
            if(url_index >= urls_length) {
                break;
            }
 
            url = url_list[url_index];
            args_bytes_clone = args_bytes.clone();
            if(StringUtils.isEmpty(url)) {
                ++url_index;
                continue;
            }
 
            url = NetUtil.addCommonParams(SemUtils.updateUrl(this.b.mContext, url), true);
            Logger.debug();
 
            try {
                if(!this.is_encrypt()) {
                    if(is_in_10min_from_last_request) {
                        url = url + "&config_retry=b";
                    }
 
                    response = NetworkClient.getDefault().post(url, args_bytes, true, "application/json; charset=utf-8", false);
                    break;
                }
 
                try {
                    response = NetUtil.sendEncryptLog(url, ((byte[])args_bytes_clone), this.b.mContext, is_in_10min_from_last_request);
                    break;
                }
                catch(RuntimeException v0_3) {
                    if(is_in_10min_from_last_request) {
                        try {
                            url = url + "&config_retry=b";
                            response = NetworkClient.getDefault().post(url, args_bytes, true, "application/json; charset=utf-8", false);
                            break;
                        }
                        catch(Throwable v0) {
                            try {
                                if(!this.b.shouldRetryWhenError(v0)) {
                                    throw v0;
                                }
                                ++url_index;
                                continue;
                            }
                            catch(Throwable v0) {
                                return false;
                            }
                        }
                    }
 
                    response = NetworkClient.getDefault().post(url, args_bytes, true, "application/json; charset=utf-8", false);
                    break;
                }
            }
            catch(Throwable v0) {
                try {
                    if(!this.b.shouldRetryWhenError(v0)) {
                        throw v0;
                    }
                    ++url_index;
                    continue;
                }
                catch(Throwable v0) {
                    return false;
                }
            }
        }
    }
    catch(Throwable v0) {
        return false;
    }
 
    if(response != null) {
        try {
            if(response.length() != 0) {
                this.parse_net_config(new JSONObject(response));
                return true;
            }
        }
        catch(Throwable v0) {
            return false;
        }
    }
 
    return false;
}

这样就基本上能明白它在干什么了。

简单来说就是先拿到一个设备注册的url地址列表,然后遍历访问,如果访问成功了就结束,访问的时候先添加公共参数,然后判断是否需要加密请求,如果需要加密请求则调用加密请求函数,否则调用普通请求函数,另外如果调用加密请求函数抛异常时也会去调用普通请求函数,还有一些重试参数等等。这时候想到之前抓包的时候找到的设备注册请求都是加密的,那么可以判断is_encrypt()这个函数的都是返回的true,那么如果这个函数返回false的时候是不是就是不加密的请求了呢?好了,静态分析部分就先到这里,下面来动态分析一下,也就是尝试一下这个函数返回false的时候的效果。

动态分析

动态分析也就是在app运行时,对其进行分析,比较常见的是分析app的网络请求行为,调用系统API的行为,访问文件的行为等等。由于本文是研究设备id的生成,其实也就是研究设备注册时的一个网络接口,那么就只分析网络请求行为即可。笔者比较常用的网络抓包工具是Charles,需要在手机端配置代理和安装Charles的证书,这部分也不详细说了。

上文也提到了,本文主要是研究这个接口:https://log.snssdk.com/servic... ,正常情况下设备在注册的时候会加密访问这个接口,比如新安装了app或者app被清理数据之后,再打开app时就会去访问这个接口,访问时是个POST请求,参数是明文,但是带的data是加密的,如下图所示:

通过上文静态分析可知is_encrypt()这个函数能够控制是否加密传输,所以可以想到利用Xposed进行Hook函数,使得该函数的返回值固定为false。

Xposed很强大,它能够在其他app运行时注入代码,具体原理和使用教程这里也不再详述,下面笔者贴一下hook部分的代码吧,其实很短:

findAndHookMethod("com.ss.android.deviceregister.b.a", param.classLoader, "isEncrypt", new XC_MethodHook() {
    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        boolean oldResult = (boolean) param.getResult();
        Log.i(TAG, "com.ss.android.deviceregister.b.a.isEncrypt(), oldResult=" + oldResult + ", newResult=" + false);
        param.setResult(false);
    }
});

 可以看到这里的data部分已经变成明文了。 

至此,找到了设备注册的明文接口,但是看前辈的工作,他用密文请求注册的方式模拟生成的device_id和iid不能用,所以笔者用这种明文注册方式进行了模拟生成设备id,模拟方式同前辈的代码类似,然后用模拟生成的设备id进行了其他接口的访问测试,貌似可以用。

总结

整体来看,抖音火山版比之前的火山小视频的安全强度要高一些,或许是和抖音有一些关系吧,毕竟都是头条系产品,技术上复用一下也不太困难,而且感觉在关键的代码逻辑部分应该存在一定的反反编译,就像上面笔者手动还原的那个函数。本文只是针对一个小功能进行了较为详细的逆向分析,还并没有涉及到native层,那一块的对抗估计更加强烈,所以,先这样吧。

声明

请勿使用本技术于商用或大量抓取!

若因使用本技术与抖音火山版官方造成不必要的纠纷,本人盖不负责,存粹技术爱好,若侵犯抖音火山版相关公司的权益,请告知!

转载请注明出处,谢谢!

你可能感兴趣的