Android的反編譯(佈局植入篇)

Nightmare_夢魘獸發表於2020-04-06

第一次接觸到安卓反編譯是在我念初中的時候,那個時候會反編譯修改一些東西,但是沒有原生開發技術的支援,想想這麼早就接觸到了反編譯,到目前還這麼菜,最近想起了撿一撿

首先分享自己反編譯植入佈局的一些小經驗

  • 不要動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.新建一個空專案

Android的反編譯(佈局植入篇)

2.點選右上角把這個簡單的app跑起來

Android的反編譯(佈局植入篇)

3.新建一個java檔案,隨便編寫一個自定義View

Android的反編譯(佈局植入篇)

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中

Android的反編譯(佈局植入篇)
run起來康康
Android的反編譯(佈局植入篇)

沒有獲取到天氣,不過無事,可能是因為app的許可權不是系統級別的,這篇也是講個方法

植入部分

1.複製出你的狀態列apk

如下gif

Android的反編譯(佈局植入篇)

2.反編譯狀態列

Android的反編譯(佈局植入篇)

由於我的工具箱還沒有具備文字編寫功能,所以我們用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.回編譯

回編譯耗時比較久,點選資料夾後面那個按鈕即可看到彈窗

Android的反編譯(佈局植入篇)

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都有

Android的反編譯(佈局植入篇)
我們只需要它的classes.dex

7.重新命名classes.dex為classes2.dex

這步無細節操作,設為標題是怕大家忽略了

8.將classes2.dex新增進狀態列apk

Android的反編譯(佈局植入篇)
是不是覺得熟悉,這裡也是用到了Mutldex的做法 當然我們還可以將AIDE工程run出來的apk反編譯,得到它的smali,將smali新增進狀態列apk反編譯後的smali,隨後回編譯狀態列也就順便把我們植入的smali一起回編譯進了一個dex,提取出來,覆蓋回原狀態列

最後效果

Android的反編譯(佈局植入篇)

總結

植入佈局的流程(只適用於無混淆無加固的app)

  • 需要得到自己java對應的dex
  • 反編譯需要植入的app
  • 新增布局到對應的xml
  • 回編譯app並提取出對應的xml
  • 新增回編譯後的xml到原apk
  • 新增自己的dex到原apk

安卓原生動態新增布局用得好的話,植入addView對應的安卓位元組碼去新增一些View也是可行的

相關文章