FridaHookAndroid

本文旨在覆盖使用 Frida 对 Android App 进行 hook 的绝大多数场景。文章提到的所有代码以及被测 App,详见:https://github.com/liyansong2018/FridaHookAndroid(更多更新也见此文档

  • Frida-Android 进阶
  • frida 版本:12.11.18
  • 系统:Ubuntu 20.04 LTS
  • 被测系统:某安卓设备/某模拟器

0x10 官方 API

0x11 Java 运行时

官方API地址: https://www.frida.re/docs/javascript-api/#java ,这里给出几个常用的 API

Java.perform(fn)

确保当前线程被附加到 VM 上,并且调用fn函数。此函数在内部调用 VM::AttachCurrentThread,然后执行 fn 回调函数中的 Javascript 脚本操作 Java 运行时,最后使用 VM::DetachCurrentThread 释放资源。

Java.use(className)

通过类名获得 Java 类,返回一个包裹好的 Javascript 对象。通过该对象,可访问类成员。通过调用 $new()调用构造函数,实例化对象。

Java.perform(function(){var Activity = Java.use("android.app.Activity");  // 反射获取类var Exception = Java.use("java.lang.Exception");Activity.onResume.implementation = function(){      // 调用构造器抛出异常throw Exception.$new("Oh noes!");};
});

Java.choose(className, callback)

在内存中扫描 Java 堆,枚举 Java 对象(className)实例。比如可以使用 java.lang.String 扫描内存中的字符串。callbacks 提供两个参数:onMatch(instance)onComplete,分别是找到匹配对象和扫描完成调用。

Java.scheduleOnMainThread(fn)

在 VM 主线程(UI 线程)执行回调函数。Android 中操作 UI 元素需要在主线程中执行代码,scheduleOnMainThread 的作用就是用来在主线程中执行函数。此函数需要使用 Java.perform 包裹。

Java.perform(function(){var Toast = Java.use("android.widget.Toast");// 获取 contextvar currentApplication = Java.use("android.app.ActivityThread").currentApplication();var context = currentApplication.getApplicationContext();// 在主线程中运行回调Java.scheduleOnMainThread(function(){Toast.makeText(context, "Hello frida!", Toast.LENGTH_LONG.value).show();});
});

enumerateLoadedClasses(callbacks)

枚举当前已加载的类。callbacks 参数是一个对象,需要提供两个回调函数—— onMatch(className)onComplete。每次找到一个类就会调用一次 onMatch,全部找完之后,调用 onComplete

0x20 hook 案例

0x21 通用案例

方法一:运行时 hook

import frida
import sysdef read_js(file):with open(file) as fp:return fp.read()def on_message(message, data):if message["type"] == "send":print("[+] {}".format(message["payload"]))else:print("[-] {}".format(message))remote_device = frida.get_usb_device()
session = remote_device.attach("com.example.testfrida")src = read_js("./test.js")
script = session.create_script(src)
script.on("message", on_message)
script.load()
sys.stdin.read()

方法二:spawn 拉起进程

如果需要 hook app 执行 onCreate() 方法中的一些功能,就需要使用 spawn 模式

frida -U -l test.js -f com.example.testfrida

参数

  • -U connect to USB device
  • -l SCRIPT, --load=SCRIPT
  • -f FILE, --file=FILE spawn FILE

后文中的案例,由于 hook 的显示结果通过 Toast 展示,Toast 会在 app 刚启动时加载。

  • 使用方法一,运行时 hook,需要在 app 刚启动时,就运行 python 程序;
  • 使用方法二,用 spawn 拉起 app

普通方法

方法一:Java.use

这种方式比较通用,但是在某些动态加载的类中,可能无法hook

Java.perform(function (){send("start hook...");var Animal = Java.use("com.example.testfrida.Animal");Animal.getAnimalInfo.implementation = function (){send("hijack getAnimalInfo");return "hello, frida!";};
});

脚本输出结果

/usr/bin/python3.8 /home/lys/PycharmProjects/honor5/main.py
[+] start hook...
[+] hijack getAnimalInfo

方法二:Java.choose

这种方式适用于一些动态方法,静态方法不适用此方式

