转载:http://blog.csdn.net/jiangwei0910410003/article/details/48415225

一、前言

今天又到周末了,憋了好久又要出博客了,今天来介绍一下Android中的如何对Apk进行加固的原理。现阶段。我们知道Android中的反编译工作越来越让人操作熟练,我们辛苦的开发出一个apk,结果被人反编译了,那心情真心不舒服。虽然我们混淆,做到native层,但是这都是治标不治本。反编译的技术在更新,那么保护Apk的技术就不能停止。现在网上有很多Apk加固的第三方平台,最有名的应当属于:爱加密和梆梆加固了。其实加固有些人认为很高深的技术,其实不然,说的简单点就是对源Apk进行加密,然后在套上一层壳即可,当然这里还有一些细节需要处理,这就是本文需要介绍的内容了。


二、原理解析

下面就来看一下Android中加壳的原理:


我们在加固的过程中需要三个对象:

1、需要加密的Apk(源Apk)

2、壳程序Apk(负责解密Apk工作)

3、加密工具(将源Apk进行加密和壳Dex合并成新的Dex)


主要步骤:

我们拿到需要加密的Apk和自己的壳程序Apk,然后用加密算法对源Apk进行加密在将壳Apk进行合并得到新的Dex文件,最后替换壳程序中的dex文件即可,得到新的Apk,那么这个新的Apk我们也叫作脱壳程序Apk.他已经不是一个完整意义上的Apk程序了,他的主要工作是:负责解密源Apk.然后加载Apk,让其正常运行起来。


在这个过程中我们可能需要了解的一个知识是:如何将源Apk和壳Apk进行合并成新的Dex

这里就需要了解Dex文件的格式了。下面就来简单介绍一下Dex文件的格式

具体Dex文件格式的详细介绍可以查看这个文件:http://download.csdn.net/detail/jiangwei0910410003/9102599

主要来看一下Dex文件的头部信息,其实Dex文件和Class文件的格式分析原理都是一样的,他们都是有固定的格式,我们知道现在反编译的一些工具:

1、jd-gui:可以查看jar中的类,其实他就是解析class文件,只要了解class文件的格式就可以

2、dex2jar:将dex文件转化成jar,原理也是一样的,只要知道Dex文件的格式,能够解析出dex文件中的类信息就可以了

当然我们在分析这个文件的时候,最重要的还是头部信息,应该他是一个文件的开始部分,也是索引部分,内部信息很重要。


我们今天只要关注上面红色标记的三个部分:

1) checksum

文件校验码 ,使用alder32 算法校验文件除去 maigc ,checksum 外余下的所有文件区域 ,用于检查文件错误 。

2) signature

使用 SHA-1 算法 hash 除去 magic ,checksum 和 signature 外余下的所有文件区域 ,用于唯一识别本文件 。

3) file_size

Dex 文件的大小 。

为什么说我们只需要关注这三个字段呢?

因为我们需要将一个文件(加密之后的源Apk)写入到Dex中,那么我们肯定需要修改文件校验码(checksum).因为他是检查文件是否有错误。那么signature也是一样,也是唯一识别文件的算法。还有就是需要修改dex文件的大小。

不过这里还需要一个操作,就是标注一下我们加密的Apk的大小,因为我们在脱壳的时候,需要知道Apk的大小,才能正确的得到Apk。那么这个值放到哪呢?这个值直接放到文件的末尾就可以了。

所以总结一下我们需要做:修改Dex的三个文件头,将源Apk的大小追加到壳dex的末尾就可以了。

我们修改之后得到新的Dex文件样式如下:


那么我们知道原理了,下面就是代码实现了。所以这里有三个工程:

1、源程序项目(需要加密的Apk)

2、脱壳项目(解密源Apk和加载Apk)

3、对源Apk进行加密和脱壳项目的Dex的合并


三、项目案例

下面先来看一下源程序

1、需要加密的源程序Apk项目:ForceApkObj


需要一个Application类,这个到后面说为什么需要:

MyApplication.Java

[java] view plain copy
  1. packagecom.example.forceapkobj;
  2. importandroid.app.Application;
  3. importandroid.util.Log;
  4. publicclassMyApplicationextendsApplication{
  5. @Override
  6. publicvoidonCreate(){
  7. super.onCreate();
  8. Log.i("demo","sourceapkonCreate:"+this);
  9. }
  10. }

就是打印一下onCreate方法。


MainActivity.java

[java] view plain copy
  1. packagecom.example.forceapkobj;
  2. importandroid.app.Activity;
  3. importandroid.content.Intent;
  4. importandroid.os.Bundle;
  5. importandroid.util.Log;
  6. importandroid.view.View;
  7. importandroid.view.View.OnClickListener;
  8. importandroid.widget.TextView;
  9. publicclassMainActivityextendsActivity{
  10. @Override
  11. protectedvoidonCreate(BundlesavedInstanceState){
  12. super.onCreate(savedInstanceState);
  13. TextViewcontent=newTextView(this);
  14. content.setText("IamSourceApk");
  15. content.setOnClickListener(newOnClickListener(){
  16. @Override
  17. publicvoidonClick(Viewarg0){
  18. Intentintent=newIntent(MainActivity.this,SubActivity.class);
  19. startActivity(intent);
  20. }});
  21. setContentView(content);
  22. Log.i("demo","app:"+getApplicationContext());
  23. }
  24. }
也是打印一下内容。


2、加壳程序项目:DexShellTools


加壳程序其实就是一个Java工程,因为我们从上面的分析可以看到,他的工作就是加密源Apk,然后将其写入到脱壳Dex文件中,修改文件头,得到一个新的Dex文件即可。

看一下代码:

