12

Android避坑指南,发现了一个极度不安全的操作

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzAxMTI4MTkwNQ%3D%3D&%3Bmid=2650831352&%3Bidx=1&%3Bsn=beb24471c0df44f8df11579225b59629
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

最近发现微信多了个专辑功能,可以把一系列的原创文章聚合,刚好我每周都会遇到很多同学问我各种各样的问题,部分问题还是比较有意义的,我会在周末详细的写demo验证,简单扩展一下写成文章分享给大家。

当然不鼓励大家随便私聊我问问题,大家可以去星球或者QQ群提问,我毕竟还是有工作要忙。

1

先看一个问题

来一起看一段代码:

public class Student  {
    private Student() {
        throw new IllegalArgumentException("can not create.");
    }
    public String name;

}

我们如何通过Java代码创建一个Student对象?

我们先想下通过Java创建对象大概有哪些方式:

  1. new Student() // 私有

  2. 反射调用构造方法 //throw ex

  3. 反序列化 // 需要实现相关序列化接口

  4. clone // 需要实现clone相关接口

  5. ...

好了,已经超出我的知识点范畴了。

不免心中嘀咕:

这题目太偏了,毫无意义,而且文章标题是 Android 避坑指南,看起来毫无关系

是的,确实很偏,跳过这个问题,我们往下看,看看是怎么在Android开发过程中遇到的,而且看完后,这个问题就迎刃而解了。

2

问题的来源

上周一个群有个小伙伴,遇到了一个Kotlin写的Bean,在做Gson将字符串转化成具体的Bean对象时,发生了一个不符合预期的问题。

因为是他们项目的代码,我就不贴了,我写了个类似的小例子来替代。

对于Java Bean,kotlin可以用data class,网上也有很多博客表示:

在 Kotlin 中,不需要自己动手去写一个 JavaBean,可以直接使用 DataClass,使用 DataClass 编译器会默默地帮我们生成一些函数。

我们先写个Bean:

data class Person(var name: String, var age: Int) {


}

这个Bean是用于接收服务器数据,通过Gson转化为对象的。

简化一下代码为:

val gson = Gson()
val person = gson.fromJson<Person>("{\"age\":\"12\"}", Person::class.java)

我们传递了一个json字符串,但是没有包含key为name的值,并且注意:

在Person中name的类型是String,也就是说是不允许name=null的

那么上面的代码,我运行起来结果是什么呢?

  1. 报错,毕竟没有传name的值;

  2. 不报错,name 默认值为"";

  3. 不报错,name=null;

感觉1最合理,也符合Kotlin的空安全检查。

验证一下,修改一下代码,看一下输出:

val gson = Gson()
val person = gson.fromJson<Person>("{\"age\":\"12\"}", Person::class.java)
println(person.name )

输出结果:

null

是不是有些奇怪, 感觉意外绕过了Kotlin的空类型检查。

所以那位出问题的同学,在这里之后数据就出了问题,导致一直排查困难。

我们再改一下代码:

data class Person(var name: String, var age: Int): People(){
}

我们让Person继承自People类:

public class People {

    public People(){
        System.out.println("people cons");
    }

}

在People类的构造方法中打印日志。

我们都清楚,正常情况下,一般构造子类对象,必然会先执行父类的构造方法。

运行一下:

没有执行父类构造方法,但对象构造出来了

这里可以猜到, Person对象的构建,并不是常规的构建对象,没有走构造方法。

那么它是怎么做到的呢?

只能去Gson的源码中去找答案了。

找到其怎么做的,其实就相当于解答了我们文首的问题。

3

追查原因

Gson这样构造出一个对象,但是没有走父类构造这种,如果真是的这样,那么是极其危险的。

会让程序完全不符合运行预期,少了一些必要逻辑。

所以我们提前说一下,大家不用太惊慌,并不是Gson很容易出现这样的情况,而是恰好上例的写法碰上了,我们一会会说清楚。

首先我们把Person这个kotlin的类,转成Java,避免背后藏了一些东西:

# 反编译之后的显示
public final class Person extends People {
   @NotNull
   private String name;
   private int age;

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final void setName(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.name = var1;
   }

   public final int getAge() {
      return this.age;
   }

   public final void setAge(int var1) {
      this.age = var1;
   }

   public Person(@NotNull String name, int age) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
      this.age = age;
   }

   // 省略了一些方法。
}

