本文博客地址:http://blog.csdn.net/qq1084283172/article/details/78092365

Dalvik模式下的Android加固技术已经很成熟了,Dalvik虚拟机模式下的Android加固技术也在不断的发展和加强,鉴于Art虚拟机比Dalvik虚拟机的设计更复杂,Art虚拟机模式下兼容性更严格,一些Dalvik虚拟机模式下的Android加固技术并不能马上移植到Art模式下以及鉴于Art虚拟机模式下的设计复杂和兼容性考虑,暂时相对来说,Art模式下的Android加固并没有Dalvik虚拟机模式下的粒度细和强。

本文给出的 Art模式下基于Xposed Hook开发脱壳工具的思路和流程不是我原创,主要是源自于看雪论坛的文章《一个基于xposed和inline hook的一代壳脱壳工具》,思路和流程有原作者smartdon提供,文中提到的Art模式下的dexdump脱壳工具源码github下载地址:https://github.com/smartdone/dexdump。原作者提供的脱壳操作步骤稍微复杂了一些,在此基础上我对原作者的代码进行了修改,使脱壳更加方便,原作者的代码是Android Studio的工程,顺手将其转化为了Eclipse下的工程。作者smartdon的Art模式下脱壳思路如下图所示:

要学习Android加固的脱壳还是需要先了解一下Dalvik模式下和Art模式下Android加固的流程和思路,熟悉一下 DexClassLoader 的代码执行流程。虽然Dalvik模式下和Art模式下DexClassLoader的java层实现代码是一样的,但是从 OpenDexFileNative函数 之后Dalvik模式下和Art模式下Native层的代码实现就不一样了,后面有空花时间整理一下Android加固相关方面的知识。Art模式下基于Xposed Hook开发的脱壳工具只对整体dex加固的Android应用脱壳才有效果,对于dex文件类方法抽离这类加固处理的Android应用就显得比较苍白了。

ART模式下基于Xposed Hook开发脱壳工具的思路整理。

1. Art模式下,Inline Hook时机 的选择

Android加固的一般思路:在外壳Apk应用调用 android.app.Application类 的成员函数 attach 时,内存解密出被保护的原始dex文件使用DexClassLoader进行内存加载,Art虚拟机模式下DexClassLoader进行dex文件的加载过程中绕不开函数 const DexFile* DexFile::OpenMemory(const std::string& location, uint32_t location_checksum, MemMap* mem_map, std::string* error_msg),因此我们选择在 art::DexFile::OpenMemory函数 处进行dex文件的内存dump处理。基于Art模式下的Xposed Hook实现在外壳apk应用调用 android.app.Application类 的成员函数 attach 时,在被保护的dex文件加载之前Inline Hook OpenMemory函数,对内存解密后的原始dex文件进行拦截。

Art模式下,Xposed Hook外壳apk应用 android.app.Application类(实现的代理子类) 的成员函数 attach。

2. Art模式下,Inline Hook函数点 的选择

Art虚拟机模式下,对 art::DexFile::OpenMemory函数 进行Inline Hook操作所采用的Hook框架为作者 Ele7enxxh 编写的Android平台的Inline Hook库。作者Ele7enxxh关于该Inline Hook库的介绍和描述可以参考作者的博文《Android Arm Inline Hook》,该Inline Hook库的github下载地址为:https://github.com/ele7enxxh/Android-Inline-Hook。由于Art虚拟机模式下,dex文件的加载DexClassLoader的代码实现流程中绕不开art::DexFile::OpenMemory函数的执行,更重要的是该函数的传入参数 base表示的是dex文件所在的内存地址, size表示的是dex文件的字节长度,因此选择在art::DexFile::OpenMemory函数处进行dex文件的内存dump处理。

Android 5.0版以后ART模式下,OpenMemory函数的形式:http://androidxref.com/5.0.0_r2/xref/art/runtime/dex_file.cc#325

ART模式下基于Xposed Hook和Ele7enxxh Inline Hook开发的脱壳工具dexdump的代码详细分析。

1. 作者smartdon写了个获取当前安装应用的列表界面,用以选择需要脱壳的apk应用,然后根据选择脱壳apk应用的包名,在sdcard文件夹下生成脱壳需要的配置文件dumdex.js,dumdex.js文件中保存着需要脱壳的apk应用的包名。

