原理:
首先有下面三个工程
1、源程序项目(需要加密的Apk)
2、脱壳项目(解密源Apk和加载Apk)
3、对源Apk进行加密和脱壳项目的Dex的合并
以byte数组格式读取源apk,并按自己设计的算法进行加密,然后直接拼接到脱壳apk的dex文件后面,然后按照标准dex文件的格式对拼接后的dex进行修改文件头中的文件大小位、SHA-1校验位、sum校验位,
- checksum:文件校验码 ,使用alder32 算法校验文件除去 maigc ,checksum 外余下的所有文件区域 ,用于检查文件错误 。
- signature:使用 SHA-1 算法 hash 除去 magic ,checksum 和 signature 外余下的所有文件区域 ,用于唯一识别本文件 。
- file_size:Dex 文件的大小
然后在脱壳apk中重写Application方法,在方法运行之初去读取dex文件按照指定拼接位置读取出加密apk文件,然后解密出源apk文件,再通过动态加载技术将当前apk的dexClassLoader替换为源apk的dexClassLoader,
来看下具体代码:
一,源apk(随便写一个应用)
应用只有一个MainActivity和MyApplication
AndroidManifest.xml,这里没什么需要注意的,正常来写就行了
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.smm.forceapkobj">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:name="com.smm.forceapkobj.MyApplication">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
MainActivity.java,也没啥需要注意的,需要的话打个Log就行了
package com.smm.forceapkobj;
import android.support.v7.app.AppCompatActivity;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.i("测试记录","进入到了源程序MainActivityOnCreate");
super.onCreate(savedInstanceState);
TextView content = new TextView(this);
content.setText("I am Source Apk");
content.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View arg0){
Toast myToast = Toast.makeText(getApplicationContext(),"this is MainActivity!!!",Toast.LENGTH_SHORT);
myToast.show();
}
});
setContentView(content);
}
}
MyApplication.java
package com.smm.forceapkobj;
import android.app.Application;
import android.util.Log;
public class MyApplication extends Application{
@Override
public void onCreate(){
Log.i("测试记录","进入到了源程序Application");
super.onCreate();
Log.i("测试记录","source apk onCreate:" + this);
}
}
二,编写脱壳程序
结构也很简单,主要这里不要有其他组件,如果有MainActivity的话执行完了源程序的Application之后就会执行这里的MainActivity。
AndroidManifest.xml,这里除了要注册ProxyApplication之外还要注册源程序中的组件,另外还要用<meta-data>标签获取源程序中的Application对象。
标签如下:
<meta-data android:name="APPLICATION_CLASS_NAME" android:value="com.smm.forceapkobj.MyApplication"/>
,后面会在ProxyApplication的onCreate代码中看到具体获取方式。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.smm.reforceapk">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:name="com.smm.reforceapk.ProxyApplication">
<meta-data android:name="APPLICATION_CLASS_NAME" android:value="com.smm.forceapkobj.MyApplication"/>
<activity android:name="com.smm.forceapkobj.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
ProxyApplication.java
package com.smm.reforceapk;
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.os.Bundle;
import android.os.Debug;
import android.util.ArrayMap;
import android.util.Log;
import dalvik.system.DexClassLoader;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class ProxyApplication extends Application{
private static final String appkey = "APPLICATION_CLASS_NAME";
private String apkFileName;
private String odexPath;
private String libPath;
@Override
protected void attachBaseContext(Context base){
//Debug.waitForDebugger();
super.attachBaseContext(base);
try{
//从dex中读取apk并解密,还原出源apk,payload.apk
File odex = this.getDir("payload_odex",MODE_PRIVATE);
File libs = this.getDir("payload_lib",MODE_PRIVATE);
odexPath = odex.getAbsolutePath();
libPath = libs.getAbsolutePath();
apkFileName = odex.getAbsolutePath() + "/payload.apk";
File dexFile = new File(apkFileName);
Log.i("demo","apk size:" + dexFile.length());
if(!dexFile.exists()) {
dexFile.createNewFile();
byte[] dexdata = this.readDexFileFromApk();
this.splitPayLoadFromDex(dexdata);
}
//配置动态加载环境
//获取主线程对象ActivityTread
Object currentActivityTread = RefInvoke.invokeStaticMethod("android.app.ActivityThread","currentActivityThread",new Class[]{},new Object[]{});
String packageName = this.getPackageName();
//当前app的包名传递给mPachages来获得当前app的弱引用,从而找到当前app的类加载器mClassLoader
ArrayMap mPackages = (ArrayMap)RefInvoke.getFieldObject("android.app.ActivityThread",currentActivityTread,"mPackages");
WeakReference wr = (WeakReference)mPackages.get(packageName);
//创建被加壳apk的DexClassLoader对象,加载apk内的类和native代码
DexClassLoader dLoader = new DexClassLoader(apkFileName,odexPath,libPath,
(ClassLoader)RefInvoke.getFieldObject("android.app.LoadedApk",wr.get(),"mClassLoader"));
//把当前进程的DexClassLoader设置成被加壳apk的DexClassLoader
RefInvoke.setFieldObject("android.app.LoadedApk","mClassLoader",wr.get(),dLoader);
Log.i("demo","classloader:" + dLoader);
}catch (Exception e){
Log.i("demo","error:" + Log.getStackTraceString(e));
e.printStackTrace();
}
}
@Override
public void onCreate(){
String appClassName = null;
try{
//获取am文件中的meta标签,通过检测是否包含```APPLICATION_CLASS_NAME```字符来判断是否获取源apk的Application对象
ApplicationInfo ai = this.getPackageManager().getApplicationInfo(this.getPackageName(),PackageManager.GET_META_DATA);
Bundle bundle = ai.metaData;
if(bundle != null && bundle.containsKey("APPLICATION_CLASS_NAME")){
appClassName = bundle.getString("APPLICATION_CLASS_NAME");
}else{
Log.i("demo","have no application class name");
return;
}
}catch(NameNotFoundException e){
Log.i("demo","error:" + Log.getStackTraceString(e));
e.printStackTrace();
}
//调用该Application
Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread","currentActivityThread",
new Class[]{},new Object[]{});
Object mBoundApplication = RefInvoke.getFieldObject("android.app.ActivityThread",currentActivityThread,"mBoundApplication");
Object loadedApkInfo = RefInvoke.getFieldObject("android.app.ActivityThread$AppBindData",mBoundApplication,"info");
//把当前进程的Application设置为null
RefInvoke.setFieldObject("android.app.LoadedApk","mApplication",loadedApkInfo,null);
Object oldApplication = RefInvoke.getFieldObject("android.app.ActivityThread",currentActivityThread,"mInitialApplication");
ArrayList<Application> mAllApplications = (ArrayList<Application>)RefInvoke.getFieldObject("android.app.ActivityThread",
currentActivityThread,"mAllApplications");
mAllApplications.remove(oldApplication);
ApplicationInfo appinfo_In_LoadedApk = (ApplicationInfo)RefInvoke.getFieldObject("android.app.LoadedApk",loadedApkInfo,"mApplicationInfo");
ApplicationInfo appinfo_In_AppBindData = (ApplicationInfo)RefInvoke.getFieldObject("android.app.ActivityThread$AppBindData",
mBoundApplication,"appInfo");
//将源apk的application对象名赋给LoadedApk
appinfo_In_LoadedApk.className = appClassName;
appinfo_In_AppBindData.className = appClassName;
Application app = (Application)RefInvoke.invokeMethod("android.app.LoadedApk","makeApplication",loadedApkInfo,
new Class[]{boolean.class,Instrumentation.class},
new Object[]{false,null});//执行makeApplication(false,null)
RefInvoke.setFieldObject("android.app.ActivityThread","mInitialApplication",currentActivityThread,app);
ArrayMap mProviderMap = (ArrayMap)RefInvoke.getFieldObject("android.app.ActivityThread",currentActivityThread,"mProviderMap");
Iterator it = mProviderMap.values().iterator();
while(it.hasNext()){
Object providerClientRecord = it.next();
Object localProvider = RefInvoke.getFieldObject("android.app.ActivityThread$ProviderClientRecord",
providerClientRecord,"mLocalProvider");
RefInvoke.setFieldObject("android.content.ContentProvider","mContext",localProvider,app);
}
Log.i("demo","app:" + app);
app.onCreate();
}
private byte[] readDexFileFromApk() throws IOException{
ByteArrayOutputStream dexByteArrayOutputStream = new ByteArrayOutputStream();
ZipInputStream localZipInputStream = new ZipInputStream(
new BufferedInputStream(new FileInputStream(
this.getApplicationInfo().sourceDir
))
);
while(true){
ZipEntry localZipEntry = localZipInputStream.getNextEntry();
if(localZipEntry == null){
localZipInputStream.close();
break;
}
if(localZipEntry.getName().equals("classes.dex")){
byte[] arrayOfByte = new byte[1024];
while(true){
int i = localZipInputStream.read(arrayOfByte);
if(i == -1)
break;
dexByteArrayOutputStream.write(arrayOfByte,0,i);
}
}
localZipInputStream.closeEntry();
}
localZipInputStream.close();
return dexByteArrayOutputStream.toByteArray();
}
private void splitPayLoadFromDex(byte[] apkdata)throws IOException{
int ablen = apkdata.length;
byte[] dexlen = new byte[4];
System.arraycopy(apkdata,ablen - 4, dexlen,0,4);
ByteArrayInputStream bais = new ByteArrayInputStream(dexlen);
DataInputStream in = new DataInputStream(bais);
int readInt = in.readInt();
System.out.println(Integer.toHexString(readInt));
byte[] newdex = new byte[readInt];
System.arraycopy(apkdata,ablen - 4 - readInt,newdex,0,readInt);
newdex = decrypt(newdex);
//写入apk文件
File file = new File(apkFileName);
try{
FileOutputStream localFileOutputStream = new FileOutputStream(file);
localFileOutputStream.write(newdex);
localFileOutputStream.close();
}catch(IOException localIOException){
throw new RuntimeException(localIOException);
}
//分析被加壳的apk文件
ZipInputStream localZipInputStream = new ZipInputStream(
new BufferedInputStream(new FileInputStream(file))
);
while(true){
ZipEntry localZipEntry = localZipInputStream.getNextEntry();
if(localZipEntry == null){
localZipInputStream.close();
break;
}
String name = localZipEntry.getName();
if(name.startsWith("lib/") && name.endsWith(".so")){
File storeFile = new File(libPath + "/" + name.substring(name.lastIndexOf("/")));
storeFile.createNewFile();
FileOutputStream fos = new FileOutputStream(storeFile);
byte[] arrayOfByte = new byte[1024];
while(true){
int i = localZipInputStream.read(arrayOfByte);
if(i == -1)
break;
fos.write(arrayOfByte,0,i);
}
fos.flush();
fos.close();
}
localZipInputStream.closeEntry();
}
localZipInputStream.close();
}
private byte[] decrypt(byte[] srcdata){
for(int i = 0;i < srcdata.length;i++){
srcdata[i] = (byte)(0xFF ^srcdata[i]);
}
return srcdata;
}
}
RefInvoke.java,反射类,主要通过java.lang.reflect.Method
的invoke
方法定义了几类调用和赋值方放。
package com.smm.reforceapk;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class RefInvoke {
public static Object invokeStaticMethod(String class_name,String method_name,Class[] pareTyple,Object[] pareVaules){
try{
Class obj_class = Class.forName(class_name);
Method method = obj_class.getMethod(method_name,pareTyple);
return method.invoke(null,pareVaules);
}catch(SecurityException e){
e.printStackTrace();
}catch(IllegalArgumentException e){
e.printStackTrace();
}catch(IllegalAccessException e){
e.printStackTrace();
}catch(NoSuchMethodException e){
e.printStackTrace();
}catch(InvocationTargetException e){
e.printStackTrace();
}catch(ClassNotFoundException e){
e.printStackTrace();
}
return null;
}
public static Object invokeMethod(String class_name,String method_name,Object obj,Class[] pareTyple,Object[] pareVaules){
try{
Class obj_class = Class.forName(class_name);
Method method = obj_class.getMethod(method_name,pareTyple);
return method.invoke(obj,pareVaules);
}catch(SecurityException e){
e.printStackTrace();
}catch(IllegalArgumentException e){
e.printStackTrace();
}catch(IllegalAccessException e){
e.printStackTrace();
}catch(NoSuchMethodException e){
e.printStackTrace();
}catch(InvocationTargetException e){
e.printStackTrace();
}catch(ClassNotFoundException e){
e.printStackTrace();
}
return null;
}
public static Object getFieldObject(String class_name,Object obj,String fieldName){
try{
Class obj_class = Class.forName(class_name);
Field field = obj_class.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
}catch(SecurityException e){
e.printStackTrace();
}catch(IllegalArgumentException e){
e.printStackTrace();
}catch(NoSuchFieldException e){
e.printStackTrace();
}catch(IllegalAccessException e){
e.printStackTrace();
}catch(ClassNotFoundException e){
e.printStackTrace();
}
return null;
}
public static Object getStaticFileObject(String class_name,String fieldName){
try{
Class obj_class = Class.forName(class_name);
Field field = obj_class.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(null);
}catch(SecurityException e){
e.printStackTrace();
}catch(NoSuchFieldException e){
e.printStackTrace();
}catch(IllegalArgumentException e){
e.printStackTrace();
}catch(IllegalAccessException e){
e.printStackTrace();
}catch(ClassNotFoundException e){
e.printStackTrace();
}
return null;
}
public static void setFieldObject(String classname,String fieldName,Object obj,Object fieldVaule){
try{
Class obj_class = Class.forName(classname);
Field field = obj_class.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj,fieldVaule);
}catch(SecurityException e){
e.printStackTrace();
}catch(NoSuchFieldException e){
e.printStackTrace();
}catch(IllegalArgumentException e){
e.printStackTrace();
}catch(IllegalAccessException e){
e.printStackTrace();
}catch(ClassNotFoundException e){
e.printStackTrace();
}
}
public static void setStaticObject(String class_name,String fieldName,Object fieldVaule){
try{
Class obj_class = Class.forName(class_name);
Field field = obj_class.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(null,fieldVaule);
}catch(SecurityException e){
e.printStackTrace();
}catch(NoSuchFieldException e){
e.printStackTrace();
}catch(IllegalArgumentException e){
e.printStackTrace();
}catch(IllegalAccessException e){
e.printStackTrace();
}catch(ClassNotFoundException e){
e.printStackTrace();
}
}
}
然后通过buildAPK编译应用,注意此时编译出的apk是不能运行的。
三,加壳程序
加壳程序是个java工程,原理很简单如下:
package shellApk;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.zip.Adler32;
public class DexShellTool {
public static void main(String[] args) {
try {
File payloadSrcFile = new File("D:\\shellProject\\p1\\force.apk");
System.out.println("apk size:" + payloadSrcFile.length());
File unShellDexFile = new File("D:\\shellProject\\p1\\reforce.dex");
byte[] payloadArray = encrypt(readFileBytes(payloadSrcFile));
byte[] unShellDexArray = readFileBytes(unShellDexFile);
int payloadLen = payloadArray.length;
System.out.println("payloadLen:" + payloadLen);
int unShellDexLen = unShellDexArray.length;
System.out.println("unShellLen:" + unShellDexLen);
int totalLen = payloadLen + unShellDexLen + 4;
byte[] newdex = new byte[totalLen];
System.arraycopy(unShellDexArray,0,newdex,0,unShellDexLen);
System.arraycopy(payloadArray, 0, newdex, unShellDexLen, payloadLen);
System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen - 4, 4);
fixFileSizeHeader(newdex);
fixFileSHA1Header(newdex);
fixCheckSumHeader(newdex);
String str = "D:\\shellProject\\p1\\classes.dex";
File file = new File(str);
if(!file.exists()) {
file.createNewFile();
}
FileOutputStream localFileOutputStream = new FileOutputStream(str);
localFileOutputStream.write(newdex);
localFileOutputStream.flush();
localFileOutputStream.close();
}catch(Exception e) {
e.printStackTrace();
}
}
private static void fixFileSizeHeader(byte[] dexBytes) {
byte[] newfs = intToByte(dexBytes.length);
System.out.println(Integer.toHexString(dexBytes.length));
byte[] refs = new byte[4];
for(int i = 0;i < 4;i++) {
refs[i] = newfs[newfs.length - 1 - i];
System.out.println(Integer.toHexString(newfs[i]));
}
System.arraycopy(refs, 0, dexBytes, 32, 4);
}
private static void fixFileSHA1Header(byte[] dexBytes)throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(dexBytes, 32, dexBytes.length - 32);
byte[] newdt = md.digest();
System.arraycopy(newdt, 0, dexBytes, 12, 20);
String hexstr = "";
for(int i = 0;i < newdt.length;i++) {
hexstr += Integer.toString((newdt[i] & 0xff) + 0x100,16).substring(1);
}
System.out.println(hexstr);
}
private static void fixCheckSumHeader(byte[] dexBytes) {
Adler32 adler = new Adler32();
adler.update(dexBytes,12,dexBytes.length - 12);
long value = adler.getValue();
int va = (int)value;
byte[] newcs = intToByte(va);
System.out.println("newcs Len:" + newcs.length);
byte[] recs = new byte[4];
for(int i = 0;i < 4;i++) {
recs[i] = newcs[newcs.length - 1 - i];
System.out.println(Integer.toHexString(newcs[i]));
}
System.arraycopy(recs,0,dexBytes,8,4);
System.out.println(Long.toHexString(value));
System.out.println();
}
public static byte[] intToByte(int number) {
byte[] b = new byte[4];
for(int i = 3; i >= 0;i--) {
b[i] = (byte)(number % 256);
number >>= 8;
}
return b;
}
private static byte[] encrypt(byte[] srcdata) {
for(int i = 0;i < srcdata.length;i++) {
srcdata[i] = (byte)(0xFF ^ srcdata[i]);
}
return srcdata;
}
private static byte[] readFileBytes(File file)throws IOException{
byte[] arrayOfByte = new byte[1024];
ByteArrayOutputStream localByteArrayOutputStream = new ByteArrayOutputStream();
FileInputStream fis = new FileInputStream(file);
while(true) {
int i = fis.read(arrayOfByte);
if(i != -1) {
localByteArrayOutputStream.write(arrayOfByte,0,i);
}else {
return localByteArrayOutputStream.toByteArray();
}
}
}
}
其中的force.apk是源apk,reforce.dex是脱壳apk的dex文件,运行后生成一个新的classes.dex,用新得到的dex替换掉reforce.apk的dex文件,然后重签名即可。
最后运行界面可见就是源apk的运行界面;
最后运行界面
Tips:
过程中遇到的几个坑:
1,7-zip对reforce.apk解压失败,提示文件格式错误,改用winRAR后正常,原因暂时未知。
2,应用按流程从reforce.apk中的ProxyApplication中运行到源apk中的MyApplication中后又返回到的reforce.apk中的MainActivity,原因是脱壳程序不能有MainActivity,删除后恢复。
3,自签名和用Androidkiller签名的apk往4.4的手机上安装时出现错误Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES]
、Failure [INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION]
,原因是重打包时要删掉原来的cert文件,删掉后再用自签名工具签名即可。
参考https://github.com/l123456789jy/Lazy/blob/master/lazylibrary/src/main/java/com/github/lazylibrary/util/AntiEmulatorUtiles.java
https://blog.csdn.net/androidsecurity/article/details/8809542
http://www.520monkey.com/archives/553
网友评论