可以看到Person有一个包含两参的构造方法,并且这个构造方法中有name的空安全检查。

也就是说,正常通过这个构造方法构建一个Person对象,是不会出现空安全问题的。

那么只能去看看Gson的源码了:

Gson的逻辑,一般都是根据读取到的类型,然后找对应的TypeAdapter去处理,本例为Person对象,所以会最终走到`ReflectiveTypeAdapterFactory.create`然后返回一个TypeAdapter。

我们看一眼其内部代码:

# ReflectiveTypeAdapterFactory.create
@Override 
public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
    Class<? super T> raw = type.getRawType();

    if (!Object.class.isAssignableFrom(raw)) {
      return null; // it's a primitive!
    }

    ObjectConstructor<T> constructor = constructorConstructor.get(type);
    return new Adapter<T>(constructor, getBoundFields(gson, type, raw));
}

重点看constructor这个对象的赋值,它一眼就知道跟构造对象相关。

# ConstructorConstructor.get
public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
    final Type type = typeToken.getType();
    final Class<? super T> rawType = typeToken.getRawType();

    // ...省略一些缓存容器相关代码

    ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType);
    if (defaultConstructor != null) {
      return defaultConstructor;
    }

    ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType);
    if (defaultImplementation != null) {
      return defaultImplementation;
    }

    // finally try unsafe
    return newUnsafeAllocator(type, rawType);
  }

可以看到该方法的返回值有3个流程:

  1. newDefaultConstructor

  2. newDefaultImplementationConstructor

  3. newUnsafeAllocator 

我们先看第一个newDefaultConstructor

private <T> ObjectConstructor<T> newDefaultConstructor(Class<? super T> rawType) {
    try {
      final Constructor<? super T> constructor = rawType.getDeclaredConstructor();
      if (!constructor.isAccessible()) {
        constructor.setAccessible(true);
      }
      return new ObjectConstructor<T>() {
        @SuppressWarnings("unchecked") // T is the same raw type as is requested
        @Override public T construct() {
            Object[] args = null;
            return (T) constructor.newInstance(args);

            // 省略了一些异常处理
      };
    } catch (NoSuchMethodException e) {
      return null;
    }
  }

可以看到,很简单,尝试获取了无参的构造函数,如果能够找到,则通过newInstance反射的方式构建对象。

追随到我们的Person的代码,其实该类中只有一个两参的构造函数,并没有无参构造,从而会命中NoSuchMethodException,返回null。

返回null会走newDefaultImplementationConstructor,这个方法里面都是一些集合类相关对象的逻辑,直接跳过。

那么,最后只能走: newUnsafeAllocator 方法了。

从命名上面就能看出来,这是个不安全的操作。

newUnsafeAllocator最终是怎么不安全的构建出一个对象呢?

往下看,最终执行的是:

public static UnsafeAllocator create() {
// try JVM
// public class Unsafe {
//   public Object allocateInstance(Class<?> type);
// }
try {
  Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
  Field f = unsafeClass.getDeclaredField("theUnsafe");
  f.setAccessible(true);
  final Object unsafe = f.get(null);
  final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class);
  return new UnsafeAllocator() {
    @Override
    @SuppressWarnings("unchecked")
    public <T> T newInstance(Class<T> c) throws Exception {
      assertInstantiable(c);
      return (T) allocateInstance.invoke(unsafe, c);
    }
  };
} catch (Exception ignored) {
}

// try dalvikvm, post-gingerbread use ObjectStreamClass
// try dalvikvm, pre-gingerbread , ObjectInputStream

}

可以看到Gson在没有找到无参的构造方法后,通过 sun.misc.Unsafe 构造了一个对象。

注意:Unsafe该类并不是所有的Android 版本中都包含,不过目前新版本都包含,所以Gson这个方法中有3段逻辑都是用来生成对象的,你可以认为3重保险,针对不同平台。本文测试设备:Android 29模拟器

我们这里暂时只讨论sun.misc.Unsafe,其他的其实一个意思。

`sun.misc.Unsafe`何许API?

Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。

https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html

具体可以参考美团的这篇文章。

好了,到这里就真相大白了。

原因是我们Person没有提供默认的构造方法,Gson在没有找到默认构造方法时,它就直接通过Unsafe的方法,绕过了构造方法,直接构建了一个对象。

