1. 创建Xposed工程

在Android Studio中新建一个app工程,修改其中的 AndroidManifest.xml 文件,在<application></application>标签中增加如下代码

<meta-dataandroid:name="xposedmodule"android:value="true" />
<meta-dataandroid:name="xposeddescription"android:value="hello xposed" />
<meta-dataandroid:name="xposedminversion"android:value="82" />

在 app/libs 目录下添加Xposed的依赖库,api-82.jar, api-82-sources.jar(网上自行搜索)

修改 app 目录下的 build.gradle 文件,将以上的两个依赖库添加到 dependencies{} 中,如下

dependencies {compileOnly 'de.robv.android.xposed:api:82'compileOnly 'de.robv.android.xposed:api:82:sources'// other content......
}

在 mian 目录下创建一个assets文件夹,并在assets目录下创建一个 xposed_init 文件,文件中写入实现了 IXposedHookLoadPackage 接口的类名及包名;例如我当前新建了一个类 TestReverse 实现了 IXposedHookLoadPackage 接口,且当前包名为 com.test.xposed,则 xposed_init 中的内容应为

com.test.xposed.TestReverse

2. 相关API介绍

Xposed 模块API用法:XposedHelpers | Xposed Framework API

a.IXposedHookLoadPackage 接口,实现hook逻辑的类必须实现该接口,APP被加载的时候会调用该接口中的 handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) 方法,所以我们需要重写该函数,并在其中实现我们的hook逻辑

b.XC_LoadPackage.LoadPackageParam,该类下包含与正在加载的应用程序的有关信息。入下,其中packageName可以用于判断当前hook住的APP名称,processName表示当前APP所在的进程,classLoader表示当前APP使用的ClassLoader。

c.XposedHelpers类

1.findClass(String className, ClassLoader classLoader),使用特定的ClassLoader查找指定类,返回值为指定类的class;使用如下

final Class<?> callContextClass = XposedHelpers.findClass("com.alibaba.ariver.engine.api.bridge.model.NativeCallContext",lpparam.classLoader);

2.XposedHelpers.findAndHookMethod(String className, ClassLoader classLoader, String methodName, Object... parameterTypesAndCallback),hook指定的方法。

其中 Object... parameterTypesAndCallback是java中的范式,可以传递多个参数;当使用XposedHelpers.findAndHookMethod()方法时,需要传递被hook的函数所需要的全部参数的字节码,这些字节码可以通过.class获取到,或者findClass反射获取;java自带的类型比如String,就可以用String.class获取到。

回调对象XC_MethodHook()需重写两个方法 beforeHookedMethod(MethodHookParam param):这个方法中的代码逻辑会在hook住的方法调用前执行 ;afterHookedMethod(MethodHookParam param) 方法中的代码逻辑则会在hook住的方法调用完成后执行。

使用如下,以下代码逻辑表示,在com.baidu.swan.apps.jsbridge.SwanAppGlobalJsBridge 类下的 dispatchOnUiThread() 函数已经被hook住,在这个函数执行前后,beforeHookedMethod() 和 afterHookedMethod() 中的逻辑会分别执行。