[java] view plain copy
  1. packagecom.example.reforceapk;
  2. importjava.io.ByteArrayOutputStream;
  3. importjava.io.File;
  4. importjava.io.FileInputStream;
  5. importjava.io.FileOutputStream;
  6. importjava.io.IOException;
  7. importjava.security.MessageDigest;
  8. importjava.security.NoSuchAlgorithmException;
  9. importjava.util.zip.Adler32;
  10. publicclassmymain{
  11. /**
  12. *@paramargs
  13. */
  14. publicstaticvoidmain(String[]args){
  15. //TODOAuto-generatedmethodstub
  16. try{
  17. FilepayloadSrcFile=newFile("force/ForceApkObj.apk");//需要加壳的程序
  18. System.out.println("apksize:"+payloadSrcFile.length());
  19. FileunShellDexFile=newFile("force/ForceApkObj.dex");//解客dex
  20. byte[]payloadArray=encrpt(readFileBytes(payloadSrcFile));//以二进制形式读出apk,并进行加密处理//对源Apk进行加密操作
  21. byte[]unShellDexArray=readFileBytes(unShellDexFile);//以二进制形式读出dex
  22. intpayloadLen=payloadArray.length;
  23. intunShellDexLen=unShellDexArray.length;
  24. inttotalLen=payloadLen+unShellDexLen+4;//多出4字节是存放长度的。
  25. byte[]newdex=newbyte[totalLen];//申请了新的长度
  26. //添加解壳代码
  27. System.arraycopy(unShellDexArray,0,newdex,0,unShellDexLen);//先拷贝dex内容
  28. //添加加密后的解壳数据
  29. System.arraycopy(payloadArray,0,newdex,unShellDexLen,payloadLen);//再在dex内容后面拷贝apk的内容
  30. //添加解壳数据长度
  31. System.arraycopy(intToByte(payloadLen),0,newdex,totalLen-4,4);//最后4为长度
  32. //修改DEXfilesize文件头
  33. fixFileSizeHeader(newdex);
  34. //修改DEXSHA1文件头
  35. fixSHA1Header(newdex);
  36. //修改DEXCheckSum文件头
  37. fixCheckSumHeader(newdex);
  38. Stringstr="force/classes.dex";
  39. Filefile=newFile(str);
  40. if(!file.exists()){
  41. file.createNewFile();
  42. }
  43. FileOutputStreamlocalFileOutputStream=newFileOutputStream(str);
  44. localFileOutputStream.write(newdex);
  45. localFileOutputStream.flush();
  46. localFileOutputStream.close();
  47. }catch(Exceptione){
  48. e.printStackTrace();
  49. }
  50. }
  51. //直接返回数据,读者可以添加自己加密方法
  52. privatestaticbyte[]encrpt(byte[]srcdata){
  53. for(inti=0;i<srcdata.length;i++){
  54. srcdata[i]=(byte)(0xFF^srcdata[i]);
  55. }
  56. returnsrcdata;
  57. }
  58. /**
  59. *修改dex头,CheckSum校验码
  60. *@paramdexBytes
  61. */
  62. privatestaticvoidfixCheckSumHeader(byte[]dexBytes){
  63. Adler32adler=newAdler32();
  64. adler.update(dexBytes,12,dexBytes.length-12);//从12到文件末尾计算校验码
  65. longvalue=adler.getValue();
  66. intva=(int)value;
  67. byte[]newcs=intToByte(va);
  68. //高位在前,低位在前掉个个
  69. byte[]recs=newbyte[4];
  70. for(inti=0;i<4;i++){
  71. recs[i]=newcs[newcs.length-1-i];
  72. System.out.println(Integer.toHexString(newcs[i]));
  73. }
  74. System.arraycopy(recs,0,dexBytes,8,4);//效验码赋值(8-11)
  75. System.out.println(Long.toHexString(value));
  76. System.out.println();
  77. }
  78. /**
  79. *int转byte[]
  80. *@paramnumber
  81. *@return
  82. */
  83. publicstaticbyte[]intToByte(intnumber){
  84. byte[]b=newbyte[4];
  85. for(inti=3;i>=0;i--){
  86. b[i]=(byte)(number%256);
  87. number>>=8;
  88. }
  89. returnb;
  90. }
  91. /**
  92. *修改dex头sha1值
  93. *@paramdexBytes
  94. *@throwsNoSuchAlgorithmException
  95. */
  96. privatestaticvoidfixSHA1Header(byte[]dexBytes)
  97. throwsNoSuchAlgorithmException{
  98. MessageDigestmd=MessageDigest.getInstance("SHA-1");
  99. md.update(dexBytes,32,dexBytes.length-32);//从32为到结束计算sha--1
  100. byte[]newdt=md.digest();
  101. System.arraycopy(newdt,0,dexBytes,12,20);//修改sha-1值(12-31)
  102. //输出sha-1值,可有可无
  103. Stringhexstr="";
  104. for(inti=0;i<newdt.length;i++){
  105. hexstr+=Integer.toString((newdt[i]&0xff)+0x100,16)
  106. .substring(1);
  107. }
  108. System.out.println(hexstr);
  109. }
  110. /**
  111. *修改dex头file_size值
  112. *@paramdexBytes
  113. */
  114. privatestaticvoidfixFileSizeHeader(byte[]dexBytes){
  115. //新文件长度
  116. byte[]newfs=intToByte(dexBytes.length);
  117. System.out.println(Integer.toHexString(dexBytes.length));
  118. byte[]refs=newbyte[4];
  119. //高位在前,低位在前掉个个
  120. for(inti=0;i<4;i++){
  121. refs[i]=newfs[newfs.length-1-i];
  122. System.out.println(Integer.toHexString(newfs[i]));
  123. }
  124. System.arraycopy(refs,0,dexBytes,32,4);//修改(32-35)
  125. }
  126. /**
  127. *以二进制读出文件内容
  128. *@paramfile
  129. *@return
  130. *@throwsIOException
  131. */
  132. privatestaticbyte[]readFileBytes(Filefile)throwsIOException{
  133. byte[]arrayOfByte=newbyte[1024];
  134. ByteArrayOutputStreamlocalByteArrayOutputStream=newByteArrayOutputStream();
  135. FileInputStreamfis=newFileInputStream(file);
  136. while(true){
  137. inti=fis.read(arrayOfByte);
  138. if(i!=-1){
  139. localByteArrayOutputStream.write(arrayOfByte,0,i);
  140. }else{
  141. returnlocalByteArrayOutputStream.toByteArray();
  142. }
  143. }
  144. }
  145. }


下面来分析一下:

红色部分其实就是最核心的工作:

1>、加密源程序Apk文件

[java] view plain copy
  1. byte[]payloadArray=encrpt(readFileBytes(payloadSrcFile));//以二进制形式读出apk,并进行加密处理//对源Apk进行加密操作
加密算法很简单:

[java] view plain copy
  1. //直接返回数据,读者可以添加自己加密方法
  2. privatestaticbyte[]encrpt(byte[]srcdata){
  3. for(inti=0;i<srcdata.length;i++){
  4. srcdata[i]=(byte)(0xFF^srcdata[i]);
  5. }
  6. returnsrcdata;
  7. }
对每个字节进行异或一下即可。

