使用Shizuku和LSPatch对应用注入代码

通常来讲,想要修改第三方安卓应用的代码需要反编译。然而,反编译的操作很复杂,不仅要面对被混淆得面目全非的类、方法、字段名,还要考虑重新打包后会不会被应用的签名校验识别到。而现在,出现了一些工具,使得修改应用变得更加简单。

如果你的设备已经Root,可以直接安装LSPosed模块(使用KernelSU或者Magisk安装)。

如果你的设备没有Root,那也可以使用Shizuku和LSPatch来代替,这两个工具便是今天要介绍的主角。

一、安装

Shizuku: https://github.com/RikkaApps/Shizuku/releases 

LSPatch: https://github.com/LSPosed/LSPatch/release 

安装后激活Shizuku,给LSPatch授予Shizuku权限,然后自行选定一个目录用于存放LSPatch生成的安装包。

二、添加想要修改的应用

点击加号,点击“选择已安装的应用程序”,找到自己想要修改的应用。

修补模式有两种,这里讲述一下区别。如果想要自由选择应用使用哪些模块,随时开启或关闭,建议选择“本地模式”,此模式需要LSPatch保持运行;如果已经确定使用哪些模块,并且后期无需做修改,建议使用“集成模式”,该模式下应用的运行无需依赖LSPatch。当然,在开发过程中建议选择“本地模式”,毕竟修改起来更方便。

这里我选择自己写一个简单的应用,作为需要修改的应用。(应用名:XposedTestApp,包名:com.magicianguo.xposedtestapp)

布局文件 activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:textColor="@color/black"
        android:textSize="24sp" />

    <com.magicianguo.xposedtestapp.MySurfaceView
        android:layout_width="150dp"
        android:layout_height="150dp"
        android:layout_marginTop="20dp" />

</LinearLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private TextView mTvTitle;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTvTitle = findViewById(R.id.tv_title);
        mTvTitle.setText("你好,世界");
        // 窗口禁止截屏
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
    }
}

MySurfaceView.java

public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {
    private final Paint mPaintBg = new Paint();
    private final Paint mPaintLine = new Paint();
    private final Path mPath = new Path();

    public MySurfaceView(Context context) {
        super(context);
        init();
    }

    public MySurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MySurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaintBg.setColor(Color.rgb(0x33, 0x88, 0xFF));
        mPaintBg.setStyle(Paint.Style.FILL);
        mPaintLine.setColor(Color.WHITE);
        mPaintLine.setStyle(Paint.Style.STROKE);
        mPaintLine.setStrokeWidth(getResources().getDimension(R.dimen.sv_line_width));
        getHolder().addCallback(this);
        // 禁止截屏
        setSecure(true);
    }

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder holder) {

    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
        Canvas canvas = holder.lockCanvas();
        // 绘制整个背景
        canvas.drawRect(new Rect(0, 0, width, height), mPaintBg);
        // 绘制“Z”
        mPath.reset();
        mPath.moveTo(width * 0.25F, width * 0.25F);
        mPath.lineTo(width * 0.75F, width * 0.25F);
        mPath.lineTo(width * 0.25F, width * 0.75F);
        mPath.lineTo(width * 0.75F, width * 0.75F);
        canvas.drawPath(mPath, mPaintLine);
        holder.unlockCanvasAndPost(canvas);
    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder holder) {

    }
}

运行效果如图。此时点击截图会提示无法截图,因为MainActivity中添加了相关代码。

打开LSPatch,点击加号,选择已安装的应用程序,选择想要修改的应用,选择本地模式,点击开始修补、安装,替换原有的应用。(注意安装后的签名和原本的应用会不一致)

三、添加模块并使用

添加模块前,要思考自己的目的,是要对应用做哪些修改。

3.1 解除应用的截屏限制

举个例子,假如我想要在此应用中截图,也就意味着需要让MainActivity的以下方法失效:

getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);

查看Window.java源码,发现最终需要对setFlags方法的参数值进行修改。目的是让传入的多个FLAG中,只有FLAG_SECURE失效,其他的FLAG不影响。

public void addFlags(int flags) {
    setFlags(flags, flags);
}

明确思路就开始执行。新建模块“XposedAllowScreenshot”,添加如下依赖:

