Netty服务被攻击实例分析

80次阅读
没有评论

共计 3901 个字符,预计需要花费 10 分钟才能阅读完成。

本篇内容介绍了“Netty 服务被攻击实例分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让丸趣 TV 小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

故事前奏

Netty 服务是公司比较边缘的服务,只有一台设备在使用,而且代码是之前技术 Leader(已离职)写的,加上一直赶工期,所以就没抽出时间去彻底解决这事。

当初被攻击没排查代码,看到遭到疯狂请求、CPU 跑满、日志打满,还以为是遭遇 DDoS 攻击了。

临时采取了几个措施:

分离服务器,确保该服务遭到攻击时不会拖垮其他服务;

换了一个 IP 和端口;

针对攻击的 IP 添加黑名单;

在代码层,发现非法请求强制关闭连接;

添加日志信息,追溯攻击报文和源头;

对攻击服务的 IP(上海阿里云的)进行举报;

但没多久,黑客又找上门来了,十天半月来一次攻击,好像知道服务 IP 和后台代码似的,阴魂不散。

这不,今天被逮到了,而且之前添加了日志打印,也拿到了攻击的报文内容,复现了攻击操作。

//  攻击者第一次尝试的报文  8000002872FE1D130000000000000002000186A00001977C0000000000000000000000000000000000000000 //  攻击者第二次尝试的报文  8000002872FE1D130000000000000002000186A00001977C00000000000000000000000000000000

上述报文,第一次的报文触发了攻击,第二次的报文没有影响(与正常业务报文格式无异)。

下面就带大家分析分析攻击的逻辑和代码中存在的漏洞。

知识储备

要了解攻击的原理,我们需要有一定的 Netty 技术知识。关于 Netty 如何实现客户端和服务器端的代码这里就不展开了,可以看一下实现实例:https://github.com/secbr/netty-all/tree/main/netty-decoder

我们重点了解一下自定义解码器和 io.netty.buffer.ByteBuf。其中自定义解码器用于对报文进行解析,而报文内容通过 ByteBuf 进行缓存传输。

上面的攻击报文格式表明,黑客已经“猜到”我们是基于 16 进制 Btye 格式进行内容传输的(黑客竟然也知道)。

自定义解码器

要自定义解码器,继承 MessageToMessageDecoder 类并实现 decode 方法即可,下面展示一下示例代码:

public class MyDecoder extends MessageToMessageDecoder ByteBuf  { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List Object  out) { } }

其中解析报文的逻辑便是在 decode 方法内进行处理。其中 ByteBuf  in 就是接收传入报文的容器,而 List out 用于输出解析之后的结果。

下面来看一下有 bug 的代码(已经过脱敏处理):

protected void decode(ChannelHandlerContext ctx, ByteBuf in, List Object  out) { int readableBytes = in.readableBytes(); while (readableBytes   3) { in.skipBytes(2); int pkgLength = in.readUnsignedShort(); in.readerIndex(in.readerIndex() - 4); if (in.readableBytes()   pkgLength) { return; } out.add(in.readBytes(pkgLength)); readableBytes = in.readableBytes(); } }

上面的代码在跑正常业务时是没问题的,但当被攻击时,就进入了死循环。因此,导致虽然在业务处理时添加了关闭连接的操作也是无效的。

在分析上面代码之前,我们还得先详细分析一下 ByteBuf 的原理。

ByteBuf 的原理

ByteBuf 中会维护两个索引:一个索引 (readIndex) 用于读取,一个索引 (writeIndex) 用于写入。

当从 ByteBuf 读取时,readIndex 会被递增已经被读取的字节数,当向 ByteBuf 中写入数据时,writeIndex 也会被递增。

netty-ByteBuf

上面图以攻击的报文为例进行展示,攻击者用了 44 个字节的报文进行攻击。由于使用的是 16 进制,所以两个字符占用 1 个字节。

readIndex 和 writeIndex 的起始位置的索引位置都为 0,当执行 ByteBuf 中的 readXXX 或 writeXXX 方法时,会推进对应的索引。当执行 setXXX 或 getXXX 方法的操作时则不会。

了解了 ByteBuf 的基本处理原理之后,我们就来对照攻击者的报文和源代码来进行攻击过程的还原。

攻击还原

下面直接通过源代码一步步的分析,主要涉及 ByteBuf 类的方法。有效攻击的报文为上面提到的第一个报文。

//  攻击者第一次尝试的报文  8000002872FE1D130000000000000002000186A00001977C0000000000000000000000000000000000000000

下面来看代码:

int readableBytes = in.readableBytes();

这行代码通过 readableBytes 方法获取到当前 ByteBuf 中可以读到的字节数,上述攻击报文 88 个字符,所以这里得到 44 个字节。

当 readableBytes 大于 3 时便进行具体的解析处理:

in.skipBytes(2);

很明显,通过 skipBytes 方法跳过了两个字节。

netty-ByteBuf

int pkgLength = in.readUnsignedShort();

通过 readUnsignedShort 方法,获得了 2 个字节的内容,这两个字节对应的十六进制值为“0028”,对应十进制为“40”。这两个字节在报文中的含义是 (部分或整个) 报文的长度。

报文的长度往往有两种算法:第一,长度代表整个报文的长度(业务中使用的含义); 第二,长度代表除前 4 个字节之后的报文长度(攻击者使用的含义)。

其实,正是因为这个长度含义的定义,导致正常业务可以执行,而攻击报文会进入死循环。

下面继续分享代码:

in.readerIndex(in.readerIndex() - 4);

经上面的 skipBytes 和 readUnsignedShort 的调用,ByteBuf 的读索引已经跑到了第 4 个字节上了。所以这里 in.readerIndex()返回的值为 4,而 in.readerIndex(4-4)的作用就是将读索引重置为 0,也就是从头开始读。

if (in.readableBytes()   pkgLength) { return; }

这个判断是在读索引移动到 0 之后,看看报文的可读字节数是否小于报文内容中指定的字节数。很显然,in.readableBytes()对应的值为 44 个字节,而 pkgLength 为 40 个字节,不会进行 return。

out.add(in.readBytes(pkgLength));

读取 40 个字节,进行输出。还剩下 4 个字节的内容,readIndex 指向第 40 个字节的位置。

readableBytes = in.readableBytes();

由于 readIndex 已经指向第 40 个字节,所以此时可读字节数为 4。

然后,进入第二轮循环。此时,神奇的情况就出现了。我们可以看到攻击的后 4 个字节的报文值全为 0。

in.skipBytes(2); int pkgLength = in.readUnsignedShort();

因此跳过 2 个字节后,readIndex 为 42,pkgLength 获取第 43 和 44 字节的值:0。

in.readerIndex(in.readerIndex() - 4);

上述代码又将 readIndex 设置到第 40 个字节。

if (in.readableBytes()   pkgLength) { return; }

此时会发现 readableBytes 返回值为 4,但 pkgLength 已经变为 0 了,不会 return。

接下读取内容时就出现状况了:

out.add(in.readBytes(pkgLength)); //  这里还剩下 4 个字节  readableBytes = in.readableBytes();

上述 readBytes 读取字节数为 0,而 readableBytes 始终为 4。此时,整个 while 循环进入了死循环,大量消耗 CPU 资源。

此时还没完,最多只是把 CPU 跑到 100%,但是当不停的将空字符写到接收数据的缓冲区域之后,缓冲区开始疯狂调用处理业务的 Handler,进一步侵入到业务处理逻辑当中。

虽然业务逻辑层做了判断,也进行了连接的关闭,但此时已经与连接无关,while 循环已经进入死循环,关掉连接也没什么作用。同时,业务层有日志输出,大量的日志输出到磁盘当中,导致磁盘被刷满。

最终导致服务器的 CPU 监控和磁盘监控报警。乍一看,还以为是又一次 DDoS 攻击。

“Netty 服务被攻击实例分析”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注丸趣 TV 网站,丸趣 TV 小编将为大家输出更多高质量的实用文章!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-08-04发表,共计3901字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)