(说明:这里是为了简单,所以就用了很简单的加密算法了,其实为了增加破解难度,我们应该使用更高效的加密算法,同事最好将加密操作放到native层去做)


2>、合并文件:将加密之后的Apk和原脱壳Dex进行合并

[java] view plain copy
  1. intpayloadLen=payloadArray.length;
  2. intunShellDexLen=unShellDexArray.length;
  3. inttotalLen=payloadLen+unShellDexLen+4;//多出4字节是存放长度的。
  4. byte[]newdex=newbyte[totalLen];//申请了新的长度
  5. //添加解壳代码
  6. System.arraycopy(unShellDexArray,0,newdex,0,unShellDexLen);//先拷贝dex内容
  7. //添加加密后的解壳数据
  8. System.arraycopy(payloadArray,0,newdex,unShellDexLen,payloadLen);//再在dex内容后面拷贝apk的内容

3>、在文件的末尾追加源程序Apk的长度

[java] view plain copy
  1. //添加解壳数据长度
  2. System.arraycopy(intToByte(payloadLen),0,newdex,totalLen-4,4);//最后4为长度

4>、修改新Dex文件的文件头信息:file_size; sha1; check_sum [java] view plain copy
  1. //修改DEXfilesize文件头
  2. fixFileSizeHeader(newdex);
  3. //修改DEXSHA1文件头
  4. fixSHA1Header(newdex);
  5. //修改DEXCheckSum文件头
  6. fixCheckSumHeader(newdex);
具体修改可以参照之前说的文件头格式,修改指定位置的字节值即可。


这里我们还需要两个输入文件:

1>、源Apk文件:ForceApkObj.apk

2>、脱壳程序的Dex文件:ForceApkObj.dex

那么第一个文件我们都知道,就是上面的源程序编译之后的Apk文件,那么第二个文件我们怎么得到呢?这个就是我们要讲到的第三个项目:脱壳程序项目,他是一个Android项目,我们在编译之后,能够得到他的classes.dex文件,然后修改一下名称就可。


3、脱壳项目:ReforceApk


在讲解这个项目之前,我们先来了解一下这个脱壳项目的工作:

1>、通过反射置换android.app.ActivityThread 中的mClassLoader为加载解密出APK的DexClassLoader,该DexClassLoader一方面加载了源程序、另一方面以原mClassLoader为父节点,这就保证了即加载了源程序又没有放弃原先加载的资源与系统代码。

关于这部分内容,不了解的同学可以看一下ActivityThread.java的源码:


或者直接看一下这篇文章:

http://blog.csdn.net/jiangwei0910410003/article/details/48104455

如何得到系统加载Apk的类加载器,然后我们怎么将加载进来的Apk运行起来等问题都在这篇文章中说到了。


2>、找到源程序的Application,通过反射建立并运行。

这里需要注意的是,我们现在是加载一个完整的Apk,让他运行起来,那么我们知道一个Apk运行的时候都是有一个Application对象的,这个也是一个程序运行之后的全局类。所以我们必须找到解密之后的源Apk的Application类,运行的他的onCreate方法,这样源Apk才开始他的运行生命周期。这里我们如何得到源Apk的Application的类呢?这个我们后面会说道。使用meta标签进行设置。


下面来看一下整体的流程图:



所以我们看到这里还需要一个核心的技术就是动态加载。关于动态加载技术,不了解的同学可以看这篇文章:

http://blog.csdn.net/jiangwei0910410003/article/details/48104581


下面来看一下代码:

[java] view plain copy
  1. packagecom.example.reforceapk;
  2. importjava.io.BufferedInputStream;
  3. importjava.io.ByteArrayInputStream;
  4. importjava.io.ByteArrayOutputStream;
  5. importjava.io.DataInputStream;
  6. importjava.io.File;
  7. importjava.io.FileInputStream;
  8. importjava.io.FileOutputStream;
  9. importjava.io.IOException;
  10. importjava.lang.ref.WeakReference;
  11. importjava.lang.reflect.Method;
  12. importjava.util.ArrayList;
  13. importjava.util.HashMap;
  14. importjava.util.Iterator;
  15. importjava.util.zip.ZipEntry;
  16. importjava.util.zip.ZipInputStream;
  17. importandroid.app.Application;
  18. importandroid.app.Instrumentation;
  19. importandroid.content.Context;
  20. importandroid.content.pm.ApplicationInfo;
  21. importandroid.content.pm.PackageManager;
  22. importandroid.content.pm.PackageManager.NameNotFoundException;
  23. importandroid.content.res.AssetManager;
  24. importandroid.content.res.Resources;
  25. importandroid.content.res.Resources.Theme;
  26. importandroid.os.Bundle;
  27. importandroid.util.ArrayMap;
  28. importandroid.util.Log;
  29. importdalvik.system.DexClassLoader;
  30. publicclassProxyApplicationextendsApplication{
  31. privatestaticfinalStringappkey="APPLICATION_CLASS_NAME";
  32. privateStringapkFileName;
  33. privateStringodexPath;
  34. privateStringlibPath;
  35. //这是context赋值
  36. @Override
  37. protectedvoidattachBaseContext(Contextbase){
  38. super.attachBaseContext(base);
  39. try{
  40. //创建两个文件夹payload_odex,payload_lib私有的,可写的文件目录
  41. Fileodex=this.getDir("payload_odex",MODE_PRIVATE);
  42. Filelibs=this.getDir("payload_lib",MODE_PRIVATE);
  43. odexPath=odex.getAbsolutePath();
  44. libPath=libs.getAbsolutePath();
  45. apkFileName=odex.getAbsolutePath()+"/payload.apk";
  46. FiledexFile=newFile(apkFileName);
  47. Log.i("demo","apksize:"+dexFile.length());
  48. if(!dexFile.exists())
  49. {
  50. dexFile.createNewFile();//在payload_odex文件夹内,创建payload.apk
  51. //读取程序classes.dex文件
  52. byte[]dexdata=this.readDexFileFromApk();
  53. //分离出解壳后的apk文件已用于动态加载
  54. this.splitPayLoadFromDex(dexdata);
  55. }
  56. //配置动态加载环境
  57. ObjectcurrentActivityThread=RefInvoke.invokeStaticMethod(
  58. "android.app.ActivityThread","currentActivityThread",
  59. newClass[]{},newObject[]{});//获取主线程对象http://blog.csdn.net/myarrow/article/details/14223493
  60. StringpackageName=this.getPackageName();//当前apk的包名
  61. //下面两句不是太理解
  62. ArrayMapmPackages=(ArrayMap)RefInvoke.getFieldOjbect(
  63. "android.app.ActivityThread",currentActivityThread,
  64. "mPackages");
  65. WeakReferencewr=(WeakReference)mPackages.get(packageName);
  66. //创建被加壳apk的DexClassLoader对象加载apk内的类和本地代码(c/c++代码)
  67. DexClassLoaderdLoader=newDexClassLoader(apkFileName,odexPath,
  68. libPath,(ClassLoader)RefInvoke.getFieldOjbect(
  69. "android.app.LoadedApk",wr.get(),"mClassLoader"));
  70. //base.getClassLoader();是不是就等同于(ClassLoader)RefInvoke.getFieldOjbect()?有空验证下//?
  71. //把当前进程的DexClassLoader设置成了被加壳apk的DexClassLoader----有点c++中进程环境的意思~~
  72. RefInvoke.setFieldOjbect("android.app.LoadedApk","mClassLoader",
  73. wr.get(),dLoader);
  74. Log.i("demo","classloader:"+dLoader);
  75. try{
  76. ObjectactObj=dLoader.loadClass("com.example.forceapkobj.MainActivity");
  77. Log.i("demo","actObj:"+actObj);
  78. }catch(Exceptione){
  79. Log.i("demo","activity:"+Log.getStackTraceString(e));
  80. }
  81. }catch(Exceptione){
  82. Log.i("demo","error:"+Log.getStackTraceString(e));
  83. e.printStackTrace();
  84. }
  85. }
  86. @Override
  87. publicvoidonCreate(){
  88. {
  89. //loadResources(apkFileName);
  90. Log.i("demo","onCreate");
  91. //如果源应用配置有Appliction对象,则替换为源应用Applicaiton,以便不影响源程序逻辑。
  92. StringappClassName=null;
  93. try{
  94. ApplicationInfoai=this.getPackageManager()
  95. .getApplicationInfo(this.getPackageName(),
  96. PackageManager.GET_META_DATA);
  97. Bundlebundle=ai.metaData;
  98. if(bundle!=null&&bundle.containsKey("APPLICATION_CLASS_NAME")){
  99. appClassName=bundle.getString("APPLICATION_CLASS_NAME");//className是配置在xml文件中的。
  100. }else{
  101. Log.i("demo","havenoapplicationclassname");
  102. return;
  103. }
  104. }catch(NameNotFoundExceptione){
  105. Log.i("demo","error:"+Log.getStackTraceString(e));
  106. e.printStackTrace();
  107. }
  108. //有值的话调用该Applicaiton
  109. ObjectcurrentActivityThread=RefInvoke.invokeStaticMethod(
  110. "android.app.ActivityThread","currentActivityThread",
  111. newClass[]{},newObject[]{});
  112. ObjectmBoundApplication=RefInvoke.getFieldOjbect(
  113. "android.app.ActivityThread",currentActivityThread,
  114. "mBoundApplication");
  115. ObjectloadedApkInfo=RefInvoke.getFieldOjbect(
  116. "android.app.ActivityThread$AppBindData",
  117. mBoundApplication,"info");
  118. //把当前进程的mApplication设置成了null
  119. RefInvoke.setFieldOjbect("android.app.LoadedApk","mApplication",
  120. loadedApkInfo,null);
  121. ObjectoldApplication=RefInvoke.getFieldOjbect(
  122. "android.app.ActivityThread",currentActivityThread,
  123. "mInitialApplication");
  124. //http://www.codeceo.com/article/android-context.html
  125. ArrayList<Application>mAllApplications=(ArrayList<Application>)RefInvoke
  126. .getFieldOjbect("android.app.ActivityThread",
  127. currentActivityThread,"mAllApplications");
  128. mAllApplications.remove(oldApplication);//删除oldApplication
  129. ApplicationInfoappinfo_In_LoadedApk=(ApplicationInfo)RefInvoke
  130. .getFieldOjbect("android.app.LoadedApk",loadedApkInfo,
  131. "mApplicationInfo");
  132. ApplicationInfoappinfo_In_AppBindData=(ApplicationInfo)RefInvoke
  133. .getFieldOjbect("android.app.ActivityThread$AppBindData",
  134. mBoundApplication,"appInfo");
  135. appinfo_In_LoadedApk.className=appClassName;
  136. appinfo_In_AppBindData.className=appClassName;
  137. Applicationapp=(Application)RefInvoke.invokeMethod(
  138. "android.app.LoadedApk","makeApplication",loadedApkInfo,
  139. newClass[]{boolean.class,Instrumentation.class},
  140. newObject[]{false,null});//执行makeApplication(false,null)
  141. RefInvoke.setFieldOjbect("android.app.ActivityThread",
  142. "mInitialApplication",currentActivityThread,app);
  143. ArrayMapmProviderMap=(ArrayMap)RefInvoke.getFieldOjbect(
  144. "android.app.ActivityThread",currentActivityThread,
  145. "mProviderMap");
  146. Iteratorit=mProviderMap.values().iterator();
  147. while(it.hasNext()){
  148. ObjectproviderClientRecord=it.next();
  149. ObjectlocalProvider=RefInvoke.getFieldOjbect(
  150. "android.app.ActivityThread$ProviderClientRecord",
  151. providerClientRecord,"mLocalProvider");
  152. RefInvoke.setFieldOjbect("android.content.ContentProvider",
  153. "mContext",localProvider,app);
  154. }
  155. Log.i("demo","app:"+app);
  156. app.onCreate();
  157. }
  158. }
  159. /**
  160. *释放被加壳的apk文件,so文件
  161. *@paramdata
  162. *@throwsIOException
  163. */
  164. privatevoidsplitPayLoadFromDex(byte[]apkdata)throwsIOException{
  165. intablen=apkdata.length;
  166. //取被加壳apk的长度这里的长度取值,对应加壳时长度的赋值都可以做些简化
  167. byte[]dexlen=newbyte[4];
  168. System.arraycopy(apkdata,ablen-4,dexlen,0,4);
  169. ByteArrayInputStreambais=newByteArrayInputStream(dexlen);
  170. DataInputStreamin=newDataInputStream(bais);
  171. intreadInt=in.readInt();
  172. System.out.println(Integer.toHexString(readInt));
  173. byte[]newdex=newbyte[readInt];
  174. //把被加壳apk内容拷贝到newdex中
  175. System.arraycopy(apkdata,ablen-4-readInt,newdex,0,readInt);
  176. //这里应该加上对于apk的解密操作,若加壳是加密处理的话
  177. //?
  178. //对源程序Apk进行解密
  179. newdex=decrypt(newdex);
  180. //写入apk文件
  181. Filefile=newFile(apkFileName);
  182. try{
  183. FileOutputStreamlocalFileOutputStream=newFileOutputStream(file);
  184. localFileOutputStream.write(newdex);
  185. localFileOutputStream.close();
  186. }catch(IOExceptionlocalIOException){
  187. thrownewRuntimeException(localIOException);
  188. }
  189. //分析被加壳的apk文件
  190. ZipInputStreamlocalZipInputStream=newZipInputStream(
  191. newBufferedInputStream(newFileInputStream(file)));
  192. while(true){
  193. ZipEntrylocalZipEntry=localZipInputStream.getNextEntry();//不了解这个是否也遍历子目录,看样子应该是遍历的
  194. if(localZipEntry==null){
  195. localZipInputStream.close();
  196. break;
  197. }
  198. //取出被加壳apk用到的so文件,放到libPath中(data/data/包名/payload_lib)
  199. Stringname=localZipEntry.getName();
  200. if(name.startsWith("lib/")&&name.endsWith(".so")){
  201. FilestoreFile=newFile(libPath+"/"
  202. +name.substring(name.lastIndexOf('/')));
  203. storeFile.createNewFile();
  204. FileOutputStreamfos=newFileOutputStream(storeFile);
  205. byte[]arrayOfByte=newbyte[1024];
  206. while(true){
  207. inti=localZipInputStream.read(arrayOfByte);
  208. if(i==-1)
  209. break;
  210. fos.write(arrayOfByte,0,i);
  211. }
  212. fos.flush();
  213. fos.close();
  214. }
  215. localZipInputStream.closeEntry();
  216. }
  217. localZipInputStream.close();
  218. }
  219. /**
  220. *从apk包里面获取dex文件内容(byte)
  221. *@return
  222. *@throwsIOException
  223. */
  224. privatebyte[]readDexFileFromApk()throwsIOException{
  225. ByteArrayOutputStreamdexByteArrayOutputStream=newByteArrayOutputStream();
  226. ZipInputStreamlocalZipInputStream=newZipInputStream(
  227. newBufferedInputStream(newFileInputStream(
  228. this.getApplicationInfo().sourceDir)));
  229. while(true){
  230. ZipEntrylocalZipEntry=localZipInputStream.getNextEntry();
  231. if(localZipEntry==null){
  232. localZipInputStream.close();
  233. break;
  234. }
  235. if(localZipEntry.getName().equals("classes.dex")){
  236. byte[]arrayOfByte=newbyte[1024];
  237. while(true){
  238. inti=localZipInputStream.read(arrayOfByte);
  239. if(i==-1)
  240. break;
  241. dexByteArrayOutputStream.write(arrayOfByte,0,i);
  242. }
  243. }
  244. localZipInputStream.closeEntry();
  245. }
  246. localZipInputStream.close();
  247. returndexByteArrayOutputStream.toByteArray();
  248. }
  249. ////直接返回数据,读者可以添加自己解密方法
  250. privatebyte[]decrypt(byte[]srcdata){
  251. for(inti=0;i<srcdata.length;i++){
  252. srcdata[i]=(byte)(0xFF^srcdata[i]);
  253. }
  254. returnsrcdata;
  255. }
  256. //以下是加载资源
  257. protectedAssetManagermAssetManager;//资源管理器
  258. protectedResourcesmResources;//资源
  259. protectedThememTheme;//主题
  260. protectedvoidloadResources(StringdexPath){
  261. try{
  262. AssetManagerassetManager=AssetManager.class.newInstance();
  263. MethodaddAssetPath=assetManager.getClass().getMethod("addAssetPath",String.class);
  264. addAssetPath.invoke(assetManager,dexPath);
  265. mAssetManager=assetManager;
  266. }catch(Exceptione){
  267. Log.i("inject","loadResourceerror:"+Log.getStackTraceString(e));
  268. e.printStackTrace();
  269. }
  270. ResourcessuperRes=super.getResources();
  271. superRes.getDisplayMetrics();
  272. superRes.getConfiguration();
  273. mResources=newResources(mAssetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
  274. mTheme=mResources.newTheme();
  275. mTheme.setTo(super.getTheme());
  276. }
  277. @Override
  278. publicAssetManagergetAssets(){
  279. returnmAssetManager==null?super.getAssets():mAssetManager;
  280. }
  281. @Override
  282. publicResourcesgetResources(){
  283. returnmResources==null?super.getResources():mResources;
  284. }
  285. @Override
  286. publicThemegetTheme(){
  287. returnmTheme==null?super.getTheme():mTheme;
  288. }
  289. }

首先我们来看一下具体步骤的代码实现:

1>、得到脱壳Apk中的dex文件,然后从这个文件中得到源程序Apk.进行解密,然后加载

[java] view plain copy
  1. //这是context赋值
  2. @Override
  3. protectedvoidattachBaseContext(Contextbase){
  4. super.attachBaseContext(base);
  5. try{
  6. //创建两个文件夹payload_odex,payload_lib私有的,可写的文件目录
  7. Fileodex=this.getDir("payload_odex",MODE_PRIVATE);
  8. Filelibs=this.getDir("payload_lib",MODE_PRIVATE);
  9. odexPath=odex.getAbsolutePath();
  10. libPath=libs.getAbsolutePath();
  11. apkFileName=odex.getAbsolutePath()+"/payload.apk";
  12. FiledexFile=newFile(apkFileName);
  13. Log.i("demo","apksize:"+dexFile.length());
  14. if(!dexFile.exists())
  15. {
  16. dexFile.createNewFile();//在payload_odex文件夹内,创建payload.apk
  17. //读取程序classes.dex文件
  18. byte[]dexdata=this.readDexFileFromApk();
  19. //分离出解壳后的apk文件已用于动态加载
  20. this.splitPayLoadFromDex(dexdata);
  21. }
  22. //配置动态加载环境
  23. ObjectcurrentActivityThread=RefInvoke.invokeStaticMethod(
  24. "android.app.ActivityThread","currentActivityThread",
  25. newClass[]{},newObject[]{});//获取主线程对象http://blog.csdn.net/myarrow/article/details/14223493
  26. StringpackageName=this.getPackageName();//当前apk的包名
  27. //下面两句不是太理解
  28. ArrayMapmPackages=(ArrayMap)RefInvoke.getFieldOjbect(
  29. "android.app.ActivityThread",currentActivityThread,
  30. "mPackages");
  31. WeakReferencewr=(WeakReference)mPackages.get(packageName);
  32. //创建被加壳apk的DexClassLoader对象加载apk内的类和本地代码(c/c++代码)
  33. DexClassLoaderdLoader=newDexClassLoader(apkFileName,odexPath,
  34. libPath,(ClassLoader)RefInvoke.getFieldOjbect(
  35. "android.app.LoadedApk",wr.get(),"mClassLoader"));
  36. //base.getClassLoader();是不是就等同于(ClassLoader)RefInvoke.getFieldOjbect()?有空验证下//?
  37. //把当前进程的DexClassLoader设置成了被加壳apk的DexClassLoader----有点c++中进程环境的意思~~
  38. RefInvoke.setFieldOjbect("android.app.LoadedApk","mClassLoader",
  39. wr.get(),dLoader);
  40. Log.i("demo","classloader:"+dLoader);
  41. try{
  42. ObjectactObj=dLoader.loadClass("com.example.forceapkobj.MainActivity");
  43. Log.i("demo","actObj:"+actObj);
  44. }catch(Exceptione){
  45. Log.i("demo","activity:"+Log.getStackTraceString(e));
  46. }
  47. }catch(Exceptione){
  48. Log.i("demo","error:"+Log.getStackTraceString(e));
  49. e.printStackTrace();
  50. }
  51. }
这里需要注意的一个问题,就是我们需要找到一个时机,就是在脱壳程序还没有运行起来的时候,来加载源程序的Apk,执行他的onCreate方法,那么这个时机不能太晚,不然的话,就是运行脱壳程序,而不是源程序了。查看源码我们知道。Application中有一个方法:attachBaseContext这个方法,他在Application的onCreate方法执行前就会执行了,那么我们的工作就需要在这里进行

1)、从脱壳程序Apk中找到源程序Apk,并且进行解密操作

[java] view plain copy
  1. //创建两个文件夹payload_odex,payload_lib私有的,可写的文件目录
  2. Fileodex=this.getDir("payload_odex",MODE_PRIVATE);
  3. Filelibs=this.getDir("payload_lib",MODE_PRIVATE);
  4. odexPath=odex.getAbsolutePath();
  5. libPath=libs.getAbsolutePath();
  6. apkFileName=odex.getAbsolutePath()+"/payload.apk";
  7. FiledexFile=newFile(apkFileName);
  8. Log.i("demo","apksize:"+dexFile.length());
  9. if(!dexFile.exists())
  10. {
  11. dexFile.createNewFile();//在payload_odex文件夹内,创建payload.apk
  12. //读取程序classes.dex文件
  13. byte[]dexdata=this.readDexFileFromApk();
  14. //分离出解壳后的apk文件已用于动态加载
  15. this.splitPayLoadFromDex(dexdata);
  16. }
这个脱壳解密操作一定要和我们之前的加壳以及加密操作对应,不然就会出现Dex加载错误问题

A) 从Apk中获取到Dex文件

[java] view plain copy
  1. /**
  2. *从apk包里面获取dex文件内容(byte)
  3. *@return
  4. *@throwsIOException
  5. */
  6. privatebyte[]readDexFileFromApk()throwsIOException{
  7. ByteArrayOutputStreamdexByteArrayOutputStream=newByteArrayOutputStream();
  8. ZipInputStreamlocalZipInputStream=newZipInputStream(
  9. newBufferedInputStream(newFileInputStream(
  10. this.getApplicationInfo().sourceDir)));
  11. while(true){
  12. ZipEntrylocalZipEntry=localZipInputStream.getNextEntry();
  13. if(localZipEntry==null){
  14. localZipInputStream.close();
  15. break;
  16. }
  17. if(localZipEntry.getName().equals("classes.dex")){
  18. byte[]arrayOfByte=newbyte[1024];
  19. while(true){
  20. inti=localZipInputStream.read(arrayOfByte);
  21. if(i==-1)
  22. break;
  23. dexByteArrayOutputStream.write(arrayOfByte,0,i);
  24. }
  25. }
  26. localZipInputStream.closeEntry();
  27. }
  28. localZipInputStream.close();
  29. returndexByteArrayOutputStream.toByteArray();
  30. }
其实就是解压Apk文件,直接得到dex文件即可


B) 从脱壳Dex中得到源Apk文件

[java] view plain copy
  1. /**
  2. *释放被加壳的apk文件,so文件
  3. *@paramdata
  4. *@throwsIOException
  5. */
  6. privatevoidsplitPayLoadFromDex(byte[]apkdata)throwsIOException{
  7. intablen=apkdata.length;
  8. //取被加壳apk的长度这里的长度取值,对应加壳时长度的赋值都可以做些简化
  9. byte[]dexlen=newbyte[4];
  10. System.arraycopy(apkdata,ablen-4,dexlen,0,4);
  11. ByteArrayInputStreambais=newByteArrayInputStream(dexlen);
  12. DataInputStreamin=newDataInputStream(bais);
  13. intreadInt=in.readInt();
  14. System.out.println(Integer.toHexString(readInt));
  15. byte[]newdex=newbyte[readInt];
  16. //把被加壳apk内容拷贝到newdex中
  17. System.arraycopy(apkdata,ablen-4-readInt,newdex,0,readInt);
  18. //这里应该加上对于apk的解密操作,若加壳是加密处理的话
  19. //?
  20. //对源程序Apk进行解密
  21. newdex=decrypt(newdex);
  22. //写入apk文件
  23. Filefile=newFile(apkFileName);
  24. try{
  25. FileOutputStreamlocalFileOutputStream=newFileOutputStream(file);
  26. localFileOutputStream.write(newdex);
  27. localFileOutputStream.close();
  28. }catch(IOExceptionlocalIOException){
  29. thrownewRuntimeException(localIOException);
  30. }
  31. //分析被加壳的apk文件
  32. ZipInputStreamlocalZipInputStream=newZipInputStream(
  33. newBufferedInputStream(newFileInputStream(file)));
  34. while(true){
  35. ZipEntrylocalZipEntry=localZipInputStream.getNextEntry();//不了解这个是否也遍历子目录,看样子应该是遍历的
  36. if(localZipEntry==null){
  37. localZipInputStream.close();
  38. break;
  39. }
  40. //取出被加壳apk用到的so文件,放到libPath中(data/data/包名/payload_lib)
  41. Stringname=localZipEntry.getName();
  42. if(name.startsWith("lib/")&&name.endsWith(".so")){
  43. FilestoreFile=newFile(libPath+"/"
  44. +name.substring(name.lastIndexOf('/')));
  45. storeFile.createNewFile();
  46. FileOutputStreamfos=newFileOutputStream(storeFile);
  47. byte[]arrayOfByte=newbyte[1024];
  48. while(true){
  49. inti=localZipInputStream.read(arrayOfByte);
  50. if(i==-1)
  51. break;
  52. fos.write(arrayOfByte,0,i);
  53. }
  54. fos.flush();
  55. fos.close();
  56. }
  57. localZipInputStream.closeEntry();
  58. }
  59. localZipInputStream.close();
  60. }


C) 解密源程序Apk [java] view plain copy
  1. ////直接返回数据,读者可以添加自己解密方法
  2. privatebyte[]decrypt(byte[]srcdata){
  3. for(inti=0;i<srcdata.length;i++){
  4. srcdata[i]=(byte)(0xFF^srcdata[i]);
  5. }
  6. returnsrcdata;
  7. }
这个解密算法和加密算法是一致的


2>、加载解密之后的源程序Apk

[java] view plain copy
  1. //配置动态加载环境
  2. ObjectcurrentActivityThread=RefInvoke.invokeStaticMethod(
  3. "android.app.ActivityThread","currentActivityThread",
  4. newClass[]{},newObject[]{});//获取主线程对象http://blog.csdn.net/myarrow/article/details/14223493
  5. StringpackageName=this.getPackageName();//当前apk的包名
  6. //下面两句不是太理解
  7. ArrayMapmPackages=(ArrayMap)RefInvoke.getFieldOjbect(
  8. "android.app.ActivityThread",currentActivityThread,
  9. "mPackages");
  10. WeakReferencewr=(WeakReference)mPackages.get(packageName);
  11. //创建被加壳apk的DexClassLoader对象加载apk内的类和本地代码(c/c++代码)
  12. DexClassLoaderdLoader=newDexClassLoader(apkFileName,odexPath,
  13. libPath,(ClassLoader)RefInvoke.getFieldOjbect(
  14. "android.app.LoadedApk",wr.get(),"mClassLoader"));
  15. //base.getClassLoader();是不是就等同于(ClassLoader)RefInvoke.getFieldOjbect()?有空验证下//?
  16. //把当前进程的DexClassLoader设置成了被加壳apk的DexClassLoader----有点c++中进程环境的意思~~
  17. RefInvoke.setFieldOjbect("android.app.LoadedApk","mClassLoader",
  18. wr.get(),dLoader);
  19. Log.i("demo","classloader:"+dLoader);
  20. try{
  21. ObjectactObj=dLoader.loadClass("com.example.forceapkobj.MainActivity");
  22. Log.i("demo","actObj:"+actObj);
  23. }catch(Exceptione){
  24. Log.i("demo","activity:"+Log.getStackTraceString(e));
  25. }


2)、找到源程序的Application程序,让其运行 [java] view plain copy
  1. @Override
  2. publicvoidonCreate(){
  3. {
  4. //loadResources(apkFileName);
  5. Log.i("demo","onCreate");
  6. //如果源应用配置有Appliction对象,则替换为源应用Applicaiton,以便不影响源程序逻辑。
  7. StringappClassName=null;
  8. try{
  9. ApplicationInfoai=this.getPackageManager()
  10. .getApplicationInfo(this.getPackageName(),
  11. PackageManager.GET_META_DATA);
  12. Bundlebundle=ai.metaData;
  13. if(bundle!=null&&bundle.containsKey("APPLICATION_CLASS_NAME")){
  14. appClassName=bundle.getString("APPLICATION_CLASS_NAME");//className是配置在xml文件中的。
  15. }else{
  16. Log.i("demo","havenoapplicationclassname");
  17. return;
  18. }
  19. }catch(NameNotFoundExceptione){
  20. Log.i("demo","error:"+Log.getStackTraceString(e));
  21. e.printStackTrace();
  22. }
  23. //有值的话调用该Applicaiton
  24. ObjectcurrentActivityThread=RefInvoke.invokeStaticMethod(
  25. "android.app.ActivityThread","currentActivityThread",
  26. newClass[]{},newObject[]{});
  27. ObjectmBoundApplication=RefInvoke.getFieldOjbect(
  28. "android.app.ActivityThread",currentActivityThread,
  29. "mBoundApplication");
  30. ObjectloadedApkInfo=RefInvoke.getFieldOjbect(
  31. "android.app.ActivityThread$AppBindData",
  32. mBoundApplication,"info");
  33. //把当前进程的mApplication设置成了null
  34. RefInvoke.setFieldOjbect("android.app.LoadedApk","mApplication",
  35. loadedApkInfo,null);
  36. ObjectoldApplication=RefInvoke.getFieldOjbect(
  37. "android.app.ActivityThread",currentActivityThread,
  38. "mInitialApplication");
  39. //http://www.codeceo.com/article/android-context.html
  40. ArrayList<Application>mAllApplications=(ArrayList<Application>)RefInvoke
  41. .getFieldOjbect("android.app.ActivityThread",
  42. currentActivityThread,"mAllApplications");
  43. mAllApplications.remove(oldApplication);//删除oldApplication
  44. ApplicationInfoappinfo_In_LoadedApk=(ApplicationInfo)RefInvoke
  45. .getFieldOjbect("android.app.LoadedApk",loadedApkInfo,
  46. "mApplicationInfo");
  47. ApplicationInfoappinfo_In_AppBindData=(ApplicationInfo)RefInvoke
  48. .getFieldOjbect("android.app.ActivityThread$AppBindData",
  49. mBoundApplication,"appInfo");
  50. appinfo_In_LoadedApk.className=appClassName;
  51. appinfo_In_AppBindData.className=appClassName;
  52. Applicationapp=(Application)RefInvoke.invokeMethod(
  53. "android.app.LoadedApk","makeApplication",loadedApkInfo,
  54. newClass[]{boolean.class,Instrumentation.class},
  55. newObject[]{false,null});//执行makeApplication(false,null)
  56. RefInvoke.setFieldOjbect("android.app.ActivityThread",
  57. "mInitialApplication",currentActivityThread,app);
  58. ArrayMapmProviderMap=(ArrayMap)RefInvoke.getFieldOjbect(
  59. "android.app.ActivityThread",currentActivityThread,
  60. "mProviderMap");
  61. Iteratorit=mProviderMap.values().iterator();
  62. while(it.hasNext()){
  63. ObjectproviderClientRecord=it.next();
  64. ObjectlocalProvider=RefInvoke.getFieldOjbect(
  65. "android.app.ActivityThread$ProviderClientRecord",
  66. providerClientRecord,"mLocalProvider");
  67. RefInvoke.setFieldOjbect("android.content.ContentProvider",
  68. "mContext",localProvider,app);
  69. }
  70. Log.i("demo","app:"+app);
  71. app.onCreate();
  72. }
  73. }
