一、前言

今天又到周末了,憋了好久又要出博客了,今天来介绍一下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

 

 
  1. package com.example.forceapkobj;

  2.  
  3. import android.app.Application;

  4. import android.util.Log;

  5.  
  6. public class MyApplication extends Application{

  7.  
  8. @Override

  9. public void onCreate() {

  10. super.onCreate();

  11. Log.i("demo", "source apk onCreate:"+this);

  12. }

  13.  
  14. }

 

就是打印一下onCreate方法。

 

MainActivity.java

 

 
  1. package com.example.forceapkobj;

  2.  
  3. import android.app.Activity;

  4. import android.content.Intent;

  5. import android.os.Bundle;

  6. import android.util.Log;

  7. import android.view.View;

  8. import android.view.View.OnClickListener;

  9. import android.widget.TextView;

  10.  
  11. public class MainActivity extends Activity {

  12.  
  13. @Override

  14. protected void onCreate(Bundle savedInstanceState) {

  15. super.onCreate(savedInstanceState);

  16.  
  17. TextView content = new TextView(this);

  18. content.setText("I am Source Apk");

  19. content.setOnClickListener(new OnClickListener(){

  20. @Override

  21. public void onClick(View arg0) {

  22. Intent intent = new Intent(MainActivity.this, SubActivity.class);

  23. startActivity(intent);

  24. }});

  25. setContentView(content);

  26.  
  27. Log.i("demo", "app:"+getApplicationContext());

  28.  
  29. }

  30.  
  31. }

也是打印一下内容。

 

 

2、加壳程序项目:DexShellTools

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

