本教程所用Android Studio测试项目已上传:https://github.com/PrettyUp/SecTest

一、混淆

对于很多人而言是因为java才接触到“混淆”这个词,由于在前移动互联网时代在java程序中“混淆”也只是针对java代码,所以混淆基本就和对java源代码进行混淆等价。

但说到混淆的本质,不过就是将变量名、函数名由有助于开发维护人员理解其用途的名称(如my_name,get_key)改用a,b,c,d这种简短无意义的词去替换。从这个思路出发,资源其实也是可以混淆的。

在前移动互联网时代,对于B/S而言,前端页面资源则是放在服务器上且名称不适宜随便修改;对于C/S而言,由于java不擅长写界面所以java写的程序要么用到的资源很少要么就直接没有图形界面。

也就是说在前移动互联网时代,资源混淆确实是没多大用途和意义的。但在移动互联网时代,或者更直接一点对app而言,资源混淆还是有用武之地的。一是可以减小apk的大小,二是对比电脑客户端更多将信息直接放在变量中而言app将更多的信息存放于xml文件中,进行混淆有助于提高逆向者理解程序逻辑的难度。

1.1 资源混淆

1.1.1 资源混淆操作步骤

从这里要介绍的资源混淆操作方法看,因为是直接对apk进行操作所以放在最后讲才更合适。但顺从认识而言,资源混淆这种不是主角的东西就该放最前面讲。

资源混淆我们这里使用微信团队的AndResGuard。

1)进入tool_output目录,下载AndResGuard jar文件和config.xml模板配置文件(注意不要右键直接保存那样下载的是html文件,jar文件点进去下载,config.xml点进去复制内容自己在本地新建个config.xml。或才直接下载整个项目再找出这两个文件)

2) 修改config.xml

config.xml各配置项具体说明见官方说明,我的大概理解是默认会对res目录下的各xml文件进行混淆,在config.xml可以配置不进行混淆的白名单(Whitelist项)及是否使用7z对图片进行压缩(Compress项)等。其中注意不是写在config.xml中的项就是启用的,各项自己的isactive值为"true"时才是启用的。

我这里只修改最后的sign项,配置签名信息其他都不做修改(其实出于安全考虑签名最好用-signature选项而不是配置在config.xml中,但出于教程的统一和简洁这里我就这么操作)

3)进行资源混淆

执行以下命令进行混淆,注意我这里是用到的文件都放在了当前目录下(C:\Users\ls\Desktop\app),如果不在要注意使用全路径或写好相对路径。

-jar指定----AndResGuard程序

SecTest.apk----是我做好的测试app,改成自己的

-config----指定使用的配置文件

-7zip----指定7z可执行文件的路径,改成自己的;其实如果不指定命令会报错,但只是不能生成经过压缩的apk而已,未压缩的apk还是成功生成了的。

-zipalign----指定zipalign可执行文件的路径,这个程序在android sdk中就有,到sdk目录下找就行了。

官方文档中说,若7zip或zipalign的路径已设置环境变量中,这两项不需要单独设置。一是这两个安装时都不会自己加到环境变量,二是官方文档7z用的是7za这个名字的可执行程序在我安装的7z版本中是没有的。也就是说推荐用直接指定命令位置而不是改环境变量的方法。

mkdir test_dirjava -jar AndResGuard-cli-1.2.12.jar SecTest.apk -config  config.xml -out test_dir -7zip D:\7-Zip\7z.exe -zipalign D:\Language\ASDK\build-tools\28.0.0\zipalign.exe

如果出现“java.io.IOException: the signature file do not exit, raw”等报错,那多半是文件名等信息写错了,重新检查一遍。

最后test_dir中得到的有以下文件,各文件官方有说明,就我这里想要的是混淆并进行了签名的SecTest_signed.apk

1.1.2 验证资源混淆成功【可选】

以activity_main.xml为例,项目中代码如下:

使用反编译工具查看layout,可以看到生成了一堆名为a,b,c,d的xml的文件。我找了半天才找到a2.xml是activity_main.xml,且可以看到其中的控件id和字符串名称等都已混淆

1.2 代码混淆

1.2.1 代码混淆操作步骤

代码混淆这里以Android Studio中使用ProGuard为例,Eclipse看了一下也都是指定一下规则文件而已就不多做介绍。至于其他混淆工具并没有研究。

