Bootstrap

某某虾App加密参数分析


0x01 写在前面

对于AppHook这项技术,说难不难,说简单也不简单,唯一的特点就是比较费头发。因为你需要在别人浩如烟海的代码中推导出你想要的东西,而且最终的推导结果还不一定如你所愿。所以搞这种东西之前,我们优先考虑的是自己的发量,而不是对它的研究兴趣。


0x02 所用工具

系统环境

    网易mumu模拟器 + Android 6.0【系统版本是模拟器自定义的

在这里插入图片描述


链接工具
    adb 这是文档

应用软件
    某某虾 version3.0

抓包工具
    Fiddler

反编译工具
    jadx-gui

Hook工具:
    python + frida

安卓编写工具
    Android Studio version4.0

调用工具:
    nanohttpd + xposed

这里需要注意两点

    1. 目前github xposed作者将下载请求的协议改成了HTTPS,所以安装过程中会出现激活失败的情况,这里是我的解决该问题的参考方案

    2. 在选择模拟器时尽量选择版本比较低的进行安装,如果最新版本在安装完xposed后,开机动画会可能会卡在94%无法载入,这种情况不明所以,这里是我的选择的模拟器版本


0x03抓包分析

在这里插入图片描述

通过抓包我们不难发现,请求header里有两个加密参数:X-SS-QUERIES、X-Gorgon ,而这两个参数正是我们今天的研究的对象。


0x04 逆向源码

这款App没有进行加壳,我们借助jadx-gui工具轻而易举就能将它扒光逆向,然后直接全局搜索X-SS-QUERIES关键字,记得勾选code

搜索参数

搜索到结果后对代码进行跟进

定位函数

直接对它所对应的函数a进行hook,分析一下的传入值和返回值

/* code for javascript */
 Java.perform(function () {
        let RequestEncrypt = Java.use("com.bytedance.frameworks.core.encrypt.RequestEncryptUtils");
        // overload function
        RequestEncrypt.a.overload("java.lang.String", "java.lang.String").implementation = function (x1, x2){
            console.log("【INPUT x1】:", x1);
            console.log("【INPUT x2】:", x2);
            let result = this.a(x1, x2);
            console.log("【OUTPUT 】:", result);
            return result;
        }
    })
}

运行结果

在这里插入图片描述

从Hook后的结果我们发现一大堆乱码的数据,但如果你细心研究一下,就会有更为惊奇的发现:它们不止乱码而且看起来还挺费眼睛。
结论:【input x1】传入值属于加密数据,【input x2】传入的UTF8编码格式。所以,我们要从x1入手去推导它是如何进行加密的,只有把这一步整明白,那么整套算法流程就会不攻自破。

在这里插入图片描述

根据上图所圈点的代码区块,我们可以把大致的加密流程整理出来

在这里插入图片描述

从上述流程中我们可以分析出加密的初始值,也就是String 类型的a2变量,换句话来解释:加密的源头是a2所对应的数值

看一下a2对应的函数

在这里插入图片描述


直接Hook

/* code for javascript */
  RequestEncrypt.a.overload('java.util.List', 'boolean', 'java.lang.String').implementation = function (x1, x2, x3){
            console.log("【INPUT x1】:", x1);
            console.log("【INPUT x2】:", x2);
            console.log("【INPUT x3】:", x3);
            let result = this.a(x1, x2, x3);
            console.log("【OUTPUT 】:", result);
            return result;

        };

在这里插入图片描述

从这次Hook结果来看,我们得到了一种明文数据,而且这种明文数据又符合urlencode编码格式。所以我们直接给定结论或是提出大胆的假设:它正是执行加密的原始数据。但对于这种假设我们一会儿拿到xposed模板统一去验证,这里先按下暂停键。


0x05 分析X-Gorgon参数

应该是版本迭代或者是官方故意隐藏,这个参数我们直接在逆向工具上搜索很难搜到,而且用我老师提供的堆栈追踪法也无法定位。最终得益于神通广大的网友助力,才让此参数的研究思路初现端倪。(PS:具体哪篇文章我忘了,如果有侵权烦请告知。

在这里插入图片描述

切入到函数内部

在这里插入图片描述

再次定位方法

在这里插入图片描述

直接Hook

// code for javascript
let NetworkParams = Java.use("com.bytedance.frameworks.baselib.network.http.NetworkParams");
        NetworkParams.tryAddSecurityFactor.overload('java.lang.String', 'java.util.Map').implementation = function (x1, x2){
            console.log("【input x1】:", x1);
            console.log("【input x2】:", x2);
            let result = this.tryAddSecurityFactor(x1, x2);
            console.log("【output 】:", result);
            return result;

        }
    })

在这里插入图片描述

分析一下:
【input x1】传入值是网络请求的链接
【input x2】应该是一种Java专有的数据类型叫做:HashMap(见识少,勿喷)
【output 】正是我们想要的X-Gorgon加密参数

  值得注意的是:x2的传入值里面有很多密文,这些是App程序对你系统信息的一些收录,在之后调用的过程中可以模拟出来,不用刻意研究。


0x07 浅谈android编辑器用法

关于安卓程序的编写,我也是刚学不久,属于初级菜鸟,如果你对此也感兴趣的话,我会在文末给出我老师的知识星球坐标,共勉。至于有什么收获,你所看到的既是我的收获。

选择创建的模块(不知道这样叫准不准确,勿喷)

在这里插入图片描述

选择安卓版本
    这里需要注意几点:
      1. 你所选择的android版本要跟SDK版本一一对应上,不然编译成App时直接报错。
      2.创建App签名时,一定要选用系统方式创建,不然即使编译成功也会安装失败。

在这里插入图片描述


0x08 Xposed编写

项目app目录下AndroidMainfest.xml文件中增加几行代码

在这里插入图片描述

<-- code for android !-->
 <meta-data
     android:name="xposedmodule"
     android:value="true" />
  <meta-data
      android:name="xposeddescription"
      android:value="your defined description" />
  <meta-data
      android:name="xposedminversion"
      android:value="53" />

项目app目录下build.gradle文件增加几行代码

dependencies {
	...
	compileOnly 'de.robv.android.xposed:api:82'
    compileOnly 'de.robv.android.xposed:api:82:sources'
}

项目app目录下src/main/java/com.example.xxx/目录中增加Java文件命名为HookLoader

在这里插入图片描述

项目app目录下src/main/创建assets文件夹并在文件夹创建xposed_init文件添加一行代码:com.example.xxx.HookLoader

编译成App成功后执行adb安装指令:adb install -t app-release.apk,然后我们就会发现我们的程序就推送到xposed模板中了

在这里插入图片描述

重启模拟器验证效果

在这里插入图片描述

打开App后打印拦截提示

在这里插入图片描述


0x09 搭建android服务

晾晒在文档开头的nanohttpd工具终于可以轮到它抛头露面了,我们把它下载完成后添加到项目app目录下libs文件夹中

在这里插入图片描述

再次在项目app目录下build.gradle文件增加一行代码

compileOnly 'org.nanohttpd:nanohttpd:2.3.1'

重新回到HookLoader文件中编写代码如何搭建服务,nanohttpd文档中有详细介绍我这里就张贴代码了,见谅

在这里插入图片描述

重新编译App并推送到模拟器中,当模拟器重启后,打开某某虾App会发现我们的服务启动成功了。

在这里插入图片描述

执行adb forward tcp:8889 tcp:8889将端口映射到本地,然后在本地浏览器访问http://127.0.0.1:8889/hello

成功!

在这里插入图片描述


0x10 xposed调用

这一步要做的跟frida实现的功能有异曲同工之妙,只不过frida对内部函数进行拦截用于我们分析,而这一步我们使用xposed对内部函数进行调用,据为己用