看一下代码:

 

 
  1. package com.example.reforceapk;

  2.  
  3. import java.io.ByteArrayOutputStream;

  4. import java.io.File;

  5. import java.io.FileInputStream;

  6. import java.io.FileOutputStream;

  7. import java.io.IOException;

  8. import java.security.MessageDigest;

  9. import java.security.NoSuchAlgorithmException;

  10. import java.util.zip.Adler32;

  11.  
  12.  
  13. public class mymain {

  14. /**

  15. * @param args

  16. */

  17. public static void main(String[] args) {

  18. // TODO Auto-generated method stub

  19. try {

  20. File payloadSrcFile = new File("force/ForceApkObj.apk"); //需要加壳的程序

  21. System.out.println("apk size:"+payloadSrcFile.length());

  22. File unShellDexFile = new File("force/ForceApkObj.dex"); //解客dex

  23. byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile));//以二进制形式读出apk,并进行加密处理//对源Apk进行加密操作

  24. byte[] unShellDexArray = readFileBytes(unShellDexFile);//以二进制形式读出dex

  25. int payloadLen = payloadArray.length;

  26. int unShellDexLen = unShellDexArray.length;

  27. int totalLen = payloadLen + unShellDexLen +4;//多出4字节是存放长度的。

  28. byte[] newdex = new byte[totalLen]; // 申请了新的长度

  29. //添加解壳代码

  30. System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);//先拷贝dex内容

  31. //添加加密后的解壳数据

  32. System.arraycopy(payloadArray, 0, newdex, unShellDexLen, payloadLen);//再在dex内容后面拷贝apk的内容

  33. //添加解壳数据长度

  34. System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen-4, 4);//最后4为长度

  35. //修改DEX file size文件头

  36. fixFileSizeHeader(newdex);

  37. //修改DEX SHA1 文件头

  38. fixSHA1Header(newdex);

  39. //修改DEX CheckSum文件头

  40. fixCheckSumHeader(newdex);

  41.  
  42. String str = "force/classes.dex";

  43. File file = new File(str);

  44. if (!file.exists()) {

  45. file.createNewFile();

  46. }

  47.  
  48. FileOutputStream localFileOutputStream = new FileOutputStream(str);

  49. localFileOutputStream.write(newdex);

  50. localFileOutputStream.flush();

  51. localFileOutputStream.close();

  52.  
  53.  
  54. } catch (Exception e) {

  55. e.printStackTrace();

  56. }

  57. }

  58.  
  59. //直接返回数据,读者可以添加自己加密方法

  60. private static byte[] encrpt(byte[] srcdata){

  61. for(int i = 0;i

  62. srcdata[i] = (byte)(0xFF ^ srcdata[i]);

  63. }

  64. return srcdata;

  65. }

  66.  
  67. /**

  68. * 修改dex头,CheckSum 校验码

  69. * @param dexBytes

  70. */

  71. private static void fixCheckSumHeader(byte[] dexBytes) {

  72. Adler32 adler = new Adler32();

  73. adler.update(dexBytes, 12, dexBytes.length - 12);//从12到文件末尾计算校验码

  74. long value = adler.getValue();

  75. int va = (int) value;

  76. byte[] newcs = intToByte(va);

  77. //高位在前,低位在前掉个个

  78. byte[] recs = new byte[4];

  79. for (int i = 0; i < 4; i++) {

  80. recs[i] = newcs[newcs.length - 1 - i];

  81. System.out.println(Integer.toHexString(newcs[i]));

  82. }

  83. System.arraycopy(recs, 0, dexBytes, 8, 4);//效验码赋值(8-11)

  84. System.out.println(Long.toHexString(value));

  85. System.out.println();

  86. }

  87.  
  88.  
  89. /**

  90. * int 转byte[]

  91. * @param number

  92. * @return

  93. */

  94. public static byte[] intToByte(int number) {

  95. byte[] b = new byte[4];

  96. for (int i = 3; i >= 0; i--) {

  97. b[i] = (byte) (number % 256);

  98. number >>= 8;

  99. }

  100. return b;

  101. }

  102.  
  103. /**

  104. * 修改dex头 sha1值

  105. * @param dexBytes

  106. * @throws NoSuchAlgorithmException

  107. */

  108. private static void fixSHA1Header(byte[] dexBytes)

  109. throws NoSuchAlgorithmException {

  110. MessageDigest md = MessageDigest.getInstance("SHA-1");

  111. md.update(dexBytes, 32, dexBytes.length - 32);//从32为到结束计算sha--1

  112. byte[] newdt = md.digest();

  113. System.arraycopy(newdt, 0, dexBytes, 12, 20);//修改sha-1值(12-31)

  114. //输出sha-1值,可有可无

  115. String hexstr = "";

  116. for (int i = 0; i < newdt.length; i++) {

  117. hexstr += Integer.toString((newdt[i] & 0xff) + 0x100, 16)

  118. .substring(1);

  119. }

  120. System.out.println(hexstr);

  121. }

  122.  
  123. /**

  124. * 修改dex头 file_size值

  125. * @param dexBytes

  126. */

  127. private static void fixFileSizeHeader(byte[] dexBytes) {

  128. //新文件长度

  129. byte[] newfs = intToByte(dexBytes.length);

  130. System.out.println(Integer.toHexString(dexBytes.length));

  131. byte[] refs = new byte[4];

  132. //高位在前,低位在前掉个个

  133. for (int i = 0; i < 4; i++) {

  134. refs[i] = newfs[newfs.length - 1 - i];

  135. System.out.println(Integer.toHexString(newfs[i]));

  136. }

  137. System.arraycopy(refs, 0, dexBytes, 32, 4);//修改(32-35)

  138. }

  139.  
  140.  
  141. /**

  142. * 以二进制读出文件内容

  143. * @param file

  144. * @return

  145. * @throws IOException

  146. */

  147. private static byte[] readFileBytes(File file) throws IOException {

  148. byte[] arrayOfByte = new byte[1024];

  149. ByteArrayOutputStream localByteArrayOutputStream = new ByteArrayOutputStream();

  150. FileInputStream fis = new FileInputStream(file);

  151. while (true) {

  152. int i = fis.read(arrayOfByte);

  153. if (i != -1) {

  154. localByteArrayOutputStream.write(arrayOfByte, 0, i);

  155. } else {

  156. return localByteArrayOutputStream.toByteArray();

  157. }

  158. }

  159. }

  160. }

 

下面来分析一下:

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

1>、加密源程序Apk文件

byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile));//以二进制形式读出apk,并进行加密处理//对源Apk进行加密操作

加密算法很简单:

 

 
  1. //直接返回数据,读者可以添加自己加密方法

  2. private static byte[] encrpt(byte[] srcdata){

  3. for(int i = 0;i

  4. srcdata[i] = (byte)(0xFF ^ srcdata[i]);

  5. }

  6. return srcdata;

  7. }

对每个字节进行异或一下即可。

 

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

 

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

 
  1. int payloadLen = payloadArray.length;

  2. int unShellDexLen = unShellDexArray.length;

  3. int totalLen = payloadLen + unShellDexLen +4;//多出4字节是存放长度的。

  4. byte[] newdex = new byte[totalLen]; // 申请了新的长度

  5. //添加解壳代码

  6. System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);//先拷贝dex内容

  7. //添加加密后的解壳数据

  8. System.arraycopy(payloadArray, 0, newdex, unShellDexLen, payloadLen);//再在dex内容后面拷贝apk的内容

 

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

 
  1. //添加解壳数据长度

  2. System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen-4, 4);//最后4为长度


4>、修改新Dex文件的文件头信息:file_size; sha1; check_sum

 
  1. //修改DEX file size文件头

  2. fixFileSizeHeader(newdex);

  3. //修改DEX SHA1 文件头

  4. fixSHA1Header(newdex);

  5. //修改DEX CheckSum文件头

  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

 