将项目切换到Project视图,找到app文件夹下的build.gradle并打开,锁定到buildTypes节区,如图所示其中有minifyEnabled项该项控制编译时是否启用混淆,默认为false表示不使用。

minifyEnabled下方的proguardFiles用于指定混淆规则文件,其中的proguard-android.txt是Android Studio自带的基本的混淆规则(一般在$SDK_PATH\tools\proguard目录下)这个一般不要去做修改。另一个proguard-rules.pro是专门供写个性化混淆规则用的,如果有个性化混淆需求将自己的规则写入其中即可(在下图中也可看到改文件与build.gradle一样同处app目录下)。

proguard-android.txt中已排除了android关键组件然后对除此之外的java代码都进行混淆,已符合我当前需要,所以我这里只将minifyEnabled项由默认的false改为true,其他都不做改动。

如果要写个性化规则可参考:https://blog.csdn.net/Two_Water/article/details/70233983

1.2.2 验证代码成功混淆【可选】

以MainActivity.java为例,项目中OnCreate函数部分代码如下

使用反编译工具反编译代码,对应片段代码如下,可以看到变量名称已被i,j,k等代替

另外再查看AESCoder.java,也已被成功混淆

二、签名验证

签名验证,就是在APP中写入自己私钥的hash值,和一个获取当前签名中的私钥hash值的函数两个值相一致,那么就说明APP没有被改动允许APP运行。如果两值不一致那么说明APP是被二次打包的,APP就自我销毁进程。

签名验证又可以在两个地方做,一个是在MainActivity.java的OnCreate函数中做,一个是在原生代码文件的JNI_OnLoad函数中做。

在OnCreate函数中做,短处是反编译者只要找到在OnCreate中定位到验证函数,然后将其注释,重新打包APP就可以成功运行;好处就是代码简单。

在JNI_OnLoad中做,短处是比较复杂(需要创建支持C/C++原生代码的项目,获取hash需要绕道java代码获取等);好处就是反编译者需要进一步掌握ida等反汇编工具将验证函数删除才能绕过验证。

为了最大限度地提高安全性,可以考滤两种验证都使用。

最后为了避免争议,在此要做一下统一声明,以下代码基本我个人都不是原作者,个人在本节的作用是将几个方案整合成了一个比较合理的方案,并验证这些代码和整合出来的方案是可行的。

2.1 在MainActivity.java的OnCreate函数中进行签名验证

OnCreate函数内、setContentView后加入以下代码:

       // 获取当前上下文Context context = getApplicationContext();// 发布apk时用来签名的keystore中查看到的sha1值,改成自己的String cert_sha1 = "937FF2936CDB81EEF4A776290EA9E076B3BC03A9";// 调用isOrgApp()获取比较结果boolean is_org_app = isOrgApp(context,cert_sha1);// 如果比较初始从证书里查看到的sha1,与代码获取到的当前证书中的sha1不一致,那么就自我销毁if(! is_org_app){android.os.Process.killProcess(android.os.Process.myPid());}

在MainActivity类内,OnCreate函数外加入以下代码:

    // 此函数用于返回比较结果public static boolean isOrgApp(Context context,String cert_sha1){String current_sha1 = getAppSha1(context,cert_sha1);// 返回的字符串带冒号形式,用replace去掉current_sha1 = current_sha1.replace(":","");return current_sha1.equals(current_sha1);}// 此函数用于获取当前APP证书中的sha1值public static String getAppSha1(Context context,String cert_sha1) {try {PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);byte[] cert = info.signatures[0].toByteArray();MessageDigest md = MessageDigest.getInstance("SHA1");byte[] publicKey = md.digest(cert);StringBuffer hexString = new StringBuffer();for (int i = 0; i < publicKey.length; i++) {String appendString = Integer.toHexString(0xFF & publicKey[i]).toUpperCase(Locale.US);if (appendString.length() == 1)hexString.append("0");hexString.append(appendString);hexString.append(":");}String result = hexString.toString();return result.substring(0, result.length()-1);} catch (PackageManager.NameNotFoundException e) {e.printStackTrace();} catch (NoSuchAlgorithmException e) {e.printStackTrace();}return null;}

我自己调试结果如下,确实可以成功获取sha1值(获取sha1函数代码原文链接)

