好友
阅读权限20
听众
最后登录1970-1-1
|
red1y
发表于 2022-10-23 19:04
本帖最后由 red1y 于 2022-10-23 19:08 编辑
0. 本文包括
一、防护分析
-
提示不能在模拟器里运行
-
-
定位检测代码
入口:com.niming.weipa.ui.splash.SplashActivity
@Override // com.niming.weipa.base.BaseActivity
protected void onCreate(@nullable Bundle arg2) {
if(com.blankj.utilcode.util.e.d()) {
com.blankj.utilcode.util.e.b(this, false);
}
super.onCreate(arg2);
Intent v2 = this.getIntent();
Intrinsics.checkExpressionValueIsNotNull(v2, "intent");
if((v2.getFlags() & 0x400000) != 0) {
this.finish();
}
两次super 到父类的onCreate 函数中,查看整个启动逻辑,定位到initView 函数
@Override // androidx.appcompat.app.AppCompatActivity
protected void onCreate(@Nullable Bundle arg5) {
super.onCreate(arg5);
this.supportRequestWindowFeature(1);
/* other codes */
this.activity = this;
this.loadingDialogFragment = new com.niming.framework.widget.dialog.LoadingDialogFragment.a().a(true).a();
this.pageStatusHelper = new PageStatusHelper(this, this.getPageBuilder());
this.pageStatusHelper.a(this);
this.init();
this.initView(arg5); // key
}
回到SplashActivity 的initView 函数,发现this.m() 函数
@Override // com.niming.weipa.base.BaseActivity
public void initView(@Nullable Bundle arg1) {
super.initView(arg1);
v0.d(this);
com.blankj.utilcode.util.e.d(this, false);
com.blankj.utilcode.util.e.a(false);
this.m(); // key
}
进入this.m() ,发现检测逻辑
private final void m() {
LogUtils.b("niming", new Object[]{"===AppUtils.isAppRoot():" + com.blankj.utilcode.util.c.isAppRoot() + " DeviceUtils.isDeviceRooted() " + w.isDeviceRoot()});
if(!this.checkRoot() && !n.isEmulator(this.activity)) {
if(this.checkProxy(this)) {
NoticeAppDialogFragment v0 = NoticeAppDialogFragment.a("检测到您使用了代{过}{滤}理软件,不允许继续使用");
/* notice codes */
v0.c(this.activity);
return;
}
h.a().a("api_domain", "");
this.i();
return;
}
NoticeAppDialogFragment v0_1 = NoticeAppDialogFragment.a("检测到您使用的是模拟器或者设备已经root,不允许继续使用");
/* notice codes */
v0_1.c(this.activity);
}
-
root 检测方法
- 通过
getRuntime 尝试执行su 命令
public static b a(String[] arg8, String[] arg9, boolean arg10, boolean arg11) {
StringBuilder v8_2;
StringBuilder v11;
StringBuilder v10_2;
Process v9 = null;
BufferedReader v4 = null;
BufferedReader v5 = null;
DataOutputStream v10 = null;
String v1 = "";
int v2 = -1;
if(arg8 != null && arg8.length != 0) {
BufferedReader v3 = null;
try {
Runtime v4_1 = Runtime.getRuntime();
String v10_1 = arg10 ? "su" : "sh";
v9 = v4_1.exec(v10_1, arg9, null);
v10 = new DataOutputStream(v9.getOutputStream());
}
/* other codess */
return new b(v2, v8_6, v1);
}
return new b(-1, "", "");
}
- 检查特定目录下的
su 文件是否存在
public static boolean isDeviceRoot() {
String[] v0 = {"/system/bin/", "/system/xbin/", "/sbin/", "/system/sd/xbin/", "/system/bin/failsafe/", "/data/local/xbin/", "/data/local/bin/", "/data/local/", "/system/sbin/", "/usr/bin/", "/vendor/bin/"};
int v3;
for(v3 = 0; v3 < v0.length; ++v3) {
if(new File(v0[v3] + "su").exists()) {
return true;
}
}
return false;
}
-
emulator 检测方法
- 检测系统属性
private static final boolean c(Context arg1) {
return "1".equals(n.a(arg1, "ro.kernel.qemu"));
}
- 检测特征值
if((Build.MANUFACTURER.equals("unknown")) || (Build.MANUFACTURER.equals("Genymotion")) || (Build.MANUFACTURER.contains("Andy")) || (Build.MANUFACTURER.contains("MIT")) || (Build.MANUFACTURER.contains("nox")) || (Build.MANUFACTURER.contains("TiantianVM"))) {
++v15;
}
/* 很多特征值 */
if((Build.HARDWARE.equals("goldfish")) || (Build.HARDWARE.equals("vbox86")) || (Build.HARDWARE.contains("nox")) || (Build.HARDWARE.contains("ttVM_x86"))) {
++v15;
}
/* 很多特征值 */
if((Build.FINGERPRINT.contains("generic/sdk/generic")) || (Build.FINGERPRINT.contains("generic_x86/sdk_x86/generic_x86")) || (Build.FINGERPRINT.contains("Andy")) || (Build.FINGERPRINT.contains("ttVM_Hdragon")) || (Build.FINGERPRINT.contains("generic_x86_64")) || (Build.FINGERPRINT.contains("generic/google_sdk/generic")) || (Build.FINGERPRINT.contains("vbox86p")) || (Build.FINGERPRINT.contains("generic/vbox86p/vbox86p"))) {
++v15;
}
- 检测
OPENGL 的属性值
try {
String v2_1 = GLES20.glGetString(0x1F01);
if(v2_1 != null) {
if(v2_1.contains("Bluestacks")) {
goto label_260;
}
else {
boolean v2_2 = v2_1.contains("Translator");
goto label_257;
}
}
}
catch(Exception v2) {
v2.printStackTrace();
}
goto label_262;
label_257:
if(v2_2) {
v15 += 10;
goto label_262;
label_260:
v15 += 10;
}
- 检测共享文件夹
try {
label_262:
boolean v2_4 = new File(Environment.getExternalStorageDirectory().toString() + File.separatorChar + "windows" + File.separatorChar + "BstSharedFolder").exists();
}
catch(Exception v2_3) {
v2_3.printStackTrace();
n.b = v15;
return n.b > 3;
}
-
处理:hook掉
-
hook失败,定位反hook代码
-
SplashActivity 中没有发现相关代码,到application 的app com.niming.weipa.App 的onCreate 函数中寻找
<application android:allowBackup="true" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:icon="@drawable/icon_douying_logo" android:label="抖x" android:name="com.niming.weipa.App"
-
发现disableXposed 函数 (名字当然是我改的...)
@Override // com.niming.framework.base_app.BaseApplication
public void onCreate() {
super.onCreate();
App.u0 = this;
if(this.a(this)) {
g1.a(this);
this.b();
this.c();
com.shuyu.gsyvideoplayer.i.c.a(com.niming.weipa.d.c.class);
com.shuyu.gsyvideoplayer.f.a.a(com.niming.weipa.d.d.class);
/* other codes */
com.lahm.library.d.disableXposed(); // 反hook
/* other codes */
}
}
-
查看disableXposed 实现,通过反射设置disableHooks 字段为true
public boolean disableXposed() {
try {
Field v1_3 = ClassLoader.getSystemClassLoader().loadClass("de.robv.android.xposed.XposedBridge").getDeclaredField("disableHooks");
v1_3.setAccessible(true);
v1_3.set(null, Boolean.TRUE);
return true;
}
/* exception codes return false */
}
-
处理:hook掉,在函数返回后,同样使用反射将disableHooks 设置为false
-
抓不到包,分析反抓包机制
-
定位反抓包代码,从app 的onCreate 入手
@Override // com.niming.framework.base_app.BaseApplication
public void onCreate() {
super.onCreate();
App.u0 = this;
if(this.a(this)) {
g1.a(this);
this.b();
this.c();
com.shuyu.gsyvideoplayer.i.c.a(com.niming.weipa.d.c.class);
com.shuyu.gsyvideoplayer.f.a.a(com.niming.weipa.d.d.class);
/* other codes */
com.lahm.library.d.disableXposed(); // 反hook
/* other codes */
}
}
-
进入this.c() ,进行了大量关于http 请求的配置,AntiCapture 是我一开关注的类,后来发现这个类什么都没干;最终得出结论:抓不到包是因为设置了NO_PROXY 属性,app 不走系统代{过}{滤}理,因此抓不到包
private void c() {
HttpHeaders v0 = new HttpHeaders();
v0.put("User-Agent", "Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleDart/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17");
try {
this.t0 = new b();
this.t0.d(30000L, TimeUnit.MILLISECONDS);
/* other codes */
AntiCapture v1 = a.a(); // 这里获得有关ssl证书的参数,并在下一行设置
this.t0.a(v1.a, v1.b); // 经过分析,这两个参数都没用,并不是反抓包的实现机制
this.t0.a(new c());
this.t0.a(new com.niming.weipa.app.a.a());
this.t0.a(new com.niming.weipa.app.a.b());
/* other codes */
this.t0.a(new ProxySelector() {
@Override
public void connectFailed(URI arg1, SocketAddress arg2, IOException arg3) {
}
@Override
public List select(URI arg1) {
/* 抓不到包的原因所在! */
return Collections.singletonList(Proxy.NO_PROXY);
/* 抓不到包的原因所在! */
}
});
c.f.a.b.k().a(this).a(this.t0.a()).a(CacheMode.NO_CACHE).a(-1L).a(0).a(v0);
}
/* other codes */
}
-
处理:hook掉,在函数调用前,将参数设置为Proxy.getDefault() ,默认走系统代{过}{滤}理
-
签名校验
- 生成可调试版本后,
app 会停在某个页面,通过查看日志发现是native 层作了签名校验
- 我一般不分析签名校验,有兴趣可以自己分析
-
防护类分析
-
通过disableXposed 那个函数,发现了一个很完整的java 层加密类,里面实现了很多的防护函数,这里完全拷贝下来,名称我都作了修改
package com.lahm.library;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
import android.os.Debug;
import android.os.Process;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.Iterator;
public class i {
static class b {
private static final i a;
static {
b.a = new i(null);
}
}
private static final String a = "de.robv.android.xposed.XposedHelpers";
private static final String b = "de.robv.android.xposed.XposedBridge";
private i() {
}
i(com.lahm.library.i.a arg1) {
}
public String getAppMetaData(Context arg1, String arg2) {
return arg1.getApplicationInfo().metaData.getString(arg2);
}
public boolean isDebuggerConnected() { // 检测调试器是否连接
return Debug.isDebuggerConnected();
}
public boolean isPortAvailable(int arg2) { // 检测端口是否被占用,可用于反调试、反hook
try {
return this.isNetAddressAvailable("127.0.0.1", arg2);
}
catch(Exception unused_ex) {
return true;
}
}
public boolean isApkDebuggable(Context arg1) { // 检测apk:debuggable属性是否为true
return (arg1.getApplicationInfo().flags & 2) != 0;
}
public boolean isLibOrJarLoaded(String arg6) { // 检测加载的jar/so库
try {
HashSet v0 = new HashSet();
BufferedReader v1 = new BufferedReader(new FileReader("/proc/" + Process.myPid() + "/maps"));
while(true) {
String v2 = v1.readLine();
if(v2 != null) {
goto label_34;
}
v1.close();
Iterator v0_1 = v0.iterator();
while(true) {
label_24:
if(!v0_1.hasNext()) {
return false;
}
Object v1_1 = v0_1.next();
if(!((String)v1_1).contains(arg6)) {
goto label_24;
}
return true;
label_34:
if(!v2.endsWith(".so") && !v2.endsWith(".jar")) {
break;
}
v0.add(v2.substring(v2.lastIndexOf(" ") + 1));
break;
}
}
}
catch(Exception unused_ex) {
return false;
}
}
public boolean isNetAddressAvailable(String arg2, int arg3) throws UnknownHostException {
InetAddress v2 = InetAddress.getByName(arg2); // 检测网络地址是否可用
try {
new Socket(v2, arg3);
return true;
}
catch(IOException unused_ex) {
return false;
}
}
public boolean isDeviceRooted() { //检测设备是否root,里面两个函数的实现在下面
return this.isDeviceRootedByRoSecure() == 0 ? true : this.isDeviceRootedByFile();
}
public boolean isEmulatorByBattery(Context arg4) { // 通过检查电量检测模拟器
Intent v4 = arg4.registerReceiver(null, new IntentFilter("android.intent.action.BATTERY_CHANGED"));
return v4 == null ? false : v4.getIntExtra("plugged", -1) == 2;
}
public String getPackageSignature(Context arg5) { // 获取签名
try {
Signature[] v5_1 = arg5.getPackageManager().getPackageInfo(arg5.getPackageName(), 0x40).signatures;
StringBuilder v0 = new StringBuilder();
int v2;
for(v2 = 0; v2 < v5_1.length; ++v2) {
v0.append(v5_1[v2].toCharsString());
}
return v0.toString();
}
catch(PackageManager.NameNotFoundException v5) {
v5.printStackTrace();
return "";
}
}
public boolean isXposedExistByStack() { // 通过异常栈检测Xposed框架
try {
throw new Exception("gg");
}
catch(Exception v0) {
StackTraceElement[] v0_1 = v0.getStackTrace();
int v3;
for(v3 = 0; v3 < v0_1.length; ++v3) {
if(v0_1[v3].getClassName().contains("de.robv.android.xposed.XposedBridge")) {
return true;
}
}
return false;
}
}
@Deprecated
public boolean isXposedExistByClassLoader() { // 通过类加载检测Xposed框架
try {
ClassLoader.getSystemClassLoader().loadClass("de.robv.android.xposed.XposedHelpers").newInstance();
}
catch(InstantiationException v0_1) {
v0_1.printStackTrace();
return true;
}
catch(IllegalAccessException v0) {
v0.printStackTrace();
return true;
}
catch(ClassNotFoundException v1) {
v1.printStackTrace();
return false;
}
try {
ClassLoader.getSystemClassLoader().loadClass("de.robv.android.xposed.XposedBridge").newInstance();
return true;
}
catch(InstantiationException v0_3) {
v0_3.printStackTrace();
return true;
}
catch(IllegalAccessException v0_2) {
v0_2.printStackTrace();
return true;
}
catch(ClassNotFoundException v1_1) {
v1_1.printStackTrace();
return false;
}
}
public boolean isDebuggingByTarcePid() { // 通过TracePid检测程序是否出于被调试状态
try {
BufferedReader v1 = new BufferedReader(new FileReader("/proc/" + Process.myPid() + "/status"));
String v2 = "";
do {
label_17:
String v3 = v1.readLine();
if(v3.contains("TracerPid")) {
v2 = v3.substring(v3.indexOf(":") + 1, v3.length()).trim();
}
else if(v3 != null) {
goto label_17;
}
break;
}
while(true);
v1.close();
return !"0".equals(v2);
}
catch(Exception unused_ex) {
return false;
}
}
public boolean disableXposed() { // disable xposed
try {
Field v1_3 = ClassLoader.getSystemClassLoader().loadClass("de.robv.android.xposed.XposedBridge").getDeclaredField("disableHooks");
v1_3.setAccessible(true);
v1_3.set(null, Boolean.TRUE);
return true;
}
catch(NoSuchFieldException v1_2) {
v1_2.printStackTrace();
return false;
}
catch(ClassNotFoundException v1_1) {
v1_1.printStackTrace();
return false;
}
catch(IllegalAccessException v1) {
v1.printStackTrace();
return false;
}
}
public static final i getInstance() {
return b.a;
}
private int isDeviceDebuggable() { // ro.debuggable为1的设备可以调试任何应用,即时应用的debuggable属性为false
String v0 = c.a().b("ro.debuggable");
return v0 == null || !"0".equals(v0) ? 1 : 0;
}
private int isDeviceRootedByRoSecure() { // 检测ro.secure
String v0 = c.a().b("ro.secure");
return v0 == null || !"0".equals(v0) ? 1 : 0;
}
private boolean isDeviceRootedByFile() { // 通过文件检测设备是否root
String[] v0 = {"/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su", "/system/bin/failsafe/su", "/data/local/su"};
int v3;
for(v3 = 0; v3 < v0.length; ++v3) {
if(new File(v0[v3]).exists()) {
return true;
}
}
return false;
}
}
二、加密分析
-
定位加密代码
-
这个app 通过拦截器实现请求加密,以及携带特殊参数,通过app 的onCreate ,进入this.c() ,接着定位到加密拦截器
private void c() {
HttpHeaders v0 = new HttpHeaders();
v0.put("User-Agent", "Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleDart/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17");
try {
/* other codes */
this.t0.a(new c()); // TOKEN拦截器
this.t0.a(new com.niming.weipa.app.a.a()); // 加密拦截器
this.t0.a(new com.niming.weipa.app.a.b());
/* other codes */
}
catch(Exception v0_1) {
v0_1.printStackTrace();
}
}
-
进入com.niming.weipa.app.a.a() ,发现其中的b 函数
private b0 a(b0 arg9) {
/* other codes */
v1.put("data", v2);
String v1_1 = g.a(v1);
LogUtils.b(new Object[]{"===realParamsString: " + v1_1});
String v1_2 = URLEncoder.encode(com.niming.weipa.utils.a.b(v1_1)); // 加密函数
LogUtils.b(new Object[]{"===encrypt realParamsString: " + v1_2});
new okhttp3.s.a().b("data", v1_2);
return arg9;
}
-
定位到加密类com.niming.weipa.utils.a ,使用AES 加密,ECB 模式,PKCS7 的padding
package com.niming.weipa.utils;
import android.text.TextUtils;
import com.blankj.utilcode.util.y;
public class a {
private static String a = "";
private static String b = "pcv#Cg1vbdl#r2hm";
private static String c = "AES/ECB/PKCS7Padding"; // 加密算法、加密模式、padding模式
private static String d; // 加密密钥
static {
/* other codes */
v0 = TextUtils.equals("release", "staging") ? TestUtil.getSecretPre() : TestUtil.getSecret();
a.d = v0;
}
public static String b(String arg3) {
if(a.c.equals("AES/ECB/NoPadding")) {
while(arg3.getBytes().length % 16 != 0) {
arg3 = arg3 + ' ';
}
}
byte[] v0 = a.d.getBytes(); // 获得密钥
String v1 = a.c; // 加密方式
byte[] v3 = y.k(arg3.getBytes(), v0, v1, null);
return v3 == null ? "" : new String(v3);
}
}
-
获得加密密钥
-
密钥通过native 函数获得,可通过hook 方式得到加密密钥
package com.niming.weipa.utils;
public class TestUtil {
static {
System.loadLibrary("security");
}
public static native String getSecret() {
}
public static native String getSecret2() {
}
public static native String getSecret3() {
}
public static native String getSecretPre() {
}
public static native String getSecretVP() {
}
}
-
验证加密算法
三、设备注册分析
-
本地sqlite 数据库
-
用户信息存储在本地sqlite 数据库中,可以通过sqlite3 或者导出数据的方式查看数据库数据,数据库路径为/data/data/package_name/database/db_name
package com.niming.framework.basedb;
import android.content.Context;
import androidx.room.Database;
import androidx.room.RoomDatabase;
import androidx.room.q1;
@Database(entities = {e.class, a.class}, version = 1)
public abstract class BaseAppDatabase extends RoomDatabase {
private static volatile BaseAppDatabase a;
static BaseAppDatabase a(Context arg2) {
Class v0 = BaseAppDatabase.class;
if(BaseAppDatabase.a == null) {
synchronized(v0) {
if(BaseAppDatabase.a == null) {
BaseAppDatabase.a = (BaseAppDatabase)q1.a(arg2.getApplicationContext(), v0, "base_database").a().b();
}
return BaseAppDatabase.a;
}
}
return BaseAppDatabase.a;
}
}
-
如果本地不存在数据库以及用户信息,会触发新设备注册逻辑,可以手动删除数据库进而触发该逻辑
-
设备注册逻辑
-
首次注册设备时,向服务器发送的未加密数据格式为,其中device_no 唯一标识一个设备,且在客户端生成,可以随意修改
{"channel":"","code":"","device_no":"cd17c0a9-1d41-3377-8b3c-2599755976f8","device_type":"A","version":"4.0.5"}
-
在服务器返回的响应中会包含token ,与device_no 对应,用于生成XTOKEN 请求头参数,作为用户身份标识,后续所有请求需携带该字段
-
XTOKEN 认证参数生成
-
可直接通过搜索XTOKEN 关键字定位到生成逻辑,很简单,通过注册设备时发送给服务器的deivce 信息以及服务器返回的token 信息,经序列化后加密即生成了最终的X-TOKEN
@Override // okhttp3.w
public d0 a(a arg6) throws IOException {
/* other codes */
HashMap v2 = new HashMap();
v2.put("token", v0_3);
v2.put("device_no", h.a().c("device_id"));
v2.put("device_type", "A");
v2.put("version", "4.0.5");
JSONObject v3 = new JSONObject(v2);
if(!TextUtils.isEmpty(v0_3)) {
v1.a("X-TOKEN", com.niming.weipa.utils.a.b(String.valueOf(v3)));
}
return arg6.a(v1.a());
}
-
获得新帐号、绑定邀请码、获得vip
- 在以上分析的基础上,通过不同的
device_no ,生成相应的XTOKEN 即可拥有新用户帐号,利用该app 邀请码绑定送vip 机制,可以获得大量用户凭证并绑定自己的邀请码,获得持续vip 功能,演示可以观看bilibili 视频
|
免费评分
-
查看全部评分
|