溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Netty服務(wù)被攻擊實例分析

發(fā)布時間:2022-01-06 15:39:48 來源:億速云 閱讀:260 作者:iii 欄目:服務(wù)器

本篇內(nèi)容介紹了“Netty服務(wù)被攻擊實例分析”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細閱讀,能夠?qū)W有所成!

故事前奏

Netty服務(wù)是公司比較邊緣的服務(wù),只有一臺設(shè)備在使用,而且代碼是之前技術(shù)Leader(已離職)寫的,加上一直趕工期,所以就沒抽出時間去徹底解決這事。

當(dāng)初被攻擊沒排查代碼,看到遭到瘋狂請求、CPU跑滿、日志打滿,還以為是遭遇DDoS攻擊了。

臨時采取了幾個措施:

  • 分離服務(wù)器,確保該服務(wù)遭到攻擊時不會拖垮其他服務(wù);

  • 換了一個IP和端口;

  • 針對攻擊的IP添加黑名單;

  • 在代碼層,發(fā)現(xiàn)非法請求強制關(guān)閉連接;

  • 添加日志信息,追溯攻擊報文和源頭;

  • 對攻擊服務(wù)的IP(上海阿里云的)進行舉報;

但沒多久,黑客又找上門來了,十天半月來一次攻擊,好像知道服務(wù)IP和后臺代碼似的,陰魂不散。

這不,今天被逮到了,而且之前添加了日志打印,也拿到了攻擊的報文內(nèi)容,復(fù)現(xiàn)了攻擊操作。

// 攻擊者第一次嘗試的報文 8000002872FE1D130000000000000002000186A00001977C0000000000000000000000000000000000000000 // 攻擊者第二次嘗試的報文 8000002872FE1D130000000000000002000186A00001977C00000000000000000000000000000000

上述報文,第一次的報文觸發(fā)了攻擊,第二次的報文沒有影響(與正常業(yè)務(wù)報文格式無異)。

下面就帶大家分析分析攻擊的邏輯和代碼中存在的漏洞。

知識儲備

要了解攻擊的原理,我們需要有一定的Netty技術(shù)知識。關(guān)于Netty如何實現(xiàn)客戶端和服務(wù)器端的代碼這里就不展開了,可以看一下實現(xiàn)實例:https://github.com/secbr/netty-all/tree/main/netty-decoder

我們重點了解一下自定義解碼器和io.netty.buffer.ByteBuf。其中自定義解碼器用于對報文進行解析,而報文內(nèi)容通過ByteBuf進行緩存?zhèn)鬏敗?/p>

上面的攻擊報文格式表明,黑客已經(jīng)“猜到”我們是基于16進制Btye格式進行內(nèi)容傳輸?shù)?黑客竟然也知道)。

自定義解碼器

要自定義解碼器,繼承MessageToMessageDecoder類并實現(xiàn)decode方法即可,下面展示一下示例代碼:

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

其中解析報文的邏輯便是在decode方法內(nèi)進行處理。其中ByteBuf  in就是接收傳入報文的容器,而List out用于輸出解析之后的結(jié)果。

下面來看一下有bug的代碼(已經(jīng)過脫敏處理):

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();     } }

上面的代碼在跑正常業(yè)務(wù)時是沒問題的,但當(dāng)被攻擊時,就進入了死循環(huán)。因此,導(dǎo)致雖然在業(yè)務(wù)處理時添加了關(guān)閉連接的操作也是無效的。

在分析上面代碼之前,我們還得先詳細分析一下ByteBuf的原理。

ByteBuf的原理

ByteBuf中會維護兩個索引:一個索引(readIndex)用于讀取,一個索引(writeIndex)用于寫入。

當(dāng)從ByteBuf讀取時,readIndex會被遞增已經(jīng)被讀取的字節(jié)數(shù),當(dāng)向ByteBuf中寫入數(shù)據(jù)時,writeIndex也會被遞增。

Netty服務(wù)被攻擊實例分析

netty-ByteBuf

上面圖以攻擊的報文為例進行展示,攻擊者用了44個字節(jié)的報文進行攻擊。由于使用的是16進制,所以兩個字符占用1個字節(jié)。

readIndex和writeIndex的起始位置的索引位置都為0,當(dāng)執(zhí)行ByteBuf中的readXXX或writeXXX方法時,會推進對應(yīng)的索引。當(dāng)執(zhí)行setXXX或getXXX方法的操作時則不會。