dependencies {
    compileOnly 'de.robv.android.xposed:api:82'
    // 这个注释掉,否则代码提示会消失!
//    compileOnly 'de.robv.android.xposed:api:82:sources'
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round">
        <!--是否为xposed模块,置为true生效-->
        <meta-data
            android:name="xposedmodule"
            android:value="true" />
        <!--xposed模块的描述-->
        <meta-data
            android:name="xposeddescription"
            android:value="@string/xposed_description" />
        <!--xposed模块最低API版本-->
        <meta-data
            android:name="xposedminversion"
            android:value="30" />
    </application>
</manifest>

strings.xml

<resources>
    <string name="app_name">XposedAllowScreenshot</string>
    <string name="xposed_description">用于解除应用的截屏限制</string>
</resources>

新建 HookAllowScreenshot 类,实现 IXposedHookLoadPackage 接口,在 handleLoadPackage 方法中添加相关逻辑。

package com.magicianguo.xposedallowscreenshot;

import android.view.SurfaceView;
import android.view.Window;
import android.view.WindowManager;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

public class HookAllowScreenshot implements IXposedHookLoadPackage {
    private final XC_MethodHook mHookDeleteWindowFlagSecure = new XC_MethodHook() {
        @Override
        protected void beforeHookedMethod(MethodHookParam param) {
            Integer flags = (Integer) param.args[0];
            // 只把FLAG_SECURE移除,其他不受影响
            param.args[0] = flags & (~WindowManager.LayoutParams.FLAG_SECURE);
        }
    };

    private final XC_MethodHook mHookDeleteSurfaceSecure = new XC_MethodHook() {
        @Override
        protected void beforeHookedMethod(MethodHookParam param) {
            param.args[0] = false;
        }
    };

    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) {
        XposedHelpers.findAndHookMethod(Window.class, "setFlags", Integer.TYPE, Integer.TYPE, mHookDeleteWindowFlagSecure);
        XposedHelpers.findAndHookMethod(SurfaceView.class, "setSecure", Boolean.TYPE, mHookDeleteSurfaceSecure);
    }
}

findAndHookMethod 方法的作用是“追踪”某个特定的方法。当应用执行到此方法之前/之后你都可以执行一些额外的逻辑,或者对传入的参数值、返回值进行修改。

XC_MethodHook 对象中的 beforeHookedMethod 会在指定方法执行前回调( 同理, afterHookedMethod 对应执行后),param.args[0]代表指定方法中传入的第一个参数,param.args[1]代表第二个,依此类推。

如果想要修改返回值,可以在 afterHookedMethod 调用 param.setResult(object) 方法。

因此,以上的代码主要作用是在 Window 的 setFlags 方法执行前,将传入的 flags 参数进行处理,移除里面的 FLAG_SECURE ;在 SurfaceView 的 setSecure 方法执行前把布尔参数 true 改成 false。

之后,需要在模块的assets文件夹中新建“xposed_init”文件,里面写上以上类的包名加类名:

com.magicianguo.xposedallowscreenshot.HookAllowScreenshot

于是模块写完了。点击运行使其运行到设备中,在LSPatch中能够看到此模块:

点击之前的应用,选择模块作用域,选择刚才添加的模块,重新运行应用。会发现应用可以正常截屏了。

3.2 修改Activity的布局

假如,我想要在应用的布局中加入3个按钮,并且修改标题文字,就像如下效果。

并且点击第一个按钮跳转到系统设置页,点击第二个按钮重新绘制图中的SurfaceView,点击第三个按钮跳转应用内的其他Activity。该如何实现?

添加模块的操作和前面的类似。这里我们新建“XposedChangeView”模块。

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round">
        <meta-data
            android:name="xposedmodule"
            android:value="true" />
        <meta-data
            android:name="xposeddescription"
            android:value="@string/xposed_description" />
        <meta-data
            android:name="xposedminversion"
            android:value="30" />
        <!-- 用于说明此模块适合作用于哪些应用(包名) -->
        <meta-data
            android:name="xposedscope"
            android:resource="@array/xposed_scope" />
    </application>
</manifest>

strings.xml

<resources>
    <string name="app_name">XposedChangeView</string>
    <string name="xposed_description">修改应用XposedTestApp的布局(对其他应用无效)</string>
</resources>

arrays.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <array name="xposed_scope">
        <item>com.magicianguo.xposedtestapp</item>
    </array>
</resources>

新建“HookChangeView”类:

package com.magicianguo.xposedchangeview;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.view.SurfaceView;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

public class HookChangeView implements IXposedHookLoadPackage {
    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) {
        if (TextUtils.equals(lpparam.packageName, "com.magicianguo.xposedtestapp")) {
            Class<?> clsMainActivity = XposedHelpers.findClass("com.magicianguo.xposedtestapp.MainActivity", lpparam.classLoader);
            XposedHelpers.findAndHookMethod(clsMainActivity, "onCreate", Bundle.class, new XC_MethodHook() {
                @Override
                protected void afterHookedMethod(MethodHookParam param) {
                    Activity activity = (Activity) param.thisObject;
                    Toast.makeText(activity, "APP已被Xposed修改!", Toast.LENGTH_SHORT).show();
                    TextView mTvTitle = (TextView) XposedHelpers.getObjectField(activity, "mTvTitle");
                    mTvTitle.setText("你好,Xposed框架");
                    // 找到mTvTitle的父布局
                    LinearLayout parent = (LinearLayout) mTvTitle.getParent();
                    SurfaceView surfaceView = null;
                    for (int i = 0; i < parent.getChildCount(); i++) {
                        View v = parent.getChildAt(i);
                        if (v instanceof SurfaceView) {
                            surfaceView = (SurfaceView) v;
                            break;
                        }
                    }
                    Class<?> clsWebViewActivity = XposedHelpers.findClass("com.magicianguo.xposedtestapp.WebViewActivity", lpparam.classLoader);
                    parent.addView(ViewTools.createView(activity, surfaceView, clsWebViewActivity), 0);
                }
            });
        } else if (lpparam.packageName.contains(".android.webview")) {
            // ignore
        } else {
            new Handler(Looper.getMainLooper()).postDelayed(() -> System.exit(-1), 100L);
            throw new RuntimeException("请不要将此插件集成在包名“com.magicianguo.xposedtestapp”以外的应用!packageName: "+lpparam.packageName);
        }
    }
}

以上代码中,我们根据类名找到了MainActivity的class对象,并且“追踪”它的 onCreate 方法。使用param.thisObject 获取到 MainActivity 对象,然后就可以逐渐找到 其包含的View对象,然后添加View。(对于一个没有源代码的应用,可以使用jadx查看安装包,查看里面的类名和资源文件)

ViewTools.java

package com.magicianguo.xposedchangeview;

import android.app.Activity;
import android.content.Intent;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.provider.Settings;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;

import org.jetbrains.annotations.Nullable;

public class ViewTools {
    public static View createView(Activity activity, @Nullable SurfaceView surfaceView, Class<?> clsWebViewActivity) {
        // 获取dpi,便于使用dp为单位的尺寸
        int dpi = activity.getResources().getConfiguration().densityDpi;

        LinearLayout linearLayout = new LinearLayout(activity);
        LinearLayout.LayoutParams linearlayoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        linearlayoutParams.bottomMargin = 20 * dpi / 160;
        linearLayout.setLayoutParams(linearlayoutParams);
        linearLayout.setOrientation(LinearLayout.VERTICAL);
        // 使用dp为单位
        int padding = 10 * dpi / 160;
        linearLayout.setPadding(padding, padding, padding, padding);
        linearLayout.setBackgroundColor(Color.rgb(0xEB, 0xA8, 0xFF));

        Button btn1 = new Button(activity);
        btn1.setText("打开设置页");
        btn1.setOnClickListener(v -> {
            Intent intent = new Intent(Settings.ACTION_SETTINGS);
            activity.startActivity(intent);
        });
        linearLayout.addView(btn1);

        Button btn2 = new Button(activity);
        btn2.setText("重新绘制SurfaceView");
        btn2.setAllCaps(false);
        btn2.setOnClickListener(v -> {
            if (surfaceView != null) {
                int width = surfaceView.getMeasuredWidth();
                int height = surfaceView.getMeasuredHeight();
                SurfaceHolder holder = surfaceView.getHolder();
                Canvas canvas = holder.lockCanvas();
                Paint paintBg = new Paint();
                paintBg.setColor(Color.rgb(0x1D, 0xED, 0x99));
                paintBg.setStyle(Paint.Style.FILL);
                canvas.drawRect(new Rect(0, 0, width, height), paintBg);

                // 绘制“A”
                Path path = new Path();
                path.moveTo(width * 0.5F, height * 0.2F);
                path.lineTo(width * 0.3F, height * 0.8F);
                path.moveTo(width * 0.5F, height * 0.2F);
                path.lineTo(width * 0.7F, height * 0.8F);
                path.moveTo(width * 0.4F, height * 0.5F);
                path.lineTo(width * 0.6F, height * 0.5F);
                Paint paintLine = new Paint();
                paintLine.setColor(Color.WHITE);
                paintLine.setStyle(Paint.Style.STROKE);
                paintLine.setStrokeWidth((5 * dpi) / 160F);
                canvas.drawPath(path, paintLine);
                holder.unlockCanvasAndPost(canvas);
            }
        });
        linearLayout.addView(btn2);

        Button btn3 = new Button(activity);
        btn3.setText("打开WebViewActivity");
        btn3.setAllCaps(false);
        btn3.setOnClickListener(v -> {
            Intent intent = new Intent(activity, clsWebViewActivity);
            activity.startActivity(intent);
        });
        linearLayout.addView(btn3);

        return linearLayout;
    }
}

安装模块并使其生效,效果如下:

四、总结

使用LSPatch和Shizuku可以对应用注入代码,修改方法的参数或返回值,但是它需要对应用重新打包,重新打包后,签名会发生改变。虽然它提供了跳过签名校验的选项,但是部分应用(例如MT管理器)仍然能够检测到应用被修改,导致应用无法正常运行。因此最靠谱的方案还是Root之后使用LSPosed模块,该方案无需对应用重新打包,可以直接将模块作用在应用上。

源代码: GitHub - MagicianGuo/Android-XposedTest: Xposed模块,实现修改应用内布局、解除应用截图限制等功能。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值