2.2 原生代码文件的JNI_OnLoad函数中进行签名验证

开始看到这位小哥哥的文章,就喜欢这种有图有真相的文章,说明其代码应该是真的可以获取到APP当前的sha1值的。

但后来理清他的做法是:从java中把context传过去,在c++中完成比较返回true或false;也就是说决定程序退不退出的if语句还是在java中的,这种做法和2.1中全在java中做除了显示技术比较强之外安全效果完全一样并没有提升啊。if应当在c++中实现,context也需要c++自己获取。

后来找到另一位小哥哥的文章,其指出判断需要在c++中做而且是在JNI_OnLoad函数中做并给出了方法,但是他获取context时实现的NoProGuard我没搞清楚在哪导入。

最后找到了又一位小哥哥的文章,其给出了JNI获取context的方案,验证也确实是可行的。

所以整合的方案就是:第二位小哥哥在JNI_OnLoad函数中做的思想+第三位小哥哥获取context的方法+第一位小哥哥获取sha1的方法。

(其实第二位小哥哥还有一个思想就是debug时不需要验证release才要验证,这也是可取的,我这里也采用了。但debug时要做验证也不是不可以的,只是要注意debug时运行在avd中的app使用的是Android Studio自己生成的keystore而不是我们发布apk时自己的keystore,所以此时填的sha1的值应当是Android Studio自己生成的keystore的sha1,

当然第二位小哥哥获取md5的方法改一下好像也是能获取正确的sha1值的)

2.2.1 C++中验证签名代码

最终C++中验证签名的代码如下,自己使用时要注意将其中的app_sha1赋值成自己keystore中的sha1值

#include <jni.h>
#include <string>// const char *app_sha1="FAAB30C11EEF7333C81D48FECA25D21A18E2C789";
// 这里是keystore中的sha1值,改成自己的
const char *app_sha1 = "937FF2936CDB81EEF4A776290EA9E076B3BC03A9";
const char hexcode[] = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};jobject getGlobalContext(JNIEnv *env)
{//获取Activity Thread的实例对象jclass activityThread = env->FindClass("android/app/ActivityThread");jmethodID currentActivityThread = env->GetStaticMethodID(activityThread, "currentActivityThread", "()Landroid/app/ActivityThread;");jobject at = env->CallStaticObjectMethod(activityThread, currentActivityThread);//获取Application,也就是全局的ContextjmethodID getApplication = env->GetMethodID(activityThread, "getApplication", "()Landroid/app/Application;");jobject context = env->CallObjectMethod(at, getApplication);return context;
}char* getSha1(JNIEnv *env){// 调用getGlobalContext,获取上下文jobject context_object = getGlobalContext(env);if (context_object == NULL){printf("context is NULL");return NULL;}jclass context_class = env->GetObjectClass(context_object);//反射获取PackageManagerjmethodID methodId = env->GetMethodID(context_class, "getPackageManager", "()Landroid/content/pm/PackageManager;");jobject package_manager = env->CallObjectMethod(context_object, methodId);if (package_manager == NULL) {printf("package_manager is NULL!!!");return NULL;}//反射获取包名methodId = env->GetMethodID(context_class, "getPackageName", "()Ljava/lang/String;");jstring package_name = (jstring)env->CallObjectMethod(context_object, methodId);if (package_name == NULL) {printf("package_name is NULL!!!");return NULL;}env->DeleteLocalRef(context_class);//获取PackageInfo对象jclass pack_manager_class = env->GetObjectClass(package_manager);methodId = env->GetMethodID(pack_manager_class, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");env->DeleteLocalRef(pack_manager_class);jobject package_info = env->CallObjectMethod(package_manager, methodId, package_name, 0x40);if (package_info == NULL) {printf("getPackageInfo() is NULL!!!");return NULL;}env->DeleteLocalRef(package_manager);//获取签名信息jclass package_info_class = env->GetObjectClass(package_info);jfieldID fieldId = env->GetFieldID(package_info_class, "signatures", "[Landroid/content/pm/Signature;");env->DeleteLocalRef(package_info_class);jobjectArray signature_object_array = (jobjectArray)env->GetObjectField(package_info, fieldId);if (signature_object_array == NULL) {printf("signature is NULL!!!");return NULL;}jobject signature_object = env->GetObjectArrayElement(signature_object_array, 0);env->DeleteLocalRef(package_info);//签名信息转换成sha1值jclass signature_class = env->GetObjectClass(signature_object);methodId = env->GetMethodID(signature_class, "toByteArray", "()[B");env->DeleteLocalRef(signature_class);jbyteArray signature_byte = (jbyteArray) env->CallObjectMethod(signature_object, methodId);jclass byte_array_input_class=env->FindClass("java/io/ByteArrayInputStream");methodId=env->GetMethodID(byte_array_input_class,"<init>","([B)V");jobject byte_array_input=env->NewObject(byte_array_input_class,methodId,signature_byte);jclass certificate_factory_class=env->FindClass("java/security/cert/CertificateFactory");methodId=env->GetStaticMethodID(certificate_factory_class,"getInstance","(Ljava/lang/String;)Ljava/security/cert/CertificateFactory;");jstring x_509_jstring=env->NewStringUTF("X.509");jobject cert_factory=env->CallStaticObjectMethod(certificate_factory_class,methodId,x_509_jstring);methodId=env->GetMethodID(certificate_factory_class,"generateCertificate",("(Ljava/io/InputStream;)Ljava/security/cert/Certificate;"));jobject x509_cert=env->CallObjectMethod(cert_factory,methodId,byte_array_input);env->DeleteLocalRef(certificate_factory_class);jclass x509_cert_class=env->GetObjectClass(x509_cert);methodId=env->GetMethodID(x509_cert_class,"getEncoded","()[B");jbyteArray cert_byte=(jbyteArray)env->CallObjectMethod(x509_cert,methodId);env->DeleteLocalRef(x509_cert_class);jclass message_digest_class=env->FindClass("java/security/MessageDigest");methodId=env->GetStaticMethodID(message_digest_class,"getInstance","(Ljava/lang/String;)Ljava/security/MessageDigest;");jstring sha1_jstring=env->NewStringUTF("SHA1");jobject sha1_digest=env->CallStaticObjectMethod(message_digest_class,methodId,sha1_jstring);methodId=env->GetMethodID(message_digest_class,"digest","([B)[B");jbyteArray sha1_byte=(jbyteArray)env->CallObjectMethod(sha1_digest,methodId,cert_byte);env->DeleteLocalRef(message_digest_class);//转换成charjsize array_size=env->GetArrayLength(sha1_byte);jbyte* sha1 =env->GetByteArrayElements(sha1_byte,NULL);char *hex_sha=new char[array_size*2+1];for (int i = 0; i <array_size ; ++i) {hex_sha[2*i]=hexcode[((unsigned char)sha1[i])/16];hex_sha[2*i+1]=hexcode[((unsigned char)sha1[i])%16];}hex_sha[array_size*2]='\0';printf("hex_sha %s ",hex_sha);return hex_sha;
}static jboolean checkSignature(JNIEnv *env) {// 调用getSha1获取app当前证书中的sha1char *sha1 = getSha1(env);// 调用checkValidity获取比较结果并直接返回// jboolean signatureValid = checkValidity(env,sha1);if (strcmp(sha1,app_sha1)==0){return JNI_TRUE;}else{return JNI_FALSE;}
}
jint JNI_OnLoad(JavaVM *vm, void *reserved) {JNIEnv *env;if (vm->GetEnv((void **) (&env), JNI_VERSION_1_6) != JNI_OK) {return -1;}// RELEASE_MODE这个宏是通过编译脚本设定的,如果是release模式,// 则RELEASE_MODE=1,否则为0或者未定义// 如果想不管release还是debug都进行签名验证,注释掉下方ifdef和endif两条预编译语句即可
#ifdef RELEASE_MODE// 如果是release版本,检查当前应用的签名是否一致;如果不签名不一致的话则返回-1,-1会引发app异常自动退出if (RELEASE_MODE == 1) {if (checkSignature(env) != JNI_TRUE) {return -1;}}
#endifreturn JNI_VERSION_1_6;
}

View Code

2.2.2 配置build_gradle

由于代码中使用了以下预编译语句,所以如果只是使用上边的代码,验证是没有生效的。说明如下:

build_gradle中未配置RELEASE_MODE=1----release/debug都不进行签名验证
build_gradle中配置RELEASE_MODE=1----release模式验证/debug模式不验证
注释掉ifdef和endif两条预编译语句----release/debug都进行签名验证

#ifdef RELEASE_MODE// 检查当前应用的签名是否一致,如果不签名不一致的话则返回-1,-1会引发app异常自动退出if (checkSignature(env) != JNI_TRUE) {return -1;}
#endif

所以为了启用验证,还需要打开app目录下的build_gradle文件,在如下图所示位置加入以下代码:

ndk {// release包定义RELEASE_MODE=1宏,供so库中的ifdef语句使用cFlags "-DRELEASE_MODE=1"
}

2.2.3 MainActivity.java中加载so文件

当然最还得要在java文件中,载入so文件才能起来作用。netive-test是我这里so的库名改成自己的

// Used to load the 'native-test' library on application startup.
static {System.loadLibrary("native-test");
}

三、反调试

反调试,这位小哥哥说可以有两个思路。

第一个是一个进程同时最多只能被一个进程所调试,所以可以自己使用ptrace()函数假装自己在调试自己,占住调试的位置以此来拒绝别的进程的调试请求。

第二个是查看/proc/{pid}/status文件如果发现TracerPid的值不等于0(TracerPid是调试进程的pid,如果不为0则表示有进程在调试),则kill掉自己。

第一个思路由于我在复现时没有起到反调试效果,未排查到原因暂且就先不管了。

第二个思路中作者给的具体做法是只是在JNI_Onload中只检测一次。在姜维的《Android 应用安全防护和逆向分析》中也提到了第二种思路,但他给出的具体做法是去启动一个线程不断地检测/proc/{pid}/status的TracerPid值,如果检测到不为0则使用exit退出。

不是很清楚有没有可能/proc/{pid}/status的TracerPid值开始为0,后来不为0的情况。但其实我是先看到姜维写的,所以这里就采用姜维的做法。

3.1 加入检测函数

在要防护的c++文件中加入以下两个函数

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
void* thread_function(void *arg){int pid = getpid();char file_name[20] = {'\0'};sprintf(file_name,"proc/%d/status",pid);char line_str[256];int i = 0,traceid;FILE *fp;while(1){i = 0;fp = fopen(file_name,"r");if(fp == NULL){break;}while(!feof(fp)){fgets(line_str,256,fp);if(i == 5){// traceid = getnumberfor_str(line_str);traceid = atoi(&line_str[10]);if(traceid > 0){exit(0);}break;}i++;}fclose(fp);sleep(5);}
}void create_thread_check_traceid(){pthread_t thread_id;int err = pthread_create(&thread_id,NULL,thread_function,NULL);if(err != 0){}
}

3.2 在JNI_OnLoad函数开头调用检测函数

要强调两点,一个是这里是反调试只有ida进行动态分析时才能起到防护效果,ida静态打开还是不能阻止的。第二个是这里是反调试,自己开发过程中使用IDE debug也是调试,如果加了以下代码那IDE debug时进程也会自我销毁的(实际发现IDE中 run也是不行的)。

也就是说,在开发时要注意先注释该函数调用,在打包生成apk时才去掉注释。

参考:

《Android 应用安全防护和逆向分析》

https://blog.csdn.net/Two_Water/article/details/70233983

https://blog.csdn.net/liyi0930/article/details/77413525

http://leehong2005.com/2016/08/08/android-so-signature-check/

https://blog.csdn.net/lb377463323/article/details/75315167

https://blog.csdn.net/leifengpeng/article/details/52681196

https://blog.csdn.net/feibabeibei_beibei/article/details/60956307

https://www.cnblogs.com/biggerman/p/6940888.html

转载于:https://www.cnblogs.com/lsdb/p/9340761.html

APP安全防护基本方法(混淆/签名验证/反调试)相关推荐

  1. Android安全防护之旅---Android应用反调试操作的几种方案解析

    一.前言 在之前介绍了很多破解相关的文章,在这个过程中我们难免会遇到一些反调试策略,当时只是简单的介绍了如何去解决反调试,其实在去年我已经介绍了一篇关于Android中的安全逆向防护之战的文章:And ...

  2. Android安全防护/检查root/检查Xposed/反调试/应用多开/模拟器检测(持续更新)

    转载请注明出处,转载时请不要抹去原始链接.代码已上传git,欢迎star/fork/issue https://github.com/lamster2018/EasyProtector 复制代码 文章 ...

  3. Python 自动化 - 浏览器chrome打开F12开发者工具自动Paused in debugger调试导致无法查看网站资源问题原因及解决方法,javascript反调试问题处理实例演示

    这是 JavaScript 常用的手法用于网站方保护源码不被大家轻易的查看到,会一直循环调用 function anonymous() {debugger} 方法使网页始终处于调试状态,干扰大家查看网 ...

  4. Android安全防护/检查root/检查Xposed/反调试/应用多开/模拟器检测(持续更新1.0.5)

    参考地址:https://www.jianshu.com/p/c37b1bdb4757 多开软件检测 多开软件的检测方案这里提供5种,首先4种来自 <Android多开/分身检测> htt ...

  5. APP加固各种反调试

    原文地址:http://drops.wooyun.org/mobile/16969 原文地址:https://blog.csdn.net/xinyu_pan/article/details/62888 ...

  6. iOS安全防护---越狱检测、二次打包检测、反调试

    最近在调研越狱设备的检测.防止APP被二次打包.防止反调试以及逆向工程,调研期间做了大量的测试来验证方案的可行性,花费了很多时间.所以,在此将调研结果总结一下,供大家参考. 一.越狱环境下,提高App ...

  7. 静态反调试技术(1)

    文章目录 声明 静态反调试目的 注意 PEB https://blog.csdn.net/CSNN2019/article/details/113113347 BeingDebugged(+0x2) ...

  8. android的反调试方法,Android平台融合多特征的APP反调试方法与流程

    本发明涉及Android平台融合多特征的APP反调试方法,属于计算机与信息科学技术领域. 背景技术: 应用程序本身并不具备反调试的功能,但是动态调试是动态分析应用逻辑.动态脱壳等攻击方式所采取的必要手 ...

  9. Android应用篇 - app 安全防护

    这篇文章来总结下 Android app 的安全防护手段. 目录: 资源混淆 代码混淆 签名校验 反调试 组件安全 Webview 的代码执行漏洞 加固 编码安全 动态加载 hook 数据存储安全 数 ...

  10. android混淆和反编译

    混淆 Android Studio:  只需在build.gradle(Module:app)中的buildTypes中增加release的编译选项即可,如下: <code class=&quo ...

最新文章

  1. 确认和回调_【短线回调,确认突破点】
  2. android多功能计算器 源码,Android计算器源码
  3. 【C++】31. Boost::circular_buffer——循环缓冲区
  4. Deno 运行时入门教程:Node.js 的替代品
  5. 【研究院】低调务实的网易人工智能,你熟悉吗?
  6. python 将字节字符串转换成十六进制字符串
  7. 已有打开的与此 Command 相关联的 DataReader,必须首先将它关闭
  8. 检测单击鼠标左键并拖动的消息_3-75 通过鼠标选择文本
  9. 版本1.8.1Go安装以及语法高亮配置
  10. HTML5video 标签
  11. 微信小程序禁止刷新之后苹果端还可以下拉的问题
  12. Machine Learning and Data Science 教授大师
  13. Oracle form培训资料,Oracle ERP FORM开发学习操作手册
  14. 坦克大战(Python)附思维导图、代码、图片音频资源
  15. MySQL中创建时间和更新时间的自动更新
  16. 自学Java day53 使用jvav实现 并查集 数据结构 从jvav到架构师
  17. SDUT 2021 Winter Individual Contest - J(Gym-101879)
  18. CTime的用法总结
  19. 【UVA】【11021】麻球繁衍
  20. Python基础笔记(1-1)

热门文章

  1. 他们为啥说我没有数据分析思维?
  2. 荐号 | “看一看”中“偷”来的很棒的公众号
  3. 干货 | 这是一份你急需的数据分析的职业规划
  4. R系列处理器是服务器,AMD全新R系列处理器领军嵌入式高性能领域
  5. Unity 2D摄像机跟随角色移动
  6. IDEA工具配置weblogic
  7. java框架--springmvc --ajax-json-upload/download+maven+ DES/MD5 请求加密
  8. 【Python】python网络协议
  9. 站立会议07(第二次冲刺)
  10. C# 缓存学习第一天