处理请求

//code for java
@Override
public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) {
     log(uri);
     //register Class
     Class<?> clazzPPx = null;
     String postData = files.get("postData");
     log("postData=" + postData);
     // direct return error
     if (StringUtils.isEmpty(postData)) {
         return newFixedLengthResponse("postData is null.");
     }
     //
     if (StringUtils.containsIgnoreCase(uri, "get-queries")) {
         try {
             clazzPPx = lpparam.classLoader.loadClass("com.bytedance.frameworks.encryptor.EncryptorUtil");
         } catch (ClassNotFoundException e) {
             e.printStackTrace();
         }
         return getQueries(clazzPPx, postData);

     } else if (StringUtils.containsIgnoreCase(uri, "get-gorgon")) {
         try {
             clazzPPx = lpparam.classLoader.loadClass("com.bytedance.frameworks.baselib.network.http.NetworkParams");
         } catch (ClassNotFoundException e) {
             e.printStackTrace();
         }
         try {
             JSONObject json = new JSONObject(postData);
             log("json------------------" + json);
             String str = (String) json.get("uri");
             log("uri------------------" + uri);
             JSONObject params = new JSONObject((String) json.get("params"));
             log("params-------------- " + params);
             HashMap hashMap = (HashMap) Utils.jsonToMap(params);
             log("hashMap-------------" + hashMap);
             return getGorgon(clazzPPx, str, hashMap);

         } catch (JSONException e) {
             e.printStackTrace();
             return newFixedLengthResponse("invalid json type.");
         }

     }
     return super.serve(uri, method, headers, parms, files);
 }

处理响应以及xposed调用

// code for java
// get x-ss-queries function
public Response getQueries(Class<?> classUse, String strData) {
    // get bytes
    byte[] dataBuf = strData.getBytes();
    // get bytes length
    int length = dataBuf.length;
    // callback native ttEncrypt function
    byte[] dataEnc = (byte[]) XposedHelpers.callStaticMethod(classUse, "ttEncrypt", dataBuf, length);
    // base64 encode
    String dataBase64 = Base64.encodeToString(dataEnc, 2);
    // url encode
    String dataUrlEncode = null;
    try {
        dataUrlEncode = URLEncoder.encode(dataBase64, "UTF-8");
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }
    log("X-SS-QUERIES:" + dataUrlEncode);
    return newFixedLengthResponse(dataUrlEncode);
}

// get x-gorgon function
public Response getGorgon(Class<?> classUse, String str, Map<String, List<String>> map) {
    log("getGorgon uri=" + str);
    log("getGorgon map=" + map);
    Map<String, String> mapGorgon = (Map<String, String>)XposedHelpers.callStaticMethod(classUse,"tryAddSecurityFactor", str, map);
    log("Map gorgon=" + mapGorgon);
    String gorgon = mapGorgon.get("X-Gorgon");
    log("gorgon=" + gorgon);
    return newFixedLengthResponse(gorgon);
}

使用python模拟请求

import requests
import json
# code for python
uri = "http://127.0.0.1:8889"

def get_queries_params():
    queries_uri = f"{uri}/get-queries"
    postData = "cell_id=7006574890992015629&count=2&api_version=1&iid=2524899796857758&device_id=61993742510055&ac" \
               "=wifi&mac_address=08%3A00%3A27%3AE4%3AE3%3AB1&channel=store_tengxun_wzl&aid=1319&app_name=super" \
               "&version_code=300&version_name=3.0.0&device_platform=android&ssmix=a&device_type=MI+6&device_brand" \
               "=Xiaomi&language=zh&os_api=23&os_version=6.0.1&uuid=300000000218617&openudid=ab925ec9fb32a36d" \
               "&manifest_version_code=300&resolution=810*1440&dpi=270&update_version_code=30050&_rticket" \
               "=1631346727236&cdid=6eb9b3b7-4651-4038-a48e-107dc0f75c71&app_region=CN&sys_region=CN&time_zone=Asia" \
               "%2FShanghai&app_language=ZH&carrier_region=&last_channel=&last_update_version_code=0 "
    res = requests.post(url=queries_uri, data=postData)
    if res.status_code == 200:
        return res.text