Java.choose("com.xxx.class", {onMatch: function(instance) {// hook 类},onComplete: function() {}});

构造函数

形参类型通过 overload 传递

  • 可以通过 arguments 列表获取待 hook 函数的形参

  • 也可以通过implementation = function (a, b, c...) 获取

Java.perform(function (){send("start hook...");var Animal = Java.use("com.example.testfrida.Animal");Animal.$init.overload("java.lang.String", "int").implementation = function (){send("hijack Animal()");send("参数1:" + arguments[0]);send("参数2:" + arguments[1]);return this.$init("frida", 999);    // 修改};
});

脚本输出结果

/usr/bin/python3.8 /home/lys/PycharmProjects/honor5/main.py
[+] start hook...
[+] hijack Animal()
[+] 参数1:duck
[+] 参数2:2

内部类

内部类的一般写法是 Class$InnerClass,对于混淆的内部类,查看 smali 源码就能看出 $1、$2 这样的类就是内部类。

Java.perform(function (){send("start hook...");var InnerTest = Java.use("com.example.testfrida.Animal$InnerlTest");InnerTest.getClassInfo.implementation = function (){send("hijack inner Class");return "hello, frida!";}
});

脚本输出结果

/usr/bin/python3.8 /home/lys/PycharmProjects/honor5/main.py
[+] start hook...
[+] hijack inner Class

匿名内部类

查看反编译得到的 smali 源码,smali 文件会将每个类作为一个单独文件保存,如下图所示的 $1 就是匿名内部类。

其余用法跟普通类是一样的。

Java.perform(function (){send("start hook...");var AnoyClass = Java.use("com.example.testfrida.Animal$1");AnoyClass.getAnimalInfo.implementation = function (){return "hello,frida"; // 修改函数返回值};
});

匿名内部类 hook 还有一些独特技巧,比如说,如果你不想通过反编译的方式得到匿名内部类的符号,可以直接通过 hook 构造函数的方式获取类名,如下所示

Java.perform(function (){send("start hook...");var Animal = Java.use("com.example.testfrida.Animal");Animal.$init.overload("java.lang.String", "int").implementation = function (){send("constructor called from " + this.$className);const NewAnimal = Java.use(this.$className);NewAnimal.getAnimalInfo.implementation = function (){return "hello, frida";};};
});

私有属性

如果 app 没有调用 get 等函数,怎么直接获取类的私有属性呢?早期版本的 frida 支持使用 java 提供的反射,js 同样也提供了该功能。

     var objectionInstance = Animal.$new("test", 0);var ob = Java.cast(objectionInstance.getClass(), clazz);var name = ob.getDeclaredField("name");     // 获得某个属性对象var value = ob.get(name);                   // 获得obj中对应的属性值send(value);

经过多次实践,发现该脚本并不能在笔者的环境中正常运行,后来才发现,新版 frida (笔者使用的版本:12.11.18)支持直接获取类中的私有属性,而不需要使用 Java 的高级特性——反射。

Java.perform(function (){send("start hook...");var Animal = Java.use("com.example.testfrida.Animal");Animal.getAge.implementation = function (){send("obtain key");// 直接调用类中的函数send("call public function >> getName(): " + this.getName());send("call private function >> getKey(): " + this.getKey());// 直接调用类中的私有属性send("call private property >> name: " + this.name.value);send("call private property >> age: " + this.age.value);send("call private property >> key: " + this.key.value);return 9999;};
});

脚本输出结果

/usr/bin/python3.8 /home/lys/PycharmProjects/honor5/main.py
[+] start hook...
[+] obtain key
[+] call public function >> getName(): penguin
[+] call private function >> getKey(): AEKL3KJK23KLASLDKOCVL
[+] call private property >> name: penguin
[+] call private property >> age: 5
[+] call private property >> key: AEKL3KJK23KLASLDKOCVL

修改私有属性也一样简单

     // 修改私有属性this.age.value = 999;

0x22 进阶用法

获取内存中加载的所有类

Java.perform(function(){send("enumerating classes...");Java.enumerateLoadedClasses({onMatch: function(className){send("found class >> " + className);},onComplete: function(){send("class enumration complete");}});
});

脚本输出结果

/usr/bin/python3.8 /home/lys/PycharmProjects/honor5/main.py
[+] enumerating classes...
[+] found class >> com.huawei.iaware.IAwareFunctionAL
[+] found class >> com.huawei.iaware.IAwareConstantAL$StringConstants
[+] found class >> com.huawei.iaware.IAwareConstantAL
[+] found class >> com.huawei.iaware.IAwareCallbackAL
[+] found class >> com.huawei.iaware.IAwareCallbackAL$MultiWinCallBackHandlerIAL
[+] found class >> vendor.huawei.hardware.hwsched.V1_0.ISched
...

获取类中的所有方法

Java.perform(function(){send("obtain methods...");var Animal = Java.use("com.example.testfrida.Animal");var methods = Animal.class.getDeclaredMethods();for(var i = 0; i < methods.length; i++){console.log(methods[i]);;}
});

输出结果

/usr/bin/python3.8 /home/lys/PycharmProjects/honor5/main.py
[+] obtain methods...
static java.lang.String com.example.testfrida.Animal.access$000(com.example.testfrida.Animal)
static int com.example.testfrida.Animal.access$100(com.example.testfrida.Animal)
private java.lang.String com.example.testfrida.Animal.getKey()
public int com.example.testfrida.Animal.getAge()
public java.lang.String com.example.testfrida.Animal.getAnimalInfo()
public java.lang.String com.example.testfrida.Animal.getAnoymousClass()
public com.example.testfrida.Animal$InnerlTest com.example.testfrida.Animal.getInnerTestInstance()
public java.lang.String com.example.testfrida.Animal.getName()
public void com.example.testfrida.Animal.setAge(int)
public void com.example.testfrida.Animal.setName(java.lang.String)

打印调用栈

有两种方法可以跟踪函数的调用栈,推荐使用第一种。

  1. 直接在脚本中直接输出错误日志
  2. 触发异常,在 Android log 中输出
Java.perform(function(){send("print stack...");var Animal = Java.use("com.example.testfrida.Animal");Animal.getAge.implementation = function (){console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));//throw Java.use("java.lang.Exception").$new();return this.getAge();};
});

1.Python 输出

/usr/bin/python3.8 /home/lys/PycharmProjects/honor5/main.py
[+] print stack...
java.lang.Exceptionat com.example.testfrida.Animal.getAge(Native Method)at com.example.testfrida.MainActivity.getPrivte(MainActivity.java:92)at com.example.testfrida.MainActivity.onClick(MainActivity.java:60)at android.view.View.performClick(View.java:7216)at android.view.View.performClickInternal(View.java:7190)at android.view.View.access$3500(View.java:827)at android.view.View$PerformClick.run(View.java:27663)at android.os.Handler.handleCallback(Handler.java:900)at android.os.Handler.dispatchMessage(Handler.java:103)at android.os.Looper.loop(Looper.java:219)at android.app.ActivityThread.main(ActivityThread.java:8291)at java.lang.reflect.Method.invoke(Native Method)at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)
  1. Android 日志输出
lys@lys-VirtualBox:~$ adb logcat -s AndroidRuntime
--------- beginning of system
--------- beginning of main
12-19 17:50:22.313 31328 31328 D AndroidRuntime: Shutting down VM
--------- beginning of crash
12-19 17:50:22.314 31328 31328 E AndroidRuntime: FATAL EXCEPTION: main
12-19 17:50:22.314 31328 31328 E AndroidRuntime: Process: com.example.testfrida, PID: 31328
12-19 17:50:22.314 31328 31328 E AndroidRuntime: java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
12-19 17:50:22.314 31328 31328 E AndroidRuntime:        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:523)
12-19 17:50:22.314 31328 31328 E AndroidRuntime:        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)
12-19 17:50:22.314 31328 31328 E AndroidRuntime: Caused by: java.lang.reflect.InvocationTargetException
12-19 17:50:22.314 31328 31328 E AndroidRuntime:        at java.lang.reflect.Method.invoke(Native Method)
12-19 17:50:22.314 31328 31328 E AndroidRuntime:        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
12-19 17:50:22.314 31328 31328 E AndroidRuntime:        ... 1 more
12-19 17:50:22.314 31328 31328 E AndroidRuntime: Caused by: java.lang.Exception
12-19 17:50:22.314 31328 31328 E AndroidRuntime:        at com.example.testfrida.Animal.getAge(Native Method)
12-19 17:50:22.314 31328 31328 E AndroidRuntime:        at com.example.testfrida.MainActivity.getPrivte(MainActivity.java:92)
12-19 17:50:22.314 31328 31328 E AndroidRuntime:        at com.example.testfrida.MainActivity.onClick(MainActivity.java:60)
12-19 17:50:22.314 31328 31328 E AndroidRuntime:        at android.view.View.performClick(View.java:7216)
12-19 17:50:22.314 31328 31328 E AndroidRuntime:        at android.view.View.performClickInternal(View.java:7190)
12-19 17:50:22.314 31328 31328 E AndroidRuntime:        at android.view.View.access$3500(View.java:827)
12-19 17:50:22.314 31328 31328 E AndroidRuntime:        at android.view.View$PerformClick.run(View.java:27663)
12-19 17:50:22.314 31328 31328 E AndroidRuntime:        at android.os.Handler.handleCallback(Handler.java:900)
12-19 17:50:22.314 31328 31328 E AndroidRuntime:        at android.os.Handler.dispatchMessage(Handler.java:103)
12-19 17:50:22.314 31328 31328 E AndroidRuntime:        at android.os.Looper.loop(Looper.java:219)
12-19 17:50:22.314 31328 31328 E AndroidRuntime:        at android.app.ActivityThread.main(ActivityThread.java:8291)
12-19 17:50:22.314 31328 31328 E AndroidRuntime:        ... 3 more

Frida Hook 插件类

App 插件往往会改变系统默认的 classloader,这时候如果直接 hook 插件中的类,就会发现 frida 提示找不到该类。Java.enumerateClassLoaders 用来枚举当前所有的 classloader,以 360 的 replugin 插件为例,相应的 hook 代码如下所示

Java.perform(function(){var HostApi;Java.enumerateClassLoaders({"onMatch": function(loader) {//console.log(loader);if (loader.toString().startsWith("com.qihoo360.replugin.PluginDexClassLoader")) {Java.classFactory.loader = loader; // 将当前class factory中的loader指定为我们需要的}},"onComplete": function() {console.log("success");}});Java.choose("com.xxx.class", {onMatch: function(instance) {HostApi = instance;},onComplete: function() {}});HostApi.funcname.implementation = function (arg1, arg2, arg3) {//console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));return this.funcname(arg1, arg2, arg3);};
});

Frida Hook 按钮onclick监控事件

var jclazz = null;
var jobj = null;function getObjClassName(obj) {if (!jclazz) {var jclazz = Java.use("java.lang.Class");}if (!jobj) {var jobj = Java.use("java.lang.Object");}return jclazz.getName.call(jobj.getClass.call(obj));
}function watch(obj, mtdName) {var listener_name = getObjClassName(obj);var target = Java.use(listener_name);if (!target || !mtdName in target) {return;}// send("[WatchEvent] hooking " + mtdName + ": " + listener_name);target[mtdName].overloads.forEach(function (overload) {overload.implementation = function () {//send("[WatchEvent] " + mtdName + ": " + getObjClassName(this));console.log("[WatchEvent] " + mtdName + ": " + getObjClassName(this))return this[mtdName].apply(this, arguments);};})
}function OnClickListener() {Java.perform(function () {//以spawn启动进程的模式来attach的话Java.use("android.view.View").setOnClickListener.implementation = function (listener) {if (listener != null) {watch(listener, 'onClick');}return this.setOnClickListener(listener);};//如果frida以attach的模式进行attch的话Java.choose("android.view.View$ListenerInfo", {onMatch: function (instance) {instance = instance.mOnClickListener.value;if (instance) {console.log("mOnClickListener name is :" + getObjClassName(instance));watch(instance, 'onClick');}},onComplete: function () {}})})
}
setImmediate(OnClickListener);

0x23 助理函数

ArrayBuffer 转换

function ab2Hex(buffer) {var arr = Array.prototype.map.call(new Uint8Array(buffer), function (x) {return ('00' + x.toString(16)).slice(-2)}).join(" ").toUpperCase();return "[" + arr + "]";
}function ab2Str(buffer) {return String.fromCharCode.apply(null, new Uint8Array(buffer));
}

获取 JS 对象类型

function getParamType(obj) {return obj == null ? String(obj) : Object.prototype.toString.call(obj).replace(/\[object\s+(\w+)\]/i, "$1") || "object";
}

例如

Java.perform(function(){send("obtain methods...");var Animal = Java.use("com.example.testfrida.Animal");var methods = Animal.class.getDeclaredMethods();console.log(getParamType(Animal));console.log(getParamType(methods));console.log(Animal.class);
});

输出

/usr/bin/python3.8 /home/lys/PycharmProjects/honor5/main.py
[+] obtain methods...
Object
Array
class com.example.testfrida.Animal

字节数组转十六进制

// thanks: https://awakened1712.github.io/hacking/hacking-frida/
function bytes2hex(array) {var result = '';for (var i = 0; i < array.length; ++i)result += ('0' + (array[i] & 0xFF).toString(16)).slice(-2);return result;
}

0x30 典型案例

0x31 hook BLE

分析

简要说明一下,在 BLE 协议栈中 ,有几个关键概念

  • GATT:通过 BLE 连接,读写属性类数据的 Profile 通用规范
  • Characteristic:特征,可以理解为一个数据类型,包括 value 和 descriptor(特征值和描述)
  • descriptor:是对 Characteristic 的描述,如范围、计量单位等
  • Service:一组 Characteristic 的集合

一个蓝牙设备有多个 profile,每个profile 有多个 service,每个 service 有多个 characteristic

例如,一个名为“Device Information”的 Service,可能包含多个 Characteristics,比如 “Manufacture Name String”,每个名字在编程中,都有一个统一的表示方法,唯一识别,这就是 UUID。

在 Android SDK 中,低功耗蓝牙使用BluetoothGattCallback 提供的回调,调用 onCharacteristicWrite /Read 方法进行读写,具体用法可参见 https://developer.android.com/reference/android/bluetooth/BluetoothGattCallback。

Android SDK 的 BLE 编程,通过 BluetoothDevice 类中的 connectGatt 注册 GATT 回调函数,在回调中完成读写操作。

final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);  // 得到设备
mBluetoothGatt = device.connectGatt(this, false, mGattCallback);   //注册回调函数// 回调函数的实现
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback(){// 重写Servcie/Characteristic的各个操作,读写数据的方法
}

hook

所以,想要 hook 住与设备传输的蓝牙数据,必然是拦截 BluetoothGattCallback 这个构造方法。

开源项目:https://github.com/optiv/blemon/blob/master/frida/blemon.js 给了我们一个很好的案例,掌握这些回调函数以及 Frida hook 蓝牙的一些方法。

if (Java.available) {Java.perform(function () {var BTGattCB = Java.use("android.bluetooth.BluetoothGattCallback");// 想 hook 匿名内部类的函数,从父类的构造方法入手BTGattCB.$init.overload().implementation = function () {console.log("[+] BluetoothGattCallback constructor called from " + this.$className);// 当前hook的类匿名类const NewCB = Java.use(this.$className);NewCB.onCharacteristicRead.implementation = function (g, c, s) {const retVal = NewCB.onCharacteristicRead.call(this, g, c, s);var uuid = c.getUuid();console.log(Color.Blue + "[BLE Read   <=]" + Color.Light.Black + " UUID: " + uuid.toString() + Color.Reset + " data: 0x" + bytes2hex(c.getValue()));return retVal;};NewCB.onCharacteristicWrite.implementation = function (g, c, s) {// ...};NewCB.onCharacteristicChanged.implementation = function (g, c) {// ...};return this.$init();};}); // end perform
}

Frida Hook Android App 进阶用法之 Java 运行时相关推荐

  1. 如何自己开发一个Android APP(4)——JAVA

    资源使用 在java文件中,通过资源id完成对资源的访问.可以通过对象.getId()的方法得到组件. 因为XML布局文件与java文件实际上是不互通的,也就是说我们的xml只控制外观,当你需要为某个 ...

  2. android 解决APP退出后以及后台运行时,再次点击图标的运行问题

    需求:现有闪屏界面SplashActivity,要求在无后台运行APP的情况下,点开应用,首先出现闪屏,2秒过后,进入到主功能界面MianActivity,运行时,将APP关至后台,再次点击图标,返回 ...

  3. android 后台执行js,android - 当应用程序在后台运行时,Android WebView消耗大量电能...

    我的Android应用程序中有一个WebView,并且此WebView运行的网站上带有相当多的Javascript.当我的应用程序在后台运行时,用户报告了高功耗,我希望这是由于此javascript. ...

  4. java运行时_java编译时与运行时概念与实例详解

    Java编译时与运行时很重要的概念,但是一直没有明晰,这次专门博客写明白概念. 基础概念 编译时 编译时顾名思义就是正在编译的时候.那啥叫编译呢?就是编译器帮你把源代码翻译成机器能识别的代码.(当然只 ...

  5. 利用Frida绕过Android App(途牛apk)的SSL Pinning

    0x00 前言 做APP测试过程中,使用burp无法抓到数据包或提示网络错误可能是因为APP启用了SSL Pinning,刚好最近接触到途牛apk就是这种情况,于是便有了本文. 0x01 SSL Pi ...

  6. 解决Frida hook某些App,明明包名写对了,却找不到进程

    这是因为,有些时候,hook的包名可能就是app的名字,比如最典型的,口碑App 解决这个问题的关键就是要知道所有进程是什么名字 可以用下面这个命令打印一下看看 #process = frida.ge ...

  7. Java运行时,指定程序文件的编码

    在命令行cmd里面运行 java -jar test.jar的时候,发现里面执行的汉字发生乱码.原来指定的是UTF-8. 解决如下: java -Dfile.encoding=UTF-8 -jar - ...

  8. 不反编译、无逆向基础也能轻松编写Android App Hook插件? Xposed的远房表弟,Hookworm来也!

    前言 Xposed的大名相信很多同学都不陌生,它提供了一种能力,可以在不修改原apk的情况下,以插件的方式改变目标App的某些行为. 但随着Android系统版本的迭代,原来的Xposed已经不适合在 ...

  9. 我的Android进阶之旅------Java字符串格式化方法String.format()格式化float型时小数点变成逗号问题...

    今天接到一个波兰的客户说有个APP在英文状态下一切运行正常,但是当系统语言切换到波兰语言的时候,程序奔溃了.好吧,又是我来维护. 好吧,先把系统语言切换到波兰语,切换到波兰语的方法查看文章 我的And ...

最新文章

  1. NeuIPS|在知识图谱上嵌入逻辑查询
  2. 传指针与指针引用的区别
  3. Tengine怎么去安装第三方模块、以及安装源码中的模块
  4. python中的特殊成员
  5. linux一键启动脚本,Linux一键启动、停止、重启Tomcat sh脚本
  6. scrapy去重原理,scrapy_redis去重原理和布隆过滤器的使用
  7. 深入浅出理解索引结构
  8. Python官方文档学习心得(第五篇)
  9. Linux环境下FTP工具的使用方法
  10. Android View框架总结(六)View布局流程之Draw过程
  11. swagger 上传文件 参数_跟我一起学.NetCore之Swagger让前后端不再烦恼及界面自定义...
  12. uboot之uboot中环境变量
  13. SpringMvc生成Excel和PDF
  14. python中的换行与不换行
  15. 《unix环境高级编程》--- 终端I/O
  16. unity的2d屏幕坐标转3d世界坐标
  17. JS实现TTS语音播报
  18. 天秤座男人对爱情的态度(图
  19. 搜狗皮肤 php,搜狗皮肤PHP怎样运用 搜狗输入法皮肤PHP运用办法
  20. Markdown输入数学公式

热门文章

  1. puppy linux u盘 分区,让安装在U盘上的Puppy Linux像安装在硬盘上一样工作
  2. 每日一题 - 剪绳子
  3. 经典Java面试题-Java中Char类型的运算
  4. nba2k15正版服务器,NBA2K15正版联机帐户过期怎么办_NBA2K15正版联机帐户过期解决方法_快吧单机游戏...
  5. Python9-前端基础知识-day47
  6. Abbirb120型工业机器人_abb IRB120资料的具体介绍
  7. 参与开源项目可以找到更合适的工作之5大理由;GNOME 基金会因专利被起诉等;开源之道每周评论(2019 09 29)...
  8. java计算机二级知识点、易错点整理(二)
  9. 膳食营养与健康类毕业论文文献都有哪些?
  10. C++中cout的基础语法与换行符endl的用法