下面来看一下代码:

 

 
  1. package com.example.reforceapk;

  2.  
  3. import java.io.BufferedInputStream;

  4. import java.io.ByteArrayInputStream;

  5. import java.io.ByteArrayOutputStream;

  6. import java.io.DataInputStream;

  7. import java.io.File;

  8. import java.io.FileInputStream;

  9. import java.io.FileOutputStream;

  10. import java.io.IOException;

  11. import java.lang.ref.WeakReference;

  12. import java.lang.reflect.Method;

  13. import java.util.ArrayList;

  14. import java.util.HashMap;

  15. import java.util.Iterator;

  16. import java.util.zip.ZipEntry;

  17. import java.util.zip.ZipInputStream;

  18.  
  19. import android.app.Application;

  20. import android.app.Instrumentation;

  21. import android.content.Context;

  22. import android.content.pm.ApplicationInfo;

  23. import android.content.pm.PackageManager;

  24. import android.content.pm.PackageManager.NameNotFoundException;

  25. import android.content.res.AssetManager;

  26. import android.content.res.Resources;

  27. import android.content.res.Resources.Theme;

  28. import android.os.Bundle;

  29. import android.util.ArrayMap;

  30. import android.util.Log;

  31. import dalvik.system.DexClassLoader;

  32.  
  33. public class ProxyApplication extends Application{

  34. private static final String appkey = "APPLICATION_CLASS_NAME";

  35. private String apkFileName;

  36. private String odexPath;

  37. private String libPath;

  38.  
  39. //这是context 赋值

  40. @Override

  41. protected void attachBaseContext(Context base) {

  42. super.attachBaseContext(base);

  43. try {

  44. //创建两个文件夹payload_odex,payload_lib 私有的,可写的文件目录

  45. File odex = this.getDir("payload_odex", MODE_PRIVATE);

  46. File libs = this.getDir("payload_lib", MODE_PRIVATE);

  47. odexPath = odex.getAbsolutePath();

  48. libPath = libs.getAbsolutePath();

  49. apkFileName = odex.getAbsolutePath() + "/payload.apk";

  50. File dexFile = new File(apkFileName);

  51. Log.i("demo", "apk size:"+dexFile.length());

  52. if (!dexFile.exists())

  53. {

  54. dexFile.createNewFile(); //在payload_odex文件夹内,创建payload.apk

  55. // 读取程序classes.dex文件

  56. byte[] dexdata = this.readDexFileFromApk();

  57.  
  58. // 分离出解壳后的apk文件已用于动态加载

  59. this.splitPayLoadFromDex(dexdata);

  60. }

  61. // 配置动态加载环境

  62. Object currentActivityThread = RefInvoke.invokeStaticMethod(

  63. "android.app.ActivityThread", "currentActivityThread",

  64. new Class[] {}, new Object[] {});//获取主线程对象 http://blog.csdn.net/myarrow/article/details/14223493

  65. String packageName = this.getPackageName();//当前apk的包名

  66. //下面两句不是太理解

  67. ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(

  68. "android.app.ActivityThread", currentActivityThread,

  69. "mPackages");

  70. WeakReference wr = (WeakReference) mPackages.get(packageName);

  71. //创建被加壳apk的DexClassLoader对象 加载apk内的类和本地代码(c/c++代码)

  72. DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath,

  73. libPath, (ClassLoader) RefInvoke.getFieldOjbect(

  74. "android.app.LoadedApk", wr.get(), "mClassLoader"));

  75. //base.getClassLoader(); 是不是就等同于 (ClassLoader) RefInvoke.getFieldOjbect()? 有空验证下//?

  76. //把当前进程的DexClassLoader 设置成了被加壳apk的DexClassLoader ----有点c++中进程环境的意思~~

  77. RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",

  78. wr.get(), dLoader);

  79.  
  80. Log.i("demo","classloader:"+dLoader);

  81.  
  82. try{

  83. Object actObj = dLoader.loadClass("com.example.forceapkobj.MainActivity");

  84. Log.i("demo", "actObj:"+actObj);

  85. }catch(Exception e){

  86. Log.i("demo", "activity:"+Log.getStackTraceString(e));

  87. }

  88.  
  89.  
  90. } catch (Exception e) {

  91. Log.i("demo", "error:"+Log.getStackTraceString(e));

  92. e.printStackTrace();

  93. }

  94. }

  95.  
  96. @Override

  97. public void onCreate() {

  98. {

  99. //loadResources(apkFileName);

  100.  
  101. Log.i("demo", "onCreate");

  102. // 如果源应用配置有Appliction对象,则替换为源应用Applicaiton,以便不影响源程序逻辑。

  103. String appClassName = null;

  104. try {

  105. ApplicationInfo ai = this.getPackageManager()

  106. .getApplicationInfo(this.getPackageName(),

  107. PackageManager.GET_META_DATA);

  108. Bundle bundle = ai.metaData;

  109. if (bundle != null && bundle.containsKey("APPLICATION_CLASS_NAME")) {

  110. appClassName = bundle.getString("APPLICATION_CLASS_NAME");//className 是配置在xml文件中的。

  111. } else {

  112. Log.i("demo", "have no application class name");

  113. return;

  114. }

  115. } catch (NameNotFoundException e) {

  116. Log.i("demo", "error:"+Log.getStackTraceString(e));

  117. e.printStackTrace();

  118. }

  119. //有值的话调用该Applicaiton

  120. Object currentActivityThread = RefInvoke.invokeStaticMethod(

  121. "android.app.ActivityThread", "currentActivityThread",

  122. new Class[] {}, new Object[] {});

  123. Object mBoundApplication = RefInvoke.getFieldOjbect(

  124. "android.app.ActivityThread", currentActivityThread,

  125. "mBoundApplication");

  126. Object loadedApkInfo = RefInvoke.getFieldOjbect(

  127. "android.app.ActivityThread$AppBindData",

  128. mBoundApplication, "info");

  129. //把当前进程的mApplication 设置成了null

  130. RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",

  131. loadedApkInfo, null);

  132. Object oldApplication = RefInvoke.getFieldOjbect(

  133. "android.app.ActivityThread", currentActivityThread,

  134. "mInitialApplication");

  135. //http://www.codeceo.com/article/android-context.html

  136. ArrayList mAllApplications = (ArrayList) RefInvoke

  137. .getFieldOjbect("android.app.ActivityThread",

  138. currentActivityThread, "mAllApplications");

  139. mAllApplications.remove(oldApplication);//删除oldApplication

  140.  
  141. ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke

  142. .getFieldOjbect("android.app.LoadedApk", loadedApkInfo,

  143. "mApplicationInfo");

  144. ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke

  145. .getFieldOjbect("android.app.ActivityThread$AppBindData",

  146. mBoundApplication, "appInfo");

  147. appinfo_In_LoadedApk.className = appClassName;

  148. appinfo_In_AppBindData.className = appClassName;

  149. Application app = (Application) RefInvoke.invokeMethod(

  150. "android.app.LoadedApk", "makeApplication", loadedApkInfo,

  151. new Class[] { boolean.class, Instrumentation.class },

  152. new Object[] { false, null });//执行 makeApplication(false,null)

  153. RefInvoke.setFieldOjbect("android.app.ActivityThread",

  154. "mInitialApplication", currentActivityThread, app);

  155.  
  156.  
  157. ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldOjbect(

  158. "android.app.ActivityThread", currentActivityThread,

  159. "mProviderMap");

  160. Iterator it = mProviderMap.values().iterator();

  161. while (it.hasNext()) {

  162. Object providerClientRecord = it.next();

  163. Object localProvider = RefInvoke.getFieldOjbect(

  164. "android.app.ActivityThread$ProviderClientRecord",

  165. providerClientRecord, "mLocalProvider");

  166. RefInvoke.setFieldOjbect("android.content.ContentProvider",

  167. "mContext", localProvider, app);

  168. }

  169.  
  170. Log.i("demo", "app:"+app);

  171.  
  172. app.onCreate();

  173. }

  174. }

  175.  
  176. /**

  177. * 释放被加壳的apk文件,so文件

  178. * @param data

  179. * @throws IOException

  180. */

  181. private void splitPayLoadFromDex(byte[] apkdata) throws IOException {

  182. int ablen = apkdata.length;

  183. //取被加壳apk的长度 这里的长度取值,对应加壳时长度的赋值都可以做些简化

  184. byte[] dexlen = new byte[4];

  185. System.arraycopy(apkdata, ablen - 4, dexlen, 0, 4);

  186. ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);

  187. DataInputStream in = new DataInputStream(bais);

  188. int readInt = in.readInt();

  189. System.out.println(Integer.toHexString(readInt));

  190. byte[] newdex = new byte[readInt];

  191. //把被加壳apk内容拷贝到newdex中

  192. System.arraycopy(apkdata, ablen - 4 - readInt, newdex, 0, readInt);

  193. //这里应该加上对于apk的解密操作,若加壳是加密处理的话

  194. //?

  195.  
  196. //对源程序Apk进行解密

  197. newdex = decrypt(newdex);

  198.  
  199. //写入apk文件

  200. File file = new File(apkFileName);

  201. try {

  202. FileOutputStream localFileOutputStream = new FileOutputStream(file);

  203. localFileOutputStream.write(newdex);

  204. localFileOutputStream.close();

  205. } catch (IOException localIOException) {

  206. throw new RuntimeException(localIOException);

  207. }

  208.  
  209. //分析被加壳的apk文件

  210. ZipInputStream localZipInputStream = new ZipInputStream(

  211. new BufferedInputStream(new FileInputStream(file)));

  212. while (true) {

  213. ZipEntry localZipEntry = localZipInputStream.getNextEntry();//不了解这个是否也遍历子目录,看样子应该是遍历的

  214. if (localZipEntry == null) {

  215. localZipInputStream.close();

  216. break;

  217. }

  218. //取出被加壳apk用到的so文件,放到 libPath中(data/data/包名/payload_lib)

  219. String name = localZipEntry.getName();

  220. if (name.startsWith("lib/") && name.endsWith(".so")) {

  221. File storeFile = new File(libPath + "/"

  222. + name.substring(name.lastIndexOf('/')));

  223. storeFile.createNewFile();

  224. FileOutputStream fos = new FileOutputStream(storeFile);

  225. byte[] arrayOfByte = new byte[1024];

  226. while (true) {

  227. int i = localZipInputStream.read(arrayOfByte);

  228. if (i == -1)

  229. break;

  230. fos.write(arrayOfByte, 0, i);

  231. }

  232. fos.flush();

  233. fos.close();

  234. }

  235. localZipInputStream.closeEntry();

  236. }

  237. localZipInputStream.close();

  238.  
  239.  
  240. }

  241.  
  242. /**

  243. * 从apk包里面获取dex文件内容(byte)

  244. * @return

  245. * @throws IOException

  246. */

  247. private byte[] readDexFileFromApk() throws IOException {

  248. ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream();

  249. ZipInputStream localZipInputStream = new ZipInputStream(

  250. new BufferedInputStream(new FileInputStream(

  251. this.getApplicationInfo().sourceDir)));

  252. while (true) {

  253. ZipEntry localZipEntry = localZipInputStream.getNextEntry();

  254. if (localZipEntry == null) {

  255. localZipInputStream.close();

  256. break;

  257. }

  258. if (localZipEntry.getName().equals("classes.dex")) {

  259. byte[] arrayOfByte = new byte[1024];

  260. while (true) {

  261. int i = localZipInputStream.read(arrayOfByte);

  262. if (i == -1)

  263. break;

  264. dexByteArrayOutputStream.write(arrayOfByte, 0, i);

  265. }

  266. }

  267. localZipInputStream.closeEntry();

  268. }

  269. localZipInputStream.close();

  270. return dexByteArrayOutputStream.toByteArray();

  271. }

  272.  
  273.  
  274. // //直接返回数据,读者可以添加自己解密方法

  275. private byte[] decrypt(byte[] srcdata) {

  276. for(int i=0;i

  277. srcdata[i] = (byte)(0xFF ^ srcdata[i]);

  278. }

  279. return srcdata;

  280. }

  281.  
  282.  
  283. //以下是加载资源

  284. protected AssetManager mAssetManager;//资源管理器

  285. protected Resources mResources;//资源

  286. protected Theme mTheme;//主题

  287.  
  288. protected void loadResources(String dexPath) {

  289. try {

  290. AssetManager assetManager = AssetManager.class.newInstance();

  291. Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);

  292. addAssetPath.invoke(assetManager, dexPath);

  293. mAssetManager = assetManager;

  294. } catch (Exception e) {

  295. Log.i("inject", "loadResource error:"+Log.getStackTraceString(e));

  296. e.printStackTrace();

  297. }

  298. Resources superRes = super.getResources();

  299. superRes.getDisplayMetrics();

  300. superRes.getConfiguration();

  301. mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration());

  302. mTheme = mResources.newTheme();

  303. mTheme.setTo(super.getTheme());

  304. }

  305.  
  306. @Override

  307. public AssetManager getAssets() {

  308. return mAssetManager == null ? super.getAssets() : mAssetManager;

  309. }

  310.  
  311. @Override

  312. public Resources getResources() {

  313. return mResources == null ? super.getResources() : mResources;

  314. }

  315.  
  316. @Override

  317. public Theme getTheme() {

  318. return mTheme == null ? super.getTheme() : mTheme;

  319. }

  320.  
  321. }


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

 

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

 
  1. //这是context 赋值

  2. @Override

  3. protected void attachBaseContext(Context base) {

  4. super.attachBaseContext(base);

  5. try {

  6. //创建两个文件夹payload_odex,payload_lib 私有的,可写的文件目录

  7. File odex = this.getDir("payload_odex", MODE_PRIVATE);

  8. File libs = this.getDir("payload_lib", MODE_PRIVATE);

  9. odexPath = odex.getAbsolutePath();

  10. libPath = libs.getAbsolutePath();

  11. apkFileName = odex.getAbsolutePath() + "/payload.apk";

  12. File dexFile = new File(apkFileName);

  13. Log.i("demo", "apk size:"+dexFile.length());

  14. if (!dexFile.exists())

  15. {

  16. dexFile.createNewFile(); //在payload_odex文件夹内,创建payload.apk

  17. // 读取程序classes.dex文件

  18. byte[] dexdata = this.readDexFileFromApk();

  19.  
  20. // 分离出解壳后的apk文件已用于动态加载

  21. this.splitPayLoadFromDex(dexdata);

  22. }

  23. // 配置动态加载环境

  24. Object currentActivityThread = RefInvoke.invokeStaticMethod(

  25. "android.app.ActivityThread", "currentActivityThread",

  26. new Class[] {}, new Object[] {});//获取主线程对象 http://blog.csdn.net/myarrow/article/details/14223493

  27. String packageName = this.getPackageName();//当前apk的包名

  28. //下面两句不是太理解

  29. ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(

  30. "android.app.ActivityThread", currentActivityThread,

  31. "mPackages");

  32. WeakReference wr = (WeakReference) mPackages.get(packageName);

  33. //创建被加壳apk的DexClassLoader对象 加载apk内的类和本地代码(c/c++代码)

  34. DexClassLoader dLoader = new DexClassLoader(apkFileName, odexPath,

  35. libPath, (ClassLoader) RefInvoke.getFieldOjbect(

  36. "android.app.LoadedApk", wr.get(), "mClassLoader"));

  37. //base.getClassLoader(); 是不是就等同于 (ClassLoader) RefInvoke.getFieldOjbect()? 有空验证下//?

  38. //把当前进程的DexClassLoader 设置成了被加壳apk的DexClassLoader ----有点c++中进程环境的意思~~

  39. RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader",

  40. wr.get(), dLoader);

  41.  
  42. Log.i("demo","classloader:"+dLoader);

  43.  
  44. try{

  45. Object actObj = dLoader.loadClass("com.example.forceapkobj.MainActivity");

  46. Log.i("demo", "actObj:"+actObj);

  47. }catch(Exception e){

  48. Log.i("demo", "activity:"+Log.getStackTraceString(e));

  49. }

  50.  
  51.  
  52. } catch (Exception e) {

  53. Log.i("demo", "error:"+Log.getStackTraceString(e));

  54. e.printStackTrace();

  55. }

  56. }