def get_gorgon_params(x_ss_queries):
    gorgon_uri = f"{uri}/get-gorgon"
    hashMap = {
        "accept-encoding": "gzip",
        "cookie": "odin_tt=1c9cb7f29b113286bcf3ddd0e7a3d117cc88da5926f098f4fa09c1bc690efeffff835ecaf4fa53c35158071f87239f1f74a1545586d7acd74917405bc034b92f; passport_csrf_token_default=6237903143a1db30225c2f7e60e76afc; install_id=2524899796857758; ttreq=1$a5fa436781e70b00229f4e96ba6aa97fa8471fc1",
        "sdk-version": "1",
        "user-agent": "ttnet okhttp/3.10.0.2",
        "x-ss-queries": x_ss_queries,
        "x-ss-req-ticket": "1631346727238"
    }
    postData = {
        "uri": "https://i.snssdk.com/bds/cell/immersion_comment/?cell_type=1&cell_id=7006574890992015629&count=2"
               "&api_version=1&iid=2524899796857758&device_id=61993742510055&ac=wifi&mac_address=08%3A00%3A27%3AE4"
               "%3AE3%3AB1&channel=store_tengxun_wzl&aid=1319&app_name=super&version_code=300&version_name=3.0.0"
               "&device_platform=android&ssmix=a&device_type=MI+6&device_brand=Xiaomi&language=zh&os_api=23"
               "&os_version=6.0.1&uuid=300000000218617&openudid=ab925ec9fb32a36d&manifest_version_code=300&resolution"
               "=810*1440&dpi=270&update_version_code=30050&_rticket=1631346727236&cdid=6eb9b3b7-4651-4038-a48e"
               "-107dc0f75c71&app_region=CN&sys_region=CN&time_zone=Asia%2FShanghai&app_language=ZH&carrier_region"
               "=&last_channel=&last_update_version_code=0&ts=1631346727&as=a111111111111111111111&cp"
               "=a000000000000000000000&mas=01950e0f880e41b17ece8e51d3ddfbe6a08c8c8c8c8c8c8c8c8c8c ",
        "params": json.dumps(hashMap)
    }

    res = requests.post(url=gorgon_uri, data=json.dumps(postData))
    if res.status_code == 200:
        print(f"X-Gorgon 请求成功:", res.text)


def main():
    x_ss_queries = get_queries_params()
    print(f"X-SS-Queries 请求成功{x_ss_queries}")
    get_gorgon_params(x_ss_queries)


if __name__ == '__main__':
    main()

完美!!!

在这里插入图片描述

差点忘了,鉴于python没有HashMap的数据类型,在向服务端请求的时候,我只用使用json去传递数据,Java端需要把我传递的json数据转成HashMap类型, 这里是转换的方法:

// code for java
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
public class Utils {
    public static Map jsonToMap(JSONObject json) throws JSONException {
        HashMap hashMap = new HashMap();
        JSONObject items = new JSONObject();
        Iterator<String > it = json.keys();
        while (it.hasNext()){
            String key = it.next();
            items.put(key, new ArrayList<>());;
            ArrayList itemArr = (ArrayList) items.get(key);
            itemArr.add(json.get(key));
            hashMap.put(key, itemArr);
        }
        return hashMap;
    }
}

0x11 结语

这是本人一次尝试写技术博客,如果有错误之处还望大家多多评判指正,因为这样的话,我就能明白自己的脸皮到底有多厚。最后,如果大家想学习这项技术的话,我推荐一个星球给大家,星主不光对这方面有很深得造诣,而且人长得还很帅。
在这里插入图片描述

;