再来讲讲android的字节码插桩

我们一般使用的jvm是hotspot虚拟机,其实市面上还有很多其它用途的虚拟机,比如安卓的虚拟机dalvik,在hotspot虚拟机中加载的xxx.class文件,而dalvik中加载的时xxx.smali文件,本质上没什么区别,smali的格式看起来会比class文件来的更加清晰和简洁一点,代码看起来也更容易阅读。
既然在xxx.class中可以插桩那么smali是不是一样可以插桩呢?,这篇文章给你演示一个手动插桩的过程,让你对这个东西不再那么疑惑

使用到的工具如下

  • apktool2.7
  • jadx
  • vscode
  • dex2jar-2.0

这些工具你都可以在开源平台下载到,当你下载好对应工具后,我们先来熟悉下对应的工具vscode编辑器我就不讲了。jadx工具也不需要多解释,就跟jd反编译工具是一样的,你可以将一个.apk文件拖入到工程中它会给你反编译成对应的java代码,以及解析AndroidManifest.xml文件,让你可以查看对应的页面入口类
主要先介绍下apktool的命令
首先解压一个包使用命令

apktool.bat d xxx.apk

就会在当前目录生成一个对应的文件夹,并且将class.dex文件解压成smali文件

此时使用vscode打开对应的文件夹就可以进行编辑或者修改对应的文件了

那么可以通过使用jadx打开apk,阅读对应的代码,找到你要修改的对应关键点,然后回到vscode中进行修改。
那么如何进行插桩呢?比如我们要收集日志,那我们就写一个简单的LogUtil尽量少引入其它包,利用现有的android中自带的包来实现我们想实现的功能。基本上所有的android项目都会引入okhttp3所以我这边使用okhttp3的包构建了一个LogUtil,发送到一个远端的log接口

package com.qimo.util;

import okhttp3.*;

import java.io.IOException;

public class LogUtil {
    public static final String url="http://localhost:8080/log";

