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

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

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

1. 先看一个问题

来一起看一段代码:

  1. public class Student  { 
  2.     private Student() { 
  3.         throw new IllegalArgumentException("can not create."); 
  4.     } 
  5.     public String name; 
  6.  

我们如何通过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:

  1. data class Person(var name: String, var age: Int) { 
  2.  
  3.  

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

简化一下代码为:

  1. val gson = Gson() 
  2. 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的空安全检查。

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

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

输出结果:

  1. null 

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

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

我们再改一下代码:

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

我们让Person继承自People类:

  1. public class People { 
  2.  
  3.     public People(){ 
  4.         System.out.println("people cons"); 
  5.     } 
  6.  

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

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

运行一下:

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

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

那么它是怎么做到的呢?

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

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

3. 追查原因

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

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

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

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

  1. # 反编译之后的显示 
  2. public final class Person extends People { 
  3.    @NotNull 
  4.    private String name; 
  5.    private int age; 
  6.  
  7.    @NotNull 
  8.    public final String getName() { 
  9.       return this.name; 
  10.    } 
  11.  
  12.    public final void setName(@NotNull String var1) { 
  13.       Intrinsics.checkParameterIsNotNull(var1, "<set-?>"); 
  14.       this.name = var1; 
  15.    } 
  16.  
  17.    public final int getAge() { 
  18.       return this.age; 
  19.    } 
  20.  
  21.    public final void setAge(int var1) { 
  22.       this.age = var1; 
  23.    } 
  24.  
  25.    public Person(@NotNull String name, int age) { 
  26.       Intrinsics.checkParameterIsNotNull(name, "name"); 
  27.       super(); 
  28.       this.name = name; 
  29.       this.age = age; 
  30.    } 
  31.  
  32.    // 省略了一些方法。 

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

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

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

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

我们看一眼其内部代码:

  1. # ReflectiveTypeAdapterFactory.create 
  2. @Override  
  3. public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) { 
  4.     Class<? super T> raw = type.getRawType(); 
  5.  
  6.     if (!Object.class.isAssignableFrom(raw)) { 
  7.       return null// it's a primitive! 
  8.     } 
  9.  
  10.     ObjectConstructor<T> constructor = constructorConstructor.get(type); 
  11.     return new Adapter<T>(constructor, getBoundFields(gson, type, raw)); 

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

  1. # ConstructorConstructor.get 
  2. public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) { 
  3.     final Type type = typeToken.getType(); 
  4.     final Class<? super T> rawType = typeToken.getRawType(); 
  5.  
  6.     // ...省略一些缓存容器相关代码 
  7.  
  8.     ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType); 
  9.     if (defaultConstructor != null) { 
  10.       return defaultConstructor; 
  11.     } 
  12.  
  13.     ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType); 
  14.     if (defaultImplementation != null) { 
  15.       return defaultImplementation; 
  16.     } 
  17.  
  18.     // finally try unsafe 
  19.     return newUnsafeAllocator(type, rawType); 
  20.   } 

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

  1. newDefaultConstructor

  2. newDefaultImplementationConstructor

  3. newUnsafeAllocator 

我们先看第一个newDefaultConstructor

  1. private <T> ObjectConstructor<T> newDefaultConstructor(Class<? super T> rawType) { 
  2.     try { 
  3.       final Constructor<? super T> constructor = rawType.getDeclaredConstructor(); 
  4.       if (!constructor.isAccessible()) { 
  5.         constructor.setAccessible(true); 
  6.       } 
  7.       return new ObjectConstructor<T>() { 
  8.         @SuppressWarnings("unchecked"// T is the same raw type as is requested 
  9.         @Override public T construct() { 
  10.             Object[] args = null
  11.             return (T) constructor.newInstance(args); 
  12.  
  13.             // 省略了一些异常处理 
  14.       }; 
  15.     } catch (NoSuchMethodException e) { 
  16.       return null
  17.     } 
  18.   } 

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

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

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

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

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

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

往下看,最终执行的是:

  1. public static UnsafeAllocator create() { 
  2. // try JVM 
  3. // public class Unsafe { 
  4. //   public Object allocateInstance(Class<?> type); 
  5. // } 
  6. try { 
  7.   Class<?> unsafeClass = Class.forName("sun.misc.Unsafe"); 
  8.   Field f = unsafeClass.getDeclaredField("theUnsafe"); 
  9.   f.setAccessible(true); 
  10.   final Object unsafe = f.get(null); 
  11.   final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class); 
  12.   return new UnsafeAllocator() { 
  13.     @Override 
  14.     @SuppressWarnings("unchecked"
  15.     public <T> T newInstance(Class<T> c) throws Exception { 
  16.       assertInstantiable(c); 
  17.       return (T) allocateInstance.invoke(unsafe, c); 
  18.     } 
  19.   }; 
  20. catch (Exception ignored) { 
  21.  
  22. // try dalvikvm, post-gingerbread use ObjectStreamClass 
  23. // try dalvikvm, pre-gingerbread , ObjectInputStream 
  24.  

可以看到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对象呢?

  1. public class Student  { 
  2.     private Student() { 
  3.         throw new IllegalArgumentException("can not create."); 
  4.     } 
  5.     public String name; 

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

  1. try { 
  2.     val unsafeClass = Class.forName("sun.misc.Unsafe"
  3.     val f = unsafeClass.getDeclaredField("theUnsafe"
  4.     f.isAccessible = true 
  5.     val unsafe = f.get(null
  6.     val allocateInstance = unsafeClass.getMethod("allocateInstance", Class::class.java) 
  7.     val student = allocateInstance.invoke(unsafe, Student::class.java) 
  8.     (student as Student).apply { 
  9.         name = "zhy" 
  10.     } 
  11.     println(student.name) 
  12. catch (ignored: Exception) { 
  13.     ignored.printStackTrace() 

输出:

  1. shy 

成功构建。

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就可以了。

参考代码:

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

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

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

参考代码:

  1. @Keep 
  2. public class ReflectWrapper { 
  3.  
  4.     //just for finding the java.lang.Class classLoader field's offset 
  5.     @Keep 
  6.     private Object classLoaderOffsetHelper; 
  7.  
  8.     static { 
  9.         try { 
  10.             Class<?> VersionClass = Class.forName("android.os.Build$VERSION"); 
  11.             Field sdkIntField = VersionClass.getDeclaredField("SDK_INT"); 
  12.             sdkIntField.setAccessible(true); 
  13.             int sdkInt = sdkIntField.getInt(null); 
  14.             if (sdkInt >= 28) { 
  15.                 Field classLoader = ReflectWrapper.class.getDeclaredField("classLoaderOffsetHelper"); 
  16.                 long classLoaderOffset = UnSafeWrapper.getUnSafe().objectFieldOffset(classLoader); 
  17.                 if (UnSafeWrapper.getUnSafe().getObject(ReflectWrapper.class, classLoaderOffset) instanceof ClassLoader) { 
  18.                     Object originalClassLoader = UnSafeWrapper.getUnSafe().getAndSetObject(ReflectWrapper.class, classLoaderOffset, null); 
  19.                 } else { 
  20.                     throw new RuntimeException("not support"); 
  21.                 } 
  22.             } 
  23.         } catch (Exception e) { 
  24.             throw new RuntimeException(e); 
  25.         } 
  26.     } 
  27. 来自作者区长:一种纯 Java 层绕过 Android P 私有函数调用限制的方式,一文。 

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

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

责任编辑:张燕妮 来源: 鸿洋
相关推荐

2014-04-09 09:37:29

2021-06-02 08:00:57

WebAsyncTas项目异步

2021-10-29 11:45:26

Python代码Python 3.

2015-07-01 14:48:51

2015-05-27 13:19:23

2022-11-30 09:18:51

JavaMyBatisMQ

2023-02-26 01:02:22

2021-04-22 07:47:47

JavaJDKMYSQL

2021-12-29 19:20:41

数据GitHub服务器

2021-01-26 11:16:12

漏洞网络安全网络攻击

2018-01-20 20:46:33

2023-06-01 19:24:16

2015-10-16 10:03:36

安卓设备安全终端安全

2020-06-12 11:03:22

Python开发工具

2011-11-28 09:20:38

2019-01-14 11:10:43

机器学习人工智能计算机

2024-05-20 08:25:55

2016-12-09 09:00:32

大数据风控金融

2017-07-24 20:55:42

2019-03-30 14:39:11

点赞
收藏

51CTO技术栈公众号