写点什么

Tomcat 架构之为 Bypass 内存马检测铺路 (内存马系列篇四)

作者:Java-fenn
  • 2022 年 9 月 25 日
    湖南
  • 本文字数:7033 字

    阅读完需:约 23 分钟

Tomcat 写在前面继前面三种常见的 Tomcat 的内存马构造方式,后面将会讲解更加独特的 Tomcat 内存马构造,这是内存马系列文章第四篇,主要讲解 Tomcat 的架构,为后面 Tomcat 组件内存马做铺垫(不是在容器中实现的内存马,可以一定程度避免查杀)。


前置顶层架构我们可以通过一张图来表示。


架构足够模块化,从图中我们可以知道最上层为 Server 服务器,为 Service 服务提供一个生存环境,掌握每个 Service 服务的生命周期,至于 Service 则是嘴歪提供服务。


而在每隔 Service 中有着多个 Connector 和一个 Container,他们的作用分别是 Connector:负责接收浏览器的 TCP 连接请求,提供 Socket 与 Request、Response 的相关转化,与请求端交换数据,Container:用于封装和管理 Servlet,以及具体处理 Request 请求,是所有子容器的父接口(包括 Engine、Host、Context、Wrapper)。


Engine -- 引擎


Host -- 主机


Context -- 上下文


Wrapper -- 包装器


Service 服务之下还有各种 支撑组件 ,下面简单罗列一下这些组件。


Manager -- 管理器,用于管理会话 Session


Logger -- 日志器,用于管理日志


Loader -- 加载器,和类加载有关,只会开放给 Context 所使用


Pipeline -- 管道组件,配合 Valve 实现过滤器功能


Valve -- 阀门组件,配合 Pipeline 实现过滤器功能


Realm -- 认证授权组件


同样可以在 tomcat 的 server.xml 中看到一些配置。


<GlobalNamingResources><Resource name="UserDatabase" auth="Container"type="org.apache.catalina.UserDatabase"description="User database that can be updated and saved"factory="org.apache.catalina.users.MemoryUserDatabaseFactory"pathname="conf/tomcat-users.xml" /></GlobalNamingResources>


<Service name="Catalina">


<Connector port="8080" protocol="HTTP/1.1"           connectionTimeout="20000"           redirectPort="8443" />
<Engine name="Catalina" defaultHost="localhost"> <Realm className="org.apache.catalina.realm.LockOutRealm"> <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/> </Realm>
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true"> <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="localhost_access_log" suffix=".txt" pattern="%h %l %u %t "%r" %s %b" />
</Host></Engine>
复制代码


</Service></Server>我们可以知道开放的端口,第一部分就是 tomcat 默认的监听器,第二部分就是资源,第三部分就是一些 Service 服务,上面的这个 xml 文件只配置了一个 Connector,提供了 HTTP/1.1 的连接支持,但是 Tomcat 是支持多个 Connector 配置的,比如:


存在多个配置


之后还存在一些 Engine 等等结构,当然除了这些还有着负责 jsp 页面解析、jsp 属性验证,同时也负责将 jsp 页面动态转换为 java 代码并编译为 class 文件的 Jasper 组件,提供命名服务的 Naming 组件,提供 Session 服务的 Session 组件,负责日志记录的 Logging 组件,提供 JVM 监控服务的 JMX 组件。


生命周期整个 Tomcat 的生命周期以观察者模式为基础


Subject 抽象主题:负责管理所有观察者的引用,同时定义主要的事件操作,而 Tomcat 核心类 LifeCycle 就是这个抽象主题。


ConcretSubject 具体主题:实现抽象主题定义的所有接口,发生变化通知观察者,如 StandardServer


Observer 观察者:监听主题变化的操作接口,LifeCycleListener 为这个抽象观察者


我们可以来看看 Lifecycle 的方法。


定义了许多的方法,分别为添加监听器,获取监听器,删除监听器或者是初始化,启动方法和停止消毁相关的方法。


存在有一个对 LifecycleBase 类对 Lifecycle 进行了实现,对各个方法进行了重写,我们来看看其中的几个方法。


init()


这里判断了 state 只有为 NEW 的时候才会进行初始化,首先将其状态设置为 INITIALIZING ,之后调用 initInternal 进行初始化操作,在初始化完成之后设置状态,如果在过程中出现错误,将会抛出异常


值得注意的是 initInternal 方法是一个抽象方法,需要各个组件进行重写,其他的方法也就不详细写了,师傅们可以跟一下。


从上述源码看得出来, LifecycleBase 是使用了状态机+模板模式来实现的。模板方法有下面这几个:


