• frida 安装及操作


    安装运行

    1. 电脑端安装
    pip3 install frida
    pip3 install frida-tools
    
    1. 下载对应版本server https://github.com/frida/frida/releases
      image.png

    传到手机上

    adb push frida-server-12.11.12-android-arm64 /data/local/tmp
    
    1. 运行frida-server
    adb shell
    cd /data/local/tmp
    
    chmod 755 ./frida-server-12.11.12-android-arm64
    
    ./frida-server-12.11.12-android-arm64 &
    
    
    1. 转发端口:
    adb forward tcp:27042 tcp:27043
    
    1. 验证(列出正在运行的包名):
    frida-ps -U
    

    常用工具函数

    1. 打印某个类的所有成员变量
    function dumpAllFieldValue(obj) {
        if (obj === null) {
            return;
        }
    
        console.log("Dump all fields value for  " + obj.getClass() + " :");
    
        var cls = obj.getClass();
    
        while (cls !== null && !cls.equals(Java.use("java.lang.Object").class)) {
            var fields = cls.getDeclaredFields();
            if (fields === null || fields.length === 0) {
                cls = cls.getSuperclass();
                continue;
            }
    
            if (!cls.equals(obj.getClass())) {
                console.log("Dump super class  " + cls.getName() + " fields:");
            }
    
            for (var i = 0; i < fields.length; i++) {
                var field = fields[i];
                field.setAccessible(true);
                var name = field.getName();
                var value = field.get(obj);
                var type = field.getType();
                console.log(type + " " + name + "=" + value);
            }
    
            cls = cls.getSuperclass();
        }
    }
    
    1. 获取成员变量的值
    function getFieldValue(obj, fieldName) {
        var cls = obj.getClass();
        var field = cls.getDeclaredField(fieldName);
        field.setAccessible(true);
        var name = field.getName();
        var value = field.get(obj);
        // console.log("field: " + field + "	name:" + name + "	value:" + value);
        return value;
    }
    
    1. 打印调用堆栈
    function printStack() {
        Java.perform(function() {
            var Exception = Java.use("java.lang.Exception");
            var ins = Exception.$new("Exception");
            var straces = ins.getStackTrace();
            if (straces != undefined && straces != null) {
                var strace = straces.toString();
                var replaceStr = strace.replace(/,/g, "
    ");
                console.log(
                    "============================= Stack start ======================="
                );
                console.log(replaceStr);
                console.log(
                    "============================= Stack end =======================
    "
                );
                Exception.$dispose();
            }
        });
    }
    

    堆栈调用顺序为自下而上

    java层hook

    1. hook模板
    # -*- coding: utf-8 -*-
    import frida
    import sys
    
    hook_code = """
    Java.perform(function(){
    
        var utils = Java.use("类名路径");
        utils.方法名.implementation = function(a, b){
    
            return retval;
        }
    });
    """
    process = frida.get_usb_device().attach('包名')
    script = process.create_script(hook_code)
    script.load()
    sys.stdin.read()
    
    1. hook方法(非重载方法不用写方法类型)
    var utils = Java.use("类名路径");
        utils.方法名.implementation = function(a, b){
    
        a = 123;
        b = 456;
    
        var retval = this.方法名(a, b);
        console.log(a, b, retval);
    
        return retval;
    }
    
    1. hook重载方法
    var utils = Java.use("类名路径");
    utils.方法名.overload("方法类型").implementation = function(a, b){
    
        a = 123;
        b = 456;
    
        var retval = this.方法名(a, b);
        console.log(a, b, retval);
    
        return retval;
    }
    
    1. hook所有重载方法
    var utils = Java.use("类名路径");
        //console.log(utils.方法名.overloads.length);
        for(var i = 0; i < utils.方法名.overloads.length; i++){
            utils.方法名.overloads[i].implementation = function(){
                //console.log(JSON.stringify(arguments));
    
                if(arguments.length == 0){
                    return "调用了没有参数的";
                }else if(arguments.length == 1){
                    if(JSON.stringify(arguments).indexOf("Money") != -1){
                        return "调用了Money参数的";
                    }else{
                        return "调用了int参数的";
                    }
                }
    
                arguments[0] = 1000;
                return this.方法名.apply(this, arguments);
            }
        }
    
    
    1. hook 构造方法
    Java.perform(function hookTest4(){
        var money = Java.use("类名路径");
        money.$init.overload('重载的参数1', '重载的参数2').implementation = function(str, num){
            console.log(str, num);
            str = "欧元";
            num = 2000;
            this.$init(str, num);
        }
    });
    
    
    1. 对象实例化
    Java.perform(function hookTest2(){
        var utils = Java.use("utils类路径");
        var money = Java.use("money类路径");
        utils.方法名.overload('重载参数').implementation = function(a){
            a = 888;
            var retval = this.方法名(money.$new("日元", 100000));//对象实例化
            console.log(a, retval);
            return retval;
        }
    });
    
    1. 修改类的字段
    Java.perform(function(){
        //静态字段的修改
        var money = Java.use("money类路径");
        //console.log(JSON.stringify(money.字段名));
        money.字段名.value = "xxxxx";
        console.log(money.flag.value);
    
        //非静态字段的修改
        Java.choose("money类路径", {
            onMatch: function(obj){
                obj._name.value = "ouyuan"; //字段名与函数名相同,前面加个下划线
                obj.name.value = "ouyuan"; //字段名与函数名不同
            },
            onComplete: function(){
            }
        });
    });
    
    

    也可以通过java的反射方式修改

    function setFieldValue(obj, fieldName, fieldValue) {
        var cls = obj.getClass();
        var field = cls.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, fieldValue);
    }
    
    1. hook内部类与匿名类
    hook_code = """
    Java.perform(function hookTest6(){
        Java.perform(function(){
            var innerClass = Java.use("money类路径$内部类名");
            console.log(innerClass);
            innerClass.$init.implementation = function(a, b){
                a = "nb";
                b = 888;
                return this.$init(a, b);
            }
    
            var xxx = Java.use("xxx类路径$smali中查看匿名类数字编号");
            console.log(xxx);
            xxx.getInfo.implementation = function(){
                return "匿名类被Hook了"
            }
        });
    });
    """
    
    1. hook类的所有方法
    hook_code = """
    Java.perform(function hookTest8(){
        Java.perform(function(){
            var md5 = Java.use("md5类路径");
            var methods = md5.class.getDeclaredMethods();
            for(var j = 0; j < methods.length; j++){
                var methodName = methods[j].getName();
                console.log(methodName);
    
                for(var k = 0; k < md5[methodName].overloads.length; k++){
    
                    md5[methodName].overloads[k].implementation = function(){
                        for(var i = 0; i < arguments.length; i++){
                            console.log(arguments[i]);
                        }
                        return this[methodName].apply(this, arguments);
                    }
                }
            }
        });
    });
    """
    
    1. Hook动态加载的dex(Android 7以上)
    hook_code = """
    Java.perform(function () {
        Java.enumerateClassLoaders({
            onMatch: function (loader) {
                try {
                    if (loader.loadClass("com.xiaojianbang.app.Dynamic")) {
                        Java.classFactory.loader = loader;
                        var Dynamic = Java.use("com.xiaojianbang.app.Dynamic");
                        console.log(Dynamic);
                        Dynamic.sayHello.implementation = function () {
                            return "9999999";
                        }
                    }
                } catch (error) {
                }
            },
            onComplete: function () {
            }
        });
    });
    """
    
    
    1. Java特殊类型的遍历与修改(Map举例)
    hook_code = """
    Java.perform(function () {
        var ShufferMap = Java.use("com.xiaojianbang.app.ShufferMap");
        console.log(ShufferMap);
        ShufferMap.show.implementation = function (map) {
            console.log(JSON.stringify(map));
            //Java map的遍历
            var key = map.keySet();
            var it = key.iterator();
            var result = "";
            while(it.hasNext()){
                var keystr = it.next();
                var valuestr = map.get(keystr);
                result += valuestr;
            }
            console.log(result);
            // return result;
    
            //Java map的修改
            map.put("pass", "zygx8");
            map.put("guanwang", "www.zygx8.com");
    
            var retval = this.show(map);
            console.log(retval);
            return retval;
        }
    });
    """
    
    1. 打印HashMap
    
    console.log(JSON.stringify(arguments))
    var Map = Java.use('java.util.HashMap');
    var args_map = Java.cast(arguments[1], Map);
    console.log(args_map.toString());
    
    1. Java层主动调用
    hook_code = """
    Java.perform(function () {
        //静态方法的主动调用
        var rsa = Java.use("com.xiaojianbang.app.RSA");
        var str = Java.use("java.lang.String");
        var base64 = Java.use("android.util.Base64");
        var bytes = str.$new("xiaojianbang").getBytes();
        console.log(JSON.stringify(bytes));
        var retval = rsa.encrypt(bytes);
        var result = base64.encodeToString(retval, 0);
        console.log(result);
        //非静态方法的主动调用1 (新建一个对象去调用)
        var res = Java.use("com.xiaojianbang.app.Money").$new("日元", 300000).getInfo();
        console.log(res);
        var utils = Java.use("com.xiaojianbang.app.Utils");
        res = utils.$new().myPrint(["xiaojianbang", "is very good", " ", "zygx8", "is very good"]);
        console.log(res);
        //非静态方法的主动调用2 (获取已有的对象调用)
        Java.choose("com.xiaojianbang.app.Money", {
            onMatch: function (obj) {
                if (obj._name.value == "美元") {
                    res = obj.getInfo();
                    console.log(res);
                }
            },
            onComplete: function () {
            }
        });
    });
    """
    
    
    1. 删除对象引用
    $.dispose
    
    1. 获取参数类型
    xxx.class.getType()
    
    1. 用frida注入dex文件
    hook_code = """
    Java.perform(function () {
        Java.openClassFile("/data/local/tmp/xiaojianbang.dex").load();
        var xiaojianbang = Java.use("com.xiaojianbang.test.xiaojianbang");
    
        var ShufferMap = Java.use("com.xiaojianbang.app.ShufferMap");
        ShufferMap.show.implementation = function (map) {
            var retval = xiaojianbang.sayHello(map);
            console.log(retval);
            return retval;
        }
    
    });
    """
    
    1. 端口检测解决方案
    ./data/local/tmp/frida_server_arm64 -l 127.0.0.1:9999
    
    adb forward tcp:9999 tcp:9999
    
    1. frida启动前注入
    frida -H 127.0.0.1:9999 -f com.xjb.cpp -l hook.js --no-pause
    

    so 层hook

    枚举导入导出表(ELF即so文件)

    1. 枚举导入表
    var imports = Module.enumerateImports("libxiaojianbang.so");
    for(var i = 0; i < imports.length; i++){
        if(imports[i].name == "strncat"){
            console.log(JSON.stringify(imports[i]));
            console.log(imports[i].address);
        }
    }
    
    1. 枚举导出表
    var exports = Module.enumerateExports("libxiaojianbang.so");
    for(var i = 0; i < exports.length; i++){
        //if(exports[i].name == "strncat"){
            console.log(JSON.stringify(exports[i]));
        //}
    }
    
    1. hook导出函数
    hook_code = """
    Java.perform(function hookTest2() {
        var helloAddr = Module.findExportByName("libxiaojianbang.so", "Java_com_xiaojianbang_app_NativeHelper_add");
        console.log(helloAddr);
        if (helloAddr != null) {
            Interceptor.attach(helloAddr, {
                onEnter: function (args) {
                    //args参数数组
                    console.log(args[0]);
                    console.log(args[1]);
                    console.log(args[2]);
                    console.log(args[3]);
                    console.log(args[4].toInt32());
                },
                onLeave: function (retval) {
                    //retval函数返回值
                    console.log(retval);
                    console.log("retval", retval.toInt32());
                }
            });
        }
    });
    """
    
    1. 函数地址计算
    function hookTest14(){
        var soAddr = Module.findBaseAddress("libxiaojianbang.so");
        console.log(soAddr);
        var funcAddr = soAddr.add(0x23F4);
        console.log(funcAddr);
    }
    
    1. Hook未导出函数
    function hookTest14(){
        var soAddr = Module.findBaseAddress("libxiaojianbang.so");
        console.log(soAddr);
        var funcAddr = soAddr.add(0x23F4);
        console.log(funcAddr);
    
        if(funcAddr != null){
            Interceptor.attach(funcAddr,{
                onEnter: function(args){
    
                },
                onLeave: function(retval){
                    console.log(hexdump(retval));
                }
            });
         }
    }
    
    1. 获取指针参数返回值
    function hookTest5(){
        var soAddr = Module.findBaseAddress("libxiaojianbang.so");
        console.log(soAddr);
        var sub_930 = soAddr.add(0x930); //函数地址计算 thumb+1 ARM不加
        console.log(sub_930);
    
         var sub_208C = soAddr.add(0x208C); //函数地址计算 thumb+1 ARM不加
         console.log(sub_208C);
         if(sub_208C != null){
            Interceptor.attach(sub_208C,{
                onEnter: function(args){
                    this.args1 = args[1];
                },
                onLeave: function(retval){
                    console.log(hexdump(this.args1));
                }
            });
         }
    }
    
    1. Hook_dlopen
    function hookTest6(){
        var dlopen = Module.findExportByName(null, "dlopen");
        console.log(dlopen);
        if(dlopen != null){
            Interceptor.attach(dlopen,{
                onEnter: function(args){
                    var soName = args[0].readCString();
                    console.log(soName);
                    if(soName.indexOf("libxiaojianbang.so") != -1){
                        this.hook = true;
                    }
                },
                onLeave: function(retval){
                    if(this.hook) { hookTest5() };
                }
            });
        }
    
        var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
        console.log(android_dlopen_ext);
        if(android_dlopen_ext != null){
            Interceptor.attach(android_dlopen_ext,{
                onEnter: function(args){
                    var soName = args[0].readCString();
                    console.log(soName);
                    if(soName.indexOf("libxiaojianbang.so") != -1){
                        this.hook = true;
                    }
                },
                onLeave: function(retval){
                    if(this.hook) { hookTest5() };
                }
            });
        }
    
    }
    
    1. 内存读写
    function hookTest7(){
        var soAddr = Module.findBaseAddress("libxiaojianbang.so");
        console.log(soAddr);
        if(soAddr != null){
            //console.log(soAddr.add(0x2C00).readCString());
            //console.log(hexdump(soAddr.add(0x2C00)));  //读取指定地址的字符串
    
            //var strByte = soAddr.add(0x2C00).readByteArray(16); //读内存
            //console.log(strByte);
    
            //soAddr.add(0x2C00).writeByteArray(stringToBytes("xiaojianbang")); //写内存
            //console.log(hexdump(soAddr.add(0x2C00)));  //dump指定内存
    
            //var bytes = Module.readByteArray(soAddr.add(0x2C00), 16);
            //console.log(bytes);
    
        }
    }
    
    1. 主动调用JNI函数
    function hookTest8(){
        var funcAddr = Module.findExportByName("libxiaojianbang.so", "Java_com_xiaojianbang_app_NativeHelper_helloFromC");
        console.log(funcAddr);
        if(funcAddr != null){
            Interceptor.attach(funcAddr,{
                onEnter: function(args){
    
                },
                onLeave: function(retval){
                    var env = Java.vm.tryGetEnv();
                    var jstr = env.newStringUtf("www.zygx8.com");  //主动调用jni函数 cstr转jstr
                    retval.replace(jstr);
                    var cstr = env.getStringUtfChars(jstr); //主动调用 jstr转cstr
                    console.log(cstr.readCString());
                    console.log(hexdump(cstr));
                }
            });
        }
    }
    
    1. jni函数Hook(计算地址方式)
    function hookTest9(){
        Java.perform(function(){
            //console.log(JSON.stringify(Java.vm.tryGetEnv()));
            var envAddr = ptr(Java.vm.tryGetEnv().handle).readPointer();
            var newStringUtfAddr = envAddr.add(0x538).readPointer();
            var registerNativesAddr = envAddr.add(1720).readPointer();
            console.log("newStringUtfAddr", newStringUtfAddr);
            console.log("registerNativesAddr", registerNativesAddr)
            if(newStringUtfAddr != null){
                Interceptor.attach(newStringUtfAddr,{
                    onEnter: function(args){
                        console.log(args[1].readCString());
                        //args[1] = "xiaojianbang is very good!";
                    },
                    onLeave: function(retval){
    
                    }
                });
            }
            if(registerNativesAddr != null){     //Hook registerNatives获取动态注册的函数地址
                Interceptor.attach(registerNativesAddr,{
                    onEnter: function(args){
                        console.log(args[2].readPointer().readCString());
                        console.log(args[2].add(Process.pointerSize).readPointer().readCString());
                        console.log(args[2].add(Process.pointerSize * 2).readPointer());
                        console.log(hexdump(args[2]));
                        console.log("sub_289C", Module.findBaseAddress("libxiaojianbang.so").add(0x289C));
                    },
                    onLeave: function(retval){
    
                    }
                });
            }
    
        });
    }
    
    1. jni函数Hook(libart.so)
    function hookTest10(){
        var artSym = Module.enumerateSymbols("libart.so");
        var NewStringUTFAddr = null;
        for(var i = 0; i < artSym.length; i++){
            if(artSym[i].name.indexOf("CheckJNI") == -1 && artSym[i].name.indexOf("NewStringUTF") != -1){
                console.log(JSON.stringify(artSym[i]));
                NewStringUTFAddr = artSym[i].address;
            }
        };
    
        if(NewStringUTFAddr != null){
            Interceptor.attach(NewStringUTFAddr,{
                onEnter: function(args){
                    console.log(args[1].readCString());
                },
                onLeave: function(retval){
    
                }
            });
        }
    
    }
    
    1. so层函数主动调用
    function hookTest11(){
        Java.perform(function(){
            var funcAddr = Module.findBaseAddress("libxiaojianbang.so").add(0x23F4);
            var func = new NativeFunction(funcAddr, "pointer", ['pointer', 'pointer']);
            var env = Java.vm.tryGetEnv();
            console.log("env: ", JSON.stringify(env));
            if(env != null){
                var jstr = env.newStringUtf("xiaojianbang is very good!!!");
                //console.log("jstr: ", hexdump(jstr));
                var cstr = func(env, jstr);
                console.log(cstr.readCString());
                console.log(hexdump(cstr));
            }
        });
    }
    
    1. frida读写文件
    //frida API 读写文件
    function hookTest12(){
        var ios = new File("/sdcard/xiaojianbang.txt", "w");
        ios.write("xiaojianbang is very good!!!
    ");
        ios.flush();
        ios.close();
    }
    //Hook libc 读写文件
    function hookTest13() {
    
        var addr_fopen = Module.findExportByName("libc.so", "fopen");
        var addr_fputs = Module.findExportByName("libc.so", "fputs");
        var addr_fclose = Module.findExportByName("libc.so", "fclose");
    
        console.log("addr_fopen:", addr_fopen, "addr_fputs:", addr_fputs, "addr_fclose:", addr_fclose);
        var fopen = new NativeFunction(addr_fopen, "pointer", ["pointer", "pointer"]);
        var fputs = new NativeFunction(addr_fputs, "int", ["pointer", "pointer"]);
        var fclose = new NativeFunction(addr_fclose, "int", ["pointer"]);
    
        var filename = Memory.allocUtf8String("/sdcard/xiaojianbang.txt");
        var open_mode = Memory.allocUtf8String("w");
        var file = fopen(filename, open_mode);
        console.log("fopen:", file);
    
        var buffer = Memory.allocUtf8String("zygxb
    ");
        var retval = fputs(buffer, file);
        console.log("fputs:", retval);
    
        fclose(file);
    
    }
    

    RPC

    import frida
    import sys
    
    rdev = frida.get_usb_device()
    session = rdev.attach("com.yuanrenxue.onlinejudge2020")  # 包名
    
    js_code = """
    rpc.exports = {
        getsign: function(i) {
            Java.perform(function() {
                console.log("get_sign");
                var my_class1 = Java.use("com.yuanrenxue.onlinejudge2020.OnlineJudgeApp");
                var reslut = my_class1.getSign1(i);
                console.log(reslut);
                send({ "sign": reslut, "num": i })
                return reslut;
            });
        },
    };
    
    """
    
    script = session.create_script(js_code)
    
    
    def on_message(message, data):
        sign = message.get("payload").get("sign")
        num = message.get("payload").get("num")
    
    
    script.on("message", on_message)
    script.load()
    
    script.exports.getsign(1) # 调用的函数
    
    sys.stdin.read()
    

    通过wifiadb实现群控

    import frida
    import os
    import time
    
    app = "com.xxx.xxx";
    
    device_ids = [];
    devices = frida.enumerate_devices();
    for device in devices :
        # print(device)
        ## 枚举所有通过wifiadb 连的机器
        if device.id.find(":") > 0:
            #print(device.id)
            device_ids.append(device.id.replace("5555", "9999"))
    
    for id in device_ids :
        print(id)
        device = frida.get_device_manager().add_remote_device(id)
        print(device)
        pid = device.spawn([app])
        print(pid)
        device.resume(pid)
        time.sleep(1)
        session = device.attach(pid)
        with open("load_hook.js") as f:
            script = session.create_script(f.read())
            script.load()
    input()
    

    常用命令

    1. 列出正在运行的进程:
    frida-ps -U
    
    1. 列出安装的程序
    frida-ps -Uai
    
    1. 列出运行中的程序(查看包名很方便)
    frida-ps -Ua
    
    1. 连接frida到一个指定的设备上
    frida-ps -D 设备id
    

    另外还有四个分别是:frida-trace, frida-discover, frida-ls-devices, frida-kill

    objection 使用

    Frida只是提供了各种API供我们调用,在此基础之上可以实现具体的功能,比如禁用证书绑定之类的脚本,就是使用Frida的各种API来组合编写而成。于是有大佬将各种常见、常用的功能整合进一个工具,供我们直接在命令行中使用,这个工具便是objection。

    1. 安装
    pip3 install objection
    
    1. 连接app
    objection -g 包名 explore
    
    1. Memory 指令
    memory list modules  // 查看内存中加载的库
    memory list exports libssl.so  // 查看库的导出函数
    memory list exports libart.so --json /root/libart.json //将结果保存到json文件中
    memory search --string --offsets-only //搜索内存
    memory search "64 65 78 0a 30 35 00"
    
    1. root
    android root disable //尝试关闭app的root检测
    android root simulate //尝试模拟root环境
    
    
    1. activities
    android hooking list activities // 可以列出app具有的所有avtivity
    
    1. 内存漫游
    //列出内存中所有的类
    android hooking list classes
    
    //在内存中所有已加载的类中搜索包含特定关键词的类
    android hooking search classes [search_name] 
    
    //在内存中所有已加载的方法中搜索包含特定关键词的方法
    android hooking search methods [search_name] 
    
    //直接生成hook代码
    android hooking generate simple [class_name]
    
    // 查看类的全部广法
    android hooking list class_methods [class_name]
    
    1. hook 方式

    hook指定方法, 如果有重载会hook所有重载,如果有疑问可以看
    --dump-args : 打印参数
    --dump-backtrace : 打印调用栈
    --dump-return : 打印返回值

    // 查看方法的参数、返回值和调用栈
    android hooking watch class_method com.xxx.xxx.methodName --dump-args --dump-backtrace --dump-return
    
    //获取全部tostring的返回来值
    android hooking watch class_method java.lang.StringBuilder.toString --dump-return
    
    //弹窗
    android hooking watch class_method android.app.Dialog.show --dump-args --dump-backtrace --dump-return
    
    //hook指定类, 会打印该类下的所有调用 
    android hooking watch class com.xxx.xxx 
    
    //设置返回值(只支持bool类型) 
    android hooking set return_value com.xxx.xxx.methodName false
    
    1. 关闭app的ssl校验
    android sslpinning disable
    
    1. Spawn方式Hook
      从Objection的使用操作中我们可以发现,Obejction采用Attach附加模式进行Hook,这可能会让我们错过较早的Hook时机,可以通过如下的代码启动Objection,引号中的objection命令会在启动时就注入App。
    objection -g packageName explore --startup-command 'android hooking watch xxx' 
    

    ARIDA

    管理PRC脚本,自动生成http接口的工具

    1. 安装 下载
    git clone git@github.com:lateautumn4lin/arida.git
    

    使用conda安装

    conda create -n arida python==3.8
    conda install --yes --file requirements.txt
    

    使用pip安装

    virtualenv venv
    source venv/bin/activate
    
    // 下载pip安装格式的requirements.txt https://github.com/Boris-code/arida/blob/master/requirements.txt
    
    pip install -r requirements.txt
    
    1. 运行
    uvicorn main:app --reload
    watch 127.0.0.1:8000/docs
    

    image.png
    3. 开发

    Config文件中写入自己的App信息
    apps目录写开发相应的Frida-Js脚本,可参考其他两个文件

    参考

    1. Objection:https://www.cnblogs.com/qiaorui/p/13455420.html

    2. Firda:https://juejin.cn/post/6844904127705661448#heading-9

    3. ARIDA: https://github.com/lateautumn4lin/arida

  • 相关阅读:
    python:(类)私有
    Python:多继承时的继承顺序
    python基础:继承
    年终总结
    cocos版本说明
    WPF学习系列 游戏-选张图片做成9宫格拼图
    SmartAssembly使用失败记录
    WPF学习系列 绘制旋转的立方体
    自适应布局思路
    webfrom 总结
  • 原文地址:https://www.cnblogs.com/wzbk/p/14189117.html
Copyright © 2020-2023  润新知