XposedHelpers.findAndHookMethod("com.baidu.swan.apps.jsbridge.SwanAppGlobalJsBridge", lpparam.classLoader, "dispatchOnUiThread", String.class, new XC_MethodHook() {@Overrideprotected void beforeHookedMethod(MethodHookParam param) throws Throwable {super.beforeHookedMethod(param);XposedBridge.log("Baidu: dispatchOnUiThread 的线程:" + Thread.currentThread().getName());XposedBridge.log("Baidu:dispatchOnUiThread 入参:" + java.net.URLDecoder.decode(param.args[0].toString()) + "返回值:" + param.getResult());}@Overrideprotected void afterHookedMethod(MethodHookParam param) throws Throwable {super.afterHookedMethod(param);Field mCallbackHandler = jsbridge_a.getDeclaredField("mCallbackHandler");Object mmCallbackHandler = (Object) mCallbackHandler.get(param.thisObject);//com.baidu.swan.apps.core.slave.SwanAppWebViewWidgetXposedBridge.log("Baidu: mCallbackHandler在webview里调用API时的实现类:" + mmCallbackHandler.getClass().getName());});

3. XposedHelpers.findAndHookConstructor(String className, ClassLoader classLoader, Object... parameterTypesAndCallback),参数含义同上findAndHookMethod()。

使用如下,以下代码逻辑表示,当 com.baidu.swan.apps.jsbridge.SwanAppNativeSwanJsBridge 类的构造函数被调用时,在构造函数执行前后,beforeHookedMethod() 和 afterHookedMethod() 中的逻辑会分别执行。

XposedHelpers.findAndHookConstructor("com.baidu.swan.apps.jsbridge.SwanAppNativeSwanJsBridge", lpparam.classLoader, container_a, new XC_MethodHook() {@Overrideprotected void beforeHookedMethod(MethodHookParam param) throws Throwable {super.beforeHookedMethod(param);Log.d("Baidu:", "SwanAppNativeSwanJsBridge()构造函数参数: " + param.args[0].getClass().getName());}@Overrideprotected void afterHookedMethod(MethodHookParam param) throws Throwable {super.afterHookedMethod(param);}});

d. XposedBridge类

  1. XposedBridge.log(String text),将消息写入Xposed错误日志。用于打印想要了解的信息

  2. XposedBridge.hookMethod(Member hookMethod, XC_MethodHook callback),用特定的回调方法hook任意的方法或构造函数;用这种方法不需要像XposedHelpers.findAndHookMethod() 构造那么多参数,使用如下,以下代码逻辑表示先从一个类中获取到对应的method实例,然后将该method对象传递给XposedBridge.hookMethod()方法,即可直接hook住该方法

            for(final Method method: TestClass.getDeclaredMethods()){XposedBridge.hookMethod(method, new XC_MethodHook() {@Overrideprotected void beforeHookedMethod(MethodHookParam param) throws Throwable {super.beforeHookedMethod(param);XposedBridge.log("Baidu:share中的" + method.getName() + "被调用了");printStack();}});}

3. 打印函数调用栈

在逆向APP的框架逻辑时,当hook住一个被成功触发的函数,往往需要查看其调用栈,也就是该函数的上层调用函数,这些信息会对我们逆向APP框架有很大的帮助。

private void printStack() {// 获取线程的StackTraceElement[]Throwable ex = new Throwable();StackTraceElement[] stackElements = ex.getStackTrace();if (stackElements != null) {for (int i = 0; i < stackElements.length; i++) {XposedBridge.log("Baidu: Dump Stack" + i + ": " + stackElements[i].getClassName()+ "----" + stackElements[i].getFileName()+ "----" + stackElements[i].getLineNumber()+ "----" + stackElements[i].getMethodName());}}XposedBridge.log("Baidu: Dump Stack: ---------------over----------------");}

4. 修改hook住函数的参数

1. 修改基础数据类型和String类型的参数

直接赋值即可,如下

param.args[0] = 5; // int类型
param.args[1] = "hello"; //String类型

2.  修改基础数据类型和String类型的参数

先获取引用,再修改;例如

Map<String, String> target = (Map<String, String>) param.args[0]; // Map
target.put("name", "tnoy"); //向Map中添加数据

3. 修改类的实例中的成员变量

先用Field获取到相应的成员变量,再用Field.set(类的实例,修改后的值)进行修改;例如

final Class<?> model_d = XposedHelpers.findClass("com.alipay.android.phone.globalsearch.model.d", lpparam.classLoader);
Object dVar = param.args[0];
Field b = model_d.getDeclaredField("b"); // b是model类中的public的String类型的变量
b.set(dVar, "search_auto");

4. 修改Object[]类型的数组

针对Object数组中的某一个元素,先使用.getClass().getName()获取元素的类型,然后去类里面看要修改的对应的Field,然后进行上述第三点的操作,最后封装成一个新的Object[],再把这个Object[]赋值给param.args[i]。例如

Object[] obj = (Object[]) param.args[2];
for(Object o: obj){
if(o != null){// 判断当前Object的类是不是我们需要的那个类if(o.getClass().getName().equals("com.alipay.mobile.aompfavorite.base.rpc.request.MiniAppHistoryRequestPB")){// 在jadx中源码为:List<MiniAppHistoryReqItemPB> miniAppItems;Field miniAppItems = MiniAppHistoryRequestPB.getDeclaredField("miniAppItems");// 先用List<Object>反射拿到List<MiniAppHistoryReqItemPB>列表List<Object> item = (List<Object>) miniAppItems.get(o);// 获取List列表的第一个值Object item_1 =  item.get(0);Log.d("Mini 修改前", o.toString());Log.d("Mini List[0]", item_1.toString());// MiniAppHistoryReqItemPB类中的成员变量appIdField appId = MiniAppHistoryReqItemPB.getDeclaredField("appId");String AppId = (String) appId.get(item_1);Log.d("Mini appid", AppId);appId.set(item_1, "2018112262208014");Log.d("Mini 修改后", o.toString());}Log.d("Mini handler_1.1", o.getClass().getName() + " " + o.toString());}
}

5. hook动态加载进来的类

在APP框架中有些代码是动态加载进来的,比如插件化开发。使用以下代码,可以hook住动态加载进来的函数

XposedBridge.hookAllMethods(ClassLoader.class, "loadClass", new XC_MethodHook() {@Overrideprotected void afterHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {if (param.hasThrowable()) return;if (param.args.length != 1) return;Class<?> cls = (Class<?>) param.getResult();String name = cls.getName();// 判断是否是我要hook的动态加载的类if ("com.bytedance.webview.chromium.ContentSettingsAdapter".equals(name)) {// 指定要hook的methodXposedBridge.hookAllMethods(cls,"setJavaScriptEnabled",new XC_MethodHook() {protected void beforeHookedMethod(MethodHookParam param) throws Throwable {try {XposedBridge.log("commonRe: setJavaScriptEnabled被调用了: " + param.args[0]);printStack();} catch (Exception e) {XposedBridge.log("commonRe: hook error!");}}});}}});

6. 打印动态加载进来的类的存储位置(获取插件apk)

final Class<?> DexPathList = XposedHelpers.findClass("dalvik.system.DexPathList", lpparam.classLoader);for (final Method method: DexPathList.getDeclaredMethods()){XposedBridge.hookMethod(method, new XC_MethodHook() {@Overrideprotected void beforeHookedMethod(MethodHookParam param) throws Throwable {super.beforeHookedMethod(param);// splitPaths()方法传入的是动态加载进来的类在手机中的路径if(method.getName().equals("splitPaths")){XposedBridge.log("Baidu: DexPathList中的" + method.getName() + "被调用了,传参为" + param.args[0]+" " + param.args[1]);}}}) ;}

7. 自动化hook方法脚本

在某些情况下,我们拥有一系列需要hook的方法的signature,而且需要hook住这些方法获取某些信息时,我们可以通过自动化脚来实现将这些方法一一hook住,就不需要手工挨个写hook每一个方法的脚本。

代码举例实现如下

StaticInfo.java:用于从json文件中读取要hook方法的signature,并通过反射获取到对应的method对象
package com.example.ttest;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;public class StaticInfo {public static HashMap<PoolHook.normalHandlerHook, Method> HookMethods = new HashMap<>();public XC_LoadPackage.LoadPackageParam lpparam;public StaticInfo(XC_LoadPackage.LoadPackageParam lpparam) throws NoSuchFieldException {this.lpparam = lpparam;// read from filetry{InputStream is = new FileInputStream(new File("/sdcard/tmp/static_info.json"));init(is);is.close();}catch (Exception e){XposedBridge.log("AMZ: read json error! " + e.toString());}}public void init(InputStream in) throws IOException, NoSuchFieldException {BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));StringBuilder sb = new StringBuilder();String line;String ls = System.getProperty("line.separator");while ((line = bufferedReader.readLine()) != null){sb.append(line);sb.append(ls);}String text = sb.toString();XposedBridge.log("AMZ: 读取json文件成功!");JSONArray jsonArray = JSON.parseArray(text);for (int i=0; i<jsonArray.size(); i++){JSONObject jsonObject = jsonArray.getJSONObject(i);String signature = jsonObject.getString("signature");String filedName = jsonObject.getString("field");String type = jsonObject.getString("type");String declaringClass = jsonObject.getString("declaringClass");reflectMethod(signature, filedName, type, declaringClass);}}public void init(String text) throws NoSuchFieldException {JSONArray jsonArray = JSON.parseArray(text);for (int i=0; i<jsonArray.size(); i++){JSONObject jsonObject = jsonArray.getJSONObject(i);String signature = jsonObject.getString("signature");String filedName = jsonObject.getString("field");String type = jsonObject.getString("type");String declaringClass = jsonObject.getString("declaringClass");reflectMethod(signature, filedName, type, declaringClass);}}// init HashMap for Method and Field due to json filepublic void reflectMethod(String signature, String fieldName, String type, String declaringClass) throws NoSuchFieldException {XposedBridge.log("AMZ: 正在反射获取Method:"+ signature  + " " + fieldName + " " + type);Field field = XposedHelpers.findClass(declaringClass, lpparam.classLoader).getDeclaredField(fieldName);PoolHook.normalHandlerHook hook = new PoolHook.normalHandlerHook(fieldName,type, signature, field);String className = signature.split(" ")[0].substring(1,signature.split(" ")[0].length()-1);XposedBridge.log("AMZ: parse classname: " + className);try {Class<?> clazz = XposedHelpers.findClass(className, this.lpparam.classLoader);String methodName = signature.split(" ")[2].split("\\(")[0];ArrayList<String> paramTypes = new ArrayList(Arrays.asList(signature.split(" ")[2].split("\\(")[1].substring(0,signature.split(" ")[2].split("\\(")[1].length()-2).split(",")));XposedBridge.log("AMZ: 要找的方法的参数为 " + paramTypes.toString());for (Method method: clazz.getDeclaredMethods()){XposedBridge.log("AMZ: 正在匹配方法:" + method.getName() + " ==> " + methodName);XposedBridge.log("AMZ: 当前方法参数个数:" + method.getParameterTypes().length + " 目标方法的参数个数:" + paramTypes.size());if (method.getName().equals(methodName)){if (method.getParameterTypes().length == paramTypes.size()){XposedBridge.log("AMZ: 方法名和参数个数匹配上了");ArrayList<String> paramTypesCopy = (ArrayList<String>) paramTypes.clone();for (Class paramType: method.getParameterTypes()){XposedBridge.log("AMZ: 当前方法的参数类型:" + paramType.getName());if (paramTypes.contains(paramType.getName())){paramTypesCopy.remove(paramType.getName());}}if (paramTypesCopy.size() == 0 && !HookMethods.containsKey(hook)){XposedBridge.log("AMZ: find target method: " + method.toString() + " target_field: " + field);HookMethods.put(hook, method);}}else if (method.getParameterTypes().length == 0 && paramTypes.size() ==1 && paramTypes.get(0).length() == 0){XposedBridge.log("AMZ: 目标方法没有参数,匹配成功");if (!HookMethods.containsKey(hook)){XposedBridge.log("AMZ: find target method: " + method.toString() + " target_field: " + field);HookMethods.put(hook, method);}}}}}catch (Exception e){XposedBridge.log("AMZ: " + e.toString());}}
}

PoolHook.java: 继承 IXposedHookLoadPackage 接口,编写hook逻辑

package com.example.ttest;import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;public class PoolHook implements IXposedHookLoadPackage{public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable{new StaticInfo(lpparam);if (lpparam.packageName.equals("com.eg.android.AlipayGphone")){XposedBridge.log("AMZ: hook start!!");hookPackage(lpparam);}}private void hookPackage(XC_LoadPackage.LoadPackageParam lpparam){if (StaticInfo.HookMethods.isEmpty()){XposedBridge.log("AMZ: HashMap is empty");}else {XposedBridge.log("AMZ: HashMap is not empty");}for (Map.Entry<normalHandlerHook, Method> entry: StaticInfo.HookMethods.entrySet()){XposedBridge.hookMethod(entry.getValue(), entry.getKey());}}static class normalHandlerHook extends XC_MethodHook{private String fieldName;private String type;private Field field;private String signature;public normalHandlerHook(String fieldName, String type, String signature, Field field){this.fieldName = fieldName;this.type = type;this.signature = signature;this.field = field;}protected void beforeHookedMethod(MethodHookParam param) throws Throwable{System.out.println("AMZ: current method: " + param.method);XposedBridge.log("AMZ: hook field right now:" + field.toString());XposedBridge.log("AMZ: pool type:" + this.type);this.field.setAccessible(true);if (this.type.equals("HashMap")){XposedBridge.log("AMZ: this is HashMap");HashMap<Object, Object> mmap = (HashMap<Object, Object>) this.field.get(param.thisObject);for (Map.Entry<Object, Object> entry: mmap.entrySet()){Object key = entry.getKey();Object value = entry.getValue();XposedBridge.log("AMZ: signature=" + this.signature +" key=" + key + " value=" + value);}}else if(this.type.equals("Map") || this.type.equals("ConcurrentHashMap") || this.type.equals("LinkedHashMap")){XposedBridge.log("AMZ: this is Map or ConcurrentHashMap");Map<Object, Object> mmap_1 = (Map<Object, Object>) field.get(param.thisObject);for (Map.Entry<Object, Object> entry: mmap_1.entrySet()){Object key = entry.getKey();Object value = entry.getValue();XposedBridge.log("AMZ: signature=" + this.signature +" key=" + key + " value=" + value);}}}}
}

8. Xposed hook 原理

Java虚拟机JVM在加载了dex后,会将整个dex文件的内容mmap(一种内存映射文件的方法)到内存中。JVM在load一个class的时候,根据类的描述符,在内存的dex区域,查询到对应的数据,构建出ClassObject对象,以及这个ClassObject关联的Method。

Method分为directMethod和nativeMethod,分别是Java里实现的方法和C/C++里面实现的方法。Method里面有两个重要的指针:

const u2* insns;
DalvikBridgeFunc nativeFunc;

对于directMethod,insns存放了该方法在内存中的字节码指针;对于nativeMethod,会根据方法描述符,通过特定的映射关系得到一个native层的函数名(JNI method),然后去查找对应的函数,得到了函数指针后,再将这个指针赋值给insns。在nativeFunc这个桥接函数中,将insns解析为函数指针,然后进行调用。

Xposed在对java方法进行hook的时候,会先将JVM里面的这个方法的Method属性改为nativeMethod(也就是修改一个表示字段),然后将该方法的nativeFunc指向自己实现的一个native方法。于是当被hook的方法被调用到时,就会实际去调用自己实现的这个native方法。

在自己实现的native方法中,xposed直接调用了一个java方法,这个java方法里面对原方法进行了调用,并在调用前后插入了钩子,于是就hook住了这个方法。

Android所有的APP进程都是由Zygote进程启动的,所以Zygote进程中加载的代码,在后续所有fork出来的子进程中都有。Xposed替换了Zygote进程对应的可执行文件/system/bin/app_process,并用于加载xposed相关代码。

总的来说,Xposed将要hook的JAVA方法变成了native方法,在beforeMethodHook()和afterMethodHook()方法中的逻辑实现在native方法中,并将原被hook函数插在这两个函数之间正常调用,示意图如下。

Xposed常用逆向函数相关推荐

  1. VB程序逆向常用的函数

    @转自: http://www.cnblogs.com/bbdxf/p/3780187.html # 参数压栈从右往左,多的参数是返回值的 buffer 待会写文 程序逆向常用的函数 1) 数据类型转 ...

  2. jQuery中常用的函数方法总结

    jQuery中为我们提供了很多有用的方法和属性,自己总结的一些常用的函数,方法.个人认为在www.21kaiyun.com的紫微斗数星座在线排盘开发中会比较常用的,仅供大家学习和参考. 事件处理 re ...

  3. R语言广义线性模型函数GLM、广义线性模型(Generalized linear models)、GLM函数的语法形式、glm模型常用函数、常用连接函数、逻辑回归、泊松回归、系数解读、过散度分析

    R语言广义线性模型函数GLM.广义线性模型(Generalized linear models).GLM函数的语法形式.glm模型常用函数.常用连接函数.逻辑回归.泊松回归.系数解读.过散度分析 目录

  4. R语言常用sys函数汇总:sys.chmod、Sys.Date、Sys.time、Sys.getenv、Sys.getlocale、sys.getpid、sys.glob、sys.info等

    R语言常用sys函数汇总:sys.chmod.Sys.Date.Sys.time.Sys.getenv.Sys.getlocale.sys.getpid.sys.glob.sys.info等 目录

  5. mysql的聚合函数综合案例_MySQL常用聚合函数详解

    一.AVG AVG(col) 返回指定列的平均值 二.COUNT COUNT(col) 返回指定列中非NULL值的个数 三.MIN/MAX MIN(col):返回指定列的最小值 MAX(col):返回 ...

  6. excel中最常用的30个函数_最常用日期函数汇总excel函数大全收藏篇

    在我们的实际工作中,经常需要用到日期函数.日期函数那么多,你还只会用函数TODAY吗?那你就OUT了.今天一起来看下常用日期函数的用法! 1.DATE 函数DATE:返回在日期时间代码中代表日期的数字 ...

  7. MapInfo中常用查询函数及用法

    MapInfo中常用查询函数及用法: 函数用途 语法 备注 图层中选点 Str$(obj)="point": Str(String)表示字符串:point表示点: 图层中选线 St ...

  8. loadrunner写脚本常用C函数

    loadrunner写脚本常用C函数 strcat的串连两个字串. strchr返回指向第一次出现的字符串中的字符. STRCMP比较两个字符串来确定的字母顺序. STRCPY一个字符串复制到另一个地 ...

  9. linux c数字转字符串函数,Linux常用C函数—字符串转换篇

    Linux 常用C 函数-字符串转换篇 atof (将字符串转换成浮点型数) 相关函数 atoi ,atol ,strtod ,strtol ,strtoul 定义函数 double atof(con ...

最新文章

  1. PTA(BasicLevel)-1007素数对猜想
  2. 字节流代码 java_java代码字符字节流
  3. RocketMQ Filtersrv
  4. 使用字符串切割,使手机号中间四位隐藏
  5. LINUX无法运行navixat,关于RX5700XT的驱动方法以及bug解决方案
  6. Google Maps API 进级:在信息窗口GInfoWindow中嵌入Flash动画
  7. 微服务容错时,这些技术你要立刻想到
  8. 深入源码之Commons Logging[转]
  9. html5 实现 图片上传预览
  10. CCF NOI1003 猜数游戏
  11. Ubuntu12.04上编译PlateGatewayQt
  12. Oracle 提取汉子去除非汉子数据(保留标点符号)
  13. 6种 分布式限流方案,我替你整理好了
  14. php 2037时间问题
  15. 鸿蒙熔炉是真实存在的吗,古董局中局父辛爵是真的吗 父辛爵真实存在国内仅有两件...
  16. php 判断某一天是周几,php如何判断一个日期是周几
  17. HTML网页表格标签,HTML静态网页(标签、表格)
  18. 鏖战双十一:阿里直播平台面临的技术挑战
  19. Galera Cluster一致性问题
  20. “冰封”合约背后的老牌劲敌——拒绝服务漏洞 | 漏洞解析连载之二

热门文章

  1. (附源码)计算机毕业设计SSM在线二手书店
  2. 10次机会 js 猜数_JS猜数字游戏实例讲解
  3. java的字母_Java字母大小写转换的方法
  4. 搞单片机是青春饭吗?
  5. opencv Canny函数
  6. win7系统搭建svn服务器,Win7系统如何使用VisualSVN Server搭建SVN服务器?
  7. 【数据结构】二叉树——理论篇
  8. 模拟数据采集卡之ADCTDC 模拟时间/数字转换器组合应用选型指南
  9. 2005年MBA考试英语试题
  10. 派代邢孔育:独立B2C获1个新用户成本为300元