首页
社区
课程
招聘
整体加固Demo及加壳工具的编写
发表于: 1天前 381

整体加固Demo及加壳工具的编写

1天前
381

02_整体加固Demo及加壳工具的编写

一、实验环境准备

​ 首先来准备一下demo,这里写一个小程序,能够体现成功运行的效果即可。

​ 首先创建一个Empty Views Activity的项目

​ 接着画一下界面,这里就使用两个TextView组件,一个里面写的是MyApplication is not Loaded!!,另外一个是MainActivity is not Loaded。展现效果就是启动app之后,这两个textViewnot Loaded变成loaded

​ 接下来是代码的编写

  • MyApplication
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyApplication extends Application {
 
    @Override
    public void onCreate() {
        super.onCreate();
        Log.d("[+]","MyApplication's OnCreate is calling...");
    }
 
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        Log.d("[+]","MyApplication's attachBaseContext is calling...");
    }
}
  • MainActivity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MainActivity extends AppCompatActivity {
 
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        TextView tv_application = (TextView) findViewById(R.id.text_Application);
        TextView tv_activity = (TextView) findViewById(R.id.text_Activity);
        if(getApplication() instanceof MyApplication){
            tv_application.setText("MyApplication is loaded");
        }
        tv_activity.setText("MainActivity is loaded!!");
    }
}

MainActivity当中通过getApplication获取全局唯一的一个Application实例对象,来判断该对象是不是属于MyApplication这个类。最后启动之后的效果如下(未加壳状态):
3

二、加壳代码的编写

1. 提取相关dex

​ 首先通过bandzip将对应的dex文件提取出来

3

​ 这里是classes3,只把这个留下来即可,接下来将这个dex文件放到项目当中的assests文件夹当中,准备编写壳程序,对于整体加固来说分成落地加固不落地加固,**这里使用落地加固,**及直接加载assests目录当中的dex文件。

2. 编写壳程序

1. 壳代码思路

​ 在之前的源码分析中,我们知道了在handleBindApplication当中会创建一个LoadedApk,接着会使用makeApplicationInner来获取一个Application

1
后续使用到的类加载器通过`getClassLoader`获取,而这个获取的就是`LoadedApk`当中的`mClassLoader`。

​ 由于app启动时执行的是壳代码的dex的类加载器,想要应用正常执行,就需要将类加载器也就是mClassLoader换成源程序的dex的加载器才能正常执行。可以通过源码查看一下mClassLoaderLoadedApk当中的属性:

​ 可以看到这一个私有非静态成员,那么需要获取LoadedApk才能够访问到这个成员,而且由于是私有成员,那么需要通过反射来在外部访问或者修改这个成员。通过将这个mClassLoader修改成原来的ClassLoader就能够正常往下执行了。

​ 在源码分析当中,简单的分析到app的执行流程如下:attachBaseContext --> ContentProvider.onCreate --> Application.OnCreate --> Activity.OnCreate。这里的attachBaseContext是先执行的,那我们的壳代码就可以放在这里,然后完成对环境的恢复。

  • 操作如下:

​ 首先从assets目录中读取加密的dex文件进行解密操作,接着将解密后的dex存放到私有目录当中,接着动态加载解密后的dex文件,将得到的ClassLoader替换掉mClassLoader完成环境的复原。

2. 获取LoadedApk

​ 那么有了思路,就需要来想想该如何获取这个LoadedApk了。有几种方法:

  • 通过Context获取:

​ 在ContextImpl当中有一个mPackageInfo的成员,类型是LoadedApkattachBaseContext传入的参数就是ContextImpl,这里就可以使用反射来获取这个mPackageInfo,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Object LoadedApk = null;
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    try{
        Context contextImpl = base;
        while(contextImpl instanceof ContextWrapper){
            contextImpl = ((ContextWrapper) contextImpl).getBaseContext();
        }
        Class<?> contextImplClass = contextImpl.getClass();
        Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
        mPackageInfoField.setAccessible(true);
 
        LoadedApk = mPackageInfoField.get(contextImpl);
    } catch (Exception e){
        e.printStackTrace();
    }
    // ...
}
  • 通过ActivityThread获取

​ 在ActivityThread的源码当中可以看到,ActivityThread有一个mPackages的成员,是一个map,键是包名(String),值是LoadedApk,这意味着我们可以通过获取这个mPackages来获取LoadedApk。这里获取方法也是反射

