0x04 运行壳程序加载原dex文件
通过上面3个步骤,我们了解了dex文件整体加固的流程。那么,当这个加固后的dex文件加载到Dalvik虚拟机中,它又是如何工作的呢?下面我们就来讲解下壳程序在这里面发挥的重要作用。
首先,Dalvik虚拟机会加载我们经过修改的新的classes.dex文件,并最先运行ProxyApplication类。在这个类里面,有2个关键的方法:attachBaseContext和onCreate方法。ProxyApplication显示运行attachBaseContext再运行onCreate方法。
在attachBaseContext方法里,主要做两个工作:
-
读取classes.dex文件末尾记录加密dex文件大小的数值,则加密dex文件在新classes.dex文件中的位置为:len(新classes.dex文件) – len(加密dex文件大小)。然后将加密的dex文件读取出来,加密并保存到资源目录下
-
然后使用自定义的DexClassLoader加载解密后的原dex文件
在onCreate方法中,主要做两个工作:
-
通过反射修改ActivityThread类,并将Application指向原dex文件中的Application
-
创建原Application对象,并调用原Application的onCreate方法启动原程序
具体的app启动过程可以回看第2章的内容。
====================================================================================
源程序我们使用NDK开发一个简单的demo,并在assets目录存放一个txt文件读取。这样,我们就可以得到跟10.1.1一样的文件结构,也可以检测下加固后的文件是否影响lib文件的和资源目录下文件的操作。
工程结构如下:
MyApplication.java关键代码:
-
public class MyApplication extends Application{
-
@Override
-
public void onCreate() {
-
// TODO Auto-generated method stub
-
super.onCreate();
-
}
-
@Override
-
protected void attachBaseContext(Context base) {
-
// TODO Auto-generated method stub
-
super.attachBaseContext(base);
-
}
-
}
MainActivity.java关键代码:
-
public class MainActivity extends Activity
-
{
-
/** Called when the activity is first created. */
-
public native void test();
-
@Override
-
public void onCreate(Bundle savedInstanceState)
-
{
-
super.onCreate(savedInstanceState);
-
setContentView(R.layout.main);
-
System.loadLibrary(“demo1”);
-
test();
-
Log.v(“demo”, getFromAssets(“test.txt”));
-
}
-
public String getFromAssets(String fileName){
-
String Result=“”;
-
try {
-
InputStreamReader inputReader = new InputStreamReader( getResources().getAssets().open(fileName) );
-
BufferedReader bufReader = new BufferedReader(inputReader);
-
String line=“”;
-
while((line = bufReader.readLine()) != null)
-
Result += line;
-
} catch (Exception e) {
-
e.printStackTrace();
-
}
-
return Result;
-
}
-
}
demo1.cpp关键代码:
-
#include “com_demo_MainActivity.h”
-
#include
加密程序是个java工程。
Main.java源码:
-
public static void main(String[] args) throws Exception {
-
if(args.length != 2){
-
System.out.println(“Error! Please input 2 parameters!”);
-
return ;
-
}
-
//参数1:脱壳classes.dex
-
//参数2:TargetApk.zip
-
File tuokeDexFile = new File(args[0]);
-
File targetFile = new File(args[1]);
-
byte[] tuokeDexFileArray = readFileBytes(tuokeDexFile);
-
byte[] targetFileArray = encrypt(readFileBytes(targetFile));
-
int tuokeDexFileLen = tuokeDexFileArray.length;
-
int targetFileLen = targetFileArray.length;
-
int totalLen = targetFileLen + tuokeDexFileLen + 4;
-
byte[] newdex = new byte[totalLen];
-
//添加脱壳classes.dex文件
-
System.arraycopy(tuokeDexFileArray, 0, newdex, 0, tuokeDexFileLen);
-
//添加目标TargetApk.zip文件
-
System.arraycopy(targetFileArray, 0, newdex, tuokeDexFileLen, targetFileLen);
-
//添加目标zip文件大小
-
System.arraycopy(intToByte(targetFileLen), 0, newdex, tuokeDexFileLen + targetFileLen, 4);
-
//修改Dex file size文件头
-
fixFileSizeHeader(newdex);
-
//修改Dex SHA1文件头
-
fixSHA1Header(newdex);
-
//修改Dex CheckSum文件头
-
fixCheckSumHeader(newdex);
-
File file = new File(“classes.dex”);
-
if(!file.exists()){
-
file.createNewFile();
-
}
-
FileOutputStream out = new FileOutputStream(file);
-
out.write(newdex);
-
out.flush();
-
out.close();
-
System.out.println(“Enforce apk successfully!”);
-
}
修改dex头的fileSize字段
-
private static void fixSHA1Header(byte[] dexBytes)
-
throws NoSuchAlgorithmException {
-
MessageDigest md = MessageDigest.getInstance(“SHA-1”);
-
md.update(dexBytes, 32, dexBytes.length - 32);//从32为到结束计算sha–1
-
byte[] newdt = md.digest();
-
System.arraycopy(newdt, 0, dexBytes, 12, 20);//修改sha-1值(12-31)
-
//输出sha-1值,可有可无
-
String hexstr = “”;
-
for (int i = 0; i < newdt.length; i++) {
-
hexstr += Integer.toString((newdt[i] & 0xff) + 0x100, 16).substring(1);
-
}
-
}
修改dex头的checkSum字段
修改dex头的signature字段
-
private static void fixCheckSumHeader(byte[] dexBytes) {
-
Adler32 adler = new Adler32();
-
long value = adler.getValue();
-
int va = (int) value;
-
byte[] newcs = intToByte(va);
-
//高位在前,低位在前掉个个
-
byte[] recs = new byte[4];
-
for (int i = 0; i < 4; i++) {
-
recs[i] = newcs[newcs.length - 1 - i];
-
}
-
System.arraycopy(recs, 0, dexBytes, 8, 4);//效验码赋值(8-11)
-
}
加密:
-
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();
-
}
-
}
-
}
int转byte数组
-
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;
-
}
壳程序我们主要分析ProxyApplication类:
0x01 attachBaseContext方法
首先分析attachBaseContext方法:
-
protected void attachBaseContext(Context base) {
-
super.attachBaseContext(base);
-
Log.v(“demo”, “[JiaguApk]=>attachBaseContext() start…”);
-
try {
-
File odex = this.getDir(“assets”, MODE_PRIVATE);
-
String odexPath = odex.getAbsolutePath();
-
St
ring targetFilename = odexPath + “/TargetApk.zip”; -
File targetApkZipFile = new File(targetFilename);
-
if(!targetApkZipFile.exists()){
-
//从classes.dex中提取TargetApk.zip
-
targetApkZipFile.createNewFile();
-
byte[] classesDexData = readClassesDexFromApk();
-
extractTargetZipFileFromDex(classesDexData, targetFilename);
-
}
-
Object currentActivityThread = RefInvoke.invokeStaticMethod(“android.app.ActivityThread”, “currentActivityThread”, new Class[] {}, new Object[] {});
-
String packageName = getPackageName();
-
Map mPackages = (Map) RefInvoke.getFieldOjbect(“android.app.ActivityThread”, currentActivityThread, “mPackages”);
-
WeakReference wr = (WeakReference) mPackages.get(packageName);
-
DexClassLoader dLoader = new DexClassLoader(odexPath + “/TargetApk.zip”, odexPath, “/data/data/” + packageName + “/lib”, base.getClassLoader().getParent());
-
//替換成TargetApk.dex的ClassLoader
-
RefInvoke.setFieldOjbect(“android.app.LoadedApk”, “mClassLoader”, wr.get(), dLoader);
-
} catch (Exception e) {
-
Log.v(“demo”, "[JiaguApk]=>attachBaseContext() " + Log.getStackTraceString(e));
-
}
-
Log.v(“demo”, “[JiaguApk]=>attachBaseContext() end…”);
-
}
在该方法中,我们首先获取加密dex文件,并保存到assets资源目录下,然后进行解密。
由于运行加固后的apk文件时,应用程序使用的是加固后dex文件的类加载器,而不是原dex文件的类加载器。所以,我们需要事先将类加载器替换成员dex文件的类加载器。
这里,我们通过反射获取ActivityThread,通过其mPackages成员获取LoadedApk对象。通过第二章的app启动过程分析,我们知道类加载器的创建是在LoadedApk中完成,这也是为什么我们需要获取LoadedApk对象。通过该对象的mClassLoader成员,我们可以修改该成员指向我们自定义的DexClassLoader,这个类加载器就是原dex中的类加载器。这样我们就可以在后面的步骤中用该类加载器加载原dex文件。
0x02 onCreate方法
-
public void onCreate() {
-
try {
-
// 如果源应用配置有Appliction对象,则替换为源应用Applicaiton,以便不影响源程序逻辑。
-
String appClassName = “com.demo.MyApplication”;
-
/**
-
- 调用静态方法android.app.ActivityThread.currentActivityThread
-
- 获取当前activity所在的线程对象
-
*/
-
Object currentActivityThread = RefInvoke.invokeStaticMethod(
-
“android.app.ActivityThread”, “currentActivityThread”,
-
new Class[] {}, new Object[] {});
-
/**
-
- 获取currentActivityThread中的mBoundApplication属性对象,该对象是一个
-
- AppBindData类对象,该类是ActivityThread的一个内部类
-
*/
-
Object mBoundApplication = RefInvoke.getFieldOjbect(
-
“android.app.ActivityThread”, currentActivityThread,
-
“mBoundApplication”);
-
/**
-
- 获取mBoundApplication中的info属性,info 是 LoadedApk类对象
-
*/
-
Object loadedApkInfo = RefInvoke.getFieldOjbect(
-
“android.app.ActivityThread$AppBindData”,
-
mBoundApplication, “info”);
-
if(null == loadedApkInfo){
-
Log.v(“demo”, “[JiaguApk]=>onCreate()=>loadedApkInfo is null!!!”);
-
}else{
-
Log.v(“demo”, “[JiaguApk]=>onCreate()=>loadedApkInfo:” + loadedApkInfo);
-
}
-
/**
-
- loadedApkInfo对象的mApplication属性置为null
-
*/
-
RefInvoke.setFieldOjbect(“android.app.LoadedApk”, “mApplication”,
-
loadedApkInfo, null);
-
/**
-
- 获取currentActivityThread对象中的mInitialApplication属性
建议
当我们出去找工作,或者准备找工作的时候,我们一定要想,我面试的目标是什么,我自己的技术栈有哪些,近期能掌握的有哪些,我的哪些短板 ,列出来,有计划的去完成,别看前两天掘金一些大佬在驳来驳去 ,他们的观点是他们的,不要因为他们的观点,膨胀了自己,影响自己的学习节奏。基础很大程度决定你自己技术层次的厚度,你再熟练框架也好,也会比你便宜的,性价比高的替代,很现实的问题但也要有危机意识,当我们年级大了,有哪些亮点,与比我们经历更旺盛的年轻小工程师,竞争。
-
无论你现在水平怎么样一定要 持续学习 没有鸡汤,别人看起来的毫不费力,其实费了很大力,这四个字就是我的建议!!!!!!!!!
-
准备想说怎么样写简历,想象算了,我觉得,技术就是你最好的简历
-
我希望每一个努力生活的it工程师,都会得到自己想要的,因为我们很辛苦,我们应得的。
-
有什么问题想交流,欢迎给我私信,欢迎评论
【附】相关架构及资料
内含往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术
/**
-
- loadedApkInfo对象的mApplication属性置为null
-
*/
-
RefInvoke.setFieldOjbect(“android.app.LoadedApk”, “mApplication”,
-
loadedApkInfo, null);
-
/**
-
- 获取currentActivityThread对象中的mInitialApplication属性
建议
当我们出去找工作,或者准备找工作的时候,我们一定要想,我面试的目标是什么,我自己的技术栈有哪些,近期能掌握的有哪些,我的哪些短板 ,列出来,有计划的去完成,别看前两天掘金一些大佬在驳来驳去 ,他们的观点是他们的,不要因为他们的观点,膨胀了自己,影响自己的学习节奏。基础很大程度决定你自己技术层次的厚度,你再熟练框架也好,也会比你便宜的,性价比高的替代,很现实的问题但也要有危机意识,当我们年级大了,有哪些亮点,与比我们经历更旺盛的年轻小工程师,竞争。
-
无论你现在水平怎么样一定要 持续学习 没有鸡汤,别人看起来的毫不费力,其实费了很大力,这四个字就是我的建议!!!!!!!!!
-
准备想说怎么样写简历,想象算了,我觉得,技术就是你最好的简历
-
我希望每一个努力生活的it工程师,都会得到自己想要的,因为我们很辛苦,我们应得的。
-
有什么问题想交流,欢迎给我私信,欢迎评论
【附】相关架构及资料
[外链图片转存中…(img-aJQ0TFua-1716043224309)]
[外链图片转存中…(img-OeDr5jO6-1716043224309)]
内含往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术