Android apk 反编译学习记录

来源:互联网 发布:suse linux yum配置 编辑:程序博客网 时间:2024/06/06 18:12

最近因为业务需要,接触了一下apk 的反编译。项目忙完了,写篇东西来总结一下自己的学习。


我们通过一个简单的例子来简单介绍一下反编译及修改代码的方法

学习目标:

•学习如何利用开源工具反编译android 的普通 apk
•尝试修改xml 文件或 更换资源文件,并重新打包apk
•利用 DDMS 和反编译出来的代码,分析代码逻辑,帮助读懂Smali 文件
•尝试修改 Smali 文件,修改代码逻辑,并重新打包apk
•重新再真机上运行 修改过后的apk,检查修改效果
•记录并解决遇到的问题

前期准备:
只需要下载 apktool 工具,还有本机安装有 JDK,以及 dex2jar 和 jd-gui.exe 两个工具。看smali 代码推荐使用 Notepad++

先从简单的开始,我们先自己写一个 简单的 app,就实现在activity里显示一个字符串的功能,作为反编译用的例子apk

public class MainActivity extends Activity {private TextView tv;private param p;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);tv = (TextView) this.findViewById(R.id.tv);tv.setText(R.string.hello_world);}}
运行看看修改前的效果


然后,通过apktool 工具对这个apk 进行反编译,十分简单
步骤:
1.在命令行里 cd 到你apktool 的文件夹里(就是你 aapt.exe,apktool.jar,apktool.bat 这几个文件的路径)
2.执行 apktool d –f 你的apk的路径 反编译生成物的路径 (例如:apktool d -f smalitest.apk C:/out)

执行成功后,就可以看到反编译出来的文件了,xml 跟 资源文件都可以直接修改了,java 代码会被反编译成smali 文件,这是一种 虚拟机的执行语言 ,后面介绍怎么阅读及修改
现在,我们尝试修改一下 res/values 文件里的 strings.xml 里的 字符串,然后重新打包一下apk 查看效果
<resources>    <string name="app_name">SmaliTest</string>    <string name="hello_world"><span style="color:#ff0000;">It have been modified</span></string></resources>

执行 apktool b 你修改过的apk文件路径(我的命令是:apktool b c:/out)
执行完成后,你会看到多了两个文件夹,一个build 一个 dist重新打包的apk 就在 dist 文件夹下
即使生成了新的apk,但是这个apk还是不能正常安装的,因为这是一个裸的apk,木有经过数字签名。Android 系统不能接受没有签名的apk 的安装及运行,所以我们还需要对裸的apk 进行签名认证

我自己使用jdk 自带的签名工具jarsigner进行签名,这个工具位置在你的java 安装路径下的 JDK/bin 目录下
执行 jarsigner -verbose –keystore  秘钥仓库 -signedjar 签名完后的apk名字 被签名的裸apk路径 私钥
(我的命令是: jarsigner -verbose –keystore lzq.keystore -signedjar test_signed.apk test.apk lzq)

因为我使用的是跟之前apk 的签名相同的数字签名,所以可以直接安装进手机里,覆盖掉原来的apk,如果使用了与原来反编译前不同的签名,需要先把原来的apk 卸载掉才能顺利安装。
如果安装包名相同,但签名不一样的apk,会提示如下信息
Re-installation failed due todifferent application signatures.

运行重现打包的apk,

嗯,修改成功。

如果,我们需要修改代码逻辑,那又需要怎么做呢?嗯,就得去修改对应的smali 文件了
还是上面的例子,我们尝试再MainActivity.java 里去 把
tv.setText(R.string.hello_world);  改成直接出入一个字符串显示
tv.setText("It have been modified");

找到这个类反编译出来的文件,MainActivity.smali
定位到相关代码块
    iget-object v0, p0, Lcom/gdut/smalitest/MainActivity;->tv:Landroid/widget/TextView;//获取tv这个类型为TextView的对象并赋给v0    const v1, 0x7f050002  //给v1 赋值    invoke-virtual {v0, v1}, Landroid/widget/TextView;->setText(I)V  //实现v0对象的setText方法并传入v1对象



上面的V0,V1等V开头的代表寄存器,P0,P1等P开头的代表参数寄存器
我们对传入的寄存器的值进行修改
    iget-object v0, p0, Lcom/gdut/smalitest/MainActivity;->tv:Landroid/widget/TextView;    const-string v1, "It have been modified"    invoke-virtual {v0, v1}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V



重新打包签名,运行

修改有效。

上面的例子里,我们是知道源代码的情况下,可以快速定位到 smali 相关的代码处,但是,如果我们只有一个apk,并没有其源代码,也没必要一来就去阅读 smali文件,毕竟smali 语言不太好看,这效率也不高。
我们可以通过dex2jar 工具得出 一个 jar 文件,再通过jd-gui 这个工具去阅读里面的java 代码
具体步骤也好简单,先解压 你的apk,把里面的classes.dex解压出来,然后在命令行里cd 到dex2jar 工具目录下运行
dex2jar.bat 你的classes.dex文件的路径
完成后,就就可以看到在classes.dex 同一个文件夹里,多了一个 classes_dex2jar.jar文件,直接把它拖动到 jd-gui 里就可以查看代码了

现在就可以看到熟悉的 java 代码了(PS:有些apk是被代码混淆过的,所以阅读起来也比较吃力,不过可以结合smali文件一起看,还是可以慢慢理解代码逻辑的)

遇到的问题总结:
1. dex2jar 工具 反编译出来的代码里,会掺杂了好多 while 之类的 循环语句,其他源码里是没有的,观察貌似跟 源码里的 if 语句有关。具体规律可以自行尝试。注意不要被误导
2. 修改smali 文件时,木有语法错误提示,打包编译只要你的代码符合 smali 语法就不会有错,但是这个smali语法没有错,不代表java 语法没有错,所以,如果你修改的smali 代码有java 语法上的错误,会在真机运行时发生fatal,提示java.lang.verifyErro 这个exception,所以当你修改后的apk 发生这个错误,就要去检查你的修改是否有java 上的语法错误了

至此,apk的反编译和修改打包学习就讲完了,下面,我们来拿一个普通的apk 来简单实践一下

背景交代:
某朋友的老板突发奇想,要他们的手机上安装公司的打开app,要通过GPS 定位来实现每天回公司打卡。这让我朋友这位天天迟到的主懊恼不已,来跟我吐槽。
他问我能否帮忙破解一下android版本的打开客户端,我表示尽力而为。
总结一下:
Android客户端情况:名字:xxx公司圈,开发机构:xxx外包公司
实现目标:不管在哪里打开定位,都会是公司的地理坐标。

首先,自己去试着操作一次打卡,看整个流程是登录后,点打卡的选项,然后弹出一个一直转的Loading 框,转完了就会弹出显示当地时间跟位置的对话框


手机连接 DDMS,用 Hierarchy View 工具查看当前的activity

接着我们用 apktool 工具 跟 dex2jar 工具分别反编译出 smali 跟 jar 文件。
代码的包结构如下:


从上面我们知道了这个app 的主类包应该是 com.cmp.mobile,主要逻辑代码应该都在里面,而我们还发现了一个叫com.baidu 的包,点开里面看到,应该是百度地图的SDK。
这个app 的定位应该是用 百度地图的 API,那应该有个实现 BDLocationListener 接口的类,里面返回 BDLocation 类型,从中获得位置信息。而从上面可以知道,打卡对应的activity 是 com.cmp.mobile.attence.activity.AttenceActivity.java, 从这两方面去分析打卡具体流程。
中间过程略
分析结果:
继承 Application 类的  com.cmp.mobile.app.App.java 里的内部类 MyLocationListenner实现了 BDLocationListener接口
 public class MyLocationListenner    implements BDLocationListener  {    public MyLocationListenner()    {    }    public void onReceiveLocation(BDLocation paramBDLocation)    {      if (paramBDLocation == null);      do      {        do        {          return;          App.this.posY = Double.valueOf(paramBDLocation.getLatitude());          App.this.posX = Double.valueOf(paramBDLocation.getLongitude());          App.this.address = paramBDLocation.getAddrStr();        }        while (App.this.activeActivity == null);        try        {          BaseActivity localBaseActivity = (BaseActivity)App.this.activeActivity;          if ((!App.this.isAuto) && ((localBaseActivity instanceof AttenceActivity)))          {            localBaseActivity.locationSucCallBack();            return;          }        }        catch (Exception localException)        {          localException.printStackTrace();          return;        }      }      while (!App.this.isAuto);      new ComMainActivity().autoLocationSucCallBack();    }    public void onReceivePoi(BDLocation paramBDLocation)    {      if (paramBDLocation == null);    }  }

当定位完后,把返回的BDLocation 里的经纬度 和街道 信息分别放入 App.java 里的 posX,posY,address 三个public变量,然后回调 AttenceActivity.java 的 locationSucCallBack() 方法,里面通过 GetShortDistance()方法,用 App.posX 和App.posY 去跟 公司的 经纬度做距离计算 如果距离小于com.cmp.mobile.app.model.OccupierData.java 里的 distance 值,就把 App.address 值设为 "公司",不然就设为 百度地图API 返回的街道名 
(AttenceActivity.java 的逻辑,我们直接查看 对应的 smali 文件)
invoke-static/range {v0 .. v7}, Lcom/cmp/mobile/attence/activity/AttenceActivity;->GetShortDistance(DDDD)D //计算现在的地点跟公司的距离    move-result-wide v9    .line 232    .local v9, length:D    iget-object v0, p0, Lcom/cmp/mobile/attence/activity/AttenceActivity;->app:Lcom/cmp/mobile/app/App;    iget-object v0, v0, Lcom/cmp/mobile/app/App;->occupierData:Lcom/cmp/mobile/app/model/OccupierData;    iget-object v0, v0, Lcom/cmp/mobile/app/model/OccupierData;->distance:Ljava/lang/Integer;    invoke-virtual {v0}, Ljava/lang/Integer;->intValue()I    move-result v0    int-to-double v0, v0    cmpg-double v0, v9, v0    if-gtz v0, :cond_0    .line 233    iget-object v0, p0, Lcom/cmp/mobile/attence/activity/AttenceActivity;->app:Lcom/cmp/mobile/app/App;    const-string v1, "\u516c\u53f8"  //把 address 设成 公司    iput-object v1, v0, Lcom/cmp/mobile/app/App;->address:Ljava/lang/String;    .line 235    :cond_0    iget-object v0, p0, Lcom/cmp/mobile/attence/activity/AttenceActivity;->app:Lcom/cmp/mobile/app/App;    iget-object v0, v0, Lcom/cmp/mobile/app/App;->address:Ljava/lang/String;    invoke-virtual {v8, v0}, Lcom/cmp/mobile/attence/view/AttenceDialog;->setAddress(Ljava/lang/String;)V  //把address 设置成API返回的街道名



然后在打卡的对话框里,按下确定,就会new 一个 com.cmp.mobile.attence.listener.SaveListener 把 App.posX,App.posY,App.address 全都打包发给服务器

  public SaveListener(DataLoadDelegate paramDataLoadDelegate, String paramString1, String paramString2, Integer paramInteger, Double paramDouble1, Double paramDouble2)  {    super(paramDataLoadDelegate, "SaveListener");    this.address = paramString1;    this.remark = paramString2;    this.type = paramInteger;    this.posX = paramDouble1;    this.posY = paramDouble2;  }  protected Object parseJSON(Object paramObject)    throws JSONException  {    return null;  }  public void send()  {    String str = HttpUtils.makeUrl("/attence/operate.do?op=save", new Object[0]);    HashMap localHashMap = new HashMap();    localHashMap.put("posX", this.posX);    localHashMap.put("posY", this.posY);    localHashMap.put("address", this.address);    localHashMap.put("remark", this.remark);    localHashMap.put("type", this.type);    addPostRequest(str, localHashMap, 0, this);  }

需要关注的流程,就是这些。然后,思考一下如果用最少的修改量来达到目的。
我想到的最简单的办法是,把百度地图定位的返回参数BDLocation.java 里的 经纬度跟街道名给写死成公司的经纬度就好,怎么get 都是返回公司的经纬度 和 街道名
然后,开始修改
定位到 BDLocation.smali 文件,把getLongitude() 函数改成 
.method public getLongitude()D    .locals 2    const-wide v0, 0x40441647a9e2bcf9L //<span style="font-family: Arial, Helvetica, sans-serif;">赋给他一个double值 </span><span style="font-family: Arial, Helvetica, sans-serif;">116.457546</span><span style="font-family: Arial, Helvetica, sans-serif;"></span>    return-wide v0.end method




把 getLatitude()函数改成
.method public getLatitude()D    .locals 2    const-wide v0, 0x405d1d486f049a99L //赋给他一个double值 <span style="font-family: Arial, Helvetica, sans-serif;">40.174062</span>    return-wide v0.end method



把 getAddrStr()函数改成
# virtual methods.method public getAddrStr()Ljava/lang/String;    .locals 1    const-string v0, "\u5f3a\u54e5\u5df2\u7ecf\u628a\u8fd9\u4e2a\u5b9a\u4f4d\u4e3a  \u4e1c\u7ecf:116.457546\uff0c\u5317\u7eac:40.174062" //暂时把这句话改成“强哥已经把这个定位为 东经:116.457546 北纬:40.174062”    return-object v0.end method



PS:强哥不是我

然后又是重现打包,签名。再在真机上运行查看



嗯,修改有效,而且通过抓Tcp包对比普通的打卡,发上去的post 命令内容 是一样的。
大功告成


0 0
原创粉丝点击