前介
讲解前我想先问大家一个问题。 jar
与 dex
文件他们俩到底是啥关系呢?
jar
是给JVM
运行的字节码文件包。dex
是Dalvik&ART
运行的字节码文件包。
其实 jar
与 dex
根本上就是2个东西。只是 Andorid
的第一个开发语言是 Java
而 Java
的字节码文件是 class
(对应合集 Jar
包)。
因此 Google
爸爸,对 Java
生成的 Jar
包做了一次优化(好像是法律原因),变成了 Dalvik&ART
认识的字节码文件 Dex
。
其实有时候我想红色框框的过程其实没啥用,如果
Android
第一开发语言不是Java
,这个过程会不会就不存在了呢?或者新的编译器可以直接将 N个Java
文件,直接编译成一个dex
文件。
插装
前几年非常火的一项技术 面向切面编程
,在 Apk
编译过程中,得到 class
字节码,然后通过 ASM 库对 class
字节码进行修改 。
前面说了 Andorid
的字节码产物,除了 class
(jar) 还有 dex
,哪我们能不能通过修改 dex
完成插装过程呢?
Dex 结构
前面说了 Dex
是通过 Jar
优化过来的。相对于 Jar
里面是多个 class
单独文件(Zip包)。但是优化后的 Dex
并没有多个文件一说,他按照一种定义的结构,通过长度&偏移量计算出每个类或方法数据。(下图为Dex的文件结构)
举个简单的例子,我们例如 Dex
文件中有个 A 类,我们能在 class_defs
中找到这个类的数据总长度与在 data
去的数据偏移量,然后就能从 data
中读取出这个类的信息。
详细细节可以看 blog.csdn.net/sinat_18268… 这篇文章。
修改Dex
我们知道ASM是修改 class
字节码的库,那有没有修改 dex
的库呢?通过查看 Apktool 源码,我们发现了另外一个强大的库 smali 它提供了 dex
的解析与 smali
的相互转换.
解析
dex
主要用到dexlib2
学习dexlib2
我们来简单的学习下解析 Dex
的方法(其实解析简单,主要难的是如何增删改查指令,因为会用到寄存器相关的知识)。
遍历dex的类
fun openDex(path: String): org.jf.dexlib2.iface.DexFile {
// 1. 加载一个 dex
val dexBackedDexFile =
DexFileFactory.loadDexFile(File(path), Opcodes.getDefault())
// 2. 创建一个 dex Rewriter 重写器
val dexWriter = DexRewriter(object : RewriterModule() {})
// 3. 读取 dex
val newDexFile = dexWriter.dexFileRewriter.rewrite(dexBackedDexFile)
// 4. 获取dex的所有class 遍历
for (classDef in newDexFile.classes) {
print("${classDef.type} ${classDef.superclass} ${classDef.accessFlags}")
}
return newDexFile
}
修改dex类
我们简单的将一个dex中所有类中的 exemptAll
方法名,变更成 testExemptAll
方法名。
private fun changeMethodName() {
// 1. 读取dex
val dexBackedDexFile =
DexFileFactory.loadDexFile(File("/Users/wengege/Desktop/NewWx/classes-3.0.dex"), Opcodes.getDefault())
// 2. 创建dex重写器
val dexWriter = DexRewriter(object : RewriterModule() {
override fun getMethodRewriter(rewriters: Rewriters): Rewriter<Method> {
return Rewriter {
// 3. 如果有一个方法的昵称是 exemptAll
if (it.name == "exemptAll") {
object : MethodWrapper(it) {
// 将方法名变成 testExemptAll
override fun getName(): String {
return "testExemptAll"
}
}
} else {
it
}
}
}
})
// 4. 读写器开始工作
val newDexFile = dexWriter.dexFileRewriter.rewrite(dexBackedDexFile)
// 5. 将改后的dex写入文件
DexFileFactory.writeDexFile("/Users/wengege/Desktop/NewWx/new.dex", newDexFile)
}
合并2个dex
private fun margeDex() {
// 1. 创建 dex pool
val dexPool = DexPool(Opcodes.getDefault())
// 2. 打开dex1
val d1 = openDex("/Users/wengege/Desktop/NewWx/classes-3.0.dex")
// 3. 打开dex2
val d2 = openDex("/Users/wengege/Desktop/NewWx/weixin807android1920/classes10.dex")
// 4. 遍历dex1的所有类
for (classDef in d1.classes) {
// 5. 将类写入 dexPool
dexPool.internClass(classDef);
}
// 6. 遍历dex2的所有类
for (classDef in d2.classes) {
// 7. 遍历dex2的所有类
dexPool.internClass(classDef);
}
// 7. 写出合并的dex
dexPool.writeTo(FileDataStore(File("/Users/wengege/Desktop/NewWx/marge.dex")));
}
开始实战开发XpRoot
前面我讲解了,通过 dexlib2
可以实现 dex
修改。思考下以前的 ASM
是操作的 class
,所以我们一般操作的都是 apk
生成前的产物。
那如果是修改 dex
呢?是不是就能直接操作 apk
了呢?
换句话说我们改造的时机靠后了,可以改造的范围也变大了。也就是说任意 App
我们都可以改造了(包括第三方的App)。
Hook
whale 和 SandHook 都是基于应用的 Hook
框架,可以通过它来实现拦截应用中的方法。
那如果我们把这个框架注入到第三方的 apk
中,是不是就说我们有了篡改这些 App
的手段呢?
方案定制
其实最难的部分是红色框框部分。
开始敲代码
都准备好啦,接下来我们开始
解压Apk
companion object{
private const val DATA_PATTERN = "yyyy-MM-dd HH:mm:ss";
private val sdf = SimpleDateFormat(DATA_PATTERN);
}
override fun execute(): File {
val apk = File(apkPath)
if (!apk.exists()) {
throw RuntimeException("宿主APP不存在")
}
val dirName = if (IS_DEBUG) {
"debug"
} else {
sdf.format(Date())
}
val parent = File(apk.parent, "${File(apkPath).name.getBaseName()}-${dirName}")
if (parent.exists()) {
FileUtils.delete(parent)
}
val dir = File(parent, "app")
dir.mkdirs()
ZipUtils.unzipFile(apk, dir)
return dir
}
override fun complete(result: File) {
Log.d("wyz", "解压APP完成:${result.absolutePath}")
}
}
将加载核心Dex追到classesN
/**
* 增加调用入口的dex方法
*/
class CopyAppendDexTask(val unZipDir: File) : Task<File, File>() {
companion object {
private const val DEX_FILE = "xp_call_core.dex"
}
override fun execute(): File {
val dexSize = unZipDir.listFiles().filter { it.name.endsWith(".dex") }.size
Log.d("CopyAppendDexTask", "当前dex的个数为 $dexSize")
val dexFileStream = Thread.currentThread().contextClassLoader.getResourceAsStream(DEX_FILE)
// 文件写入
val appendDexFile = File(unZipDir, "classes${dexSize + 1}.dex").apply {
writeBytes(dexFileStream.readBytes())
}
return appendDexFile
}
override fun complete(result: File) {
}
}
解析AndroidManifest获取Application
class GetApplicationTask(val apkFile: File) : Task<File, String>() {
companion object {
const val ANDROID_MANIFEST = "AndroidManifest.xml"
}
override fun execute(): String {
val manifestInput = File(apkFile, ANDROID_MANIFEST).inputStream()
val value = ManifestParser.parseManifestFile(manifestInput)
val applicationName = value.applicationName
val packageName = value.packageName
return applicationName
}
override fun complete(result: String) {
Log.d("GetApplicationTask", "获取到 application $result")
}
}
宿主Application增加静态代码块
这里就要分2种情况了。Application存在与不存在的情况。来个流程图吧!
/**
* 构建静态代码块的 Mehtod
* static {
* XpRoot.start();
* }
*/
public static Method buildStaticContextMethod(String className) {
ArrayList<ImmutableInstruction> instructions = Lists.newArrayList(
ImmutableInstructionFactory.INSTANCE.makeInstruction35c(Opcode.INVOKE_STATIC, 0, 0, 0, 0, 0, 0, getStaticContextMethodRef()),
ImmutableInstructionFactory.INSTANCE.makeInstruction10x(Opcode.RETURN_VOID)
);
ImmutableMethodImplementation methodImpl = new ImmutableMethodImplementation(0, instructions, null, null);
return new ImmutableMethod(className, "<clinit>", new ArrayList<>(), "V", AccessFlags.STATIC.getValue() | AccessFlags.CONSTRUCTOR.getValue(), null, null, methodImpl);
}
/**
* 原有的静态代码块插入
* XpRoot.start() 方法
*/
public static Method buildStaticContextMethod(String className, Method mehtod) {
ArrayList<Instruction> instructions = Lists.newArrayList(
ImmutableInstructionFactory.INSTANCE.makeInstruction35c(Opcode.INVOKE_STATIC, 0, 0, 0, 0, 0, 0, getStaticContextMethodRef())
);
MethodImplementation implementation = mehtod.getImplementation();
MethodImplementation newImplementation = null;
if (implementation != null) {
int registerCount = implementation.getRegisterCount();
for (Instruction instruction : mehtod.getImplementation().getInstructions()) {
instructions.add(instruction);
}
newImplementation = new ImmutableMethodImplementation(registerCount, instructions, implementation.getTryBlocks(), implementation.getDebugItems());
}
return new ImmutableMethod(className, mehtod.getName(), mehtod.getParameters(), mehtod.getReturnType(), mehtod.getAccessFlags(), mehtod.getAnnotations(),
mehtod.getHiddenApiRestrictions(), newImplementation);
}
由于代码逻辑过长,请自行查看
ModifyApplicationDexTask
逻辑。
坑1
本以为这样的一个这样的思路是没有问题的。但是测试过程中遇到个大坑。
Caused by: org.jf.util.ExceptionWithContext: Error while writing instruction at code offset 0x2
at org.jf.dexlib2.writer.DexWriter.writeCodeItem(DexWriter.java:1320)
at org.jf.dexlib2.writer.DexWriter.writeDebugAndCodeItems(DexWriter.java:1043)
... 27 more
Caused by: org.jf.util.ExceptionWithContext: Unsigned short value out of range: 65536
at org.jf.dexlib2.writer.DexDataWriter.writeUshort(DexDataWriter.java:116)
at org.jf.dexlib2.writer.InstructionWriter.write(InstructionWriter.java:356)
at org.jf.dexlib2.writer.DexWriter.writeCodeItem(DexWriter.java:1280)
... 28 more
看上面的日志,应该是说 Dex
方法数超了 65535
。
原因由于宿主的 Application
存在的 Dex
方法数为 65535
,我增加了个 XpRoot.start()
方法,超过了 Dex
的最大方法数导致 Dex
不对。
思考下,解决 65535
问题,只有多 Dex
好像别无它法。
因此我们的思路是创建 ProxyApplication
(负责初始化Hook框架&加载插件) ,然后让 ProxyApplication
继承宿主 Application
。然后修改 AndroidManifest
的 application
为 ProxyApplication
。
这里要注意个点,宿主的Application如果为
final
修饰要去掉,因为我要ProxyApplication
继承宿主Application
。
看看结果,完美!!
来是骡子是马 Run
一下。
看日志宿主的 Application
已经创建了,但是好像没有创建成功。构造方法错了???看下 smali
代码。
我去还真是!!由于我换了 ProxyApplication
的父类,因此默认的构造方法也要换父类的。
找到问题了继续改造代码,将原有的 super()
执行真正继承类的 super()
。
// 自己的application
val changeClassDef = ClassDefWrapper(it)
// 让我的 application 继承宿主的application
changeClassDef.setSupperClass(applicationDexFile.appClassDef.type)
// 因为改了父类 因此默认的构造方法 调用的 super 方法也要改
// 1. 查找默认无参构造方法
var defaultInitMethod: Method? = null
for (method in changeClassDef.originDirectMethods) {
if (method.parameters.size == 0 && method.name == "<init>" && method.returnType == "V") {
defaultInitMethod = method
}
}
if (defaultInitMethod != null) {
// 构建新的构造方法
val newInitMethod = changeInitSuperMethod(changeClassDef.type, defaultInitMethod, changeClassDef.superclass)
// 把原来的从集合中删除掉
changeClassDef.originDirectMethods.remove(defaultInitMethod)
// 在增加新的
changeClassDef.originDirectMethods.add(newInitMethod)
}
看看结果对吗?
擦屁股
代码改造完成了,该注入的也注入了,该趟的坑也趟玩了!! 只需要重新压缩 App
,在签名 App
就行了。
// 重新压缩App
val unsignedApp = ZipTask(unApkFile).call()
// 重新签名
SignApkTask(unsignedApp).call()
验收成果
我曾经开源过一个通过 Xposed
拦截 App
一些关键方法,方便用户调试的应用 AppHook
今天就用它来测试,并注入到微信中测试下。
由于以前开源的 AppHook 存在控制,这次是将模块注入到了App中,因此需要改造下代码。
只需要改下 Core
xposedHook
去除掉判断依据就可以啦!
object Core {
fun xposedHook(classLoader: ClassLoader) {
if (isHook) return
isHook = true
MethodHook
.Builder()
.setClass(Application::class.java)
.methodName("onCreate")
.afterHookedMethod {
val app = it.thisObject as Application
appHook(app)
}
.build()
.execute()
}
}