一、报错
启动没有在 AndroidManifest 中注册的 Activity,会报错:
android.content.ActivityNotFoundException: Unable to find explicit activity class {...}; have you declared this activity in your AndroidManifest.xml?
二、思路
Android 使用的是 C/S 架构,我们的 app 是 client 客户端,内核是 Server 服务端。
Activity 是否注册的验证是在服务端进行的,所以我们客户端无法修改判断条件。但我们可以修改客户端的请求,让服务端以为我们要启动的是另外一个已注册的 Acitivity,在客户端得到启动许可后,再去启动真正的目标 Activity。
这一操作要通过 hook 来实现。
三、启动流程
android 28 启动流程:
3.1 hook 点的选择
替换:
在 android 28 上,通过 ActivityManager.getService().startActivity()
来向服务端发起请求。所以我们要在这一步之前
,将要启动的 Activity 替换为已注册的 Activity。
恢复:
通过 mInstrumentation.newActivity()
来创建 Activity。所以我们要在这一步之前
,将要启动的 Activity 替换回未注册的 Activity。
本文选择的替换点是 ActivityManager.getService().startActivity()
,即要 hook ActivityManager.getService()
。
恢复点是:ActivityThread#mH.handleMessage()
,即要 hook ActivityThread#mH#mCallback
。
3.2 版本差异
获取 AMSP:
-
android 26 及以上:
ActivityManager.getService() -
android 26 以下:
ActivityManagerNative.getDefault()
处理启动 Activity 的 message:
-
android 28 及以上
handleMessage(ActivityThread.H.EXECUTE_TRANSACTION) -
android 28 以下:
handleMessage(H.LAUNCH_ACTIVITY)
四、代码
完整代码见 github.com/Gdeeer
新建三个 Activity:
- CommonActivity 已注册,用于点击跳转 TargetActivity
- StubActivity 已注册,用于占位
- TargetActivity 已注册,用于跳转
4.1 Hook
AMSPHookHelper
class AMSPHookHelper {
static final String EXTRA_TARGET_INTENT = "extra_target_intent";
static void hookAMSP() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
hookAMSPBefore26();
} else {
hookAMSPSince26();
}
}
/**
* android 26 以下版本 AMSP 的 hook
*/
private static void hookAMSPBefore26() {
try {
Class classActivityManagerNative = Class.forName("android.app.ActivityManagerNative");
Object gDefault = FieldUtils.readStaticField(classActivityManagerNative, "gDefault");
Object mInstance = FieldUtils.readField(gDefault, "mInstance");
Class classIActivityManager = Class.forName("android.app.IActivityManager");
Object proxy = Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class[]{classIActivityManager},
new MockAMSP(mInstance)
);
FieldUtils.writeField(gDefault, "mInstance", proxy);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* android 26 及以上版本 AMSP 的 hook
*/
private static void hookAMSPSince26() {
try {
Object IActivityManagerSingleton = FieldUtils.readStaticField(ActivityManager.class, "IActivityManagerSingleton");
Object mInstance = FieldUtils.readField(IActivityManagerSingleton, "mInstance");
Class classIActivityManager = Class.forName("android.app.IActivityManager");
Object proxy = Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class[]{classIActivityManager},
new MockAMSP(mInstance)
);
FieldUtils.writeField(IActivityManagerSingleton, "mInstance", proxy);
} catch (Exception e) {
e.printStackTrace();
}
}
static void hookActivityThread() {
try {
Class classActivityThread = Class.forName("android.app.ActivityThread");
Object currentActivityThread = FieldUtils.readStaticField(classActivityThread, "sCurrentActivityThread");
Handler mH = (Handler) FieldUtils.readField(currentActivityThread, "mH");
FieldUtils.writeField(mH, "mCallback", new MockHCallback(mH));
} catch (Exception e) {
e.printStackTrace();
}
}
}
StubActivityApplication
public class StubActivityApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// 解除 android P 上的私有 api 限制,见http://weishu.me/2018/06/07/free-reflection-above-android-p/
Reflection.unseal(base);
// hook
AMSPHookHelper.hookAMSP();
AMSPHookHelper.hookActivityThread();
}
}
4.2 替换点
MockAMSP:
public class MockAMSP implements InvocationHandler {
private static final String START_ACTIVITY = "startActivity";
private Object mBase;
MockAMSP(Object base) {
mBase = base;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (START_ACTIVITY.equals(method.getName())) {
// 找到旧的 intent
Intent raw;
int index = 0;
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Intent) {
index = i;
break;
}
}
raw = (Intent) args[index];
// 创建新的 intent
Intent newIntent = new Intent();
String stubPackage = "com.gdeer.gdtesthub";
ComponentName componentName = new ComponentName(stubPackage, StubActivity.class.getName());
newIntent.setComponent(componentName);
newIntent.putExtra(AMSPHookHelper.EXTRA_TARGET_INTENT, raw);
// 替换旧的 intent 为新的 intent
args[index] = newIntent;
// 调用 "startActivity" 方法
return method.invoke(mBase, args);
}
return method.invoke(mBase, args);
}
}
4.3 恢复点
MockHCallback:
public class MockHCallback implements Handler.Callback {
/**
* android 28 以下,ActivityThread$H.LAUNCH_ACTIVITY = 100
*/
private static final int LAUNCH_ACTIVITY = 100;
/**
* android 28 上,ActivityThread$H.EXECUTE_TRANSACTION = 159
*/
private static final int EXECUTE_TRANSACTION = 159;
private Handler mBase;
MockHCallback(Handler base) {
mBase = base;
}
@Override
public boolean handleMessage(Message msg) {
handleLaunchActivity(msg);
mBase.handleMessage(msg);
return true;
}
private void handleLaunchActivity(Message msg) {
Log.d(TAG, "handleLaunchActivity");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (msg.what == EXECUTE_TRANSACTION) {
handleLaunchActivitySince28(msg);
}
} else {
if (msg.what == LAUNCH_ACTIVITY) {
handleLaunchActivityBefore28(msg);
}
}
}
private void handleLaunchActivityBefore28(Message msg) {
try {
Object obj = msg.obj;
if (obj != null) {
Intent raw = (Intent) FieldUtils.readField(obj, "intent");
Intent target = raw.getParcelableExtra(AMSPHookHelper.EXTRA_TARGET_INTENT);
if (target != null) {
raw.setComponent(target.getComponent());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void handleLaunchActivitySince28(Message msg) {
try {
Object mActivityCallbacks = FieldUtils.readField(msg.obj, "mActivityCallbacks");
if (mActivityCallbacks != null) {
List<?> list = (List<?>) mActivityCallbacks;
if (list.size() > 0) {
Object listItem = list.get(0);
Class classLaunchActivityItem = Class.forName("android.app.servertransaction.LaunchActivityItem");
if (listItem.getClass() == classLaunchActivityItem) {
Intent raw = (Intent) FieldUtils.readField(listItem, "mIntent");
Intent target = raw.getParcelableExtra(AMSPHookHelper.EXTRA_TARGET_INTENT);
raw.setComponent(target.getComponent());
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
补充:FiledUtil 工具类
public class FieldUtils {
private static Map<String, Field> sFieldCache = new HashMap<String, Field>();
private static String getKey(Class<?> cls, String fieldName) {
StringBuilder sb = new StringBuilder();
sb.append(cls.toString()).append("#").append(fieldName);
return sb.toString();
}
private static Field getField(Class<?> cls, String fieldName, final boolean forceAccess) throws ReflectIllegalArgumentsException {
Validate.assertTrue(cls != null, "The class must not be null");
Validate.assertTrue(!TextUtils.isEmpty(fieldName), "The field name must not be blank/empty");
String key = getKey(cls, fieldName);
Field cachedField;
synchronized (sFieldCache) {
cachedField = sFieldCache.get(key);
}
if (cachedField != null) {
if (forceAccess && !cachedField.isAccessible()) {
cachedField.setAccessible(true);
}
return cachedField;
}
// check up the superclass hierarchy
for (Class<?> acls = cls; acls != null; acls = acls.getSuperclass()) {
try {
final Field field = acls.getDeclaredField(fieldName);
// getDeclaredField checks for non-public scopes as well
// and it returns accurate results
if (!Modifier.isPublic(field.getModifiers())) {
if (forceAccess) {
field.setAccessible(true);
} else {
continue;
}
}
synchronized (sFieldCache) {
sFieldCache.put(key, field);
}
return field;
} catch (final NoSuchFieldException ex) { // NOPMD
// ignore
}
}
// check the public interface case. This must be manually searched for
// incase there is a public supersuperclass field hidden by a private/package
// superclass field.
Field match = null;
for (final Class<?> class1 : ReflectUtils.getAllInterfaces(cls)) {
try {
final Field test = class1.getField(fieldName);
Validate.assertTrue(match == null, "Reference to field %s is ambiguous relative to %s"
+ "; a matching field exists on two or more implemented interfaces.",
fieldName, cls);
match = test;
} catch (final NoSuchFieldException ex) { // NOPMD
// ignore
}
}
synchronized (sFieldCache) {
sFieldCache.put(key, match);
}
return match;
}
public static Object readField(final Field field, final Object target, final boolean
forceAccess) throws IllegalAccessException, ReflectIllegalArgumentsException {
Validate.assertTrue(field != null, "The field must not be null");
if (forceAccess && !field.isAccessible()) {
field.setAccessible(true);
} else {
MemberUtils.setAccessibleWorkaround(field);
}
return field.get(target);
}
public static void writeField(final Field field, final Object target, final Object value,
final boolean forceAccess)
throws IllegalAccessException, ReflectIllegalArgumentsException {
Validate.assertTrue(field != null, "The field must not be null");
if (forceAccess && !field.isAccessible()) {
field.setAccessible(true);
} else {
MemberUtils.setAccessibleWorkaround(field);
}
field.set(target, value);
}
public static Object readField(final Field field, final Object target) throws
IllegalAccessException, ReflectIllegalArgumentsException {
return readField(field, target, true);
}
public static Field getField(final Class<?> cls, final String fieldName) throws ReflectIllegalArgumentsException {
return getField(cls, fieldName, true);
}
public static Object readField(final Object target, final String fieldName) throws
IllegalAccessException, ReflectIllegalArgumentsException {
Validate.assertTrue(target != null, "target object must not be null");
final Class<?> cls = target.getClass();
final Field field = getField(cls, fieldName, true);
Validate.assertTrue(field != null, "Cannot locate field %s on %s", fieldName, cls);
// already forced access above, don't repeat it here:
return readField(field, target, false);
}
public static Object readField(final Object target, final String fieldName, final boolean
forceAccess) throws IllegalAccessException, ReflectIllegalArgumentsException {
Validate.assertTrue(target != null, "target object must not be null");
final Class<?> cls = target.getClass();
final Field field = getField(cls, fieldName, forceAccess);
Validate.assertTrue(field != null, "Cannot locate field %s on %s", fieldName, cls);
// already forced access above, don't repeat it here:
return readField(field, target, forceAccess);
}
public static void writeField(final Object target, final String fieldName, final Object
value) throws IllegalAccessException, ReflectIllegalArgumentsException {
writeField(target, fieldName, value, true);
}
public static void writeField(final Object target, final String fieldName, final Object
value, final boolean forceAccess) throws IllegalAccessException, ReflectIllegalArgumentsException {
Validate.assertTrue(target != null, "target object must not be null");
final Class<?> cls = target.getClass();
final Field field = getField(cls, fieldName, true);
Validate.assertTrue(field != null, "Cannot locate declared field %s.%s", cls.getName(),
fieldName);
// already forced access above, don't repeat it here:
writeField(field, target, value, forceAccess);
}
public static void writeField(final Field field, final Object target, final Object value)
throws IllegalAccessException, ReflectIllegalArgumentsException {
writeField(field, target, value, true);
}
public static Object readStaticField(final Field field, final boolean forceAccess) throws
IllegalAccessException, ReflectIllegalArgumentsException {
Validate.assertTrue(field != null, "The field must not be null");
Validate.assertTrue(Modifier.isStatic(field.getModifiers()), "The field '%s' is not static",
field.getName());
return readField(field, (Object) null, forceAccess);
}
public static Object readStaticField(final Class<?> cls, final String fieldName) throws
IllegalAccessException, ReflectIllegalArgumentsException {
final Field field = getField(cls, fieldName, true);
Validate.assertTrue(field != null, "Cannot locate field '%s' on %s", fieldName, cls);
// already forced access above, don't repeat it here:
return readStaticField(field, true);
}
public static void writeStaticField(final Field field, final Object value, final boolean
forceAccess) throws IllegalAccessException, ReflectIllegalArgumentsException {
Validate.assertTrue(field != null, "The field must not be null");
Validate.assertTrue(Modifier.isStatic(field.getModifiers()), "The field %s.%s is not static",
field.getDeclaringClass().getName(),
field.getName());
writeField(field, (Object) null, value, forceAccess);
}
public static void writeStaticField(final Class<?> cls, final String fieldName, final Object
value) throws IllegalAccessException, ReflectIllegalArgumentsException {
final Field field = getField(cls, fieldName, true);
Validate.assertTrue(field != null, "Cannot locate field %s on %s", fieldName, cls);
// already forced access above, don't repeat it here:
writeStaticField(field, value, true);
}
public static Field getDeclaredField(final Class<?> cls, final String fieldName, final
boolean forceAccess) throws ReflectIllegalArgumentsException {
Validate.assertTrue(cls != null, "The class must not be null");
Validate.assertTrue(!TextUtils.isEmpty(fieldName), "The field name must not be blank/empty");
try {
// only consider the specified class by using getDeclaredField()
final Field field = cls.getDeclaredField(fieldName);
if (!MemberUtils.isAccessible(field)) {
if (forceAccess) {
field.setAccessible(true);
} else {
return null;
}
}
return field;
} catch (final NoSuchFieldException e) { // NOPMD
// ignore
}
return null;
}
public static void writeDeclaredField(final Object target, final String fieldName, final
Object value) throws IllegalAccessException, ReflectIllegalArgumentsException {
Validate.assertTrue(target != null, "target object must not be null");
final Class<?> cls = target.getClass();
final Field field = getDeclaredField(cls, fieldName, true);
Validate.assertTrue(field != null, "Cannot locate declared field %s.%s", cls.getName(),
fieldName);
// already forced access above, don't repeat it here:
writeField(field, target, value, false);
}
}