搞安全的人对“内存马”这个词肯定不陌生。最近几年攻防演练、HVV值守里,越来越多的情况是:攻击方已经拿到了一次代码执行机会,但不再往磁盘上写webshell文件,而是直接把恶意逻辑塞进正在运行的JVM内存里,不落地、不重启、不触发文件监控,等风头过去再撤。Spring框架在国内业务系统里的覆盖率极高,自然成了这类玩法的重灾区。这篇文章我就把主角放在Spring MVC的HandlerInterceptor上,聊聊拦截器型内存马的原理、动态注册的实际路径,以及防守方应该从哪些角度快速排查和处置。不管是红队想理解攻击链路,还是蓝队要补检测盲区,这篇都值得看完。
1. 拦截器内存马到底是个什么东西
1.1 从请求在Spring MVC里的执行顺序说起
先回忆一下一个正常请求进入Spring MVC后是怎么走的。请求先到达Servlet容器,比如Tomcat,然后被DispatcherServlet接管。DispatcherServlet会通过HandlerMapping找到一个合适的Controller方法,把它包装成一个HandlerExecutionChain执行链。这个执行链上除了Controller方法的调用信息,还挂着一串HandlerInterceptor。
HandlerInterceptor是Spring MVC提供的拦截器接口,常见实现类里三个方法:preHandle在Controller方法调用之前执行,postHandle在Controller方法执行之后但视图渲染之前执行,afterCompletion在整个请求完成之后执行。大多数业务场景里大家用的鉴权、日志、跨域处理,都是在这三个方法上做的。
拦截器型内存马的核心思路,就是在程序运行过程中,通过反射等方式把“自己人”的HandlerInterceptor强行塞进某个全局拦截器列表里。只要这一步成功,后续每个匹配到该HandlerMapping的请求,都会先经过这个恶意拦截器,恶意逻辑就有了触发点。
为了便于理解,可以把这套流程想象成小区门禁:正常流程是访客到门口,保安按名单核对,然后放行进入单元楼。内存马干的事,就是悄悄把自己的同伙身份证塞进保安手里的名单,之后每次有人进门,同伙都能先行确认一遍:要不要做点额外的事。
1.2 内存马和传统文件马的本质差别
传统Webshell的本质是有落地文件,不管是一句话木马还是各种加密马,最终都要有一个脚本文件或二进制文件在磁盘上。文件一旦落盘,就有被静态扫描、文件完整性检测、目录敏感行为监控发现的风险。内存马则完全没有文件实体,恶意类是直接加载到JVM内存里的,常规查杀工具基于文件系统扫描,很难发现自己。
内存马也不是凭空来的,它的前提是你必须先有代码执行能力。反序列化漏洞、表达式注入、任意文件上传配合解析、模板注入等等,任何一个能让你在目标JVM里执行任意代码的入口,都可以作为内存马的前置条件。有了这个入口,再通过一段反射链把恶意对象挂到框架合适的位置上,一个无文件落地的后门就成了。
1.3 为什么有人偏偏选Interceptor而不是Filter
Spring项目里常见的动态后门还有Filter型内存马,就是往Servlet容器的过滤器链里注册一段恶意Filter。两者都能作为内存马载体,但很多人更愿意用Interceptor,原因有几个。
Filter工作在Servlet容器层,它是通过ServletContext的addFilter方法注册进去的,生命周期由容器管理,和Spring容器是两套体系。Filter里想拿到Spring容器里那些Service、Mapper,不是不行,但要额外做WebApplicationContextUtils.getWebApplicationContext这种上下文获取步骤。Interceptor就不一样,它本身就在Spring MVC体系内,只要你能拿到Spring上下文,很容易直接获取项目里注册的各类Bean,业务联动的成本低很多。
另外从触发时机看,Filter会在请求进入Servlet容器后立刻执行,连Spring容器都还没介入。Interceptor则在HandlerMapping匹配完成之后执行,此时Controller方法还没有被调用,但Spring的完整上下文已经在手里了,对于恶意逻辑来说,能拿到的东西明显更多。当然,Filter型内存马的覆盖范围更广,比如静态资源、404请求它也能拦,这个后面的章节再细对比。
2. 动态注册Interceptor的技术路线拆解
2.1 第一步:先拿到Spring容器的“总闸门”
要在Spring环境里动态注册拦截器,第一步一定是拿到当前Web应用对应的Spring容器。为什么强调“当前”,因为一个Java进程里可能同时运行多个Web应用,每个Web应用有自己独立的ClassLoader和Spring容器。尤其是在传统Tomcat多应用部署模式下,拿错容器等于什么都没做。
常见拿法有两种。
第一种,如果代码执行时当前线程还处于一个HTTP请求上下文中,可以通过RequestContextHolder拿到ServletRequestAttributes,再从里面的HttpServletRequest get到ServletContext,最后用WebApplicationContextUtils.getWebApplicationContext把Spring容器拽出来。
import javax.servlet.http.HttpServletRequest; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; public static WebApplicationContext getCurrentWebApplicationContext() { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes != null) { HttpServletRequest request = attributes.getRequest(); return WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()); } return null; }这里有一个容易被忽略的细节:RequestContextHolder基于ThreadLocal存储,只有当前线程真的在处理请求时才能拿到。如果是异步线程、定时任务线程、或者你是在反序列化的某个阶段执行代码,没有请求上下文,这条路就走不通。这时候就得换个思路,比如从已知的Spring工具类、JMX信息、或者静态缓存里找ServletContext引用。实际操作中,大多数人是在已有请求的链路里进行的,所以第一种方式更常见。
有一个比较绕但稳定的备用方案是:先从系统属性或者环境变量里找到某些Spring Boot应用暴露的context路径,再通过servletContext的全局命名空间去查。但这属于特殊场景经验,平时用得少,不展开讲。
2.2 第二步:找到HandlerMapping这个关键对象
拿到Spring容器之后,下一步不是马上反射,而是要想清楚到底要操作哪个对象。
Spring MVC里HandlerMapping是一个接口,它的职责就是根据当前请求找到对应的Handler以及对应的拦截器链。常用的实现是RequestMappingHandlerMapping,负责把@RequestMapping标注的方法映射成HandlerMethod。DispatcherServlet初始化时会从Spring容器里获取所有HandlerMapping类型的Bean,排序后存起来,每次收到请求就挨个问:你能不能处理这个请求?
RequestMappingHandlerMapping继承自AbstractHandlerMapping,而AbstractHandlerMapping内部维护着拦截器列表。代码里可以看到两个关键字段:一个是interceptors,指的是通过XML或Java Config配置的原始拦截器列表;另一个是adaptedInterceptors,是经过适配后的HandlerInterceptor列表,也是实际在getHandler时被遍历使用的对象。
我们需要操作的,就是这个adaptedInterceptors列表。只要把恶意对象add进去,后续每次getHandler构建执行链的时候,它会无条件地参与组装。获取handlerMapping的代码很简单:
import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class);如果容器里有多个HandlerMapping实例,最好按类型或名称精确取。有些项目用了Spring Cloud Gateway、自定义HandlerMapping,名字可能不同,要用getBeansOfType先看一眼。
2.3 第三步:反射往adaptedInterceptors里加料
拿到RequestMappingHandlerMapping实例后,按理说直接调用addInterceptor方法最省事,但这个方法不是公开的,真正的初始化逻辑是在AbstractHandlerMapping.initApplicationContext里完成的。所以动态注入只能走反射。
核心反射路径如下:
Class<?> abstractClass = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMapping"); Field field = abstractClass.getDeclaredField("adaptedInterceptors"); field.setAccessible(true); List<HandlerInterceptor> list = (List<HandlerInterceptor>) field.get(mapping);在Spring 5和大多数Spring Boot 2.x版本中,adaptedInterceptors字段的类型是List ,实际对象是ArrayList,可以直接add。在新一点的Spring 6版本里,字段名和类型可能有调整,但范围没变,仍然在这个类身上。
包装一个拦截器对象塞进去:
import org.springframework.web.servlet.HandlerInterceptor; class DemoInterceptor implements HandlerInterceptor { @Override public boolean preHandle(jakarta.servlet.http.HttpServletRequest request, jakarta.servlet.http.HttpServletResponse response, Object handler) throws Exception { // 这里仅作演示,没有任何恶意动作 System.out.println("[demo] interceptor triggered"); return true; } } // 反射获取list后 list.add(new DemoInterceptor());注意,如果直接add一个实现了HandlerInterceptor的普通对象,它会成为全局拦截器,作用于当前HandlerMapping能匹配到的所有请求。如果想要只对特定路径生效,应该使用MappedInterceptor,它会匹配includePatterns和excludePatterns:
MappedInterceptor mappedInterceptor = new MappedInterceptor(new String[]{"/admin/**"}, new String[]{"/admin/login"}, interceptor); list.add(mappedInterceptor);不过大多数场景下,全局拦截器已经够用了。攻击者只要找一个真实存在且会进入Controller的URL,就能触发。反射设置字段时要注意Java模块系统,某些高版本JDK在强封装下需要加--add-opens。实际打点环境大多是JDK8或JDK11,这块问题不大。
2.4 另一种思路:动态注册Bean再自动挂载
除了直接操作adaptedInterceptors,还有一类实现方式是把恶意对象通过BeanFactory动态注册成Spring Bean,然后让Spring在容器刷新时把它自动纳入拦截器列表。常见的做法是往DefaultListableBeanFactory里registerBeanDefinition,或者直接调用GenericApplicationContext的registerBean方法。
这种写法的好处是隐蔽性更强,因为对象会出现在Spring容器管理范围里,后续可以通过getBean获取,而不只是一段游离在内存里的匿名对象。坏处是步骤更多,而且Bean生命周期回调容易出问题,触发条件也更苛刻。实际攻防场景里,直接反射操作adaptedInterceptors是更短平快的方案,大多数用于应急分析和安全研究的技术文章也都把重点放在这一条路线上。
3. 实战视角:从漏洞入口到拦截器生效的完整链路
3.1 前置条件:得先有“一次代码执行”
说一句容易让人误解的话:内存马不是漏洞,它只是漏洞利用的一种载体。没有入口漏洞,再精妙的注册逻辑也执行不到。所以完整的攻击链路一定是这样的:
外部入口漏洞 → 在目标JVM里执行了一段任意代码 → 通过反射获取Spring上下文 → 获取HandlerMapping → 向adaptedInterceptors中注册恶意拦截器 → 后续请求触发拦截器执行恶意逻辑。
所谓的入口,常见的是反序列化RCE、表达式注入、模版注入、不安全的文件上传配合解析、以及某些管理端口的未授权命令执行。不管入口长什么样,最终都要落到能执行Java字节码或能调用反射API这个层面。
我在一些授权演练里看到过一种比较典型的场景:某个内部系统存在一个基于JDBC反序列化的接口,攻击方把payload发进去之后,会先弹一个经过混淆的“加载器”,这个加载器负责在当前JVM里完成内存马注册。因为整个过程没有任何文件落地,业务方在攻击发生后的几天内都没发现异常,直到红队报告出来后,蓝队才通过专门的内存马检测脚本找到了那段挂在RequestMappingHandlerMapping里的陌生拦截器。
3.2 注册关键参数怎么选:路径、类和加载器
写注册代码的时候,有三个参数需要仔细考虑。
第一个是目标路径。如果注册的是MappedInterceptor,includePatterns写成/**看起来最稳,但这里有个误区:拦截器只在能匹配到Handler时才执行,如果一个请求根本找不到Controller或静态资源处理器,HandlerMapping返回的可能是null,拦截器链压根不会构建。很多人在自己机器上测内存马,随便访问一个能正常返回数据的接口,一切正常,但在目标环境里选了一个不存在的路径,结果触发不了,还以为注册失败。
所以更靠谱的方法是先观察目标系统里有哪些公开可达的Controller路径,然后选择其中一个真实存在的请求来触发。Spring Boot自带的/error错误页路径通常也是可用的,因为BasicErrorController处理了这个路径,有对应的Handler。
第二个是类加载器。内存马所在的恶意类用什么ClassLoader加载,会直接决定它能访问哪些类。如果是在入口漏洞的同一个线程里直接new对象,一般用的就是当前线程的ContextClassLoader,它能加载到Spring相关类,基本没问题。但如果是通过反序列化从远程加载了一个Jar,再用URLClassLoader加载恶意类,这时候恶意类的父加载器不一定能看到Spring的API,就会出现ClassCastException。实战中的常见姿势是先把恶意类字节码用当前Web应用的ClassLoader去defineClass,保证类空间和Spring容器一致。
第三个是触发标记。拦截器内存马和普通登录逻辑最大的区别是:它需要通过某种方式知道自己是否被“激活”。常见做法是检查特定请求头、特定Cookie、特定URL参数,命中了才执行恶意逻辑,否则原样放行。这种做法让它在访问日志里看起来就是一个普通请求串。
3.3 触发验证:要访问哪个URL才有效
注册完成之后,验证是否生效也不能随便找一个URL。前面提到,只有HandlerMapping能够成功getHandler的路径才会走拦截器链。如果目标请求路径不存在,DispatcherServlet会走到NoHandlerFound处理逻辑,或者直接404,第一层getHandler就返回null,拦截器根本没机会跑。
这其实是很多初次接触拦截器内存马的人最容易踩的坑。Filter型内存马没有这个问题,因为Filter是Servlet容器层面的,不管Handler有没有找到都会经过它。Interceptor是Spring MVC层面的,离Controller更近,但也因此更“挑剔”。
验证时最稳妥的方式,是注册一个平台自己的心跳请求路径,比如某些管理后台的登录接口、首页、静态资源转发接口,访问一次,然后在响应或日志里看到预埋标记。为了不干扰业务,好的验证姿势是让恶意拦截器在匹配到特定触发条件时才改变响应,其他情况完全透明。
3.4 为什么类加载器是最大的坑
接着类加载器的话题往下说。Tomcat为了实现多应用隔离,给每个Web应用创建了独立的WebAppClassLoader,双亲委派顺序是先找父加载器再自己加载。Spring的类也是通过Web应用的这个加载器加载的。如果你在当前请求线程里直接写Class.forName("org.springframework.web.servlet.HandlerInterceptor"),通常能加载到,因为当前线程上下文加载器就是Web应用加载器。
但如果你的恶意逻辑是放在一个独立线程里执行的,而那个线程的ContextClassLoader是系统类加载器,再去加载Spring的类很可能失败或拿到不同命名空间里的类。这时候就要手动把线程上下文加载器切换成目标Web应用的加载器,操作完再恢复。
还有一个隐蔽问题:有些序列化框架在反序列化时会使用调用方的ClassLoader,而不是被序列化对象的ClassLoader。也就是说,即使你在生成payload时用的是应用服务器类加载器,反序列化时还可能因为类加载器上下文不同导致加载失败。这也是为什么很多内存马实现会优先用字节码defineClass而不是依赖外部Jar加载。
4. 拦截器内存马和Filter内存马,到底选谁
4.1 两类内存马的作用范围差异
Filter型内存马作用于Servlet容器层面。只要请求进入了Servlet容器,Filter就会在请求到达DispatcherServlet之前被调用。这意味着静态资源、错误页、以及那些根本没有对应Handler的404请求,Filter都能看到。Interceptor型内存马只作用于Spring MVC内部,前面已经反复强调,必须有Handler匹配成功才会执行。
从“覆盖面”来看,Filter明显更广;从“业务可达性”来看,Interceptor能直接接触Spring Bean,更灵活。两者对比可以看下面这个表。
| 对比项 | Filter内存马 | Interceptor内存马 |
|---|---|---|
| 作用层级 | Servlet容器层 | Spring MVC层 |
| 触发条件 | 任意请求进入容器即触发 | 需HandlerMapping匹配到Handler |
| 是否覆盖404 | 覆盖 | 不覆盖 |
| 获取Spring Bean | 需要额外获取上下文 | 天然在Spring环境中 |
| 注册方式 | ServletContext.addFilter | 反射操作HandlerMapping拦截器列表 |
| 典型排查位置 | 容器Filter链 | Spring容器内HandlerMapping |
4.2 获取业务对象能力的差异
真实攻防中,拿到了内存马之后,还想做深入利用,比如读配置、调内部Service、查数据库,这些都需要拿到Spring容器里的Bean。Filter型内存马虽然理论上可以通过WebApplicationContextUtils.getWebApplicationContext拿到容器,但它本身在Servlet容器层,要绕一步。Interceptor不同,它的实例虽然在内存里是被临时塞进去的,但只要代码里持有ApplicationContext引用,Controller能干的活它基本都能干。
简而言之,如果目标是长期维护权限、方便横向移动,Interceptor型内存马更有吸引力。如果目标是高覆盖率、无差别流量劫持,Filter型更合适。
4.3 排查难度和存活性差异
对防守方来说,这两类内存马的排查难度不一样。Filter型内存马排查时可以去看ServletContext里的FilterRegistration列表,Java里遍历注册信息相对直观。Interceptor型内存马没有现成的注册表,只能依赖反射拿HandlerMapping里的adaptedInterceptors列表,然后再判断列表里每一项的类名、类加载器、字节码来源是否可疑,排查门槛略高。
存活方面,两者的共同点是重启即失。内存马最大的弱点就是不能持久化,一旦JVM重启,所有动态注册的列表全部清空。所以很多攻击者会想办法同时保留一份落地脚本,等目标重启后再通过其他入口重新注册,但这又增加了暴露面。防守方只要坚持在每次重启后进行一轮内存马巡检,就能很大程度上压缩这类马的存活空间。
5. 防守方的快速排查与处置
5.1 三分钟自查:遍历HandlerMapping里的拦截器
面对一个疑似被植入内存马的Spring应用,最快的方式就是写一个小接口或直接用Java进程外脚本,遍历所有HandlerMapping里的适配拦截器列表,看有没有不该出现的东西。下面是一个简单的检测逻辑,可以放在一个临时Controller里,也可以放到JMX调用方里。
import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.handler.AbstractHandlerMapping; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public static Map<String, Object> inspectInterceptors(WebApplicationContext context) throws Exception { Map<String, Object> result = new HashMap<>(); String[] names = context.getBeanNamesForType(HandlerMapping.class); List<Map<String, String>> interceptorsInfo = new ArrayList<>(); for (String name : names) { Object handlerMapping = context.getBean(name); if (!(handlerMapping instanceof AbstractHandlerMapping)) { continue; } Field interceptorsField = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors"); interceptorsField.setAccessible(true); List<?> interceptors = (List<?>) interceptorsField.get(handlerMapping); for (Object interceptor : interceptors) { Map<String, String> info = new HashMap<>(); info.put("handlerMapping", name); info.put("interceptorClass", interceptor.getClass().getName()); info.put("classLoader", interceptor.getClass().getClassLoader().toString()); interceptorsInfo.add(info); } } result.put("interceptors", interceptorsInfo); return result; }跑了这个脚本之后,重点看两个东西:一是类名是否像随机生成的,例如UUID片段、Base64变体;二是ClassLoader来源是否正常。大部分正常业务拦截器都是项目自己写的类,加载器是应用类加载器。如果看到类是URLClassLoader从临时目录或远程地址加载的,几乎可以判定有问题。
5.2 可疑拦截器的几个特征
在真实排查经验里,内存马拦截器有几个比较明显的特征。
第一,类名非常没有“业务感”。正常拦截器会叫AuthInterceptor、LogInterceptor、XssInterceptor,内存马往往叫random类名或者干脆是内部类、动态代理类,反编译一看代码逻辑极其简短。
第二,preHandle方法逻辑非常集中。正常拦截器preHandle里会调用各种Service、写日志、处理业务,而恶意拦截器通常只有短短几行:判断触发条件、执行恶意动作、返回true或false。它的代码量少得和它的权限完全不匹配。
第三,它可能不在任何配置文件中出现。你翻遍spring-mvc.xml、WebMvcConfigurer、@ComponentScan基础包,都找不到这个类的声明,可它就是出现在运行时的adaptedInterceptors里。这说明它是被动态塞进去的,而不是通过正常配置注册的。
5.3 排查之后怎么处理
确认存在恶意拦截器之后,最简单的处置方式就是重启应用,让它随JVM一起消失。但这只是治标,因为入口漏洞还在,重启之后攻击方很可能再次打进来。更稳妥的做法是:先通过流量日志和应急响应找到漏洞入口,把入口修复之后,再做一次重启。否则就是给攻击者演示了一遍“杀马不杀源”。
另外可以借助RASP产品在底层拦截对adaptedInterceptors的add操作。很多RASP方案会Hook关键类,比如AbstractHandlerMapping.initApplicationContext、HandlerMethod等,对非白名单的拦截器注入行为直接阻断。自己写Agent也可以,但成本和维护难度不低,适合有专门安全团队的公司。
5.4 加固Spring应用的落地清单
在日常防护上,我更推荐提前做这些动作。
- 升级Spring框架和Spring Boot版本,封堵已知反序列化、表达式注入入口。
- 对上传功能做严格的白名单校验,避免任意文件落地。
- 关闭或收敛管理端点,比如Actuator、JMX、RMI等,禁止生产环境暴露。
- 对反序列化使用白名单过滤,避免不可信数据直接进入ObjectInputStream。
- 建立内存马巡检脚本,在每次发布重启后自动扫描HandlerMapping和Filter链。
- 在边界WAF层面对高危险路径和特征请求做告警,虽然不能完全防内存马,但能提供线索。
- 应用进程尽量使用独立低权限用户运行,最小化被入侵后的影响面。
- 确保日志完整,尤其要记录所有请求的URL、来源IP、User-Agent,方便事后回溯。
这些动作里,前四条本质上是在堵“入口”。内存马再厉害,也需要一个能执行代码的点。入口堵住了,内存马就是无源之水。
6. 常见问题与排查心得
6.1 这里有一份问题速查表
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| getBean(RequestMappingHandlerMapping.class)报错 | 容器中没有这个类型的Bean,或项目使用非标准HandlerMapping | 改用getBeansOfType遍历所有HandlerMapping |
| 反射获取adaptedInterceptors字段失败 | Spring版本或类结构变化 | 遍历getDeclaredFields,找到List类型字段后再操作 |
| list.add之后拦截器不生效 | 访问路径没有匹配到Handler | 换成真实存在的Controller路径,或检查MappedInterceptor的path配置 |
| 调用时出现ClassCastException | 类加载器命名空间不一致 | 确保恶意类由当前Web应用ClassLoader加载,必要时手动defineClass |
| 请求到达但preHandle没执行 | 被更早的拦截器拦截或Handler匹配失败 | 查看执行链构建情况,用检测类打印拦截器数量 |
| restart后重启生效 | 内存马特性,本来就不持久 | 配合持久化入口修复,重启清理 |
6.2 我自己的几个排查心得
内存马这个领域,技术花样虽然多,底层的道理其实很朴素:攻击者需要找到一个能在运行时往框架对象图里塞东西的办法,防守者就需要定期检查运行时对象图里有没有多出来的东西。我见过不少防守团队,买了昂贵的安全产品,却从来没想过自己写一个几十行的检测脚本,去遍历HandlerMapping的适配拦截器。其实这类代码成本很低,效果却非常直接。
另一点比较重要的体会是,内存马排查一定要结合入口视角。有些时候你找到一个陌生的拦截器,急着清掉、重启,结果过了两天又出现一模一样的加料,原因就是入口漏洞没补。真正有效的流程是:发现可疑对象 → 定位注入来源 → 修复入口 → 重启清理 → 持续观察。跳过任何一环,后面的工作都会变成打地鼠。
最后再分享一个小技巧:在巡检脚本里,除了看类名和ClassLoader,还可以对每一段字节码的SHA-256做基线管理。每次发布后自动记录正常拦截器类的哈希,巡检时一旦发现哈希不在基线库里的类,直接告警。这样连动态代理类、匿名内部类这类名字本来就很乱的异常对象也能被识别出来。这种基于基线的思路,比单纯靠人工看类名要可靠得多。