// 初始化方法 protected abstract void initInternal() throws LifecycleException;// 启动方法 protected abstract void startInternal() throws LifecycleException;// 停止方法 protected abstract void stopInternal() throws LifecycleException;// 销毁方法 protected abstract void destroyInternal() throws LifecycleException;Tomcat 类加载机制 Java 默认的类加载机制是通过 双亲委派模型 来实现的。而 Tomcat 实现的方式又和 双亲委派模型 有所区别。原因在于一个 Tomcat 容器允许同时运行多个 Web 程序,每个 Web 程序依赖的类又必须是相互隔离的。因此,如果 Tomcat 使用 双亲委派模式 来加载类的话,将导致 Web 程序依赖的类变为共享的。对于双亲委派模型的机制我们是非常熟悉的了,也不过多的讲解了。


贴个图


大概就是这样个一类加载顺序,同样存在有另一中缺陷,如果在程序中使用了第三方实现类,如果使用双亲委派模型,那么第三方实现类也需要放在 Java 核心类里面才可以,不然的话第三方实现类将不能被加载使用,但是在 java.lang.Thread 类中存在有两个方法能够获取到上下文类加载器。


我们可以通过在 SPI 类里面调用 getContextClassLoader 来获取第三方实现类的类加载器。由第三方实现类通过调用 setContextClassLoader 来传入自己实现的类加载器。


接下来来看看 Tomcat 独有的类加载机制模型。


我们可以从 官方文档 找到说明


这个类加载器结构图和之前的双亲委派机制的架构图有着很大的区别。


每个类加载器的作用如下 Common 类加载器,负责加载 Tomcat 和 Web 应用都复用的类


Catalina 类加载器,负责加载 Tomcat 专用的类,而这些被加载的类在 Web 应用中将不可见


Shared 类加载器,负责加载 Tomcat 下所有的 Web 应用程序都复用的类,而这些被加载的类在 Tomcat 中将不可见。


WebApp 类加载器,负责加载具体的某个 Web 应用程序所使用到的类,而这些被加载的类在 Tomcat 和其他的 Web 应用程序都将不可见。


Jsp 类加载器,每个 jsp 页面一个类加载器,不同的 jsp 页面有不同的类加载器,方便实现 jsp 页面的热插拔


直接来分析一下 Tomcat 启动所调用的流程。


如果在 org.apache.catalina.startup.Bootstrap 类中,来看一下类中存在的方法


其中存在 main 方法,在启动 tomcat 的同时将会调用这个方法,但是在这个类最后面存在一个静态代码块将会首先执行。


从环境变量中获取 catalina.home,在没有获取到的时候将执行后面的获取操作在第一步没获取的时候,从 bootstrap.jar 所在目录的上一级目录获取第二步中的 bootstrap.jar 可能不存在,这时我们直接把 user.dir 作为我们的 home 目录重新设置 catalinaHome 属性接下来获取 CATALINA_BASE(从系统变量中获取),若不存在,则将 CATALINA_BASE 保持和 CATALINA_HOME 相同重新设置 catalinaBase 属性而在 main 方法中,main 方法大体分成两块,一块为 init,另一块为 load+start,我们可以来看看 init 中的的代码。


public void init() throws Exception {// 非常关键的地方,初始化类加载器 s,后面我们会详细具体地分析这个方法 initClassLoaders();


// 设置上下文类加载器为catalinaLoader,这个类加载器负责加载Tomcat专用的类Thread.currentThread().setContextClassLoader(catalinaLoader);SecurityClassLoad.securityClassLoad(catalinaLoader);
// 使用catalinaLoader加载我们的Catalina类// Load our startup class and call its process() methodif (log.isDebugEnabled()) log.debug("Loading startup class");Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");Object startupInstance = startupClass.getConstructor().newInstance();
// 设置Catalina类的parentClassLoader属性为sharedLoader// Set the shared extensions class loaderif (log.isDebugEnabled()) log.debug("Setting startup class properties");String methodName = "setParentClassLoader";Class<?> paramTypes[] = new Class[1];paramTypes[0] = Class.forName("java.lang.ClassLoader");Object paramValues[] = new Object[1];paramValues[0] = sharedLoader;Method method = startupInstance.getClass().getMethod(methodName, paramTypes);method.invoke(startupInstance, paramValues);
// catalina守护对象为刚才使用catalinaLoader加载类、并初始化出来的Catalina对象catalinaDaemon = startupInstance;
复制代码


}跟进一下 initClassLoaders 。


private void initClassLoaders() {try {// 创建 commonLoader,如果未创建成果的话,则使用应用程序类加载器作为 commonLoadercommonLoader = createClassLoader("common", null);if( commonLoader == null ) {// no config file, default to this loader - we might be in a 'single' env.commonLoader=this.getClass().getClassLoader();}// 创建 catalinaLoader,父类加载器为 commonLoadercatalinaLoader = createClassLoader("server", commonLoader);// 创建 sharedLoader,父类加载器为 commonLoadersharedLoader = createClassLoader("shared", commonLoader);} catch (Throwable t) {// 如果创建的过程中出现异常了,日志记录完成之后直接系统退出 handleThrowable(t);log.error("Class loader creation threw exception", t);System.exit(1);}}接下来来看看 SecurityClassLoad.securityClassLoad 的使用其实就是使用 catalinaLoader 加载 tomcat 源代码里面的各个专用类。我们大致罗列一下待加载的类所在的 package。


org.apache.catalina.core.*


org.apache.coyote.*


org.apache.catalina.loader.*


org.apache.catalina.realm.*


org.apache.catalina.servlets.*


org.apache.catalina.session.*


org.apache.catalina.util.*


org.apache.catalina.valves.*


javax.servlet.http.Cookie


org.apache.catalina.connector.*


org.apache.tomcat.*


组件好不容易跟进了前面的 tomcat 源码,终于来到了我们的目的地组件的分析。


Server 在跟进前面的 Tomcat 启动类中调用了,server.init 方法和 server.start 方法,我们来看看 Server 的源码实现


在 org.apache.catalina.Server 接口中有着对方法的定义。


我们可以观察到其继承了我们前面分析了的生命周期的接口 Lifecycle ,这就意味着,在在调用了 server.init 或者 start 方法的同时同样会调用所有的 service 的方法。


来看看 Server 的实现类,类为 org.apache.catalina.core.StandardServer 下,在初始化的时候将会调用 initInternal 方法。


从这里的循环语句中也可以证实确实将会调用所有存在 service 的 init 方法,同样在调用 servser.start 方法的时候将会调用 startInternal 方法。


同样通过了循环的方式进行调用,我们可以通过 idea 查看该类的继承实现结构关系图。


Service 其接口在 org.apache.catalina.Service 中,存在方法的定义,同样继承了 Lifecycle 接口。我们来到 Service 的实现类 org.apache.catalina.core.StandardService 。


首先上图看一下类的关系


既然在调用 Server.init 中会调用 service.init 方法,那我们就跟进一下 service.init 讲了个什么。


其中存在对 Engine 的初始化 / Executor 的初始化(后面会提到) / connector 的初始化。同样的,跟进一下 start 方法的逻辑和上面调用 init 方法进行初始化差不多,分别调用了对应的 start 方法。


Container 对于 Container 的接口在 org.apache.catalina.Container 中,同样继承了 Lifecycle 接口


首先看一下他的继承关系。


由上图我们可以知道 Engine 包含多个 Host,Host 包含多个 Context,Context 包含多个 Wrapper,每个 Wrapper 对应一个 Servlet。


分别的说明


Engine,我们可以看成是容器对外提供功能的入口,每个 Engine 是 Host 的集合,用于管理各个 Host。


Host,我们可以看成 虚拟主机 ,一个 tomcat 可以支持多个虚拟主机。


Context,又叫做上下文容器,我们可以看成 应用服务 ,每个 Host 里面可以运行多个应用服务。同一个 Host 里面不同的 Context,其 contextPath 必须不同,默认 Context 的 contextPath 为空格("")或斜杠(/)。


Wrapper,是 Servlet 的抽象和包装,每个 Context 可以有多个 Wrapper,用于支持不同的 Servlet。另外,每个 JSP 其实也是一个个的 Servlet。


Connector 最后来到了最关键的 Connector 部分了,也是构造内存马的关键位置。


首先来看看整体的架构流程图。


描述了请求到达 Container 进行处理的全过程。


不同协议 ProtocolHandler 会有不同的实现。


ajp 和 http11 是两种不同的协议


nio、nio2 和 apr 是不同的通信方式


协议和通信方式可以相互组合


ProtocolHandler 包含三个部件: Endpoint 、 Processor 、 Adapter 。


Endpoint 用来处理底层 Socket 的网络连接, Processor 用于将 Endpoint 接收到的 Socket 封装成 Request, Adapter 用于将 Request 交给 Container 进行具体的处理。


Endpoint 由于是处理底层的 Socket 网络连接,因此 Endpoint 是用来实现 TCP/IP 协议 的,而 Processor 用来实现 HTTP 协议 的, Adapter 将请求适配到 Servlet 容器进行具体的处理。


Endpoint 的抽象实现类 AbstractEndpoint 里面定义了 Acceptor 和 AsyncTimeout 两个内部类和一个 Handler 接口 。 Acceptor 用于监听请求, AsyncTimeout 用于检查异步 Request 的超时, Handler 用于处理接收到的 Socket,在内部调用 Processor 进行处理。


接下来深入源码进行一下分析


跟进 org.apache.catalina.connector.Connector 类的代码逻辑


发现以下几点


无参构造方法,传入参数为空协议,会默认使用 HTTP/1.1


HTTP/1.1 或 null ,protocolHandler 使用 org.apache.coyote.http11.Http11NioProtocol ,不考虑 apr


AJP/1.3 ,protocolHandler 使用 org.apache.coyote.ajp.AjpNioProtocol ,不考虑 apr


其他情况,使用传入的 protocol 作为 protocolHandler 的类名


使用 protocolHandler 的类名构造 ProtocolHandler 的实例


接下来来到了 Connector#initInternal 方法的调用。


总结一下就是初始化了一个 CoyoteAdapter,并将其设置进入了 protocolHandler 中,之后接受了 body 的 method 列表,默认为 POST。


最后初始化了初始化 protocolHandler


之后将会调用 protocolHandler#init 方法


而对于 start 方法的调用,在 Connector 中


在设置了状态为 STARTING 之后就调用了 protocolHandler#start 方法。


将会调用 endpoint#start 方法。


跟进一下


调用 bind 方法


创建工作者线程池。


初始化连接 latch,用于限制请求的并发量。


开启 poller 线程。poller 用于对接受者线程生产的消息(或事件)进行处理,poller 最终调用的是 Handler 的代码。


开启 acceptor 线程


Acceptor 请求的入口是在 Acceptor 。 Endpoint.start() 方法会开启 Acceptor 线程 来处理请求


那么我们接下来就要分析一下 Acceptor 线程 中的执行逻辑


在其 run 方法中存在


运行过程中,如果 Endpoint 暂停了,则 Acceptor 进行自旋(间隔 50 毫秒)


如果 Endpoint 终止运行了,则 Acceptor 也会终止


如果请求达到了最大连接数,则 wait 直到连接数降下来


接受下一次连接的 socket


存在这样的逻辑


setSocketOptions() 这儿是关键,会将 socket 以事件的方式传递给 poller。


跟进一下 setSocketOptions


其中存在 this.getPoller0.register 的调用将 channel 注册到 poller,注意关键的两个方法, getPoller0() 和 Poller.register() 。先来分析一下 getPoller0() ,该方法比较关键的一个地方就是 以取模的方式 对 poller 数量进行轮询获取。


/**


  • The socket poller./private Poller[] pollers = null;private AtomicInteger pollerRotater = new AtomicInteger(0);/*

  • Return an available poller in true round robin fashion.

  • @return The next poller in sequence*/public Poller getPoller0() {int idx = Math.abs(pollerRotater.incrementAndGet()) % pollers.length;return pollers[idx];}接下来我们分析一下 Poller.register() 方法。因为 Poller 维持了一个 events 同步队列 ,所以 Acceptor 接受到的 channel 会放在这个队列里面。


PollerAcceptor 生成了事件 PollerEvent ,那么 Poller 必然会对这些事件进行消费。我们来分析一下 Poller.run() 方法。


在其中存在有


调用了 processKey(sk, socketWrapper)进行处理。


该方法又会根据 key 的类型,来分别处理读和写。


处理读事件,比如生成 Request 对象


处理写事件,比如将生成的 Response 对象通过 socket 写回客户端


跟进 processSocket 方法


从 processorCache 里面拿一个 Processor 来处理 socket, Processor 的实现为 SocketProcessor


将 Processor 放到工作线程池中执行


Processor 调用 service() 方法


生成 Request 和 Response 对象


调用 Adapter.service() 方法,将生成的 Request 和 Response 对象传进去


AdapterAdapter 用于连接 Connector 和 Container ,起到承上启下的作用。 Processor 会调用 Adapter.service() 方法主要做了下面几件事情


根据 coyote 框架的 request 和 response 对象,生成 connector 的 request 和 response 对象(是 HttpServletRequest 和 HttpServletResponse 的封装)


补充 header


解析请求,该方法会出现代理服务器、设置必要的 header 等操作


真正进入容器的地方,调用 Engine 容器下 pipeline 的阀门


总结就这样,深入代码的了解了整个 Tomcat 的架构(当然,还是有很多的代码没有贴出来,太长了,需要师傅们自己跟一下),跟完之后大吸一口凉气。真难

用户头像

Java-fenn

关注

需要Java资料或者咨询可加我v : Jimbye 2022.08.16 加入

还未添加个人简介

评论

发布
暂无评论
Tomcat架构之为Bypass内存马检测铺路(内存马系列篇四)_Java、_Java-fenn_InfoQ写作社区