微信远程攻击面简单的研究与分析 发布时间:2020-04-20 10:56:07 在完成了对 FaceTime 的一系列漏洞挖掘与研究后,我们决定对微信的音视频通信做一些分析。经分析后发现,当微信语音通话连接建立成功之后,微信客户端将解析远端发来的网络报文并还原成多媒体流。在还原解析的过程中,如果处理远端数据的代码存在问题时就会形成一个远程的攻击面。 在针对这个攻击面进行深入挖掘后我们发现了若干可以造成远程内存破坏的漏洞。本篇文章我们将选择一个比较有趣且复杂的漏洞进行深入的分析。该漏洞可以造成远程写溢出从而导致崩溃,其root cause隐藏的非常深,触发流程也比较复杂。研究与分析该漏洞无论是对安全研究还是软件开发的角都有一定的价值。我们将在文章中详细的分析漏洞成因和触发流程。微信已经在最新版7.0.12中修复了该漏洞。 首先我们先介绍两个比较简单的漏洞,一个属于本地代码执行,一个属于远程溢出。 ###本地代码执行 Mac版本的微信客户端处理粘贴操作时,没有有效检查粘贴板对象中内容,导致不安全的对象反序列化。当本地其他恶意应用设置粘贴板时,用户在微信客户端粘贴操作时,会导致任意对象的创建。 如下面截图所示,Mac 版本的微信在反序列化粘贴板对象的过程中,并没有使用secure coding 以及白名单等设置,导致任何可以响应 [initwithcoder:] 函数的 objective-c 对象都能被创建并使用,会引起很大的攻击面。 <figure><img src="https://blog.pangu.io/wp-content/uploads/unarchive-1-1024x873.jpeg" alt="" srcset="https://blog.pangu.io/wp-content/uploads/unarchive-1-1024x873.jpeg 1024w, https://blog.pangu.io/wp-content/uploads/unarchive-1-300x256.jpeg 300w, https://blog.pangu.io/wp-content/uploads/unarchive-1-768x655.jpeg 768w, https://blog.pangu.io/wp-content/uploads/unarchive-1.jpeg 1156w" sizes="(max-width: 1024px) 100vw, 1024px"><figcaption><em>Mac版本微信对剪切板的处理</em></figcaption></figure> 具体攻击结果可以参考[Google Project Zero在iMessage中发现的大量不安全反序列化攻击] (https://www.blackhat.com/us-19/briefings/schedule/#look-no-hands—-the-remote-interaction-less-attack-surface-of-the-iphone-15203). Mac版本微信已经对该漏洞进行了完全正确的修复,调用了 setRequiresSecureCoding: 函数,并作出了安全设置。 <figure><img src="" alt="" srcset="https://blog.pangu.io/wp-content/uploads/unarchive_fix-1024x277.png 1024w, https://blog.pangu.io/wp-content/uploads/unarchive_fix-300x81.png 300w, https://blog.pangu.io/wp-content/uploads/unarchive_fix-768x208.png 768w, https://blog.pangu.io/wp-content/uploads/unarchive_fix-1536x416.png 1536w, https://blog.pangu.io/wp-content/uploads/unarchive_fix.png 1788w" sizes="(max-width: 1024px) 100vw, 1024px"><figcaption><em>修复后的剪切板处理</em></figcaption></figure> ###远程下溢出 微信视频通话接通后,通话两端建立网络直连传递RTP报文。微信客户端传输RTP包过程中,采用了一套加密机制。但是微信客户端在RTP解密之前,没有很好验证RTP包长度。当攻击者发送很短的RTP包的时候,会引起接受端处理RTP包过程中长度计算的整数下溢出,进而导致内存越界访问。 <figure><img src="https://blog.pangu.io/wp-content/uploads/下溢出-1024x1007.jpeg" alt="" srcset="https://blog.pangu.io/wp-content/uploads/下溢出-1024x1007.jpeg 1024w, https://blog.pangu.io/wp-content/uploads/下溢出-300x295.jpeg 300w, https://blog.pangu.io/wp-content/uploads/下溢出-768x755.jpeg 768w, https://blog.pangu.io/wp-content/uploads/下溢出.jpeg 1174w" sizes="(max-width: 1024px) 100vw, 1024px"><figcaption><em>RTP包长度验证减法下溢出</em></figcaption></figure> 有趣的是,GP0 研究员在微信 CAudioJBM::InputAudioFrameToJBM 函数中发现了类似的错误 (https://bugs.chromium.org/p/project-zero/issues/detail?id=1948)。这说明微信在在包长度验证时存在一定共性缺陷。 这是一个非常明显的下溢出,但是通过对这个问题的分析,我们认为远程的攻击面中可能存在风险更高的漏洞。 ##远程写溢出成因与分析 跳过前期复杂的协商互联流程,我们在已经通过微信语音通话的状态下,微信客户端将收到远端发送来的音频数据。收到的原始数据会被层层分解处理,并根据不同的类型分发到不同的处理函数上。 ###RecvRtpPacketCng 在收到远端的网络数据后,RTP 数据包将被 RecvRtpPacketCng(__int64 XVEChannel, unsigned int *pData, __int16 len, void *a4) 函数处理,这里的参数 pData内容是语音通话的远端完全可控的。该函数会根据网络包中指定的过不同的代码解析 <pre><code> switch ( pkType ) { case 0: log1(1, "************* XVEChannel:: pkType == 0x80 \r\n\r\n"); if ( (unsigned int)UnpacketRTP( (unsigned int **)&pCur, (unsigned int *)&nCodec, &udwTimeStamp, udwSeqNum, &redundantlen, &pDataLength) == -1 ) { log1(1, "\r\nXVEChannel::RecvRtpPacket, UnpacketRTP ERROR,! \r\n"); v15 = wc_gettimeofday() - v189; v16 = "leave RecvRtpPacketCng 3,time in %llu\n"; goto LABEL_17; } //... }</code></pre> 当pkType类型为7或8时,该网络包的类型为 RTPwithRsMd <pre><code> // pcur = pdata+8 while ( 1 ) { get_subpkttype_and_subpktleft(*v54, pCur, (int *)&sub_pkt_type, &sub_pkt_left); log1(1, "subpkttype is %d,subpktleft is %d\r\n", sub_pkt_type, sub_pkt_left); LOBYTE(v185) = sub_pkt_left != 0; if ( sub_pkt_type == 1 ) break; if ( sub_pkt_type ) goto LABEL_125; v55 = (unsigned int *)operator new(4uLL, (const std::nothrow_t *)&std::nothrow); if... PacketMeta = v55; getSubPacketMetaData(*v54, (_BYTE *)pCur, v55); v57 = *PacketMeta; v58 = (unsigned __int8)(*PacketMeta >> 16); nLen = v58 + ((unsigned __int64)((*PacketMeta >> 24) & 1) << 8); v54 = (__int64 *)v179; v40 = v189; log1( 4, "RecvRtpPacket::pkttype=%d,blocknum=%d,d=%d,f=%d,k=%d,r=%d,symid=%d,symlen_high2bits= %d,symlen_low8bits= %d,len = %d\n", *PacketMeta & 3, BYTE1(v57), (*PacketMeta >> 29) & 3, *PacketMeta >> 31, (*PacketMeta >> 2) & 7, (*PacketMeta >> 5) & 7, (*PacketMeta >> 25) & 0xF, (*PacketMeta >> 24) & 1, v58, v58 + ((unsigned __int64)((*PacketMeta >> 24) & 1) << 8)); RsMdDecProcess(*v54, (unsigned __int8 *)(pCur + 4), nLen, *PacketMeta, udwTimeStamp, udwSeqNum[0], (char)v184); pCur += nLen + 4; operator delete(PacketMeta); v60 = (char)v185; LABEL_123: v53 = XVEChannel; if ( !v60 || nCodec == 8 ) goto LABEL_125; }</code></pre> 当网络包头部的 subpkt 解析完成后会调用 ParaseRemoteLostRateParam 函数: <pre><code> if ( v62 ) { v63 = v62; sub_101078AE4(*v54, (_BYTE *)pCur, v62); log1(4, "RecvRtpPacket::pkttype=%d,f=%d,subtype=%d,len = %d\n", *v63 & 3, (*v63 >> 2) & 1, *v63 >> 3, v63[1]); v64 = v63[1]; if ( *v63 <= 7u && (!byte_102A0E985 || !*(_BYTE *)(*(_QWORD *)(XVEChannel + 1800) + 3887LL)) ) { v65 = v60; ParaseRemoteLostRateParam(*(_QWORD *)(XVEChannel + 72), (unsigned __int8 *)(pCur + 2), v63[1]); //<<==== [1] v185 = (unsigned __int8 *)pCur; v67 = (unsigned int)*(__int16 *)(XVEChannel + 46276); v60 = v65; log1(4, "usSetBitrateFlag:%d,sizeofLen:%d\n", v67, 3LL); ... }</code></pre> ParaseRemoteLostRateParam 函数中,根据远端的 pData 中数据设置了XVEChannel+72 处对象的内部数据。通过参数 a2,在 pData 中读取两个字节,并最终设置到 m_RemoteLrParam 和 nFrmCnt 两个成员变量中。 <pre><code>__int64 __fastcall ParaseRemoteLostRateParam(__int64 XVEChannel_72, __int64 a2, unsigned int a3) { // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND] if ( a2 ) { if ( a3 >= 3 ) { v3 = *(unsigned __int8 *)a2; v4 = *(unsigned __int8 *)(a2 + 1); *(_BYTE *)(XVEChannel_72 + 1660) = v4; // nFrmCnt *(_BYTE *)(XVEChannel_72 + 1659) = v3; // m_RemoteLrParam k: r: d: result = log1( 4, "ParaseRemoteLostRateParam:: m_RemoteLrParam k: %d, r: %d, d: %d, nFrmCnt: %d \r\n", v3 & 7, (v3 >> 3) & 7, v3 >> 6, v4); ++*(_DWORD *)(XVEChannel_72 + 528); *(_DWORD *)(XVEChannel_72 + 1712) = 1; } } return result; }</code></pre> ###DevPutProcessRsMdCng 在接收远端的语音数据的同时,也需要将自己的语音数据通过`XVEChannel`对象发送给远端。 <pre><code>__int64 __fastcall DevPutProcessRsMdCng(__int64 XVEChannel, const void *a2, __int64 a3, unsigned int a4) { // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND] v4 = a4; nDataLen = (unsigned int)a3; v6 = a2; v93 = 0; v85 = 0; log1( 1, "===== Enter DevPutProcessRsMdCng, input len = %d,nCoderFrameLen = %d,m_bFecStatus = %d,bChannelDtxFlag :%d !\r\n", a3, *(unsigned int *)(XVEChannel + 196), *(unsigned __int8 *)(XVEChannel + 208), a4); XVEChannel_72 = *(_QWORD *)(XVEChannel + 72); if ( *(_DWORD *)(XVEChannel_72 + 1712) == 1 ) { readRemoteLrParam(XVEChannel_72, (__int64)&v92); //<=================== 读取 m_RemoteLrParam 和 nFrmCnt } else if ( (unsigned int)(*(_DWORD *)(XVEChannel + 1828) - 1) > 1 ) { v92 = 0x20A; } else { v92 = 0x301; }</code></pre> 在 readRemoteLrParam 函数中,会将刚刚设置的 m_RemoteLrParam 和 nFrmCnt 读取到栈上变量v92中。 <pre><code>char __fastcall readRemoteLrParam(__int64 a1, __int64 a2) { char result; // al char v3; // cl char v4; // dl *(_BYTE *)(a2 + 1) = *(_BYTE *)(a1 + 1660); result = *(_BYTE *)(a1 + 1659) & 7; v3 = result | *(_BYTE *)a2 & 0xF8; *(_BYTE *)a2 = v3; v4 = *(_BYTE *)(a1 + 1659) & 0x38; *(_BYTE *)a2 = v4 | v3 & 0xC7; *(_BYTE *)a2 = *(_BYTE *)(a1 + 1659) & 0xC0 | result | v4; return result; }</code></pre> 在读取`RemoteLostRateParam`到局部变量v92后,需要设置到相应的本地成员变量中 <pre><code> if ( !*(_DWORD *)(XVEChannel + 392) && (unsigned __int8)(HIBYTE(v92) - 1) <= 2u && (int)v15 <= *(_DWORD *)(XVEChannel + 384) * (*(int *)(XVEChannel + 196) >> 1) ) { DevPutProcessRsMdCng_SetLocalExpectRSPara( *(_QWORD *)(XVEChannel + 72), v92 & 7, ((unsigned __int8)v92 >> 3) & 7, (unsigned __int8)v92 >> 6); log1( 4, "DevPutProcessRsMdCng_SetLocalExpectRSPara:: m_iNetworkType = %d,nFrmCnt: %d, k: %d, r: %d, d: %d\n", *(unsigned int *)(XVEChannel + 1828), HIBYTE(v92), v92 & 7, ((unsigned __int8)v92 >> 3) & 7, (unsigned __int8)v92 >> 6); v15 = *(unsigned int *)(XVEChannel + 332); } void __fastcall DevPutProcessRsMdCng_SetLocalExpectRSPara(__int64 XVEChannel_72, char a2, char a3, char a4) { *(_BYTE *)(XVEChannel_72 + 64) = a2; *(_BYTE *)(XVEChannel_72 + 65) = a3; *(_BYTE *)(XVEChannel_72 + 66) = a4; }</code></pre> 当数据准备好后将调用函数 CAudioRS::RsMdEncProcessCng,写溢出就发生在这个函数中。 <pre><code> CAudioRS::RsMdEncProcessCng( *(_QWORD *)(XVEChannel + 72), *(const void **)(XVEChannel + 312), (unsigned int)(*(_DWORD *)(XVEChannel + 400) + *(_DWORD *)(XVEChannel + 384) + 1), (__int64)v80, &v85, *(_DWORD *)(XVEChannel + 356) - v57, v76, v87 & 1, v90 & 1);</code></pre> 当 CAudioRS::RsMdEncProcessCng 刚开始执行时会通过 XVEChannel_72+9 作为 index 写一个 byte. <pre><code>_int64 __fastcall CAudioRS::RsMdEncProcessCng(__int64 XVEChannel_72, const void *a2, __int64 a3, __int64 a4, int *a5, unsigned int a6, unsigned __int8 a7, unsigned __int8 a8, unsigned __int8 a9) { // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND] v9 = a6; v10 = a3; log1( 4, "Enter CAudioRS::RsMdEncProcessCng,nInLen is %d, uiTimeStamp is %u,m_cEncSourceCountInBlk = %d,m_cEncK = %d,m_cEncR =" " %d,bSilencePk = %d,bFirstSilencePk = %d,bCngSend = %d\r\n", a3, a6, (unsigned int)*(char *)(XVEChannel_72 + 9), (unsigned int)*(char *)(XVEChannel_72 + 4), (unsigned int)*(char *)(XVEChannel_72 + 5), a7, a8, a9); *(_DWORD *)(XVEChannel_72 + 16) = v9; if... *(_BYTE *)(XVEChannel_72 + *(char *)(XVEChannel_72 + 9) + 1668) = a7; v11 = RsMdEncQueueSourcePktCng(XVEChannel_72, a2, v10, a7 ^ 1u);</code></pre> 并在 RsMdEncQueueSourcePktCng 函数中 XVEChannel_72 + 9 将做一次自增。 <pre><code> if ( a4 ) { memcpy(v9, a2, a3); *(_DWORD *)(XVEChannel_72 + 568) = (unsigned __int16)((unsigned __int16)*(_DWORD *)XVEChannel_72 << 8) | (unsigned __int8)(32 * *(_BYTE *)(XVEChannel_72 + 5)) | (a3 << 16) & 0x1FF0000 | ((*(_BYTE *)(XVEChannel_72 + 8) & 0xF) << 25) | (4 * *(_BYTE *)(XVEChannel_72 + 4) + 28) & 0x1C | ((*(_BYTE *)(XVEChannel_72 + 6) & 3) << 29); v10 = *(_QWORD *)(XVEChannel_72 + 544); if ( v10 ) { v11 = *(char *)(XVEChannel_72 + 9); if ( v11 <= 31 ) { v12 = 1026 * v11; *(_WORD *)(v10 + v12 + 1024) = a3; memcpy((void *)(v10 + v12), a2, a3); if ( a3 > *(__int16 *)(XVEChannel_72 + 10) ) *(_WORD *)(XVEChannel_72 + 10) = a3; } } } ++*(_BYTE *)(XVEChannel_72 + 9); //自增 ++*(_BYTE *)(XVEChannel_72 + 8); v13 = 0; log1(4, "Exit RsMdEncQueueSourcePktCng Success\r\n");</code></pre> 当 CAudioRS::RsMdEncProcessCng 退出前会根据当前的状态更新成员变量。 <pre><code> update_data(XVEChannel_72); //<=====================[1] v13 = *(char *)(XVEChannel_72 + 9); if ( (_BYTE)v13 == *(_BYTE *)(XVEChannel_72 + 4) ) //<=====================[2] { while ( v13 > 0 ) { if ( *(_BYTE *)(XVEChannel_72 + v13-- + 1667) != 0 ) { v15 = *(_BYTE *)(XVEChannel_72 + 5); v16 = *(_BYTE *)(XVEChannel_72 + 8); if ( v15 > 0 ) { v16 += v15; *(_BYTE *)(XVEChannel_72 + 8) = v16; } log1(4, " bRsCodeG = false,m_cEncCountInBlk = %d", (unsigned int)v16); goto LABEL_13; } } if ( *(char *)(XVEChannel_72 + 5) > 0 ) CAudioRS::RsMdCodeGenerate(XVEChannel_72); LABEL_13: *(_DWORD *)(XVEChannel_72 + 8) = 0; //<======================[3] ++*(_DWORD *)XVEChannel_72; *(_BYTE *)(XVEChannel_72 + 12) = 1; }</code></pre> [1] 通过`update_data`根据`LocalExpectRSPara`的值修改成员变量 <pre><code>__int64 __fastcall update_data(__int64 XVEChannel_72) { char v1; // al __int16 v2; // cx v1 = *(_BYTE *)(XVEChannel_72 + 64); if ( (*(_BYTE *)(XVEChannel_72 + 4) != v1 || *(_BYTE *)(XVEChannel_72 + 5) != *(_BYTE *)(XVEChannel_72 + 65) || *(_BYTE *)(XVEChannel_72 + 6) != *(_BYTE *)(XVEChannel_72 + 66)) && *(_BYTE *)(XVEChannel_72 + 9) == 1 ) { v2 = *(_WORD *)(XVEChannel_72 + 65); //DevPutProcessRsMdCng_SetLocalExpectRSPara 根据RemoteLrParam设置 *(_BYTE *)(XVEChannel_72 + 4) = v1; *(_WORD *)(XVEChannel_72 + 5) = v2; //XVEChannel_72+5 写一个word将覆盖到XVEChannel_72+9处 } return 0LL;</code></pre> [2] 如果XVEChannel_72+9处的值与XVEChannel_72+4处的值相同,则会出发[3]处的代码将XVEChannel_72+9处写0. <strong>因为 XVEChannel_72 + 9 可以根据 pData 中的数据设置成攻击者可控的数据,当 XVEChannel_72 + 9 被设置为大于 XVEChannel_72 + 4 时,就必须一直自增且产生整数溢出后重新与 XVEChannel_72 + 4 相等时, 才能将 XVEChannel_72 + 9清零。</strong> 所以 XVEChannel_72 + 9 的取值范围时0-255。又因为` *(_BYTE *)(XVEChannel_72 + *(char *)(XVEChannel_72 + 9) + 1668) = a7;` 使用的是有符号数作为`index`。最终覆盖范围是 `XVEChannel_72+1668`处的`-128`到`127`处超过原本数据结构包含的内存。 ##触发流程 <pre><code> +------------+ +------------+ | local | | remote | +-----+------+ +------+-----+ | | | | | <-----------------------------------+ | | | +---------------------------------+ | | | RecvRtpPacketCng | | | +-+-------------------------------+ | | | +--------------------------+ | | +--> | ParaseRemoteLostRateParam| | | +--------------------------+ | | | | | | | | +---------------------------------+ | | | DevPutProcessRsMdCng | | | +-+-------------------------------+ | | | +--------------------------+ | | +--->+ readRemoteLrParam | | | | +--------------------------+ | | | +--------------------------+ | | +--->+ SetLocalExpectRSPara | | | | +--------------------------+ | | | +--------------------------+ | | +--->+ RsMdEncProcessCng | | | +--------------------------+ | | | | | | | | | |</code></pre> <ul><li>RecvRtpPacketCng 从网络报文中获取 lrParam</li><li>DevPutProcessRsMdCng 根据`lrParam 设置 LocalExpectRSPara </li><li>RsMdEncProcessCng 根据 LocalExpectRSPara 中的参数修改成员变量作为数据修改的index (XVEChannel_72 + 9 )</li><li>修改成功后会对index自增并与本地的max值做比较,如果index达到最大值index_max时(`XVEChannel_72 + 4`)将index清零<ul><li>如果通过远数据端将index设置为大于index_max的情况,则index会一直自增直到发生整数溢出后才能满足index==index_max的条件进入清零的逻辑</li><li>index在(-128,127)范围内遍历,产生越界写。越界写的范围在 (-128,127)之间。</li></ul> </li></ul> ##感谢 要特别感谢 TSRC 的认真负责。他们在我们上报漏洞后对漏洞响应及时,收到报告的次日就确认了漏洞并给出危险评级。并且在后续的漏洞修复与修复版本更新的工作中和我们保持联系。 ##TimeLine 2019/11/28 发现漏洞 2019/12/02 完成漏洞分析并上报TSRC 2019/12/03 TSRC确认漏洞并修复 2020/03/23 文章发布 Credit:漏洞由盘古实验室黄涛、王铁磊发现和分析。 Comments are closed. 原文 / From https://blog.pangu.io/?p=168 热门推荐 One-Lin3r — 轻量级渗透测试框架 360CERT 2017年终总结专题——勒索软件 通过CVE-2017-17215学习路由器漏洞分析,从入坑到放弃 RPO攻击技术浅析