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,分析一下的传入值和返回值
Java.perform(function () {
let RequestEncrypt = Java.use("com.bytedance.frameworks.core.encrypt.RequestEncryptUtils");
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
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
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对内部函数进行调用,据为己用
处理请求
@Override
public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) {
log(uri);
Class<?> clazzPPx = null;
String postData = files.get("postData");
log("postData=" + postData);
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调用
public Response getQueries(Class<?> classUse, String strData) {
byte[] dataBuf = strData.getBytes();
int length = dataBuf.length;
byte[] dataEnc = (byte[]) XposedHelpers.callStaticMethod(classUse, "ttEncrypt", dataBuf, length);
String dataBase64 = Base64.encodeToString(dataEnc, 2);
String dataUrlEncode = null;
try {
dataUrlEncode = URLEncoder.encode(dataBase64, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
log("X-SS-QUERIES:" + dataUrlEncode);
return newFixedLengthResponse(dataUrlEncode);
}
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
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类型, 这里是转换的方法:
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 结语
这是本人一次尝试写技术博客,如果有错误之处还望大家多多评判指正,因为这样的话,我就能明白自己的脸皮到底有多厚。最后,如果大家想学习这项技术的话,我推荐一个星球给大家,星主不光对这方面有很深得造诣,而且人长得还很帅。
|