这里需要注意的一个问题,就是我们需要找到一个时机,就是在脱壳程序还没有运行起来的时候,来加载源程序的Apk,执行他的onCreate方法,那么这个时机不能太晚,不然的话,就是运行脱壳程序,而不是源程序了。查看源码我们知道。Application中有一个方法:attachBaseContext这个方法,他在Application的onCreate方法执行前就会执行了,那么我们的工作就需要在这里进行

 

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

 
  1. //创建两个文件夹payload_odex,payload_lib 私有的,可写的文件目录

  2. File odex = this.getDir("payload_odex", MODE_PRIVATE);

  3. File libs = this.getDir("payload_lib", MODE_PRIVATE);

  4. odexPath = odex.getAbsolutePath();

  5. libPath = libs.getAbsolutePath();

  6. apkFileName = odex.getAbsolutePath() + "/payload.apk";

  7. File dexFile = new File(apkFileName);

  8. Log.i("demo", "apk size:"+dexFile.length());

  9. if (!dexFile.exists())

  10. {

  11. dexFile.createNewFile(); //在payload_odex文件夹内,创建payload.apk

  12. // 读取程序classes.dex文件

  13. byte[] dexdata = this.readDexFileFromApk();

  14.  
  15. // 分离出解壳后的apk文件已用于动态加载

  16. this.splitPayLoadFromDex(dexdata);

  17. }

这个脱壳解密操作一定要和我们之前的加壳以及加密操作对应,不然就会出现Dex加载错误问题

 

A) 从Apk中获取到Dex文件

 

 
  1. /**

  2. * 从apk包里面获取dex文件内容(byte)

  3. * @return

  4. * @throws IOException

  5. */

  6. private byte[] readDexFileFromApk() throws IOException {

  7. ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream();

  8. ZipInputStream localZipInputStream = new ZipInputStream(

  9. new BufferedInputStream(new FileInputStream(

  10. this.getApplicationInfo().sourceDir)));

  11. while (true) {

  12. ZipEntry localZipEntry = localZipInputStream.getNextEntry();

  13. if (localZipEntry == null) {

  14. localZipInputStream.close();

  15. break;

  16. }

  17. if (localZipEntry.getName().equals("classes.dex")) {

  18. byte[] arrayOfByte = new byte[1024];

  19. while (true) {

  20. int i = 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. return dexByteArrayOutputStream.toByteArray();

  30. }

其实就是解压Apk文件,直接得到dex文件即可

 

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

 
  1. /**

  2. * 释放被加壳的apk文件,so文件

  3. * @param data

  4. * @throws IOException

  5. */

  6. private void splitPayLoadFromDex(byte[] apkdata) throws IOException {

  7. int ablen = apkdata.length;

  8. //取被加壳apk的长度 这里的长度取值,对应加壳时长度的赋值都可以做些简化

  9. byte[] dexlen = new byte[4];

  10. System.arraycopy(apkdata, ablen - 4, dexlen, 0, 4);

  11. ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);

  12. DataInputStream in = new DataInputStream(bais);

  13. int readInt = in.readInt();

  14. System.out.println(Integer.toHexString(readInt));

  15. byte[] newdex = new byte[readInt];

  16. //把被加壳apk内容拷贝到newdex中

  17. System.arraycopy(apkdata, ablen - 4 - readInt, newdex, 0, readInt);

  18. //这里应该加上对于apk的解密操作,若加壳是加密处理的话

  19. //?

  20.  
  21. //对源程序Apk进行解密

  22. newdex = decrypt(newdex);

  23.  
  24. //写入apk文件

  25. File file = new File(apkFileName);

  26. try {

  27. FileOutputStream localFileOutputStream = new FileOutputStream(file);

  28. localFileOutputStream.write(newdex);

  29. localFileOutputStream.close();

  30. } catch (IOException localIOException) {

  31. throw new RuntimeException(localIOException);

  32. }

  33.  
  34. //分析被加壳的apk文件

  35. ZipInputStream localZipInputStream = new ZipInputStream(

  36. new BufferedInputStream(new FileInputStream(file)));

  37. while (true) {

  38. ZipEntry localZipEntry = localZipInputStream.getNextEntry();//不了解这个是否也遍历子目录,看样子应该是遍历的

  39. if (localZipEntry == null) {

  40. localZipInputStream.close();

  41. break;

  42. }

  43. //取出被加壳apk用到的so文件,放到 libPath中(data/data/包名/payload_lib)

  44. String name = localZipEntry.getName();

  45. if (name.startsWith("lib/") && name.endsWith(".so")) {

  46. File storeFile = new File(libPath + "/"

  47. + name.substring(name.lastIndexOf('/')));

  48. storeFile.createNewFile();

  49. FileOutputStream fos = new FileOutputStream(storeFile);

  50. byte[] arrayOfByte = new byte[1024];

  51. while (true) {

  52. int i = localZipInputStream.read(arrayOfByte);

  53. if (i == -1)

  54. break;

  55. fos.write(arrayOfByte, 0, i);

  56. }

  57. fos.flush();

  58. fos.close();

  59. }

  60. localZipInputStream.closeEntry();

  61. }

  62. localZipInputStream.close();

  63.  
  64.  
  65. }

 

 

C) 解密源程序Apk

 
  1. ////直接返回数据,读者可以添加自己解密方法

  2. private byte[] decrypt(byte[] srcdata) {

  3. for(int i=0;i

  4. srcdata[i] = (byte)(0xFF ^ srcdata[i]);

  5. }

  6. return srcdata;

  7. }

这个解密算法和加密算法是一致的

 

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

 
  1. //配置动态加载环境

  2. Object currentActivityThread = RefInvoke.invokeStaticMethod(

  3. "android.app.ActivityThread", "currentActivityThread",

  4. new Class[] {}, new Object[] {});//获取主线程对象 http://blog.csdn.net/myarrow/article/details/14223493

  5. String packageName = this.getPackageName();//当前apk的包名

  6. //下面两句不是太理解

  7. ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect(

  8. "android.app.ActivityThread", currentActivityThread,

  9. "mPackages");

  10. WeakReference wr = (WeakReference) mPackages.get(packageName);

  11. //创建被加壳apk的DexClassLoader对象 加载apk内的类和本地代码(c/c++代码)

  12. DexClassLoader dLoader = new DexClassLoader(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.  
  20. Log.i("demo","classloader:"+dLoader);

  21.  
  22. try{

  23. Object actObj = dLoader.loadClass("com.example.forceapkobj.MainActivity");

  24. Log.i("demo", "actObj:"+actObj);

  25. }catch(Exception e){

  26. Log.i("demo", "activity:"+Log.getStackTraceString(e));

  27. }

 

 

2)、找到源程序的Application程序,让其运行

 
  1. @Override

  2. public void onCreate() {

  3. {

  4. //loadResources(apkFileName);

  5.  
  6. Log.i("demo", "onCreate");

  7. // 如果源应用配置有Appliction对象,则替换为源应用Applicaiton,以便不影响源程序逻辑。

  8. String appClassName = null;

  9. try {

  10. ApplicationInfo ai = this.getPackageManager()

  11. .getApplicationInfo(this.getPackageName(),

  12. PackageManager.GET_META_DATA);

  13. Bundle bundle = ai.metaData;

  14. if (bundle != null && bundle.containsKey("APPLICATION_CLASS_NAME")) {

  15. appClassName = bundle.getString("APPLICATION_CLASS_NAME");//className 是配置在xml文件中的。

  16. } else {

  17. Log.i("demo", "have no application class name");

  18. return;

  19. }

  20. } catch (NameNotFoundException e) {

  21. Log.i("demo", "error:"+Log.getStackTraceString(e));

  22. e.printStackTrace();

  23. }

  24. //有值的话调用该Applicaiton

  25. Object currentActivityThread = RefInvoke.invokeStaticMethod(

  26. "android.app.ActivityThread", "currentActivityThread",

  27. new Class[] {}, new Object[] {});

  28. Object mBoundApplication = RefInvoke.getFieldOjbect(

  29. "android.app.ActivityThread", currentActivityThread,

  30. "mBoundApplication");

  31. Object loadedApkInfo = RefInvoke.getFieldOjbect(

  32. "android.app.ActivityThread$AppBindData",

  33. mBoundApplication, "info");

  34. //把当前进程的mApplication 设置成了null

  35. RefInvoke.setFieldOjbect("android.app.LoadedApk", "mApplication",

  36. loadedApkInfo, null);

  37. Object oldApplication = RefInvoke.getFieldOjbect(

  38. "android.app.ActivityThread", currentActivityThread,

  39. "mInitialApplication");

  40. //http://www.codeceo.com/article/android-context.html

  41. ArrayList mAllApplications = (ArrayList) RefInvoke

  42. .getFieldOjbect("android.app.ActivityThread",

  43. currentActivityThread, "mAllApplications");

  44. mAllApplications.remove(oldApplication);//删除oldApplication

  45.  
  46. ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo) RefInvoke

  47. .getFieldOjbect("android.app.LoadedApk", loadedApkInfo,

  48. "mApplicationInfo");

  49. ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo) RefInvoke

  50. .getFieldOjbect("android.app.ActivityThread$AppBindData",

  51. mBoundApplication, "appInfo");

  52. appinfo_In_LoadedApk.className = appClassName;

  53. appinfo_In_AppBindData.className = appClassName;

  54. Application app = (Application) RefInvoke.invokeMethod(

  55. "android.app.LoadedApk", "makeApplication", loadedApkInfo,

  56. new Class[] { boolean.class, Instrumentation.class },

  57. new Object[] { false, null });//执行 makeApplication(false,null)

  58. RefInvoke.setFieldOjbect("android.app.ActivityThread",

  59. "mInitialApplication", currentActivityThread, app);

  60.  
  61.  
  62. ArrayMap mProviderMap = (ArrayMap) RefInvoke.getFieldOjbect(

  63. "android.app.ActivityThread", currentActivityThread,

  64. "mProviderMap");

  65. Iterator it = mProviderMap.values().iterator();

  66. while (it.hasNext()) {

  67. Object providerClientRecord = it.next();

  68. Object localProvider = RefInvoke.getFieldOjbect(

  69. "android.app.ActivityThread$ProviderClientRecord",

  70. providerClientRecord, "mLocalProvider");

  71. RefInvoke.setFieldOjbect("android.content.ContentProvider",

  72. "mContext", localProvider, app);

  73. }

  74.  
  75. Log.i("demo", "app:"+app);

  76.  
  77. app.onCreate();

  78. }

  79. }