    public static void log(String data){
        OkHttpClient client = new OkHttpClient();
        MediaType mediaType = MediaType.parse("application/json");
        RequestBody body = RequestBody.create(mediaType, data);
        Request request = new Request.Builder()
                .url(url)
                .post(body)
                .build();
        try {
            client.newCall(request).execute();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

}

将该项目编译成jar包

接着使用dex2jar的相关包将jar包转成dex,在使用对应的包将dex转smali,然后引入到当前反编译的项目中
假如我生成的包为log.jar
执行如下命令

# jar转dex
d2j-dex2jar.bat  log.jar

#将dex转smali
d2j-dex2smali.bat log-jar2dex.dex

执行完成后会在当前目录生成log-jar2dex-out目录打开该目找到刚的LogUtil.smali并将它复制出来放到你反编译出来的包中,打开该文件

可以看到如下代码

//修改这个的包名为你将该文件所放置的位置
.class public Lcom/tencent/wework/common/utils/LogUtil;
.super Ljava/lang/Object;
.source "LogUtil.java"

.field public final static url:Ljava/lang/String; = "http://localhost:8080/log"

.method public constructor <init>()V
    .locals 1
    .prologue
    .line 7
    invoke-direct { p0 }, Ljava/lang/Object;-><init>()V
    return-void
.end method

.method public static log(Ljava/lang/String;)V
    .catch Ljava/io/IOException; { :L0 .. :L1 } :L2
    .locals 8
    .prologue
    .line 11
    new-instance v1, Lokhttp3/OkHttpClient;
    invoke-direct { v1 }, Lokhttp3/OkHttpClient;-><init>()V
    .line 12
    const-string v5, "application/json"
    invoke-static { v5 }, Lokhttp3/MediaType;->parse(Ljava/lang/String;)Lokhttp3/MediaType;
    move-result-object v3
    .line 13
    invoke-static { v3, p0 }, Lokhttp3/RequestBody;->create(Lokhttp3/MediaType;Ljava/lang/String;)Lokhttp3/RequestBody;
    move-result-object v0
    .line 14
    new-instance v5, Lokhttp3/Request$Builder;
    invoke-direct { v5 }, Lokhttp3/Request$Builder;-><init>()V
    const-string v6, "http://localhost:8080/log"
    .line 15
    invoke-virtual { v5, v6 }, Lokhttp3/Request$Builder;->url(Ljava/lang/String;)Lokhttp3/Request$Builder;
    move-result-object v5
    .line 16
    invoke-virtual { v5, v0 }, Lokhttp3/Request$Builder;->post(Lokhttp3/RequestBody;)Lokhttp3/Request$Builder;
    move-result-object v5
    .line 17
    invoke-virtual { v5 }, Lokhttp3/Request$Builder;->build()Lokhttp3/Request;
    move-result-object v4
    :L0
    .line 19
    invoke-virtual { v1, v4 }, Lokhttp3/OkHttpClient;->newCall(Lokhttp3/Request;)Lokhttp3/Call;
    move-result-object v5
    invoke-interface { v5 }, Lokhttp3/Call;->execute()Lokhttp3/Response;
    :L1
    .line 23
    return-void
    :L2
    .line 20
    move-exception v2
    .line 21
    new-instance v5, Ljava/lang/RuntimeException;
    invoke-direct { v5, v2 }, Ljava/lang/RuntimeException;-><init>(Ljava/lang/Throwable;)V
    throw v5
.end method

接着打开你想要插桩的文件,我们就调用这个类的方法进行收集日志就可以了
例如:我在其中一个类中插入了这两行代码就是收集对应的message.tostring的内容

invoke-virtual { p2, v0 }, Lcom/tencent/wework/foundation/model/Message;->toString()Ljava/lang/String;
    invoke-static { v0 }, Lcom/tencent/wework/common/utils/LogUtil;->log(Ljava/lang/String;)V

通过smali文件我们可以很轻易的阅读这些代码,相对于hotspot的字节码变换程度而言,看这个代码基本上和看源代码没有太大差别。
修改完成之后我们在些个简单的脚本重新打包成apk并且进行签名
将以下脚本保存为bat文件到apktool的目录下

:: 要打包的项目
@echo off
set package=%1%
set apk=%1%.apk
set align=%1%.al.apk
set sign=%1%.s.apk
@echo on
echo 清理目录
del /f /s /q .\%package%*.apk*
echo 开始打包
chcp 936
call ./apktool.bat b %package%
echo 复制文件到主目录
move %package%\dist\%apk% .\%apk%
echo 开始对齐
call zipalign -p -f -v 4 %apk% %align%
echo 开始签名
call apksigner sign --ks p12.keystore --ks-key-alias p12 --ks-pass pass:123456 --v2-signing-enabled true -v --out %sign% %align%

当然在执行脚本前你需要先生成一个证书文件p12.keystore

证书生成
在当前目录打开cmd窗口

chcp 936
生成证书
keytool -genkey -keystore keyfile -keyalg RSA -validity 36500 -alias p12
keytool -genkey -alias p12 -keyalg RSA -keysize 2048 -validity 36500 -keystore p12.keystore

然后再执行脚本

build.bat xxx目录

等打包完成就会进行签名然后就可以在正常的安卓手机上安装,执行到对应的代码你就会发现你收集到了对应的日志了。

当然,本文介绍起来看似颇为轻巧,实际上并没有说的这么轻巧哈,比较困难的地方是你得具备对应得字节码阅读能力,以及如果通过源代码找到对应得插桩点,或者通过调试得方式找到对应得插桩点等等。当然相对于一些包可能做了混淆,加密,或者加固的可能就更加困难了,还有的包可能还有反调试之类的机制,你甚至都无法进行调试,更别说找到想要的插桩点了。如果你有深入的需求,需要你不断的去专研拉。

还有很多app会把一些代码的关键逻辑写在.so动态库中此时你可能还需要学习一些动态调试工具,以及底层汇编代码的阅读能力。

你可能感兴趣的:(android,java,开发语言)