直接在脱壳的Application中的onCreate方法中进行就可以了。这里我们还可以看到是通过AndroidManifest.xml中的meta标签获取源程序Apk中的Application对象的。

下面来看一下AndoridManifest.xml文件中的内容:

在这里我们定义了源程序Apk的Application类名。


项目下载:http://download.csdn.net/detail/jiangwei0910410003/9102741


四、运行程序

那么到这里我们就介绍完了,这三个项目的内容,下面就来看看如何运行吧:

运行步骤:

第一步:得到源程序Apk文件和脱壳程序的Dex文件

运行源程序和脱壳程序项目,之后得到这两个文件(记得将classes.dex文件改名ForceApkObj.dex),然后使用加壳程序进行加壳:

这里的ForceApkObj.apk文件和ForceApkObj.dex文件是输入文件,输出的是classes.dex文件。


第二步:替换脱壳程序中的classes.dex文件

我们在第一步中得到加壳之后的classes.dex文件之后,并且我们在第一步运行脱壳项目的时候得到一个ReforceApk.apk文件,这时候我们使用解压缩软件进行替换:



第三步:我们在第二步的时候得到替换之后的ReforceApk.apk文件,这个文件因为被修改了,所以我们需要从新对他签名,不然运行也是报错的。


工具下载:http://download.csdn.net/detail/jiangwei0910410003/9102767

下载之后的工具需要用ReforeceApk.apk文件替换ReforceApk_des.apk文件,然后运行run.bat就可以得到签名之后的文件了。

run.bat文件的命令如下:

cd C:\Users\i\Desktop\forceapks
jarsigner -verbose -keystore forceapk -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar ReforceApk_des.apk ReforceApk.apk jiangwei
del ReforceApk.apk

这里最主要的命令就是中间的一条签名的命令,关于命令的参数说明如下:

jarsigner -verbose -keystore 签名文件 -storepass 密码 -keypass alias的密码 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA 签名后的文件 签名前的apk alias名称

eg:
jarsigner -verbose -keystore forceapk -storepass 123456 -keypass 123456 -sigfile CERT -digestalg SHA1 -sigalg MD5withRSA -signedjar ReforceApk_des.apk ReforceApk_src.apk jiangwei

签名文件的密码:123456
alais的密码:123456


所以这里我们在得到ReforceApk.apk文件的时候,需要签名,关于Eclipse中如何签名一个Apk的话,这里就不多说了,自己google一下吧:



那么通过上面的三个步骤之后我们得到一个签名之后的最终文件:ReforceApk_des.apk

我们安装这个Apk,然后运行,效果如下:


看到运行结果的那一瞬间,我们是多么的开心,多么的有成就感,但是这个过程中遇到的问题,是可想而知的。

我们这个时候再去反编译一下源程序Apk(这个文件是我们脱壳出来的payload.apk,看ReforeceApk中的代码,就知道他的位置了)


发现dex文件格式是不正确的。说明我们的加固是成功的。


五、遇到的问题

1、研究的过程中遇到签名不正确的地方,开始的时候,直接替换dex文件之后,就直接运行了Apk,但是总是提示签名不正确。

2、运行的过程中说找不到源程序中的Activity,这个问题其实我在动态加载的那篇文章中说道了,我们需要在脱壳程序中的AndroidManifest.xml中什么一下源程序中的Activiity:



六、技术要点

1、对Dex文件格式的了解

2、动态加载技术的深入掌握

3、Application的执行流程的了解

4、如何从Apk中得到Dex文件

5、如何从新签名一个Apk程序


七、综合概述

我们通过上面的过程可以看到,关于Apk加固的工作还是挺复杂的,涉及到的东西也挺多的,下面就在来总结一下吧:

1、加壳程序

任务:对源程序Apk进行加密,合并脱壳程序的Dex文件 ,然后输入一个加壳之后的Dex文件

语言:任何语言都可以,不限于Java语言

技术点:对Dex文件格式的解析


2、脱壳程序

任务:获取源程序Apk,进行解密,然后动态加载进来,运行程序

语言:Android项目(Java)

技术点:如何从Apk中获取Dex文件,动态加载Apk,使用反射运行Application


八、总结

Android中的Apk反编译可能是每个开发都会经历的事,但是在反编译的过程中,对于源程序的开发者来说那是不公平的,那么Apk加固也是应运而生,但是即使是这样,我们也还是做不到那么的安全,现在网上也是有很多文章在解析梆梆加固的原理了。而且有人破解成功了,那么加固还不是怎么安全。最后一句话:逆向和加固是一个永不停息的战争。

更多相关文章

  1. android学习中遇到的问题
  2. OpenCore的代码结构
  3. android - adb命令的使用
  4. 基于 Android(安卓)NDK 的学习之旅-----HelloWorld (附源码)
  5. Android(安卓)adb的使用略解
  6. 谈谈Android的so
  7. Android系统上部署usb打印机
  8. android全格式多媒体播放器(一:ffmpeg移植)
  9. NPM 和webpack 的基础使用

随机推荐

  1. mysql count详解及函数实例代码
  2. CentOS下重启Mysql的各种方法(推荐)
  3. Mysql5.7.14安装配置方法操作图文教程(密
  4. mysql 详解隔离级别操作过程(cmd)
  5. macOS Sierra安装Apache2.4+PHP7.0+MySQL
  6. MyBatis 如何写配置文件和简单使用
  7. win10免安装版本的MySQL安装配置教程
  8. Mac下安装mysql5.7 完整步骤(图文详解)
  9. ubuntu 15.04下mysql开放远程3306端口
  10. Navicat远程连接SQL Server并转换成MySQL