了解了ByteBuf的基本處理原理之后,我們就來對照攻擊者的報文和源代碼來進行攻擊過程的還原。

攻擊還原

下面直接通過源代碼一步步的分析,主要涉及ByteBuf類的方法。有效攻擊的報文為上面提到的第一個報文。

// 攻擊者第一次嘗試的報文 8000002872FE1D130000000000000002000186A00001977C0000000000000000000000000000000000000000

下面來看代碼:

int readableBytes = in.readableBytes();

這行代碼通過readableBytes方法獲取到當(dāng)前ByteBuf中可以讀到的字節(jié)數(shù),上述攻擊報文88個字符,所以這里得到44個字節(jié)。

當(dāng)readableBytes大于3時便進行具體的解析處理:

in.skipBytes(2);

很明顯,通過skipBytes方法跳過了兩個字節(jié)。

Netty服務(wù)被攻擊實例分析

netty-ByteBuf

int pkgLength = in.readUnsignedShort();

通過readUnsignedShort方法,獲得了2個字節(jié)的內(nèi)容,這兩個字節(jié)對應(yīng)的十六進制值為“0028”,對應(yīng)十進制為“40”。這兩個字節(jié)在報文中的含義是(部分或整個)報文的長度。

報文的長度往往有兩種算法:第一,長度代表整個報文的長度(業(yè)務(wù)中使用的含義);第二,長度代表除前4個字節(jié)之后的報文長度(攻擊者使用的含義)。

其實,正是因為這個長度含義的定義,導(dǎo)致正常業(yè)務(wù)可以執(zhí)行,而攻擊報文會進入死循環(huán)。

下面繼續(xù)分享代碼:

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

經(jīng)上面的skipBytes和readUnsignedShort的調(diào)用,ByteBuf的讀索引已經(jīng)跑到了第4個字節(jié)上了。所以這里in.readerIndex()返回的值為4,而in.readerIndex(4-4)的作用就是將讀索引重置為0,也就是從頭開始讀。

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

這個判斷是在讀索引移動到0之后,看看報文的可讀字節(jié)數(shù)是否小于報文內(nèi)容中指定的字節(jié)數(shù)。很顯然,in.readableBytes()對應(yīng)的值為44個字節(jié),而pkgLength為40個字節(jié),不會進行return。

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

讀取40個字節(jié),進行輸出。還剩下4個字節(jié)的內(nèi)容,readIndex指向第40個字節(jié)的位置。

readableBytes = in.readableBytes();

由于readIndex已經(jīng)指向第40個字節(jié),所以此時可讀字節(jié)數(shù)為4。

然后,進入第二輪循環(huán)。此時,神奇的情況就出現(xiàn)了。我們可以看到攻擊的后4個字節(jié)的報文值全為0。

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

因此跳過2個字節(jié)后,readIndex為42,pkgLength獲取第43和44字節(jié)的值:0。

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

上述代碼又將readIndex設(shè)置到第40個字節(jié)。

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

此時會發(fā)現(xiàn)readableBytes返回值為4,但pkgLength已經(jīng)變?yōu)?了,不會return。

接下讀取內(nèi)容時就出現(xiàn)狀況了:

out.add(in.readBytes(pkgLength)); // 這里還剩下4個字節(jié) readableBytes = in.readableBytes();

上述readBytes讀取字節(jié)數(shù)為0,而readableBytes始終為4。此時,整個while循環(huán)進入了死循環(huán),大量消耗CPU資源。

此時還沒完,最多只是把CPU跑到100%,但是當(dāng)不停的將空字符寫到接收數(shù)據(jù)的緩沖區(qū)域之后,緩沖區(qū)開始瘋狂調(diào)用處理業(yè)務(wù)的Handler,進一步侵入到業(yè)務(wù)處理邏輯當(dāng)中。

雖然業(yè)務(wù)邏輯層做了判斷,也進行了連接的關(guān)閉,但此時已經(jīng)與連接無關(guān),while循環(huán)已經(jīng)進入死循環(huán),關(guān)掉連接也沒什么作用。同時,業(yè)務(wù)層有日志輸出,大量的日志輸出到磁盤當(dāng)中,導(dǎo)致磁盤被刷滿。

最終導(dǎo)致服務(wù)器的CPU監(jiān)控和磁盤監(jiān)控報警。乍一看,還以為是又一次DDoS攻擊。

“Netty服務(wù)被攻擊實例分析”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI