Android黑科技——破解系统隐藏API

1 背景

以下内容摘录自官方文档

针对非 SDK 接口的限制

从 Android 9(API 级别 28)开始,Android 平台对应用能使用的 SDK 接口 实施了限制。只要应用引用非 SDK 接口或尝试使用反射或 JNI 来获取其句柄,这些限制就适用。这些限制旨在帮助提升用户体验和开发者体验,为用户降低应用发生崩溃的风险,同时为开发者降低紧急发布的风险。如需详细了解有关此限制的决定,请参阅通过减少非 SDK 接口的使用来提高稳定性

官方文档

developer.android.com/guide/app-c...

2 资源准备

git clone android.googlesource.com/platform/ar... --depth 1

3 方法反射(Method Reflection)

Java 类java.lang.reflect.Method实例是对类方法(Method)的反射。Method类继承自通用抽象父类Executable,其自身是不可变(Immutable)类。

方法 描述
Method[] getMethods() 返回目标类中所有可访问的公开方法,包括从父类继承的公开方法。
Method[] getDeclaredMethods() 返回目标类中所有方法,不包括从父类继承的方法。
Method getMethod(String name, Class... parameterTypes) 根据方法名与参数类型取得目标方法对象。
Method getDeclaredMethod(String name, Class... parameterTypes) 根据方法名与参数类型取得目标方法对象,不包括从父类继承的方法。

大白话解读:

  • getDeclaredMethod:获取当前类的所有声明的方法,包括public、protected和private修饰的方法。需要注意的是,这些方法一定是在当前类中声明的,从父类中继承的不算,实现接口的方法由于有声明所以包括在内。

  • getMethod:获取当前类和父类的所有public的方法。这里的父类,指的是继承层次中的所有父类。比如说,A继承B,B继承C,那么B和C都属于A的父类。

案例:

typescript 复制代码
public class Fruit {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
scala 复制代码
public class Apple extends Fruit {

    private int price;

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }
}
java 复制代码
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class ReflectTest {

    public static void main(String[] args) throws Exception {
        //正常调用
        Apple apple = new Apple();
        apple.setPrice(5);
        System.out.println("Apple Price:" + apple.getPrice());
        //使用反射调用-getMethod
        Class clz = Class.forName("Apple");
        Method setPriceMethod = clz.getMethod("setPrice", int.class);
        Constructor appleConstructor = clz.getConstructor();
        Object appleObj = appleConstructor.newInstance();
        setPriceMethod.invoke(appleObj, 14);
        Method getPriceMethod = clz.getMethod("getPrice");
        System.out.println("Apple Price:" + getPriceMethod.invoke(appleObj));

        System.out.println("================================");
        //静态方法反射调用
        Method showMethod = clz.getMethod("show",int.class);
        showMethod.invoke(null,9);

        System.out.println("================================");
        //获取本类+父类所有方法
        Method[] methods = clz.getMethods();
        for (Method method : methods) {
            System.out.println("method:" + method);
        }
        System.out.println("================================");
        //获取本类所有方法
        Method[] declaredMethods = clz.getDeclaredMethods();
        for (Method method : declaredMethods) {
            System.out.println("declaredMethod:" + method);
        }

        System.out.println("================================");
        //使用反射调用-getDeclaredMethod
        setPriceMethod = clz.getDeclaredMethod("setPrice", int.class);
        appleConstructor = clz.getConstructor();
        appleObj = appleConstructor.newInstance();
        setPriceMethod.invoke(appleObj, 22);
        getPriceMethod = clz.getDeclaredMethod("getPrice");
        System.out.println("Apple Price:" + getPriceMethod.invoke(appleObj));
    }
}

运行结果:

vbnet 复制代码
Apple Price:5
Apple Price:14
================================
show static method called,price:9
================================
method:public static void Apple.show(int)
method:public void Apple.setPrice(int)
method:public int Apple.getPrice()
method:public java.lang.String Fruit.getName()
method:public void Fruit.setName(java.lang.String)
method:public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
method:public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
method:public final void java.lang.Object.wait() throws java.lang.InterruptedException
method:public boolean java.lang.Object.equals(java.lang.Object)
method:public java.lang.String java.lang.Object.toString()
method:public native int java.lang.Object.hashCode()
method:public final native java.lang.Class java.lang.Object.getClass()
method:public final native void java.lang.Object.notify()
method:public final native void java.lang.Object.notifyAll()
================================
declaredMethod:public static void Apple.show(int)
declaredMethod:public void Apple.setPrice(int)
declaredMethod:public int Apple.getPrice()
================================
Apple Price:22

4 源码分析

以访问Ldalvik/system/VMRuntime;->setTargetSdkVersion(I)V为例

kotlin 复制代码
try {
    val runtimeClass = Class.forName("dalvik.system.VMRuntime")
    val nativeLoadMethod = runtimeClass.getDeclaredMethod(
        "setTargetSdkVersionNative",
        *arrayOf<Class<*>?>(Int::class.javaPrimitiveType)
    )
    Log.i(TAG, "setTargetSdkVersionNative success,nativeLoadMethod:$nativeLoadMethod")
} catch (e: Throwable) {
    e.printStackTrace()
}

然后运行时抛出了异常NoSuchMethodException

php 复制代码
2024-06-20 07:57:04.716 29837-29837 .ndk.hidden.api         com.ygq.ndk.hidden.api               W  Accessing hidden method Ldalvik/system/VMRuntime;->setTargetSdkVersionNative(I)V (blocked, reflection, denied)
2024-06-20 07:57:04.717 29837-29837 System.err              com.ygq.ndk.hidden.api               W  java.lang.NoSuchMethodException: dalvik.system.VMRuntime.setTargetSdkVersionNative [int]
2024-06-20 07:57:04.717 29837-29837 System.err              com.ygq.ndk.hidden.api               W          at java.lang.Class.getMethod(Class.java:2937)
2024-06-20 07:57:04.717 29837-29837 System.err              com.ygq.ndk.hidden.api               W          at java.lang.Class.getDeclaredMethod(Class.java:2914)
2024-06-20 07:57:04.717 29837-29837 System.err              com.ygq.ndk.hidden.api               W          at com.ygq.ndk.hiddenapi.MainActivity.onCreate$lambda$0(MainActivity.kt:32)
2024-06-20 07:57:04.717 29837-29837 System.err              com.ygq.ndk.hidden.api               W          at com.ygq.ndk.hiddenapi.MainActivity.$r8$lambda$bSb4PNoYIVmHUKhD73qlXfirQvk(Unknown Source:0)
2024-06-20 07:57:04.717 29837-29837 System.err              com.ygq.ndk.hidden.api               W          at com.ygq.ndk.hiddenapi.MainActivity$$ExternalSyntheticLambda0.onClick(Unknown Source:0)
2024-06-20 07:57:04.717 29837-29837 System.err              com.ygq.ndk.hidden.api               W          at android.view.View.performClick(View.java:7799)
2024-06-20 07:57:04.717 29837-29837 System.err              com.ygq.ndk.hidden.api               W          at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1218)
2024-06-20 07:57:04.717 29837-29837 System.err              com.ygq.ndk.hidden.api               W          at android.view.View.performClickInternal(View.java:7776)
2024-06-20 07:57:04.717 29837-29837 System.err              com.ygq.ndk.hidden.api               W          at android.view.View.-$$Nest$mperformClickInternal(Unknown Source:0)
2024-06-20 07:57:04.718 29837-29837 System.err              com.ygq.ndk.hidden.api               W          at android.view.View$PerformClick.run(View.java:31213)
2024-06-20 07:57:04.718 29837-29837 System.err              com.ygq.ndk.hidden.api               W          at android.os.Handler.handleCallback(Handler.java:958)
2024-06-20 07:57:04.718 29837-29837 System.err              com.ygq.ndk.hidden.api               W          at android.os.Handler.dispatchMessage(Handler.java:99)
2024-06-20 07:57:04.718 29837-29837 System.err              com.ygq.ndk.hidden.api               W          at android.os.Looper.loopOnce(Looper.java:224)
2024-06-20 07:57:04.718 29837-29837 System.err              com.ygq.ndk.hidden.api               W          at android.os.Looper.loop(Looper.java:318)
2024-06-20 07:57:04.718 29837-29837 System.err              com.ygq.ndk.hidden.api               W          at android.app.ActivityThread.main(ActivityThread.java:8754)
2024-06-20 07:57:04.718 29837-29837 System.err              com.ygq.ndk.hidden.api               W          at java.lang.reflect.Method.invoke(Native Method)
2024-06-20 07:57:04.718 29837-29837 System.err              com.ygq.ndk.hidden.api               W          at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:561)
2024-06-20 07:57:04.718 29837-29837 System.err              com.ygq.ndk.hidden.api               W          at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1013)

结论:非 sdk 接口,greylist以及whitelist不受限制,但是blacklist以及greylist-max-x会进行限制

4.1 查找漏洞

从android framework的角度分析非sdk接口限制的原理,找到系统漏洞

java.lang.Class;->getDeclaredMethod函数,源码如下:

java 复制代码
@CallerSensitive
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
    throws NoSuchMethodException, SecurityException {
    // Android-changed: ART has a different JNI layer.
    return getMethod(name, parameterTypes, false);
}

其内部调用了getMethod函数

java 复制代码
// BEGIN Android-added: Internal methods to implement getMethod(...).
private Method getMethod(String name, Class<?>[] parameterTypes, boolean recursivePublicMethods)
        throws NoSuchMethodException {
    if (name == null) {
        throw new NullPointerException("name == null");
    }
    if (parameterTypes == null) {
        parameterTypes = EmptyArray.CLASS;
    }
    for (Class<?> c : parameterTypes) {
        if (c == null) {
            throw new NoSuchMethodException("parameter type is null");
        }
    }
    Method result = recursivePublicMethods ? getPublicMethodRecursive(name, parameterTypes)
                                           : getDeclaredMethodInternal(name, parameterTypes);
    // Fail if we didn't find the method or it was expected to be public.
    if (result == null ||
        (recursivePublicMethods && !Modifier.isPublic(result.getAccessFlags()))) {
        throw new NoSuchMethodException(getName() + "." + name + " "
                + Arrays.toString(parameterTypes));
    }
    return result;
}

第三个参数recursivePublicMethods为false,所以内部实际调用的是getDeclaredMethodInternal

java 复制代码
@FastNative
private native Method getDeclaredMethodInternal(String name, Class<?>[] args);

getDeclaredMethodInternal为native函数,会调用到c++

4.2 源码分析

查看java_lang_Class.cc中getDeclaredMethodInternal源码

scss 复制代码
//art/runtime/native/java_lang_Class.cc

static jobject Class_getDeclaredMethodInternal(JNIEnv* env, jobject javaThis,
                                               jstring name, jobjectArray args) {
  ScopedFastNativeObjectAccess soa(env);
  StackHandleScope<1> hs(soa.Self());
  DCHECK_EQ(Runtime::Current()->GetClassLinker()->GetImagePointerSize(), kRuntimePointerSize);
  DCHECK(!Runtime::Current()->IsActiveTransaction());
  ObjPtr<mirror::Class> klass = DecodeClass(soa, javaThis);
  if (UNLIKELY(klass->IsObsoleteObject())) {
    ThrowRuntimeException("Obsolete Object!");
    return nullptr;
  }
  Handle<mirror::Method> result = hs.NewHandle(
      mirror::Class::GetDeclaredMethodInternal<kRuntimePointerSize>(
          soa.Self(),
          klass,
          soa.Decode<mirror::String>(name),
          soa.Decode<mirror::ObjectArray<mirror::Class>>(args),
          GetHiddenapiAccessContextFunction(soa.Self()))); /* 3.hiddenapi访问上下文 */ 
  if (result == nullptr || ShouldDenyAccessToMember(result->GetArtMethod(), soa.Self())) {
    return nullptr;
  }
  return soa.AddLocalReference<jobject>(result.Get());
}

其内部调用了mirror::Class::GetDeclaredMethodInternal

C 复制代码
//art/runtime/mirror/class.cc

template <PointerSize kPointerSize>
ObjPtr<Method> Class::GetDeclaredMethodInternal(
    Thread* self,
    ObjPtr<Class> klass,
    ObjPtr<String> name,
    ObjPtr<ObjectArray<Class>> args,
    const std::function<hiddenapi::AccessContext()>& fn_get_access_context) {
  // Covariant return types (or smali) permit the class to define
  // multiple methods with the same name and parameter types.
  // Prefer (in decreasing order of importance):
  //  1) non-hidden method over hidden
  //  2) virtual methods over direct
  //  3) non-synthetic methods over synthetic
  // We never return miranda methods that were synthesized by the runtime.
  StackHandleScope<3> hs(self);
  auto h_method_name = hs.NewHandle(name);
  if (UNLIKELY(h_method_name == nullptr)) {
    ThrowNullPointerException("name == null");
    return nullptr;
  }
  auto h_args = hs.NewHandle(args);
  Handle<Class> h_klass = hs.NewHandle(klass);
  constexpr hiddenapi::AccessMethod access_method = hiddenapi::AccessMethod::kNone;
  ArtMethod* result = nullptr;
  bool result_hidden = false;
  for (auto& m : h_klass->GetDeclaredVirtualMethods(kPointerSize)) { /* 4.遍历virtual method */ 
    if (m.IsMiranda()) {
      continue;
    }
    ArtMethod* np_method = m.GetInterfaceMethodIfProxy(kPointerSize);
    if (!np_method->NameEquals(h_method_name.Get())) { /* 5.判断方法名与参数类型 */ 
      continue;
    }
    // `ArtMethod::EqualParameters()` may throw when resolving types.
    if (!np_method->EqualParameters(h_args)) {
      if (UNLIKELY(self->IsExceptionPending())) {
        return nullptr;
      }
      continue;
    }
    bool m_hidden = hiddenapi::ShouldDenyAccessToMember(&m, fn_get_access_context, access_method); /* 6.调用ShouldDenyAccessToMember */ 
    if (!m_hidden && !m.IsSynthetic()) {
      // Non-hidden, virtual, non-synthetic. Best possible result, exit early.
      return Method::CreateFromArtMethod<kPointerSize>(self, &m);
    } else if (IsMethodPreferredOver(result, result_hidden, &m, m_hidden)) {
      // Remember as potential result.
      result = &m;
      result_hidden = m_hidden;
    }
  }

  if ((result != nullptr) && !result_hidden) {
    // We have not found a non-hidden, virtual, non-synthetic method, but
    // if we have found a non-hidden, virtual, synthetic method, we cannot
    // do better than that later.
    DCHECK(!result->IsDirect());
    DCHECK(result->IsSynthetic());
  } else {
    for (auto& m : h_klass->GetDirectMethods(kPointerSize)) { /* 7.遍历direct method */ 
      auto modifiers = m.GetAccessFlags();
      if ((modifiers & kAccConstructor) != 0) {
        continue;
      }
      ArtMethod* np_method = m.GetInterfaceMethodIfProxy(kPointerSize);
      if (!np_method->NameEquals(h_method_name.Get())) { /* 8.判断方法名与参数类型 */ 
        continue;
      }
      // `ArtMethod::EqualParameters()` may throw when resolving types.
      if (!np_method->EqualParameters(h_args)) {
        if (UNLIKELY(self->IsExceptionPending())) {
          return nullptr;
        }
        continue;
      }
      DCHECK(!m.IsMiranda());  // Direct methods cannot be miranda methods.
      bool m_hidden = hiddenapi::ShouldDenyAccessToMember(&m, fn_get_access_context, access_method); /* 9.调用ShouldDenyAccessToMember */ 
      if (!m_hidden && !m.IsSynthetic()) {
        // Non-hidden, direct, non-synthetic. Any virtual result could only have been
        // hidden, therefore this is the best possible match. Exit now.
        DCHECK((result == nullptr) || result_hidden);
        return Method::CreateFromArtMethod<kPointerSize>(self, &m);
      } else if (IsMethodPreferredOver(result, result_hidden, &m, m_hidden)) {
        // Remember as potential result.
        result = &m;
        result_hidden = m_hidden;
      }
    }
  }

  return result != nullptr
      ? Method::CreateFromArtMethod<kPointerSize>(self, result)
      : nullptr;
}

4.3 权限判断

接下来看看ShouldDenyAccessToMember函数源码

c 复制代码
//art/runtime/hidden_api.cc

template <typename T>
bool ShouldDenyAccessToMember(T* member,
                              const std::function<AccessContext()>& fn_get_access_context,
                              AccessMethod access_method) {
  DCHECK(member != nullptr);

  // First check if we have an explicit sdk checker installed that should be used to
  // verify access. If so, make the decision based on it.
  //
  // This is used during off-device AOT compilation which may want to generate verification
  // metadata only for a specific list of public SDKs. Note that the check here is made
  // based on descriptor equality and it's aim to further restrict a symbol that would
  // otherwise be resolved.
  //
  // The check only applies to boot classpaths dex files.
  Runtime* runtime = Runtime::Current();
  if (UNLIKELY(runtime->IsAotCompiler())) {
    if (member->GetDeclaringClass()->IsBootStrapClassLoaded() &&
        runtime->GetClassLinker()->DenyAccessBasedOnPublicSdk(member)) {
      return true;
    }
  }

  // Get the runtime flags encoded in member's access flags.
  // Note: this works for proxy methods because they inherit access flags from their
  // respective interface methods.
  const uint32_t runtime_flags = GetRuntimeFlags(member);

  // Exit early if member is public API. This flag is also set for non-boot class
  // path fields/methods.
  if ((runtime_flags & kAccPublicApi) != 0) { /* 1.如果方法是public api,则允许访问 */ 
    return false;
  }

  // Determine which domain the caller and callee belong to.
  // This can be *very* expensive. This is why ShouldDenyAccessToMember
  // should not be called on every individual access.
  const AccessContext caller_context = fn_get_access_context(); /* 2.获取caller的上下文 */ 
  const AccessContext callee_context(member->GetDeclaringClass()); /* 3.获取所调用方法的上下文 */ 

  // Non-boot classpath callers should have exited early.
  DCHECK(!callee_context.IsApplicationDomain());

  // Check if the caller is always allowed to access members in the callee context.
  if (caller_context.CanAlwaysAccess(callee_context)) { /* 4.caller是否可以不受约束访问callee */ 
    return false;
  }

  // Check if this is platform accessing core platform. We may warn if `member` is
  // not part of core platform API.
  switch (caller_context.GetDomain()) {  /* 5.根据domain级别区分对待hiddenapi策略 */ 
    case Domain::kApplication: {
      DCHECK(!callee_context.IsApplicationDomain());

      // Exit early if access checks are completely disabled.
      EnforcementPolicy policy = runtime->GetHiddenApiEnforcementPolicy();
      if (policy == EnforcementPolicy::kDisabled) { /* 5.1.如果policy disable,则返回false,即允许访问 */ 
        return false;
      }

      // If this is a proxy method, look at the interface method instead.
      member = detail::GetInterfaceMemberIfProxy(member);

      // Decode hidden API access flags from the dex file.
      // This is an O(N) operation scaling with the number of fields/methods
      // in the class. Only do this on slow path and only do it once.
      ApiList api_list(detail::GetDexFlags(member));
      DCHECK(api_list.IsValid());

      // Member is hidden and caller is not exempted. Enter slow path.
      return detail::ShouldDenyAccessToMemberImpl(member, api_list, access_method);
    }

    case Domain::kPlatform: {
      DCHECK(callee_context.GetDomain() == Domain::kCorePlatform);

      // Member is part of core platform API. Accessing it is allowed.
      if ((runtime_flags & kAccCorePlatformApi) != 0) {
        return false;
      }

      // Allow access if access checks are disabled.
      EnforcementPolicy policy = Runtime::Current()->GetCorePlatformApiEnforcementPolicy();
      if (policy == EnforcementPolicy::kDisabled) {
        return false;
      }

      // If this is a proxy method, look at the interface method instead.
      member = detail::GetInterfaceMemberIfProxy(member);

      // Access checks are not disabled, report the violation.
      // This may also add kAccCorePlatformApi to the access flags of `member`
      // so as to not warn again on next access.
      return detail::HandleCorePlatformApiViolation(member, caller_context, access_method, policy);
    }

    case Domain::kCorePlatform: {
      LOG(FATAL) << "CorePlatform domain should be allowed to access all domains";
      UNREACHABLE();
    }
  }
}

fn_get_access_context的源头是在java_lang_Class->getMethodIdInternal传过来的函数指针,即GetHiddenapiAccessContextFunction

c 复制代码
//art/runtime/native/java_lang_Class.cc

static std::function<hiddenapi::AccessContext()> GetHiddenapiAccessContextFunction(Thread* self) {
  return [=]() REQUIRES_SHARED(Locks::mutator_lock_) {
    return hiddenapi::GetReflectionCallerAccessContext(self);
  };
}
c 复制代码
//art/runtime/hidden_api.h

class AccessContext {
    ...
    // Returns true if this domain is always allowed to access the domain of `callee`.
    bool CanAlwaysAccess(const AccessContext& callee) const {
      return IsDomainMoreTrustedThan(domain_, callee.domain_);
    }
    ...
}
c 复制代码
//art/libartbase/base/hiddenapi_domain.h

enum class Domain : char {
  kCorePlatform = 0,
  kPlatform,
  kApplication,
};

inline bool IsDomainMoreTrustedThan(Domain domainA, Domain domainB) {
  return static_cast<char>(domainA) <= static_cast<char>(domainB);
}

也就是说,如果caller的domain值越小,能访问的hiddenapi范围越广。比如corePlatform能访问所有级别的api,但是application级别不能访问corePlatform以及platform级别的api。

4.4 Android 系统API的隐藏策略

  1. 第三方app肯定会走到这里,根据domain级别区分对待hiddenapi策略
  2. 如果policy disable,则返回false,即允许访问(重点)
  3. 最终会调用detail::ShouldDenyAccessToMemberImpl(member, api_list, access_method)
c 复制代码
//art/runtime/hidden_api.cc

template <typename T>
bool ShouldDenyAccessToMemberImpl(T* member, ApiList api_list, AccessMethod access_method) {
  DCHECK(member != nullptr);
  Runtime* runtime = Runtime::Current();
  CompatFramework& compatFramework = runtime->GetCompatFramework();

  EnforcementPolicy hiddenApiPolicy = runtime->GetHiddenApiEnforcementPolicy();
  DCHECK(hiddenApiPolicy != EnforcementPolicy::kDisabled)
      << "Should never enter this function when access checks are completely disabled";

  MemberSignature member_signature(member);

  // Check for an exemption first. Exempted APIs are treated as SDK.
  if (member_signature.DoesPrefixMatchAny(runtime->GetHiddenApiExemptions())) {
    // Avoid re-examining the exemption list next time.
    // Note this results in no warning for the member, which seems like what one would expect.
    // Exemptions effectively adds new members to the public API list.
    MaybeUpdateAccessFlags(runtime, member, kAccPublicApi);
    return false;
  }
  ...
}

这里返回了false(重点), 后边的代码省略,因为当前已经找到了两处返回false的地方(返回false表示可以访问)。

4.4.1 第一处返回false关键点

c 复制代码
//art/runtime/hidden_api.cc
      
      // Exit early if access checks are completely disabled.
      EnforcementPolicy policy = runtime->GetHiddenApiEnforcementPolicy();
      if (policy == EnforcementPolicy::kDisabled) { /* 5.1.如果policydisable,则返回false,即允许访问 */ 
        return false;
      }

也就是说policy策略关闭时,可以自由访问hiddenapi,该方案可使用FreeReflection

但是需要适配不同的系统版本。

开源的方案是通过修改runtime内存实现的, 内存hidden_api_policy_的偏移值可因为系统版本,也可因为厂家定制导致不统一,所以该方案兼容性问题较大。

c 复制代码
//art/runtime/runtime.h

hiddenapi::EnforcementPolicy GetHiddenApiEnforcementPolicy() const {
  return hidden_api_policy_;
}

void SetHiddenApiEnforcementPolicy(hiddenapi::EnforcementPolicy policy) {
  hidden_api_policy_ = policy;
}

4.4.2 第二处返回false关键点

runtime->GetHiddenApiExemptions, 顾名思义:获取豁免的hiddenapi签名

c 复制代码
//art/runtime/hidden_api.cc
  
  // Check for an exemption first. Exempted APIs are treated as SDK.
  if (member_signature.DoesPrefixMatchAny(runtime->GetHiddenApiExemptions())) {
    // Avoid re-examining the exemption list next time.
    // Note this results in no warning for the member, which seems like what one would expect.
    // Exemptions effectively adds new members to the public API list.
    MaybeUpdateAccessFlags(runtime, member, kAccPublicApi);
    return false;
  }

MemberSignature::DoesPrefixMatchAny函数

c 复制代码
//art/runtime/hidden_api.cc

bool MemberSignature::DoesPrefixMatchAny(const std::vector<std::string>& exemptions) {
  for (const std::string& exemption : exemptions) {
    if (DoesPrefixMatch(exemption)) {
      return true;
    }
  }
  return false;
}

该函数会遍历exemptions,只要有一个exemption,匹配当前访问的method->signature前缀,就可返回false。

c 复制代码
//art/runtime/hidden_api.cc

bool MemberSignature::DoesPrefixMatch(const std::string& prefix) const {
  size_t pos = 0;
  for (const char* part : GetSignatureParts()) {
    size_t count = std::min(prefix.length() - pos, strlen(part));
    if (prefix.compare(pos, count, part, 0, count) == 0) {
      pos += count;
    } else {
      return false;
    }
  }
  // We have a complete match if all parts match (we exit the loop without
  // returning) AND we've matched the whole prefix.
  return pos == prefix.length();
}

4.4.3 接下来看看MemberSignature->GetSignatureParts

c 复制代码
//art/runtime/hidden_api.cc

inline std::vector<const char*> MemberSignature::GetSignatureParts() const {
  if (type_ == kField) {
    return {class_name_.c_str(), "->", member_name_.c_str(), ":", type_signature_.c_str()};
  } else {
    DCHECK_EQ(type_, kMethod);
    return {class_name_.c_str(), "->", member_name_.c_str(), type_signature_.c_str()};
  }
}

无论MemberSignature的type_为field还是method,其返回值都是以class_name.c_str为前缀

MemberSignature的构造函数:

c 复制代码
//art/runtime/hidden_api.cc

MemberSignature::MemberSignature(const ClassAccessor::Field& field) {
  const DexFile& dex_file = field.GetDexFile();
  const dex::FieldId& field_id = dex_file.GetFieldId(field.GetIndex());
  class_name_ = dex_file.GetFieldDeclaringClassDescriptor(field_id);
  member_name_ = dex_file.GetFieldName(field_id);
  type_signature_ = dex_file.GetFieldTypeDescriptor(field_id);
  type_ = kField;
}

MemberSignature::MemberSignature(const ClassAccessor::Method& method) {
  const DexFile& dex_file = method.GetDexFile();
  const dex::MethodId& method_id = dex_file.GetMethodId(method.GetIndex());
  class_name_ = dex_file.GetMethodDeclaringClassDescriptor(method_id);
  member_name_ = dex_file.GetMethodName(method_id);
  type_signature_ = dex_file.GetMethodSignature(method_id).ToString();
  type_ = kMethod;
}

另外,我们知道一个class的签名,形式都是如Ljava/lang/String;这种,肯定是以L开头的。

接下来分析HiddenApiExemptions,从runtime->GetHiddenApiExemptions着手

综上:所以exemption只要是L,就可以达到返回false的目的。

c 复制代码
//art/runtime/runtime.h

void SetHiddenApiExemptions(const std::vector<std::string>& exemptions) {
  hidden_api_exemptions_ = exemptions;
}

const std::vector<std::string>& GetHiddenApiExemptions() {
  return hidden_api_exemptions_;
}

搜一下SetHiddenApiExemptions调用的位置

c 复制代码
//art/runtime/native/dalvik_system_VMRuntime.cc

static void VMRuntime_setHiddenApiExemptions(JNIEnv* env,
                                            jclass,
                                            jobjectArray exemptions) {
  std::vector<std::string> exemptions_vec;
  int exemptions_length = env->GetArrayLength(exemptions);
  for (int i = 0; i < exemptions_length; i++) {
    jstring exemption = reinterpret_cast<jstring>(env->GetObjectArrayElement(exemptions, i));
    const char* raw_exemption = env->GetStringUTFChars(exemption, nullptr);
    exemptions_vec.push_back(raw_exemption);
    env->ReleaseStringUTFChars(exemption, raw_exemption);
  }

  Runtime::Current()->SetHiddenApiExemptions(exemptions_vec);
}
c 复制代码
static JNINativeMethod gMethods[] = {
    ...
    NATIVE_METHOD(VMRuntime, setHiddenApiExemptions, "([Ljava/lang/String;)V"),
    ...
}

VMRuntime_setHiddenApiExemptions是jni方法,其native函数的声明在VMRuntime.java中。

c 复制代码
//libcore/libart/src/main/java/dalvik/system/VMRuntime.java

/**
* Sets the list of exemptions from hidden API access enforcement.
*
* @param signaturePrefixes
*         A list of signature prefixes. Each item in the list is a prefix match on the type
*         signature of a blacklisted API. All matching APIs are treated as if they were on
*         the whitelist: access permitted, and no logging..
*
* @hide
*/
@SystemApi(client = MODULE_LIBRARIES)
public native void setHiddenApiExemptions(String[] signaturePrefixes);

也就是说,想要调用setHiddenApiExemptions, 必须fake掉hiddenapi的限制。

之前的关键点中,caller是否可以不受约束访问callee,给了我们一些启示

caller的上下文是这样的获得的,如果该上下文domain的值越小,拥有的hiddenapi访问权限越大。

c 复制代码
  const AccessContext caller_context = fn_get_access_context(); /* 2.获取caller的上下文 */ 
c 复制代码
//art/runtime/native/java_lang_Class.cc

static std::function<hiddenapi::AccessContext()> GetHiddenapiAccessContextFunction(Thread* self) {
  return [=]() REQUIRES_SHARED(Locks::mutator_lock_) {
    return hiddenapi::GetReflectionCallerAccessContext(self);
  };
}
c 复制代码
//art/runtime/hidden_api.cc

hiddenapi::AccessContext GetReflectionCallerAccessContext(Thread* self)
    REQUIRES_SHARED(Locks::mutator_lock_) {
  // Walk the stack and find the first frame not from java.lang.Class,
  // java.lang.invoke or java.lang.reflect. This is very expensive.
  // Save this till the last.
  struct FirstExternalCallerVisitor : public StackVisitor {
    explicit FirstExternalCallerVisitor(Thread* thread)
        : StackVisitor(thread, nullptr, StackVisitor::StackWalkKind::kIncludeInlinedFrames),
          caller(nullptr) {}

    bool VisitFrame() override REQUIRES_SHARED(Locks::mutator_lock_) {
      ArtMethod* m = GetMethod();
      if (m == nullptr) {
        // Attached native thread. Assume this is *not* boot class path.
        caller = nullptr;
        return false;
      } else if (m->IsRuntimeMethod()) {
        // Internal runtime method, continue walking the stack.
        return true;
      }

      ObjPtr<mirror::Class> declaring_class = m->GetDeclaringClass();
      if (declaring_class->IsBootStrapClassLoaded()) {
        if (declaring_class->IsClassClass()) {
          return true;
        }

        // MethodHandles.makeIdentity is doing findStatic to find hidden methods,
        // where reflection is used.
        if (m == WellKnownClasses::java_lang_invoke_MethodHandles_makeIdentity) {
          return false;
        }

        // Check classes in the java.lang.invoke package. At the time of writing, the
        // classes of interest are MethodHandles and MethodHandles.Lookup, but this
        // is subject to change so conservatively cover the entire package.
        // NB Static initializers within java.lang.invoke are permitted and do not
        // need further stack inspection.
        ObjPtr<mirror::Class> lookup_class = GetClassRoot<mirror::MethodHandlesLookup>();
        if ((declaring_class == lookup_class || declaring_class->IsInSamePackage(lookup_class)) &&
            !m->IsClassInitializer()) {
          return true;
        }
        // Check for classes in the java.lang.reflect package, except for java.lang.reflect.Proxy.
        // java.lang.reflect.Proxy does its own hidden api checks (https://r.android.com/915496),
        // and walking over this frame would cause a null pointer dereference
        // (e.g. in 691-hiddenapi-proxy).
        ObjPtr<mirror::Class> proxy_class = GetClassRoot<mirror::Proxy>();
        CompatFramework& compat_framework = Runtime::Current()->GetCompatFramework();
        if (declaring_class->IsInSamePackage(proxy_class) && declaring_class != proxy_class) {
          if (compat_framework.IsChangeEnabled(kPreventMetaReflectionBlocklistAccess)) {
            return true;
          }
        }
      }

      caller = m;
      return false;
    }

    ArtMethod* caller;
  };

  FirstExternalCallerVisitor visitor(self);
  visitor.WalkStack();

  // Construct AccessContext from the calling class found on the stack.
  // If the calling class cannot be determined, e.g. unattached threads,
  // we conservatively assume the caller is trusted.
  ObjPtr<mirror::Class> caller =
      (visitor.caller == nullptr) ? nullptr : visitor.caller->GetDeclaringClass();
  return caller.IsNull() ? AccessContext(/* is_trusted= */ true) : AccessContext(caller);
}

最后一行caller.IsNull()的时候上下文传入了true

c 复制代码
// Represents the API domain of a caller/callee.
class AccessContext {
public:
  // Initialize to either the fully-trusted or fully-untrusted domain.
 explicit AccessContext(bool is_trusted)
     : klass_(nullptr),
       dex_file_(nullptr),
       domain_(ComputeDomain(is_trusted)) {}
       
private:
 static Domain ComputeDomain(bool is_trusted) {
   return is_trusted ? Domain::kCorePlatform : Domain::kApplication;
 }       
}

在此,domain_通过ComputeDomain初始化,当is_trusted为true的时候,确实会获取级别最高的hiddenapi访问权限

再来看看caller不为null时

c 复制代码
// Represents the API domain of a caller/callee.
class AccessContext {
public:
 // Initialize from Class.
 explicit AccessContext(ObjPtr<mirror::Class> klass)
     REQUIRES_SHARED(Locks::mutator_lock_)
     : klass_(klass),
       dex_file_(GetDexFileFromDexCache(klass->GetDexCache())),
       domain_(ComputeDomain(klass, dex_file_)) {}
       
private:
 static Domain ComputeDomain(ObjPtr<mirror::ClassLoader> class_loader, const DexFile*  dex_file) {
   if (dex_file == nullptr) {
     return ComputeDomain(/* is_trusted= */ class_loader.IsNull());
   }

   return dex_file->GetHiddenapiDomain();
 }
 
 static Domain ComputeDomain(ObjPtr<mirror::Class> klass, const DexFile* dex_file)
     REQUIRES_SHARED(Locks::mutator_lock_) {
   // Check other aspects of the context.
   Domain domain = ComputeDomain(klass->GetClassLoader(), dex_file);

   if (domain == Domain::kApplication &&
       klass->ShouldSkipHiddenApiChecks() &&
       Runtime::Current()->IsJavaDebuggableAtInit()) { /* 只有debugable的包才会走if中的逻辑 */
     // Class is known, it is marked trusted and we are in debuggable mode.
     domain = ComputeDomain(/* is_trusted= */ true);
   }

   return domain;
 }    
}

通过kclass拿到dex_file,然后调用computeDomain计算该dex_file的domain,最终dex_file的domain值是通过GetHiddenapiDomain()获取的

c 复制代码
//art/libdexfile/dex/dex_file.h

hiddenapi::Domain GetHiddenapiDomain() const { return hiddenapi_domain_; }
void SetHiddenapiDomain(hiddenapi::Domain value) const { hiddenapi_domain_ = value; }

查看调用位置

c 复制代码
//art/runtime/hidden_api.cc

void InitializeDexFileDomain(const DexFile& dex_file, ObjPtr<mirror::ClassLoader> class_loader) {
  Domain dex_domain = DetermineDomainFromLocation(dex_file.GetLocation(), class_loader);

  // Assign the domain unless a more permissive domain has already been assigned.
  // This may happen when DexFile is initialized as trusted.
  if (IsDomainMoreTrustedThan(dex_domain, dex_file.GetHiddenapiDomain())) {
    dex_file.SetHiddenapiDomain(dex_domain);
  }
}

DetermineDomainFromLocation顾名思义:根据dex_file的文件位置,计算出其domain值

c 复制代码
static Domain DetermineDomainFromLocation(const std::string& dex_location,
                                          ObjPtr<mirror::ClassLoader> class_loader) {
  // If running with APEX, check `path` against known APEX locations.
  // These checks will be skipped on target buildbots where ANDROID_ART_ROOT
  // is set to "/system".
  if (ArtModuleRootDistinctFromAndroidRoot()) { /* 1.只是为了判断相关的dir路径是否存在 */
    if (LocationIsOnArtModule(dex_location) /* 2.dex的路径是否是在artModule */
    || LocationIsOnConscryptModule(dex_location) /* 3.dex的路径是否是在ConscryptModule */
    ||LocationIsOnI18nModule(dex_location)) { /*4.dex的路径是否是在i18nModule */
      return Domain::kCorePlatform;
    }

    if (LocationIsOnApex(dex_location)) { /*5.dex的路径是否是在apex目录 */
      return Domain::kPlatform;
    }
  }

  if (LocationIsOnSystemFramework(dex_location)) { / *6.dex的路径是否是在system / framework目录 */
    return Domain::kPlatform;
  }

  if (LocationIsOnSystemExtFramework(dex_location)) { / *7.dex的路径是否是在system_ext/framework目录 */
    return Domain::kPlatform;
  }

  if (class_loader.IsNull()) {
    if (kIsTargetBuild && !kIsTargetLinux) {
      // This is unexpected only when running on Android.
      LOG(WARNING) << "DexFile " << dex_location
                   << " is in boot class path but is not in a known location";
    }
    return Domain::kPlatform;
  }

  return Domain::kApplication;
}

代码中一共判断了7个文件位置,分别对应不同的domain

这些位置可以在init.environ.rc中查看到:

c 复制代码
//core/rootdir/init.environ.rc.in

# set up the global environment
on early-init
    export ANDROID_BOOTLOGO 1
    export ANDROID_ROOT /system
    export ANDROID_ASSETS /system/app
    export ANDROID_DATA /data
    export ANDROID_STORAGE /storage
    export ANDROID_ART_ROOT /apex/com.android.art
    export ANDROID_I18N_ROOT /apex/com.android.i18n
    export ANDROID_TZDATA_ROOT /apex/com.android.tzdata
    export EXTERNAL_STORAGE /sdcard
    export ASEC_MOUNTPOINT /mnt/asec
    %EXPORT_GLOBAL_ASAN_OPTIONS%
    %EXPORT_GLOBAL_GCOV_OPTIONS%
    %EXPORT_GLOBAL_CLANG_COVERAGE_OPTIONS%
    %EXPORT_GLOBAL_HWASAN_OPTIONS%
    %EXPORT_GLOBAL_SCUDO_ALLOCATION_RING_BUFFER_SIZE%

1.只是为了判断相关module的dir是否存在,一般都是存在的

2.artModule路径为/apex/com.android.art(android 14)

3.conscryptModule路径为/apex/com.android.conscrypt

4.apex的路径为/apex/

5.SystemFramework的路径为/system/framework

如果caller的路径为artModule或者conscryptModule即可将domain值置为kCorePlatform,达到目的。

先随便找个debug的app看一下运行时apex路径都有哪些

cat /proc/7156/maps |grep "/apex/.*.jar"

core-oj.jar正是可以利用的点:

这里有一个java.lang.System类,该类我们经常使用其加载so库,比如System.loadLibrary。

所以,我们可以通过System.loadLibrary,然后在native层的JNI_OnLoad中通过反射调用setHiddenApiExemptions(此时caller为java.lang.System.其domain级别为corePlatform),然后就可以随意访问hiddenapi了

5 总结

  1. 系统framework代码中可以通过设置setHiddenApiExemptions,达到随意访问hiddenapi的目的
  2. 由于class VMRuntime被hide,可以在JNI_OnLoad中操作VMRuntime,达到调用setHiddenApiExemptions的目的

初步形成解决方案

系统类伪装

如果调用者是系统类,那么就允许被调用。即如果我们能以系统类的身份去反射,那么就能畅通无阻:

  1. 首先通过反射 API 拿到 getDeclaredMethod 方法 。getDeclaredMethod 是 public 的,不存在问题;这个通过反射拿到的方法网上称之为元反射方法
  2. 然后通过刚刚的元反射方法去反射调用 getDeclardMethod。这里我们就实现了以系统身份去反射的目的------反射相关的 API 都是系统类,因此我们的元反射方法也是被系统类加载的方法;所以我们的元反射方法调用的 getDeclardMethod 会被认为是系统调用的,可以反射任意的方法。
  3. 另外系统在检查豁免时是通过方法签名前缀进行匹配的,而 Java 方法签名都是 L 开头的,因此我们可以把直接传个 L 进去,那么所有的隐藏API全部被赦免了!
arduino 复制代码
//frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

/**
* Sets the list of classes/methods for the hidden API
*/
public static void setApiDenylistExemptions(String[] exemptions) {
    VMRuntime.getRuntime().setHiddenApiExemptions(exemptions);
}
ini 复制代码
try {
    Method mm = Class.class.getDeclaredMethod("forName", String.class);
    Class<?> cls = (Class)mm.invoke((Object)null, "dalvik.system.VMRuntime");
    mm = Class.class.getDeclaredMethod("getDeclaredMethod", String.class, Class[].class);
    Method m = (Method)mm.invoke(cls, "getRuntime", null);
    Object vr = m.invoke((Object)null);
    m = (Method)mm.invoke(cls, "setHiddenApiExemptions", new Class[]{String[].class});
    //Java class的签名都是以L开头的,所以这里全部进行豁免
    String[] args = new String[]{"L"};
    m.invoke(vr, args);
} catch (Throwable e) {
    e.printStackTrace();
}

Android 11.0 → 限制升级

从此版本开始,系统升级了上层接口的访问限制,直接将VMRuntime的类接口限制升级,因此只能通过native层进行访问。原理不变,利用系统加载lib库时JNI_OnLoad通过反射调用setHiddenApiExemptions,此时callerjava.lang.Systemdomain级别为libcore.api.CorePlatformApi,就可以访问hiddenapi了。

最终解决方案

scss 复制代码
#include <jni.h>
#include <string.h>
#include <android/log.h>

#define LOG_TAG "ygq_hidden_api"
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE,LOG_TAG,__VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

/**
 * frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
 *
 * Android 12+ & static method
 * Lcom/android/internal/os/ZygoteInit;->setApiDenylistExemptions([Ljava/lang/String;)V
 * <p>
 * setApiDenylistExemptions(new String[]{"L"})
 *
 * Android 9+ & static method
 * Lcom/android/internal/os/ZygoteInit;->setApiBlacklistExemptions([Ljava/lang/String;)V
 * <p>
 * setApiBlacklistExemptions(new String[]{"L"}
 *
 * @param env JNIEnv
 */
bool setApiDenylistExemptions(JNIEnv *env) {
    // Android 9.0 +
    int sdkInt = android_get_device_api_level();
    if (sdkInt < __ANDROID_API_P__) {
        LOGV("setApiDenylistExemptions below Android 9.0, just ignored");
        return true;
    }

    const char* zygoteInitClass = "com/android/internal/os/ZygoteInit";
    jclass clazz = env->FindClass(zygoteInitClass);
    if (clazz == nullptr) {
        env->ExceptionClear();
        LOGI("setApiDenylistExemptions can't find %s class", *zygoteInitClass);
        return false;
    }

    jmethodID setApiDenylistExemptions;

    if (sdkInt >= __ANDROID_API_S__) {
        setApiDenylistExemptions = env->GetStaticMethodID(clazz, "setApiDenylistExemptions",
                                                          "([Ljava/lang/String;)V");
    } else {
        setApiDenylistExemptions = env->GetStaticMethodID(clazz, "setApiBlacklistExemptions",
                                                          "([Ljava/lang/String;)V");
    }
    if (setApiDenylistExemptions == nullptr) {
        env->ExceptionClear();
        LOGI("setApiDenylistExemptions can't find %s method", "setApiDenylistExemptions");
        return false;
    }

    jclass stringClass = env->FindClass("java/lang/String");
    jstring fakeStr = env->NewStringUTF("L");
    jobjectArray fakeArray = env->NewObjectArray(1, stringClass, NULL);
    env->SetObjectArrayElement(fakeArray, 0, fakeStr);

    env->CallStaticVoidMethod(clazz, setApiDenylistExemptions, fakeArray);

    env->DeleteLocalRef(fakeStr);
    env->DeleteLocalRef(fakeArray);

    LOGD("setApiDenylistExemptions success");
    return true;
}


/**
 * libcore/libart/src/main/java/dalvik/system/VMRuntime.java
 *
 * Android 9+ & object method
 * Ldalvik/system/VMRuntime;->setHiddenApiExemptions([Ljava/lang/String;)V
 * <p>
 * setHiddenApiExemptions(new String[]{"L"})
 *
 * @param env JNIEnv
 */
bool setHiddenApiExemptions(JNIEnv *env) {
    // Android 9.0 +
    int sdkInt = android_get_device_api_level();
    if (sdkInt < __ANDROID_API_P__) {
        LOGV("setHiddenApiExemptions below Android 9.0, just ignored");
        return true;
    }

    const char* vmRuntimeClass = "dalvik/system/VMRuntime";
    jclass clazz = env->FindClass(vmRuntimeClass);
    if (clazz == nullptr) {
        env->ExceptionClear();
        LOGI("setHiddenApiExemptions can't find %s class", *vmRuntimeClass);
        return false;
    }

    jmethodID getRuntime = env->GetStaticMethodID(clazz, "getRuntime",
                                                  "()Ldalvik/system/VMRuntime;");
    if (getRuntime == nullptr) {
        env->ExceptionClear();
        LOGI("setHiddenApiExemptions can't find %s method", "getRuntime");
        return false;
    }

    jobject vmRuntime = env->CallStaticObjectMethod(clazz, getRuntime);
    if (vmRuntime == nullptr) {
        env->ExceptionClear();
        LOGI("setHiddenApiExemptions can't get vmRuntime instance");
        return false;
    }

    jmethodID setHiddenApiExemptions = env->GetMethodID(clazz, "setHiddenApiExemptions",
                                                        "([Ljava/lang/String;)V");
    if (setHiddenApiExemptions == nullptr) {
        env->ExceptionClear();
        LOGI("setHiddenApiExemptions can't find %s method", "setHiddenApiExemptions");
        return false;
    }

    jclass stringClass = env->FindClass("java/lang/String");
    jstring fakeStr = env->NewStringUTF("L");
    jobjectArray fakeArray = env->NewObjectArray(1, stringClass, NULL);
    env->SetObjectArrayElement(fakeArray, 0, fakeStr);

    env->CallVoidMethod(vmRuntime, setHiddenApiExemptions, fakeArray);

    env->DeleteLocalRef(fakeStr);
    env->DeleteLocalRef(fakeArray);

    LOGD("setHiddenApiExemptions success");
    return true;
}

bool checkHiddenApiExemptions(JNIEnv *env) {
    if (!setHiddenApiExemptions(env)){
       return setApiDenylistExemptions(env);
    }
    return true;
}

extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = nullptr;

    if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }

    // 设置 hidden-api 访问豁免
    if(!checkHiddenApiExemptions(env)){
        LOGE("checkHiddenApiExemptions error");
        return JNI_ERR;
    }

    return JNI_VERSION_1_6;
}

Demo源码

暂时无法在路特斯桌面文档外展示此内容

当前的开源方案

github.com/whulzz1993/...

github.com/ChickenHook...

github.com/tiann/FreeR...

相关推荐
waicsdn_haha1 分钟前
Java/JDK下载、安装及环境配置超详细教程【Windows10、macOS和Linux图文详解】
java·运维·服务器·开发语言·windows·后端·jdk
飞的肖10 分钟前
前端使用 Element Plus架构vue3.0实现图片拖拉拽,后等比压缩,上传到Spring Boot后端
前端·spring boot·架构
Q_192849990612 分钟前
基于Spring Boot的摄影器材租赁回收系统
java·spring boot·后端
Code_流苏14 分钟前
VSCode搭建Java开发环境 2024保姆级安装教程(Java环境搭建+VSCode安装+运行测试+背景图设置)
java·ide·vscode·搭建·java开发环境
小屁不止是运维28 分钟前
麒麟操作系统服务架构保姆级教程(五)NGINX中间件详解
linux·运维·服务器·nginx·中间件·架构
禁默1 小时前
深入浅出:AWT的基本组件及其应用
java·开发语言·界面编程
Cachel wood1 小时前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
Code哈哈笑1 小时前
【Java 学习】深度剖析Java多态:从向上转型到向下转型,解锁动态绑定的奥秘,让代码更优雅灵活
java·开发语言·学习
gb42152871 小时前
springboot中Jackson库和jsonpath库的区别和联系。
java·spring boot·后端
程序猿进阶1 小时前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot