第一次接觸到安卓反編譯是在我念初中的時候,那個時候會反編譯修改一些東西,但是沒有原生開發技術的支援,想想這麼早就接觸到了反編譯,到目前還這麼菜,最近想起了撿一撿
首先分享自己反編譯植入佈局的一些小經驗
- 不要動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
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(this, this.mHandler);
this.mContext = context;
this.mWeatherRunnable = new WeatherRunnable(this, mHandler);
setVisibility(this.GONE);
this.mContext.getContentResolver().registerContentObserver(WEATHER_URI, true, this.mWeatherObserver);
this.mContext.getContentResolver().registerContentObserver(System.getUriFor("your key"), true, this.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, null, null, null, null);
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
<?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
我們需要將其中變換的部分提取出來 也就是
MiuiSystemUI_new.apk/res/layout/quick_status_bar_expanded_header.xml
複製程式碼
按照原路徑替換回我們原始的狀態列apk
沒有完,這個替換回去後它是找不到這個自定義view的,自定義view在我們的aide專案中呢,所以我們
6.提取AIDE專案的dex
在你自己專案的路徑下,有build的產物,apk/class/dex都有
我們只需要它的classes.dex7.重新命名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也是可行的