到这里,我们收获了:

  1. Gson是如何构建对象的?

  2. 我们在写需要Gson转化为对象的类的时候,一定要记得有默认的构造方法,否则虽然不报错,但是很不安全!

  3. 我们了解到了还有这种Unsafe黑科技的方式构造对象。

4

回到文章开始的问题

Java中咋么构造一个下面的Student对象呢?

public class Student  {
    private Student() {
        throw new IllegalArgumentException("can not create.");
    }
    public String name;
}

我们模仿Gson的代码,编写如下:

try {
    val unsafeClass = Class.forName("sun.misc.Unsafe")
    val f = unsafeClass.getDeclaredField("theUnsafe")
    f.isAccessible = true
    val unsafe = f.get(null)
    val allocateInstance = unsafeClass.getMethod("allocateInstance", Class::class.java)
    val student = allocateInstance.invoke(unsafe, Student::class.java)
    (student as Student).apply {
        name = "zhy"
    }
    println(student.name)
} catch (ignored: Exception) {
    ignored.printStackTrace()
}

输出:

zhy

成功构建。

5

Unsafe 一点用没有?

看到这里,大家可能最大的收获就是了解Gson构建对象流程,以及以后写Bean的时候会注意提供默认的无参构造方法,尤其在使用Kotlin  `data class `的时候。

那么刚才我们所说的Unsafe方法在Android上就没有其他实际用处吗?

这个类,提供了类似C语言指针一样操作内存空间的能力。

大家都知道在Android P上面,Google限制了app对hidden API的访问。

但是,Google不能限制自己对hidden API访问对吧,所以它自己的相关类,是允许访问hidden API的。

那么Google是如何区分是我们app调用,还是它自己调用呢?

其中有一个办法就是通过ClassLoader,系统认为如果ClassLoader为BootStrapClassLoader则就认为是系统类,则放行。

那么,我们突破P访问限制,其中一个思路就是,搞一个类,把它的ClassLoader换成BootStrapClassLoader,从而可以反射任何hidden api。

怎么换呢?

只要把这个类的classLoader成员变量设置为null就可以了。

参考代码:

private void testJavaPojie() {
    try {
      Class reflectionHelperClz = Class.forName("com.example.support_p.ReflectionHelper");
      Class classClz = Class.class;
      Field classLoaderField = classClz.getDeclaredField("classLoader");
      classLoaderField.setAccessible(true);
      classLoaderField.set(reflectionHelperClz, null);
    } catch (Exception e) {
          e.printStackTrace();
    }
}
来自:https://juejin.im/post/5ba0f3f7e51d450e6f2e39e0

但是这样有个问题,上面的代码用到了反射修改一个类的classLoader成员,假设google有一天把反射设置classLoader也完全限制掉,就不行了。

那么怎么办?原理还是换ClassLoader,但是我们不走Java反射的方式了,而是用Unsafe:

参考代码:

@Keep
public class ReflectWrapper {

    //just for finding the java.lang.Class classLoader field's offset
    @Keep
    private Object classLoaderOffsetHelper;

    static {
        try {
            Class<?> VersionClass = Class.forName("android.os.Build$VERSION");
            Field sdkIntField = VersionClass.getDeclaredField("SDK_INT");
            sdkIntField.setAccessible(true);
            int sdkInt = sdkIntField.getInt(null);
            if (sdkInt >= 28) {
                Field classLoader = ReflectWrapper.class.getDeclaredField("classLoaderOffsetHelper");
                long classLoaderOffset = UnSafeWrapper.getUnSafe().objectFieldOffset(classLoader);
                if (UnSafeWrapper.getUnSafe().getObject(ReflectWrapper.class, classLoaderOffset) instanceof ClassLoader) {
                    Object originalClassLoader = UnSafeWrapper.getUnSafe().getAndSetObject(ReflectWrapper.class, classLoaderOffset, null);
                } else {
                    throw new RuntimeException("not support");
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
来自作者区长:一种纯 Java 层绕过 Android P 私有函数调用限制的方式,一文。

Unsafe赋予了我们操作内存的能力,也就能完成一些平时只能依赖C++完成的代码。

好了,从一位朋友遇到的问题,由此引发了一整篇文章的讨论,希望你能有所收获。

感谢郭霖,淡蓝色星期三,天空等朋友。

推荐阅读

“手把手”的性能优化文章来了!

“新技术” 又又又又来了?

ViewBinding 实战,递进优雅的写波代码

vEVjqa7.jpg!web

扫一扫  关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~

┏(^0^)┛明天见!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK