Android的反编译(布局植入篇)

第一次接触到安卓反编译是在我念初中的时候,那个时候会反编译修改一些东西,但是没有原生开发技术的支持,想想这么早就接触到了反编译,到目前还这么菜,最近想起了捡一捡

首先分享自己反编译植入布局的一些小经验

  • 不要动 resources.arsc(反编译/回编译都容易失败,容易打乱其它文件的地址)
  • 不要植入 findViewById 这类语句
  • 使用 tag 绑定布局
  • 尽量使用自定义 View
  • 将所有的 findViewById 换成 findViewByTag
  • 尽量不要植入一整个 xml 文件(植入容易改变大量 xml 的地址)
  • 在 java 中任何的 R.id/R.layout 最后编译成 class/smali 中都变成了 0x**的 16 进制地址

该篇内容需要具备以下知识:

  • 安卓原生开发基础
  • 安卓原生自定义 View
  • 内容观察者
  • 使用 Handler 更新 View
  • 简单的线程相关

使用到的软件

  • MT 管理器(使用它的文本修改功能)
  • Apktool 反编译工具(这里我用自己的 MToolkit,还有国外的同名 Apktool)
  • AIDE(可用 android studio 等替代)

我们在植入任何的 View 之前,先确保这些 View 能被正确的显示出来,也就是说,我们需要跑出一个 demo。这篇我们植入布局进手机的状态栏

AIDE 部分

1.新建一个空项目

2.点击右上角把这个简单的 app 跑起来

3.新建一个 java 文件,随便编写一个自定义 View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package com.nos;



import android.annotation.*;
import android.content.*;
import android.database.*;
import android.net.*;
import android.os.*;
import android.provider.Settings.*;
import android.text.*;
import android.util.*;
import android.view.*;
import android.widget.*;
import java.io.*;

import android.provider.Settings.System;

public class StatusWeather extends TextView
{
    static final Uri WEATHER_URI = Uri.parse("content://weather/weather");
private final Context mContext;
@SuppressLint({"HandlerLeak"})
private Handler mHandler;
private final ContentObserver mWeatherObserver;
private WeatherRunnable mWeatherRunnable;

public StatusWeather(Context context)
{
this(context, null);
}

public StatusWeather(Context context,  AttributeSet attributeSet)
{
this(context, attributeSet, -1);
}

public StatusWeather(Context context, AttributeSet attributeSet, int defStyle)
{
super(context, attributeSet, defStyle);
this.mHandler = new WeatherHandler(this);
this.mWeatherObserver = new StatusWeatherObserver(thisthis.mHandler);
this.mContext = context;
this.mWeatherRunnable = new WeatherRunnable(this, mHandler);
setVisibility(this.GONE);
this.mContext.getContentResolver().registerContentObserver(WEATHER_URI, truethis.mWeatherObserver);
this.mContext.getContentResolver().registerContentObserver(System.getUriFor("your key"), truethis.mWeatherObserver);
this.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View p1)
{
Runtime runtime = Runtime.getRuntime();
try
{
runtime.exec("input keyevent 4");
}
catch (IOException ignored)
{
}
Intent intent = new Intent();
intent.setClassName("com.miui.weather2""com.miui.weather2.ActivityWeatherMain");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
mContext.startActivity(intent);
}
});
updateWeatherInfo();
}
public void updateWeatherInfo()
{
this.mHandler.removeCallbacks(this.mWeatherRunnable);
this.mHandler.postDelayed(this.mWeatherRunnable, 200);
}
@Override
protected void onDetachedFromWindow()
{
super.onDetachedFromWindow();
if (this.mWeatherObserver != null)
{
this.mContext.getContentResolver().unregisterContentObserver(this.mWeatherObserver);
}
}
}
class WeatherHandler extends Handler
{
final StatusWeather a;

WeatherHandler(StatusWeather statusBarWeather)
{
this.a = statusBarWeather;
}
@Override
public void handleMessage(Message message)
{
String str = (String) message.obj;
this.a.setText(TextUtils.isEmpty(str) ? "点击获取\n天气数据" : str);
this.a.setVisibility(message.what != 0 ? 0 : 8);
}
}
class StatusWeatherObserver extends ContentObserver
{
final StatusWeather mStatusWeather;
public StatusWeatherObserver(StatusWeather view, Handler handler)
{
super(handler);
this.mStatusWeather = view;
}
@Override
public void onChange(boolean z)
{
mStatusWeather.updateWeatherInfo();
}
}


class WeatherRunnable implements Runnable
{
final StatusWeather this$0;
final Handler mHandler;
public WeatherRunnable(StatusWeather weatherView, Handler handler)
{
this.this$0 = weatherView;
this.mHandler = handler;
}
@Override
public void run()
{
Object obj = "";
try
{
Cursor query = this$0.getContext().getContentResolver().query(StatusWeather.WEATHER_URI, nullnullnullnull);
if (query != null)
{
if (query.moveToFirst())
{
String string = query.getString(query.getColumnIndexOrThrow("city_name"));
String string2 = query.getString(query.getColumnIndexOrThrow("description"));
String string3 = query.getString(query.getColumnIndexOrThrow("temperature"));
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(string);
stringBuilder.append("/");
stringBuilder.append(string2);
stringBuilder.append(" ");
stringBuilder.append(string3);
obj = stringBuilder.toString();
}
query.close();
}
}
catch (IllegalArgumentException e)
{
obj = "没有获取到天气信息";
}
catch (Throwable th)
{
Message obtainMessage = mHandler.obtainMessage();
obtainMessage.what = 100;
obtainMessage.obj = obj;
mHandler.sendMessage(obtainMessage);
}
Message obtainMessage2 = mHandler.obtainMessage();
int bb=System.getInt(this.this$0.getContext().getContentResolver(),"your key"0);
// TODO: Implement this method
obtainMessage2.what = bb;
obtainMessage2.obj = obj;
mHandler.sendMessage(obtainMessage2);
}
}

StatusWeather 则是我们自定义的 TextView,他用来获取本地的天气信息,内容观察者是为了其它的应用能够控制这个控件的显示与隐藏

添加进 xml 中

run 起来康康

没有获取到天气,不过无事,可能是因为 app 的权限不是系统级别的,这篇也是讲个方法

植入部分

1.复制出你的状态栏 apk

如下 gif

2.反编译状态栏

由于我的工具箱还没有具备文本编写功能,所以我们用 MT 管理器,我这里需要植入这个 TextView 到我的下拉栏

3.在下拉栏 xml 中添加代码

