






















【爬虫系列】2. 打开App逆向“潘多拉魔盒”
一、前言
那么...
究竟有没有办法呢?
下一步怎么办呢?
PS:本文所有操作均基于Android App,iOS不在本学习教程内(臣妾也不会啊)
二、先验知识
2.0 核心思路
不支持在 Docs 外粘贴 block
2.1 劝退提示
PS:如果都不会,建议早点洗洗睡。
PPS:或者加钱请我一对一指导。
2.2 工作环境准备
三、开搞
3.0 今天天气真好
PS: 让我想想,搞谁家的App比较有趣...
PPS:并不是针对谁,只是“学习学习”一下优秀代码
3.1 豆瓣App 签名破解(Java原生)
可以看到这里请求正常返回,也能看到数据了。
但是我们切换一下其他小组的数据,很快就能看到“invalid_request_996”,签名错误的提示。
细心的朋友,大概已经看到了“_sig” 这个字段了,明显就是“sign”的简写。
好了,开始干活。
豆瓣 App- 下载
wget https://img2.doubanio.com/dae...
jadx -d douban-src com.douban.frodo_douban_7.18.1_231.apk
studio douban-src
反编译结果
Android 项目代码
最简单的办法,直接搜索字段“_sig"
大概能找到这段代码:
@android.webkit.JavascriptInterface
public java.lang.String decoratorUrl(java.lang.String str) {
try {
if (!com.douban.frodo.baseproject.rexxar.RexxarConstant.a(android.net.Uri.parse(str).getHost())) {
return str;
}
android.net.Uri.Builder appendQueryParameter = android.net.Uri.parse(str).buildUpon().appendQueryParameter("udid",
com.douban.frodo.baseproject.util.FrodoUtils.a()).appendQueryParameter("rom",
com.douban.frodo.baseproject.util.Utils.i()).appendQueryParameter("apikey",
com.douban.frodo.baseproject.util.FrodoUtils.c()).appendQueryParameter(com.umeng.commonsdk.proguard.d.ao, "rexxar_new");
if (com.douban.frodo.utils.AppContext.b() != null) {
appendQueryParameter.appendQueryParameter("channel", com.douban.frodo.utils.AppContext.b().market);
}
android.util.Pair<java.lang.String, java.lang.String> a
= com.douban.frodo.network.ApiSignatureHelper.a(str, "GET", null);
if (a != null) {
appendQueryParameter.appendQueryParameter("_sig", (java.lang.String) a.first);
appendQueryParameter.appendQueryParameter("_ts", (java.lang.String) a.second);
}
str = appendQueryParameter.build().toString();
return str;
} catch (java.lang.Exception e) {
e.printStackTrace();
}
}
明显签名实现在“com.douban.frodo.network.ApiSignatureHelper”这个类,
使用了a方法对str变量签名,返回了 _sig 和 _ts ,然后扔给QueryParameter。
package com.douban.frodo.network;
public class ApiSignatureHelper {
static android.util.Pair<java.lang.String, java.lang.String> a(okhttp3.Request request) {
if (request == null) {
return null;
}
java.lang.String header = request.header(com.douban.push.internal.api.Request.HEADER_AUTHORIZATION);
if (!android.text.TextUtils.isEmpty(header)) {
header = header.substring(7);
}
return a(request.url().toString(), request.method(), header);
}
public static android.util.Pair<java.lang.String, java.lang.String> a(
java.lang.String str, java.lang.String str2, java.lang.String str3) {
if (android.text.TextUtils.isEmpty(str)) {
return null;
}
java.lang.String str4 = com.douban.frodo.network.FrodoApi.a().e.b;
if (android.text.TextUtils.isEmpty(str4)) {
return null;
}
java.lang.StringBuilder sb = new java.lang.StringBuilder();
sb.append(str2);
java.lang.String encodedPath = okhttp3.HttpUrl.parse(str).encodedPath();
if (encodedPath == null) {
return null;
}
java.lang.String decode = android.net.Uri.decode(encodedPath);
if (decode == null) {
return null;
}
if (decode.endsWith("/")) {
decode = decode.substring(0, decode.length() - 1);
}
sb.append(jodd.util.StringPool.AMPERSAND);
sb.append(android.net.Uri.encode(decode));
if (!android.text.TextUtils.isEmpty(str3)) {
sb.append(jodd.util.StringPool.AMPERSAND);
sb.append(str3);
}
long currentTimeMillis = java.lang.System.currentTimeMillis() / 1000;
sb.append(jodd.util.StringPool.AMPERSAND);
sb.append(currentTimeMillis);
return new android.util.Pair<>(com.douban.frodo.utils.crypto.HMACHash1.a(
str4, sb.toString()), java.lang.String.valueOf(currentTimeMillis));
}
}
看到这个代码就应该开心了,17 - 45行都是在处理传入的参数,
主要用了 android.net.Uri 提取参数之后,拼装成了一个用“&”分割的字符串,用熟悉的语言自行处理就完事了。
最后,签名是调用了 “com.douban.frodo.utils.crypto.HMACHash1.a”,还需要继续跟进去。
不过这个名字“ HMAC-SHA1”已经给了不少的信息了。
跟代码进入到 com.douban.frodo.utils.crypto.HMACHash1类,于是看到了下面的代码:
package com.douban.frodo.utils.crypto;
public class HMACHash1 {
public static final java.lang.String a(java.lang.String str,
java.lang.String str2) {
try {
javax.crypto.spec.SecretKeySpec secretKeySpec = new javax.crypto.spec.SecretKeySpec(
str.getBytes(), com.douban.live.internal.LiveHelper.HMAC_SHA1);
javax.crypto.Mac instance = javax.crypto.Mac.getInstance(
com.douban.live.internal.LiveHelper.HMAC_SHA1);
instance.init(secretKeySpec);
return android.util.Base64.encodeToString(
instance.doFinal(str2.getBytes()), 2);
} catch (java.lang.Exception e) {
e.printStackTrace();
return null;
}
}
}
纯Java实现的一个HMAC_SHA1 加密算法。
str 是SecretKey(ZenoConfig的某个变量),str2是传入的签名数据。
现在只剩最后一个问题了,SecretKey 怎么拿?
回到上面代码,可以看到:
java.lang.String str4 = com.douban.frodo.network.FrodoApi.a().e.b;
// str4 就是传到HMACHash1的key,e 这里是 ZenoConfig
这里可以看到ZenoConfig.b 就传入的str3,也就是我们要找的SecretKey
继续翻就能看到
java.lang.String d2 = com.douban.frodo.baseproject.util.FrodoUtils.d();
builder.c = d2;
com.douban.zeno.ZenoConfig zenoConfig = new com.douban.zeno.ZenoConfig(
builder.a, builder.b, builder.c, builder.d, builder.e,
builder.f, builder.g, builder.h, builder.i, builder.j);
package com.douban.frodo.baseproject.util;
public class FrodoUtils {
private static java.lang.String a;
private static java.lang.String b;
private static java.lang.String c;
private static java.lang.String d;
private static java.lang.String e;
public static java.lang.String e() {
return "frodo://app/oauth/callback/";
}
public static java.lang.String a() {
if (android.text.TextUtils.isEmpty(a)) {
a = com.douban.amonsul.MobileStat.g((android.content.Context) com.douban.frodo.utils.AppContext.a());
}
return a;
}
public static void a(java.lang.String str) {
e = str;
}
public static java.lang.String b() {
return e;
}
public static java.lang.String c() {
return b;
}
public static java.lang.String d() {
return c;
}
@android.annotation.SuppressLint({"PackageManagerGetSignatures"})
public static void a(boolean z) {
if (android.text.TextUtils.isEmpty(b)) {
b = "74CwfJd4+7LYgFhXi1cx0IQC35UQqYVFycCE+EVyw1E=";
}
if (android.text.TextUtils.isEmpty(c)) {
c = "bHUvfbiVZUmm2sQRKwiAcw==";
}
if (z) {
try {
java.lang.String encodeToString = android.util.Base64.encodeToString(
com.douban.frodo.utils.AppContext.a().getPackageManager().getPackageInfo(
com.douban.frodo.utils.AppContext.a().getPackageName(),
64).signatures[0].toByteArray(), 0);
b = com.douban.frodo.utils.crypto.AES.a(b, encodeToString);
c = com.douban.frodo.utils.crypto.AES.a(c, encodeToString);
} catch (android.content.pm.PackageManager.NameNotFoundException e2) {
e2.printStackTrace();
}
}
}
public static java.lang.String f() {
if (android.text.TextUtils.isEmpty(d)) {
return "d40568d833";
}
return d;
}
}
于是我们也就知道了,所谓的“SecretKey”,其实就是c = "bHUvfbiVZUmm2sQRKwiAcw=="; AES加密之后的值,encodeToString就是com.douban.frodo.utils.AppContext.a().getPackageName()包名信息。如果开发过安卓App大概会知道,这段代码用来获取当前应用的签名的,这是安卓的一种防篡改的安全机制。
虽然我们没办法直接拿到com.douban.frodo.utils.AppContext.a().getPackageName(),不过,其他应用也可以获取已安装应用的签名信息,只需要把对应app的包名作为参数传入。
于是...
Application application=(Application)getApplicationContext();
PackageInfo packageInfo=application.getPackageManager().getPackageInfo("com.douban.frodo",PackageManager.GET_SIGNATURES);
String sign=Base64.encodeToString(packageInfo.signatures[0].toByteArray(),0);
最后我们把上面的签名代码跑一下,便可以得到 SecretKey = "bf7dddc7c9cfe6f7"
很好,很给力,明显可以下班了。
代码?
我...
也...
懒...
得...
写...
需要的朋友,可以到 豆瓣app签名算法分析与解密 自取。
3.2 ratel-core Android逆向分析工具套件
同时作为一套完善闭环的工具链,平头哥的相关功能是非常多的。
一句话概括:非ROOT情况下,通过插件机制随便改现有App功能。
于是,我们来试试水。
3.2.1 准备ratel-core编译环境
参考上面这个配置好本地的ANDROID_NDK_HOME + ANDROID_SDK_ROOT
PS: 这玩意如果自己不熟悉,多翻翻教程。
下载https://github.com/virjarRate... 源码
$ git clone https://github.com/virjarRate...
$ cd ratel-core/
$ ./script/create-dist.sh
以上操作都没问题,都正常编译之后,
就可以使用 ./script/ratel.sh 来重打包 App了。
➜ script git:(master) cd dist
➜ dist git:(master) ./ratel.sh ~/Downloads/leyoujia.apk
正常情况下会生成一个新的apk文件,最后一步就是把这个apk安装到手机上了。
安装好了之后,在手机端打开App,App不崩溃的话就说明成功了。
PS:遇到崩溃的情况,去GitHub提Issues。
3.2.2 准备ratel-module项目
“乐有家”就是刚刚重打包感染的App。
$ git clone https://github.com/virjarRate...
$ cd ratel-module-template/
$ ./template.sh ~/Downloads/leyoujia.apk
然后在 android-studio中打开ratel-module-template整个项目,
等一下Index代码和还原相关包文件之列的。
试一下编译安装“crack-乐有家”到手机端是不是OK。
正常情况下,这个插件App会直接启动,
而且在 Ratel Manager 的模块里面可以看到新的插件App
默认插件项目已经有了一个有趣的功能 —“ 插入悬浮按钮”
// 添加悬浮窗
private static void addFloatingButtonForActivity(final RC_LoadPackage.LoadPackageParam lpparam) {
RposedHelpers.findAndHookMethod(Activity.class, "onCreate", Bundle.class, new RC_MethodHook() {
@Override
protected void afterHookedMethod(final MethodHookParam param) throws Throwable {
new Handler(Looper.getMainLooper())
.postDelayed(new Runnable() {
@Override
public void run() {
createAndAttachFloatingButtonOnActivity((Activity) param.thisObject);
}
}, 1000);
}
private void createAndAttachFloatingButtonOnActivity(Activity activity) {
Context context = RatelToolKit.ratelResourceInterface.createContext(lpparam.modulePath, HookEntry.class.getClassLoader(), RatelToolKit.sContext);
FrameLayout frameLayout = (FrameLayout) activity.getWindow().getDecorView();
LayoutInflater.from(context).cloneInContext(context)
.inflate(R.layout.float_button, frameLayout);
}
});}
到这里,ratel-module 插件项目已经正常运行了,
Demo代码我们也已经跑起来了。
3.2.3 再做点有趣的玩意?
A. 直接信任所有本地用户cert证书
// package ratel.com.jjs.android.butler;
// public class HookEntry implements IRposedHookLoadPackage {}
// handleLoadPackage 函数里面新增一行代码,然后重新编译安装,
// 手机上操作刷新插件后重新打开App
JustTrustMe.trustAllCertificate();
于是HTTPS请求无所遁形。
curl --location --request POST 'https://steward.leyoujia.com/...' \
--header 'host: steward.leyoujia.com' \
--header 'clientid: e14becf6-2e13-43ae-b453-bd9cd35354a4' \
--header 'd: 0' \
--header 'latitude;' \
--header 'channel: online_32' \
--header 'imsi: 460110136201976' \
--header 'uuid: e14becf6-2e13-43ae-b453-bd9cd35354a4' \
--header 'ssid: 00000000378d5761ffffffffa488b92e' \
--header 'version: 8.1.9' \
--header 'mac: 64:BC:0C:44:64:01' \
--header 'network: WIFI' \
--header 'cit: 001729' \
--header 'sid: 38e5d3e9be36f0508dff7415ffffc076' \
--header 'phonemodel: Maru on the Nexus 5X' \
--header 'phoneos: android' \
--header 'carries: 0' \
--header 'imei: 35362607298355' \
--header 'aid: APP001' \
--header 'clientsign: 39ea04d3954db18df716e21978f009df' \
--header 'androidid: aabfdc5bb198e3b7' \
--header 'oaid: 00000000378d5761ffffffffa488b92e' \
--header 'longitude;' \
--header 'timestamp: 1639312110572' \
--header 'content-type: application/x-www-form-urlencoded' \
--header 'user-agent: okhttp/3.9.1' \
--header 'Connection: close' \
--data-urlencode 'cityCode=001729'
细心一看,clientsign 签名赫然其中。
好家伙,又要开始搞事情了....
回到 3.1的操作,生成一份“leyoujia-src”走起.
B. 重新开搞leyoujia-src代码
搜索“clientSign”,大概就能看到代码了
java.lang.String encode32 = com.jjshome.common.utils.MD5.encode32(
com.jjshome.common.utils.MD5.encode32(sb.toString()));
package com.jjshome.common.utils;
public class MD5 {
}
大概故事又成了什么看到sb字符串怎么来的了。
此处省略,自行折腾了。
PS:
N1、总结
参考阅读:
未完待续....
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。