日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

原文出自:公眾號 程序新視界

原文作者:https://mp.weixin.qq.com/s/GzvwZvVNHx4yjEUVibSNBA

前言

馬上就要過春節了,本想著完成手頭的任務就可以準備過年了。沒想到Netty服務器又被攻擊了,當收到服務器報警(CPU飆升報警)信息,就知道對方又下手了。

之前是交給下面的兄弟來解決,這次為了過個好年,決定親自動手把這事給了結了。

故事前奏

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<Object> 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服務被N次攻擊之后,終于抓到現行了

 

上面圖以攻擊的報文為例進行展示,攻擊者用了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服務被N次攻擊之后,終于抓到現行了

 

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
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定