直接在脱壳的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加固也是应运而生,但是即使是这样,我们也还是做不到那么的安全,现在网上也是有很多文章在解析梆梆加固的原理了。而且有人破解成功了,那么加固还不是怎么安全。最后一句话:逆向和加固是一个永不停息的战争。

--------------------- 作者:尼古拉斯_赵四 来源:CSDN 原文:https://blog.csdn.net/jiangwei0910410003/article/details/48415225?utm_source=copy 版权声明:本文为博主原创文章,转载请附上博文链接!

更多相关文章

  1. android程序自动化生成apk的过程
  2. 仿微信、短信、QQ等消息数目右上角红色小圆球气泡显示(基于Androi
  3. Android(安卓)eclipse 项目R文件无法生成
  4. python: android批量多渠道打包
  5. Android通过URI获取文件路径
  6. android ndk生成第三方库的so方法(ndk-build,Application.mk,Andro
  7. recovery 的原理已经 rom制作的知识
  8. Android(安卓)Developers:ProGuard
  9. Android学习记录(4)—在java中学习多线程下载的基本原理和基本用法

随机推荐

  1. List集合封装获取参数
  2. CSDN的编辑技巧
  3. Map集合封装获取参数
  4. 如何快速删除卸载残余-注册表
  5. PDF Password Remover 软件及其密钥
  6. 日志收集系统elk
  7. 系统启动U盘制作全过程详解
  8. rsync实时备份备份服务搭建和使用指南
  9. rsync 备份服务搭建(完成)
  10. 磁盘未被格式化救援方法