​ 获取ActivityThread可以使用静态方法currentActivityThreadMethod,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
 
    try{
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
        currentActivityThreadMethod.setAccessible(true);
        Object currentActivityThread = currentActivityThreadMethod.invoke(null);
        Field mPackagesField = activityThreadClass.getDeclaredField("mPackages");
        mPackagesField.setAccessible(true);
        Map<String,WeakReference<?>> mPackages = (Map<String, WeakReference<?>>) mPackagesField.get(currentActivityThread);
        String packageName = base.getPackageName();
        WeakReference<?> wr = mPackages.get(packageName);
        if(wr != null){
            LoadedApk = wr.get();
        }else{
            Log.e("[+]","获取LoadedApk失败");
        }
 
    }catch (Exception e){
        Log.e("[+]","获取LoadedApk失败");
        e.printStackTrace();
    }
// ...
}

3. 封装反射

​ 由于比较多地方使用到反射,如果一直都要写一遍反射的话会比较麻烦,这里直接封装一下,定义为Ref.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.example.protectdemo_1;
 
import java.lang.reflect.Field;
import java.lang.reflect.Method;
 
public class Ref {
    public static Object invokeStaticMethod(String class_name,String method_name,Class[] pareType,Object[] pareValues){
        try{
            Class obj_class = Class.forName(class_name);
            Method method = obj_class.getMethod(method_name,pareType);
            return method.invoke(obj_class,pareValues);
        } catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
 
    public static Object invokeMethod(String class_name, String method_name, Object obj,Class[] pareTyple, Object[] pareValues){
        try{
            Class obj_class = Class.forName(class_name);
            Method method = obj_class.getMethod(method_name, pareTyple);
            return method.invoke(obj,pareValues);
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
 
    public static Object getFieldObject(String class_name, Object obj, String fileName){
        try{
            Class obj_class = Class.forName(class_name);
            Field field = obj_class.getDeclaredField(fileName);
            field.setAccessible(true);
            return field.get(obj);
        }catch(Exception e){
            e.printStackTrace();
        }
        return null;
    }
 
    public static Object getStaticFieldOjbect(String class_name, String filedName){
        try {
            Class obj_class = Class.forName(class_name);
            Field field = obj_class.getDeclaredField(filedName);
            field.setAccessible(true);
            return field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
 
    }
    public static void setFieldObject(String classname, String filename, Object obj, Object fileValue){
        try{
            Class obj_class = Class.forName(classname);
            Field field = obj_class.getDeclaredField(filename);
            field.setAccessible(true);
            field.set(obj,fileValue);
        }catch(Exception e){
            e.printStackTrace();
        }
    }
 
    public static void setStaticObject(String class_name, String filedName, Object filedValue){
        try{
            Class obj_class = Class.forName(class_name);
            Field field = obj_class.getDeclaredField(filedName);
            field.setAccessible(true);
            field.set(null,filedValue);
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

​ 使用的时候直接调用:Ref.xxx即可。

4. 壳代码编写

​ 这里使用ActivityThread的方法来获取LoadedApk,并且从文件中加载dex,使用反射替换mClassLoader。来实现环境的修复,首先需要在main文件夹下创建一个assests的文件夹,然后将被保护的dex文件放入文件夹当中(这里实验使用落地加固),如下:

实验代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        Log.i("[+]","[StubApplication attachBaseContext] start...");
        String DexName = "classes.dex";
        String DexFilePath  = getDir("shell", MODE_PRIVATE).getAbsolutePath() + File.separator + DexName;
        try{
            InputStream ins = base.getAssets().open(DexName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] bytes = new byte[1024];
            int index;
            while((index = ins.read(bytes)) != -1)
                baos.write(bytes,0,index);
 
            byte[] decDex = decrypt(baos.toByteArray());
            ins.close();
            baos.close();
 
            FileOutputStream fos = new FileOutputStream(new File(DexFilePath));
            fos.write(decDex);
            fos.close();
        } catch(IOException e){
            e.printStackTrace();
        }
 
        DexClassLoader dexClassLoader = new DexClassLoader(
            DexFilePath,
            base.getCacheDir().getAbsolutePath(),
            getApplicationInfo().nativeLibraryDir,
            getClassLoader()
        );
 
        currentActivityThread = Ref.invokeStaticMethod(
                "android.app.ActivityThread",
                "currentActivityThread",
                null,
                null
        );
 
        ArrayMap mPackages = (ArrayMap) Ref.getFieldObject(
                "android.app.ActivityThread",
                currentActivityThread,
                "mPackages"
        );
 
        WeakReference wr = (WeakReference) mPackages.get(getPackageName());
        LoadedApk = wr.get();
        Ref.setFieldObject(
                "android.app.LoadedApk",
                "mClassLoader",
                LoadedApk,
                dexClassLoader
        );
     }
    private byte[] decrypt(byte[] data){
        Log.d("[+]","开始解密...");
        /*
            解密操作
         */
        return data;
    }
}

​ 这里为了方便,就没有写加解密逻辑,可以按照自身情况来编写。运行之后效果如下:

可以发现MainActivity已经成功加载了,但是MyApplication并没有正常加载,接下来就需要处理一下这个问题了。

5. Application问题处理

​ 出现这种问题的原因是通过我们此时加载的Application是壳的,不是源程序的Application。此时需要手动创建Application然后替换一些相关的东西。通过源代码可以看到ActivityThread创建Application的方法:

​ 这里调用的是makeApplicationInner,但是实际上调用的是makeApplication。还有就是需要将下面的mInitialApplication替换成新的Application。接着需要看看makeApplication当中有什么需要修改的:

​ 首先就是这个mApplication需要置零,不然就会直接返回现存的mApplication

接着就需要修改mAppliactionInfo.className,这里是通过这个get..函数来获取mApplictionInfo当中的className成员。还有一个位置就是这里的add


​ 这里会创建的Application添加到mAllApplications当中,一开始添加的是壳的Application,所需要将其移除再添加新的Application。这个mAllApplicationArrayList的类型,可以直接使用remove将对应的项去除掉。

​ 完成这些环境修复之后,需要主动调用OnCreate。这里调用makeApplication时需要注意传入的参数:

​ 我们需要这两个条件当中的代码不执行,所以需要传入的forceDefaultAppClass = falseinstrumentation = null。综上所述,我们需要在OnCreate当中添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public void onCreate() {
    super.onCreate();
    // 替换className
    String className = "com.example.protectdemo_1.MyApplication";
    ApplicationInfo applicationInfo = (ApplicationInfo) Ref.getFieldObject(
            "android.app.LoadedApk",
            LoadedApk,
            "mApplicationInfo"
    );
    applicationInfo.className = className;
 
    // 清除mApplication
    Application oldApplication = (Application) Ref.getFieldObject(
            "android.app.LoadedApk",
            LoadedApk,
            "mApplication"
    );
 
    Ref.setFieldObject(
            "android.app.LoadedApk",
            "mApplication",
            LoadedApk,
            null
    );
 
    // 修改mAllApplication
    ArrayList mAllApplication = (ArrayList)Ref.getFieldObject(
            "android.app.ActivityThread",
            currentActivityThread,
            "mAllApplications"
    );
 
    mAllApplication.remove(oldApplication);
    Application realApp = (Application) Ref.invokeMethod(
            "android.app.LoadedApk",
            "makeApplication",
            LoadedApk,
            new Class[]{boolean.class, Instrumentation.class},
            new Object[]{false, null}
    );
 
    Ref.setFieldObject(
            "android.app.ActivityThread",
            "mInitialApplication",
            currentActivityThread,
            realApp
    );
    realApp.onCreate();
}

​ 这样一来,程序就可以正常执行了:

三、自动化加固

说明

​ 这里的自动化加固是指在有壳代码的前提下,通过脚本辅助,使用工具对指定dex文件进行加固。这里使用apktool

1. 代码优化

  • className获取方法调整

​ 在上面的Demo当中,className是写死的,如果要写自动化加固的话这样肯定不行,需要动态获取className。这里可以通过下面这个方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
String className = null;
try{
    ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
    Bundle bundle = applicationInfo.metaData;
    if(bundle != null && bundle.containsKey(appKey)){
        className = bundle.getString(appKey);
    }
}catch(PackageManager.NameNotFoundException e){
    Log.e("[+]","[Application OnCreate] NameNotFoundException!!");
    e.printStackTrace();
}
 
if(className == null){
    Log.e("[+]","[Application OnCreate] className is null!!");
    return;
}

​ 这段代码的作用是从 Android 应用的 Manifest 文件中的 MetaData 读取一个配置的类名。使用这种方法需要在AndroidManifest.xml当中加入<meta-data />标签:

1
2
3
4
5
<application>
    <meta-data
        android:name="APPLICATION_CLASS_NAME"
        android:value="com.example.protectdemo_1.MyApplication"
</application>
  • 加密函数调整

​ 这里使用一个简单的异或加密来对dex文件进行加密,对应的壳解密代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
private byte[] decrypt(byte[] data){
    Log.d("[+]","开始解密...");
    /*
        解密操作
     */
    if(data != null){
        for(int i = 0; i < data.length; i++){
            data[i] ^= 0x53;
        }
    }
    return data;
}

2. 自动化加固思路

​ 首先就是对目标apk进行解包,这里使用apktool的反编译功能,然后对目标dex进行加密,在解包后的文件夹下创建assests文件夹,将加密后的dex文件丢到assests文件夹当中,删除原来的dex文件,接着植入壳代码文件。最后使用apktool回编译,然后签名即可。

​ 知道了思路,现在就可以来着手编写工具了。这里首先需要将**壳代码(.dex)**从之前生成的apk当中取出来。这里还需要使用到tinyxml2.h的库

1. manifest_editor.hpp

​ 通过该类的方法,实现对Mainfest当中的标签进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#pragma once
#include "tinyxml2.h"
#include <string>
#include <iostream>
 
using namespace tinyxml2;
class ManifestEditor
{
private:
    const char* PROXY_APP_NAME = "com.example.protectdemo_1.StubApplication";   // 注意:这里要与自己的包名匹配
    const char* META_KEY = "APPLICATION_CLASS_NAME";
 
public:
    void modify(const std::string& xmlPath)
    {
        XMLDocument doc;
        if (doc.LoadFile(xmlPath.c_str()) != XML_SUCCESS)
        {
            std::cerr << "无法解析 AndroidManifest.xml" << std::endl;
            return;
        }
 
        XMLElement* root = doc.RootElement();
        if (!root) return;
 
        XMLElement* appNode = root->FirstChildElement("application");
        if (!appNode)return;
 
        // 1. 获取原本的Application Name
        const char* oldAppName = appNode->Attribute("android:name");
        std::string originalAppClass = (oldAppName) ? oldAppName : "";
 
        // 2. 修改Application Name为壳的入口
        appNode->SetAttribute("android:name", PROXY_APP_NAME);
 
        // 3. 插入Meta-Data保存原来的Application
        if (!originalAppClass.empty())
        {
            XMLElement* metaData = doc.NewElement("meta-data");
            metaData->SetAttribute("android:name", META_KEY);
            metaData->SetAttribute("android:value", originalAppClass.c_str());
            if (appNode->FirstChild())
                appNode->InsertFirstChild(metaData);
            else
                appNode->InsertEndChild(metaData);
        }
 
        // 4. 保存文件
        doc.SaveFile(xmlPath.c_str());
        std::cout << "[+] AndroidManifest.xml 修改完成" << std::endl;
    }
};

2. 工具类utils.hpp

​ 该代码实现的是拷贝、加密等一系列操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#pragma once
 
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <filesystem>
#include <algorithm>
 
namespace fs = std::filesystem;
 
class Utils
{
public:
    static bool runCommand(const std::string& cmd)
    {
        std::cout << "[CMD] 执行命令: " << cmd << std::endl;
        int ret = system(cmd.c_str());
        return ret == 0;
    }
 
    static std::vector<uint8_t> readFile(const std::string& filepath)
    {
        std::ifstream file(filepath, std::ios::binary);
        if(!file.is_open())
        {
            std::cerr << "无法打开文件: " << filepath << std::endl;
            return {};
        }
        return std::vector<uint8_t>((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
    }
 
    // 将字节数组写入文件
    static bool writeFile(const std::string& path, const std::vector<uint8_t>& data)
    {
        std::ofstream file(path, std::ios::binary);
        if (!file.is_open()) return false;
        file.write(reinterpret_cast<const char*>(data.data()), data.size());
        return true;
    }
 
    // 简单加密
    static std::vector<uint8_t> encrypt(std::vector<uint8_t> data)
    {
        const uint8_t key = 0x53;
        for (auto& byte : data)
        {
            byte ^= key;
        }
        return data;
    }
 
    // 递归拷贝目录
    static void copyDir(const std::string& src, const std::string& dst)
    {
        try
        {
            fs::copy(src, dst, fs::copy_options::recursive | fs::copy_options::overwrite_existing);
        }
        catch (fs::filesystem_error& e)
        {
            std::cerr << "拷贝失败: " << e.what() << std::endl;  
        }
    }
 
    // 创建目录
    static void makeDir(const std::string& dirPath)
    {
        if (!fs::exists(dirPath))
        {
            fs::create_directories(dirPath);
        }
    }
 
    // 删除文件或目录
    static void removePath(const std::string& path)
    {
        fs::remove_all(path);
    }
};

3. main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <iostream>
#include "utils.hpp"
#include "manifest_editor.hpp"
 
const std::string IN_APK = "test.apk";
const std::string OUT_APK = "appShell.apk";
const std::string TMP_DIR = "apkDecompile";
const std::string SHELL_DEX_SOURCE = "shell_files/classes.dex";
const std::string SHELL_LIBS = "shell_files/libs";
 
int main()
{
    // system("chcp 65001");
    std::cout << "==== Android APK 加壳工具 ====" << std::endl;
    Utils::removePath(TMP_DIR);
    Utils::removePath(OUT_APK);
 
    // 1. 反编译
    std::string cmdDecompile = "java -jar apktool.jar d -s " + IN_APK + " -o " + TMP_DIR + " -f";
    if (!Utils::runCommand(cmdDecompile))
    {
        std::cerr << "反编译失败, 请检查apktools是否存在!" << "\n";
        return -1;
    }
 
    // 2. 加密原始DEX并隐藏
    std::cout << "[*] 正在加密原始Dex...\r\n";
    std::string srcDexPath = TMP_DIR + "/classes.dex";              // 这里的dex是被保护app当中目标dex的名称
    std::string assetsDir = TMP_DIR + "/assets";
    std::string encDexPath = assetsDir + "/classes.dex";            // 文件名是壳代码当中的DexName
 
    Utils::makeDir(assetsDir);
    std::vector<uint8_t> dexData = Utils::readFile(srcDexPath);
    if (dexData.empty())
    {
        std::cerr << "无法读取 classes.dex" << std::endl;
        return -1;
    }
 
    std::vector<uint8_t> encData = Utils::encrypt(dexData);
    Utils::writeFile(encDexPath, encData);
 
    // 删除原始classes.dex
    Utils::removePath(srcDexPath);
 
    // 3. 植入壳代码(class.dex)
    std::cout << "正在植入壳代码..." << std::endl;
    if (!fs::exists(SHELL_DEX_SOURCE))
    {
        std::cerr << "壳文件丢失: " << SHELL_DEX_SOURCE << std::endl;
        return -1;
    }
 
    fs::copy_file(SHELL_DEX_SOURCE, srcDexPath, fs::copy_options::overwrite_existing);
    if (fs::exists(SHELL_LIBS))
        Utils::copyDir(SHELL_LIBS, TMP_DIR + "/lib");
 
    // 4. 修改AndroidManifest.xml
    std::cout << "[*] 正在修改 Manifest.xml.." << std::endl;
    ManifestEditor editor;
    editor.modify(TMP_DIR + "/AndroidManifest.xml");
 
    // 5. 回编译
    std::cout << "[+] 开始回编译..." << std::endl;
    std::string cmdBuild = "java -jar apktool.jar b " + TMP_DIR + " -o " + OUT_APK;
    if (!Utils::runCommand(cmdBuild))
    {
        std::cout << "[error] 回编译失败" << std::endl;
        return -1;
    }
 
    std::cout << "==== 加固完成! 输出文件: " << OUT_APK << " ====" << std::endl;
    return 0;
}

​ 这里需要将壳代码放在当前目录当中的shell_files的文件夹当中,如果有so层的加密则存放到shell_files/lib当中。

4. 签名

​ 成功生成appShell.apk之后需要先签名才能在手机上安装,这里签名使用keytoolapksigner来进行签名,大致命令如下:

1
2
3
4
5
6
7
8
# 生成签名文件
keytool -genkeypair -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-key-alias
 
# 签名
java -jar apksigner.jar sign --ks my-release-key.jks --out signed-app.apk appShell.apk
 
# 安装
adb install -t signed-app.apk

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 1天前 被x0rrrrr编辑 ,原因:
收藏
免费 2
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回