选择脱壳apk应用列表界面的实现代码 MainActivity.java:

package com.xposedhook.dexdump;import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.pm.PackageInfo;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ListView;
import android.widget.TextView;import java.util.ArrayList;
import java.util.List;import com.example.com.xposedhook.dexdump.R;public class MainActivity extends Activity {//    static {
//
//      // 加载动态库文件libhook.so
//        System.loadLibrary("hook");
//    }private List<Appinfo> appinfos;private ListView listView;private AppAdapter adapter;private List<String> selected;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// 设置布局文件setContentView(R.layout.activity_main);// 调用native层实现的dump函数// 对函数art::DexFile::OpenMemory进行Hook处理
//        Dumpper.dump();// 读取配置文件"/sdcard/dumdex.js"获取需要脱壳的apk应用的包名列表selected = Config.getConfig();// Apk应用信息列表appinfos = new ArrayList<>();// 用于显示apk应用的列表listView = (ListView) findViewById(R.id.applist);adapter = new AppAdapter(appinfos);// 设置ListView控件的适配器listView.setAdapter(adapter);// 创建线程new Thread(){@Overridepublic void run() {super.run();// 获取当前Android系统安装的apk应用列表getInstallAppList();}}.start();}private void getInstallAppList() {try{// 获取当前安装应用的PackageInfo列表List<PackageInfo> packageInfos = getPackageManager().getInstalledPackages(0);// 遍历当前安装应用的PackageInfo列表for(PackageInfo packageInfo : packageInfos) {Appinfo info = new Appinfo();// 设置apk应用的名称info.setAppName(packageInfo.applicationInfo.loadLabel(getPackageManager()).toString());// 设置apk应用的包名info.setAppPackage(packageInfo.packageName);// 根据当前遍历到apk应用的包名是否在配置文件中设置选中与否的现实if(Config.contains(selected, info.getAppPackage())) {info.setChecked(true);}else {info.setChecked(false);}// 添加当前遍历到apk应用的信息到apk应用的现实列表中appinfos.addAll(info);// 更新适配器的现实adapter.notifyDataSetChanged();}}catch (Exception e) {e.printStackTrace();}}// ListView列表的适配器@SuppressLint({ "ViewHolder", "InflateParams" })class AppAdapter extends BaseAdapter{private List<Appinfo> appinfos;public AppAdapter(List<Appinfo> appinfos){this.appinfos = appinfos;}@Overridepublic int getCount() {return appinfos.size();}@Overridepublic Object getItem(int i) {return appinfos.get(i);}@Overridepublic long getItemId(int i) {return i;}@Overridepublic View getView(int i, View view, ViewGroup viewGroup) {View v = LayoutInflater.from(MainActivity.this).inflate(R.layout.item, null);final int posi = i;final TextView appname = (TextView) v.findViewById(R.id.tv_appname);appname.setText(appinfos.get(i).getAppName());TextView appPackage = (TextView) v.findViewById(R.id.tv_apppackage);appPackage.setText(appinfos.get(i).getAppPackage());CheckBox checkBox = (CheckBox) v.findViewById(R.id.cb_select);if(appinfos.get(i).isChecked()) {checkBox.setChecked(true);}else {checkBox.setChecked(false);}// 监控apk应用列表的选中事件checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {@Overridepublic void onCheckedChanged(CompoundButton compoundButton, boolean b) {if(b) {// 添加选中的apk应用的包名到配置文件/sdcard/dumdex.js中// 格式: ["apk包名字符串"]Config.addOne(appinfos.get(posi).getAppPackage());} else {// 从配置文件/sdcard/dumdex.js中删除指定包名的apk引用Config.removeOne(appinfos.get(posi).getAppPackage());}}});return v;}}
}

根据用户选择的脱壳apk应用的包名,生成脱壳需要的配置文件dumdex.js文件的代码 Config.java:

package com.xposedhook.dexdump;import android.util.Log;import org.json.JSONArray;
import org.json.JSONObject;import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;/*** Created by smartdone on 2017/7/2.*/public class Config {// sdcard的问价路径最好还是通过函数来获取// File file=Environment.getExternalStorageDirectory();// 直接写死有兼容性的问题private static final String FILENAME = "/sdcard/dumdex.js";// 将JSONArray类型的数据写入到配置文件"/sdcard/dumdex.js"中public static void writeConfig(String s) {try {// 文件"/sdcard/dumdex.js"的文件写入流FileOutputStream fout = new FileOutputStream(FILENAME);// 将字符串写入到文件中fout.write(s.getBytes("utf-8"));// 刷新文件流fout.flush();fout.close();} catch (Exception e) {e.printStackTrace();}}// 添加Apk应用的包名到配置文件/sdcard/dumdex.jspublic static void addOne(String name) {List<String> ori = getConfig();if(ori == null) {JSONArray jsonArray = new JSONArray();jsonArray.put(name);writeConfig(jsonArray.toString());} else {ori.add(name);JSONArray jsonArray = new JSONArray();for(String o : ori) {jsonArray.put(o);}writeConfig(jsonArray.toString());}}// 从配置文件/sdcard/dumdex.js中删除指定包名的应用public static void removeOne(String name) {List<String> ori = getConfig();if(ori != null) {for(int i = 0; i < ori.size(); i++) {if(ori.get(i).equals(name)) {ori.remove(i);}}JSONArray jsonArray = new JSONArray();for(String s : ori) {jsonArray.put(s);}writeConfig(jsonArray.toString());}}// 读取配置文件"/sdcard/dumdex.js"获取需要脱壳的apk应用的包名列表public static List<String> getConfig() {// 打开文件"/sdcard/dumdex.js"File file = new File(FILENAME);// 判断文件是否存在if (file.exists()) {try {// 构建内存缓冲区读取流BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file)));// 分行读取文件数据String line = br.readLine();// 使用读取的一行文件数据构建JSONArray对象JSONArray jsonArray = new JSONArray(line);List<String> apps = new ArrayList<>();// 解析JSONArray数据将需要Hook的apk应用的包名添加到列表中for(int i = 0; i < jsonArray.length(); i++) {apps.add(jsonArray.getString(i));}br.close();
//                Log.e("DEX_DUMP", "需要hook的列表: " + line);return apps;} catch (Exception e) {e.printStackTrace();}}return null;}// 判断name是否在需要脱壳的apk应用的列表中public static boolean contains(List<String> lists, String name) {if(lists == null) {return false;}for(String l : lists) {if(l.equals(name)) {return true;}}return false;}}

2. 基于Art虚拟机模式下的Xposed框架 Hook外壳Apk应用 android.app.Application类 的成员函数 attach,这里提到的Xposed Hook框架需要注意一下,不能使用支持Android 4.4.x版本之前的Xposed Hook框架(只支持Dalvik虚拟机模式,不支持Art虚拟机模式),需要使用支持Android 5.0版本以后的Xposed Hook框架(支持Art虚拟机模式),Xposed Hook框架的下载地址可以参考:http://repo.xposed.info/module/de.robv.android.xposed.installer。

Art虚拟机模式下,Xposed框架 Hook外壳Apk应用 android.app.Application类 的成员函数 attach 的模块代码 com.xposedhook.dexdump.Main 编写的实现:

package com.xposedhook.dexdump;import android.content.Context;
import android.util.Log;import java.util.List;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;/*** Created by smartdone on 2017/7/1.*/// art模式下的Xposed Hook
public class Main implements IXposedHookLoadPackage {private static final String TAG = "DEX_DUMP";@Overridepublic void handleLoadPackage(final XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {// 从配置文件"/sdcard/dumdex.js"中获取需要脱壳的apk应用列表               List<String> hooklist = Config.getConfig();// 判断当前应用是否在需要脱壳的apk应用列表中if(!Config.contains(hooklist, loadPackageParam.packageName))return;XposedBridge.log("对" + loadPackageParam.packageName + "进行处理");Log.e(TAG, "开始处理: " + loadPackageParam.packageName);try{// 自定义加载动态库文件libhook.so// 可以试着使用兼容性好的Android系统函数来处理路径问题System.load("/data/data/com.xposedHook.dexdump/lib/libhook.so");} catch (Exception e) {Log.e(TAG, "加载动态库失败:" + e.getMessage());}Log.e(TAG, "加载动态库成功");// 对Android系统类android.app.Application的attach函数进行art模式下的Hook操作XposedHelpers.findAndHookMethod("android.app.Application", loadPackageParam.classLoader, "attach", Context.class, new XC_MethodHook() {private Context context;@Overrideprotected void beforeHookedMethod(MethodHookParam param) throws Throwable {super.beforeHookedMethod(param);// 在类android.app.Application的attach函数调用之前进行dex文件的内存dump操作Dumpper.dump();}@Overrideprotected void afterHookedMethod(MethodHookParam param) throws Throwable {super.afterHookedMethod(param);// 不处理}});}
}

3.在需要脱壳的apk应用进程里动态加载动态库文件/data/data/com.xposedHook.dexdump/lib/libhook.so,实现Art模式下对 const DexFile* DexFile::OpenMemory(const std::string& location, uint32_t location_checksum, MemMap* mem_map, std::string* error_msg)函数 的Inline Hook操作,在Inline Hook操作的自定义实现函数里进行dex文件的内存dump处理。使用Ele7enxxh Inline Hook框架对Art模式下的OpenMemory函数进行Hook操作的实现代码如下:

//
// Created by 袁东明 on 2017/7/1.
//extern "C" {
#include "include/inlineHook.h"
}#include "dump.h"
#include <unistd.h>
#include <android/log.h>
#include <sys/system_properties.h>
#include <stdlib.h>
#include <fcntl.h>
#include <time.h>
#include <string>
#include <dlfcn.h>
#include <dlfcn.h>#define TAG "DEX_DUMP"int isArt();
void getProcessName(int pid, char *name, int len);
void dumpFileName(char *name, int len, const char *pname, int dexlen);// 保存当前apk进程的进程名字
static char pname[256];// 判断当前所处环境是否是Android art虚拟机模式
int isArt() {char version[10];// 获取ro.build.version.sdk的属性值__system_property_get("ro.build.version.sdk", version);// 打印当前Android系统的api版本信息__android_log_print(ANDROID_LOG_INFO, TAG, "api level %s", version);// 将api版本转换成int型版本号int sdk = atoi(version);// 判断api版本是否是大于21(要求Android系统的版本为 Android 5.0以上 才可以)if (sdk >= 21) {// art虚拟机模式return 1;}return 0;
}// 读取/proc/self/cmdline文件的数据,获取当前apk进程的进程名字
void getProcessName(int pid, char *name, int len) {int fp = open("/proc/self/cmdline", O_RDONLY);memset(name, 0, len);read(fp, name, len);close(fp);
}// 格式字符串构建dump的dex文件的路径字符串
void dumpFileName(char *name, int len, const char *pname, int dexlen) {time_t now;struct tm *timenow;time(&now);// 获取当前时间(值得借鉴和学习)timenow = localtime(&now);memset(name, 0, len);// 格式化字符串得到当前dump的dex文件路径字符串sprintf(name, "/data/data/%s/dump_size_%u_time_%d_%d_%d_%d_%d_%d.dex", pname, dexlen,timenow->tm_year + 1900,timenow->tm_mon + 1,timenow->tm_mday,timenow->tm_hour,timenow->tm_min,timenow->tm_sec);
}void writeToFile(const char *pname, u_int8_t *data, size_t length) {char dname[1024];// pname为当前进程的名称// 格式字符串构建dump的dex文件的路径字符串dnamedumpFileName(dname, sizeof(dname), pname, length);__android_log_print(ANDROID_LOG_ERROR, TAG, "dump dex file name is : %s", dname);__android_log_print(ANDROID_LOG_ERROR, TAG, "start dump");// 根据dname创建新文件用于保存内存dump的dex文件int dex = open(dname, O_CREAT | O_WRONLY, 0644);if (dex < 0) {__android_log_print(ANDROID_LOG_ERROR, TAG, "open or create file error");return;}// 将内存dex文件的数据写入到新的dname文件中int ret = write(dex, data, length);if (ret < 0) {__android_log_print(ANDROID_LOG_ERROR, TAG, "write file error");} else {__android_log_print(ANDROID_LOG_ERROR, TAG, "dump dex file success `%s`", dname);}// 关闭文件close(dex);
}// 保存openmemory函数旧的地址
art::DexFile *(*old_openmemory)(const byte *base, size_t size, const std::string &location,uint32_t location_checksum, art::MemMap *mem_map,const art::OatDexFile *oat_dex_file, std::string *error_msg) = NULL;art::DexFile *new_openmemory(const byte *base, size_t size, const std::string &location,uint32_t location_checksum, art::MemMap *mem_map,const art::OatDexFile *oat_dex_file, std::string *error_msg) {__android_log_print(ANDROID_LOG_ERROR, TAG, "art::DexFile::OpenMemory is called");writeToFile(pname, (uint8_t *) base, size);// 调用原art::DexFile::OpenMemory函数return (*old_openmemory)(base, size, location, location_checksum, mem_map, oat_dex_file,error_msg);
}void hook() {// 加载动态库文件libart.sovoid *handle = dlopen("libart.so", RTLD_GLOBAL | RTLD_LAZY);if (handle == NULL) {__android_log_print(ANDROID_LOG_ERROR, TAG, "Error: unable to find the SO : libart.so");return;}// 获取导出函数const DexFile* DexFile::OpenMemory(const std::string& location, uint32_t location_checksum, MemMap* mem_map, std::string* error_msg)// 的调用地址,http://androidxref.com/5.0.0_r2/xref/art/runtime/dex_file.cc#325// 在不同的Android版本上OpenMemory函数的名称粉碎稍有不同,需要根据实际Android系统版本进行修改void *addr = dlsym(handle,"_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_");if (addr == NULL) {__android_log_print(ANDROID_LOG_ERROR, TAG,"Error: unable to find the Symbol : _ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_");return;}// 使用ele7enxxh写的inline Hook框架对art模式下的art::DexFile::OpenMemory函数进行inline Hook操作// 进行art::DexFile::OpenMemory函数inline Hook操作的Hook注册if (registerInlineHook((uint32_t) addr, (uint32_t) new_openmemory,(uint32_t **) &old_openmemory) != ELE7EN_OK) {__android_log_print(ANDROID_LOG_ERROR, TAG, "register inline hook failed");return;}// 对art模式下的art::DexFile::OpenMemory函数进行inline Hook操作if (inlineHook((uint32_t) addr) != ELE7EN_OK) {__android_log_print(ANDROID_LOG_ERROR, TAG, "inline hook failed");return;}__android_log_print(ANDROID_LOG_INFO, TAG, "inline hook success");
}// java方法Dumpper.dump()的native层实现
// com.xposedhook.dexdump.Dumpper.dump
JNIEXPORT void JNICALL Java_com_xposedhook_dexdump_Dumpper_dump(JNIEnv *env, jclass clazz) {// 获取当前apk进程的进程名字getProcessName(getpid(), pname, sizeof(pname));// 判断当前Android虚拟机是否是art模式if (isArt()) {// 当前Android系统运行在art模式下// 执行inline Hook操作hook();}
}

4. 使用当前脱壳工具进行Art虚拟机模式下的Android加固脱壳需要注意的地方:

A. 移动设备的Android系统必须是 Api 21以上即Android 5.0以上版本的Android系统并且要运行在Art虚拟机模式下,不能运行在Dalvik虚拟机模式下。

B. 移动设备上安装的Xposed Hook框架必须是支持Android 5.0以上版本的ART虚拟机模式下的Xposed Hook框架。

C. 第1次使用该脱壳工具com.xposedhook.dexdump.apk时,先使用apk应用列表界面选择需要脱壳的apk应用,生成后面脱壳需要的配置文件"/sdcard/dumdex.js"。

D. 重启移动设备使Xposed框架的Hook模块com.xposedhook.dexdump.Main生效,运行需要脱壳的apk应用等待脱壳完成,脱壳后的dex文件在 /data/data/脱壳apk应用的包名/ 文件夹下。

注释版完整的代码下载地址:http://download.csdn.net/download/qq1084283172/9996172

ART模式下基于Xposed Hook开发脱壳工具相关推荐

  1. 基于dalvik模式下的Xposed Hook开发的某加固脱壳工具

    本文博客地址:http://blog.csdn.net/qq1084283172/article/details/77966109 这段时间好好的学习了一下Android加固相关的知识和流程也大致把A ...

  2. android 5.0.1 libdvm.so,Android逆向进阶—— 脱壳的奥义(基ART模式下的dump)

    本文作者:i春秋作家HAI_ZHU 0×00 前言 市面上的资料大多都是基于Dalvik模式的dump,所以这此准备搞一个ART模式下的dump. Dalvik模式是Android 4.4及其以下采用 ...

  3. dvm,art模式下的dex文件加载流程

    dvm,art模式下的dex文件加载流程 dex加载是学习android的重中之重,刚看完几篇参考博客,对应android源码,收益匪浅,用一篇博客总结一下自己学到的东西. 1.dvm模式下的dex加 ...

  4. linux系统最好的c类语言开发软件,Linux下基于C 语言开发即时通信软件.doc

    Linux下基于C 语言开发即时通信软件 Linux下基于C++语言开发即时通信软件 关键词:聊天软件 文字聊天 Linux平台 C/S架构ICE中间件 Linux-based instant mes ...

  5. 公众号开发模式下客服消息开发

    当用户和公众号产生特定动作的交互时,微信将会把消息数据推送给开发者,开发者可以在一段时间内调用客服接口,通过POST一个JSON数据包来发送消息给普通用户.此接口主要用于客服等有人工消息处理环节的功能 ...

  6. 使用php开发,基于swoole扩展开发的工具 swoole-crontab

    2019独角兽企业重金招聘Python工程师标准>>> 使用php开发,基于swoole扩展开发的工具 swoole-crontab https://www.oschina.net/ ...

  7. AndriodStudio 开发环境下实现Xposed模块开发入门。Xposed框架模块编写教程

    前言: Xposed框架是一款开源框架,其功能是可以在不修改APK的情况下影响程序运行(修改系统)的框架服务,基于它可以制作出许多功能强大的模块,且在功能不冲突的情况下同时运作.Xposed 就好比是 ...

  8. 【Android 组件化】使用 Gradle 实现组件化 ( 组件 / 集成模式下的 Library Module 开发 )

    文章目录 一.组件模式下为组件 Module 指定 Java 源码路径 二.主应用的角色 三.BuildConfig 中生成当前 组件 / 集成 模式字段 四.Library Module 中的代码示 ...

  9. 轻松玩转树莓派Pico之三、Windows+Ubuntu虚拟机模式下VSCode C语言开发环境搭建

    目录 1.VSCode下载与安装 2.VSCode基础插件安装 3.SSH连接与配置 4.SSH免密登录 5.Pico编译 工欲善其事,必先利其器.之前的介绍的Pico流程都是通过命令行编译,没有进行 ...

最新文章

  1. linux汇编div除法,汇编:div 除法指令
  2. python计算身体质量指数_利用Python计算身体质量指数BMI来判断体型
  3. Java算法试题--猜字母/杀人游戏
  4. 考研计算机专业课统考吗,【计算机考研】你了解计算机统考408吗?
  5. 微信公众号开发经验总结
  6. VB用API实现各种对话框(总结)(转载)
  7. 9511王锋刘婧捐100万元,支持中国科大计算机学院
  8. BIO,NIO和AIO的区别
  9. 华为在推荐系统中的前沿技术研究与落地(附PPT下载链接)
  10. git修改commit注释_【Slog】Git之多人同feature的同分支开发
  11. IDEA打包的两种方式及注意事项
  12. postman9.12.2汉化包
  13. ios 请在设置中打开相机权限_iOS 检测相机权限是否打开
  14. 酪氨酸激酶、自噬等抗肿瘤抑制剂
  15. hdu 5755 Gambler Bo【gauss】
  16. 电瓶车充电桩收费平台在福建学校的应用
  17. 地球币earthcoin表情包征图大赛正式筹备准备中
  18. 微信小游戏颜色风暴自动化
  19. 汽车网络安全标准UN Regulation No.155和No.156汇总介绍
  20. 在本地如何启动Vue项目

热门文章

  1. 巴菲特投资语录(老杨版)
  2. 火影603~630 不管是真的假的,写的够精彩的。
  3. 右键菜单变宽了的原因及解决办法
  4. 【果断收藏】阿里云盘首页
  5. 构建完美的质量管理体系
  6. WEB3.0定义与未来发展趋势
  7. js禁止浏览器保存用户密码
  8. 12月份的武汉免费玩
  9. 在matlab中生成高斯小山图
  10. 【Mybatis】SqlMapConfig.xml核心配置文件