这个 xml 的路径为 res/layout/quick_status_bar_expanded_header.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="utf-8"?>
<com.android.systemui.qs.QuickStatusBarHeader android:layout_gravity="@integer/notification_panel_layout_gravity" android:id="@id/header" android:background="@android:color/transparent" android:clickable="false" android:clipChildren="false" android:clipToPadding="false" android:layout_width="fill_parent" android:layout_height="@dimen/notch_expanded_header_height" android:baselineAligned="false" android:elevation="4.0dip"
xmlns:android="http://schemas.android.com/apk/res/android">
<com.android.systemui.statusbar.HeaderView android:gravity="bottom" android:layout_gravity="start|bottom|center" android:id="@id/header_content" android:focusable="true" android:focusableInTouchMode="true" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_marginBottom="@dimen/expanded_notification_header_bottom" android:alpha="@dimen/qs_status_bar_header_alpha" android:layout_marginStart="@dimen/expanded_notification_header_start">
<com.android.systemui.statusbar.policy.Clock android:textAppearance="@style/TextAppearance.StatusBar.Expanded.Weather" android:gravity="center_vertical" android:id="@id/date_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_above="@id/carrier_layout" android:layout_alignTop="@id/system_icon_area" android:layout_alignParentBottom="true" android:layout_marginStart="@dimen/expanded_notification_weather_temperature_right" />
<LinearLayout android:id="@id/carrier_layout" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_marginStart="@dimen/expanded_notification_weather_temperature_right">
<com.android.keyguard.CarrierText android:textAppearance="@style/TextAppearance.StatusBar.Expanded.Weather" android:ellipsize="marquee" android:gravity="center_vertical" android:layout_gravity="center_vertical" android:id="@id/carrier" android:visibility="gone" android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxEms="9" android:singleLine="true" android:marqueeRepeatLimit="1" />
</LinearLayout>
<LinearLayout android:id="@id/carrier_land_layout" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignTop="@id/system_icon_area" android:layout_alignParentBottom="true" android:layout_marginStart="@dimen/notch_expanded_header_carrier_margin" android:layout_toEndOf="@id/date_time">
<com.android.keyguard.CarrierText android:textAppearance="@style/TextAppearance.StatusBar.Expanded.Weather" android:ellipsize="marquee" android:gravity="center_vertical" android:layout_gravity="center_vertical" android:id="@id/carrier_land" android:visibility="gone" android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxEms="9" android:singleLine="true" android:marqueeRepeatLimit="1" />
</LinearLayout>
<com.android.keyguard.AlphaOptimizedLinearLayout android:gravity="end" android:orientation="horizontal" android:id="@id/system_icon_area" android:focusable="true" android:focusableInTouchMode="true" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_toEndOf="@id/date_time" android:layout_alignParentEnd="true">
<com.nos.Temperature android:textSize="12.0sp" android:textStyle="bold" android:textColor="#ffffffff" android:gravity="end|center" android:layout_gravity="end|center" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/notch_settings_button_margin" />
<include layout="@layout/system_icons" />
</com.android.keyguard.AlphaOptimizedLinearLayout>
<com.android.systemui.statusbar.policy.Clock android:textAppearance="@style/TextAppearance.StatusBar.Expanded.Clock.Notch" android:id="@id/big_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_above="@id/date_time" android:layout_alignParentStart="true" />
<LinearLayout android:orientation="horizontal" android:focusable="true" android:focusableInTouchMode="true" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_above="@id/system_icon_area" android:layout_alignParentEnd="true">
<com.nos.StatusShortcut android:layout_width="wrap_content" android:layout_height="fill_parent" android:layout_marginBottom="32.0px" android:layout_marginStart="@dimen/notch_settings_button_margin" android:layout_marginEnd="0.0dip" />
<ImageView android:id="@id/notification_shade_shortcut" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/notch_settings_button_margin" android:layout_marginStart="@dimen/notch_settings_button_margin" />
</LinearLayout>
<LinearLayout android:orientation="horizontal" android:focusable="true" android:focusableInTouchMode="true" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="30.0dip" android:layout_above="@id/system_icon_area" android:layout_alignParentTop="true" android:layout_alignParentEnd="true">
<com.nos.Charge android:textSize="12.0dip" android:textColor="#ffffffff" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/notch_settings_button_margin" />
<com.nos.StatusWeather android:textSize="12.0dip" android:textColor="#ffffffff" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/notch_settings_button_margin" />
</LinearLayout>
</com.android.systemui.statusbar.HeaderView>
</com.android.systemui.qs.QuickStatusBarHeader>

可以发现这里植入了多个 view 了,不过都是差不多的

4.回编译

回编译耗时比较久,点击文件夹后面那个按钮即可看到弹窗

5.替换 xml 回原状态栏

可以观察到上面回编译已经生成新的 apk 了,这个 apk 是不能用的:

  • 没有签名
  • 地址被打乱了
  • 换回去 99%概率 fc

我们需要将其中变换的部分提取出来
也就是

1
MiuiSystemUI_new.apk/res/layout/quick_status_bar_expanded_header.xml

按照原路径替换回我们原始的状态栏 apk

没有完,这个替换回去后它是找不到这个自定义 view 的,自定义 view 在我们的 aide 项目中呢,所以我们

6.提取 AIDE 项目的 dex

在你自己项目的路径下,有 build 的产物,apk/class/dex 都有


我们只需要它的 classes.dex

7.重命名 classes.dex 为 classes2.dex

这步无细节操作,设为标题是怕大家忽略了

8.将 classes2.dex 添加进状态栏 apk


是不是觉得熟悉,这里也是用到了 Mutldex 的做法
当然我们还可以将 AIDE 工程 run 出来的 apk 反编译,得到它的 smali,将 smali 添加进状态栏 apk 反编译后的 smali,随后回编译状态栏也就顺便把我们植入的 smali 一起回编译进了一个 dex,提取出来,覆盖回原状态栏

最后效果

总结

植入布局的流程(只适用于无混淆无加固的 app)

  • 需要得到自己 java 对应的 dex
  • 反编译需要植入的 app
  • 添加布局到对应的 xml
  • 回编译 app 并提取出对应的 xml
  • 添加回编译后的 xml 到原 apk
  • 添加自己的 dex 到原 apk

安卓原生动态添加布局用得好的话,植入 addView 对应的安卓字节码去添加一些 View 也是可行的

作者

梦魇兽

发布于

2020-04-06

更新于

2023-03-11

许可协议

评论