导语:了解安全漏洞的人都知道,除了被冠以CVE外,其它所谓的漏洞都并非真正意义上的漏洞。今天我就给你介绍一些路由器和Android手机中出现的6个真正漏洞,且属于内核漏洞,其中就包括Google的Pixel(XL)和Nexus 5x。
了解安全漏洞的人都知道,除了被冠以CVE外,其它所谓的漏洞都并非真正意义上的漏洞。今天我就给你介绍一些路由器和Android手机中出现的6个真正漏洞,且属于内核漏洞,其中就包括Google的Pixel(XL)和Nexus 5x。
在过去的两年时间里,我花了很多时间来审核各种Android内核漏洞,截止目前我总共发现了8个远程漏洞,不过由于只有6个漏洞被公布了,所以为了保证安全,我只能先谈谈这6个,目前它们都已经被修复了。
查找漏洞的方法
我发现的漏洞出现在Qualcomm/Atheros开发的qcacld无线驱动程序中,像Pixel系列手机就用的是这种驱动。与Broadcom 的无线单芯片系统(SoC)不同的是,Qualcomm SoC只实现了部分的SoftMAC,这意味着一些MAC层管理服务(MACSublayerManagementEntity,MLME)在主机软件中就被进行了处理,而不是在硬件或SoC固件中。正因为如此,处理任何类型的802.11管理框架的源代码都必须在驱动程序中进行。
说起来容易做起来难,要想在qcacld驱动程序中发现漏洞可没有想象的容易。为了向你展示其中的复杂程度,你可以看看以下的代码部分。
[email protected]:~/android/msm/drivers/staging/qcacld-2.0$ find . -name '*' | xargs wc -l 177 ./wcnss/inc/halCompiler.h 826 ./wcnss/inc/wlan_nv.h 690986 total
正如你所看到的,这个单一的无线驱动程序有近69万行代码。所以,我的计划是从无线芯片组的固件中寻找一些中断,以便主机驱动程序可以随时通知具有漏洞的数据包和框架。这样我就可以通过跟踪调用链来发现一个漏洞,不过通过这种方法,我无法确定主驱动器的真正入口,所以最终我放弃了该方法。还好在不懈的努力下,我知道如何对一个无线驱动实施grep。由于我知道有进行取消认证(de-authentication)的框架,所以我就从这里开始入手。
我很快就把范围锁定在了CORE/MAC/src/pe/lim/limProcessDeauthFrame.c:96:,在确定范围后,我就对该目录进行了深挖,看看还能发现什么。
正如你所看到的,有大量的文件专门用于管理框架,这也证明了我锁定的范围是正确的。不过在对这段代码进行审核之前,我还要确定它们已经被使用了。为此,我往其中输入了一个pr_err("%s: HELLO WORLD!n", __func__);,确定了在802.11管理框架的主函数处理程序中,这段代码正在使用中。
漏洞常识
在数据包固件通知后的呼叫链中,我就将进入以下这个函数。
在这个函数中,我将验证数据包是否是一个管理框架,为此我将解析子类型并调用正确的管理处理程序。
switch (fc.type) { case SIR_MAC_MGMT_FRAME: { // Received Management frame switch (fc.subType) { case SIR_MAC_MGMT_ASSOC_REQ: // Make sure the role supports Association if (LIM_IS_BT_AMP_AP_ROLE(psessionEntry) || LIM_IS_AP_ROLE(psessionEntry)) limProcessAssocReqFrame(pMac, pRxPacketInfo, LIM_ASSOC, psessionEntry); else { // Unwanted messages - Log error limLog(pMac, LOGE, FL("unexpected message received %X"),limMsg->type); } break; case SIR_MAC_MGMT_ASSOC_RSP: limProcessAssocRspFrame(pMac, pRxPacketInfo, LIM_ASSOC,psessionEntry); break; case SIR_MAC_MGMT_REASSOC_REQ: // Make sure the role supports Reassociation if (LIM_IS_BT_AMP_AP_ROLE(psessionEntry) || LIM_IS_AP_ROLE(psessionEntry)) { limProcessAssocReqFrame(pMac, pRxPacketInfo, LIM_REASSOC, psessionEntry); } else { // Unwanted messages - Log error limLog(pMac, LOGE, FL("unexpected message received %X"),limMsg->type); } break; case SIR_MAC_MGMT_REASSOC_RSP: limProcessAssocRspFrame(pMac, pRxPacketInfo, LIM_REASSOC,psessionEntry); break; case SIR_MAC_MGMT_PROBE_REQ: limProcessProbeReqFrame_multiple_BSS(pMac, pRxPacketInfo,psessionEntry); break; case SIR_MAC_MGMT_PROBE_RSP: if(psessionEntry == NULL) limProcessProbeRspFrameNoSession(pMac, pRxPacketInfo); else limProcessProbeRspFrame(pMac, pRxPacketInfo, psessionEntry); break;
一旦正确的处理程序被调用,我就需要将数据包从原始的over-the-wire(over-the-air)表单解析为我可以使用的c-style结构。
这个转换发生在巨人文件dot11f.c中,这是编译器生成的文件并处理转换。
下面是一个通过c-style结构体传递原始字节的例子:
有趣的是,在我从线程结构解析到c-style结构之后,我还必须进一步到另一个c-style结构中解析它。 dot11f c-style结构中的每个字段都可以在80211管理框架中发挥作用,例如,如果管理框架可以包含一个功能列表,那么在dot11f“out”结构中就将会有一个结构用于函数。由于驱动程序可能用不到这些功能,因此它就用不到一个大型结构,而只需要一个将80211框架转化成很小的结构。这样,我就只能为驱动程序实际使用的部分分配内存了,进一步的细化处理会在以下函数中进行。
在细化之后,驱动程序开始使用数据,不过这个过程里漏洞的发现而远不相关,下面就是数据包的数据流演示图。
第一个漏洞(CVE-2017-11013)
现在我已经对高层次和一般数据流程进行了一个大概的了解,接下来我来介绍第一个也是最标准的漏洞。
第一个漏洞存在于dot11f.c文件中,正如我上面提到的那样,它负责将over-the-wire数据包解析为c-style结构。当dot11f.c文件获得一个over-the-wire数据包时,数据包是由一系列信息元素(IE)构成的,其中每个IE都是类型长度值(TLV)。 IE包含一个字节标签,一个字节长度和最多255个字节的值。
标签会被标准化,以方便表示特定类型的元素。
dot11f.c代码包含许多IE定义,这些定义代表了数据包中可能包含的内容。例如,假设我用手机访问家里的网络,那么在协议链中的某个时刻,访问点将返回给我一个关联响应。当手机获得关联响应数据包时,驱动程序最终会通过调用dot11fUnpackAssocResponse来立即调用UnpackCore。
现在,让我把调用分解成一些UnpackCore。pCtx是驱动程序的状态结构,pBuf是TLV的over-the-wire列表,nBuf表示的是pBuf的总字节数,FFS_AssocResponse是一个必须在pBuf中的强制IE的列表,而IES_AssocResponse则是一个在pBuf中的可选IE列表。由于pFrm是我要用到的“out”结构,这意味着解析的IE将被翻译成tDot11fAssocResponse结构的一部分。
现在,我会将大量的结构定义进行转储,这对于理解第一个漏洞是非常重要的。让我们一起来看一下IES_AssocResponse结构的定义。请注意,这只是数据包可以包含的部分可选IE列表:
现在看看“out”结构的定义tDot11fAssocResponse:
对于tDot11fAssocResponse结构的每一部分,IES_AssocResponse数组中都有相应的tIEDefn结构。现在将tIEDefn结构翻译成英语,让我们更好的理解“out”结构。
typedef struct sIEDefn { tANI_U32 offset; /* This is the offset into the out structure * where we will place the parsed data. * So for instance the out structure can * Contain tDot11fIEExtSuppRates ExtSuppRates; * We would do: * .offset = offsetof(tDot11fAssocResponse, ExtSuppRates) */ tANI_U32 presenceOffset; /* Each member in the out structure has a present * flag which gets set if the parser saw the IE in * the raw packet buffer and parsed data into the * Out member. */ tANI_U32 countOffset; /* Some IE's can be sent more than once in the raw * tlv packet. This offset represents where in the * Out structure the count for how many IEs of this * type we've parsed lives. So for example, in the * Assoc response go above and look for * tDot11fIERICDataDesc RICDataDesc[2]; * and right above you'll see: * tANI_U16 num_RICDataDesc; * So we'll have a: * .countOffset = offsetof(tDot11fAssocResponse, num_RICDataDesc); */ const char *name; /* self explanatory, name of the IE we're parsing */ tANI_U16 arraybound; /* If the parser accepts multiple IEs in a single packet * of the same type, like we showed for RICDataDesc[2], * this value represents how many IEs we can accept. * So for a Assoc response frame there are 2 array bound * vars: * tDot11fIERICDataDesc RICDataDesc[2]; * tDot11fIEWMMTSPEC WMMTSPEC[4]; * Go back up to the picture of the Assoc Resp out frame and * You'll see above each one there is a num_* representing * once again how many of theses IE's we've parsed (count offset). * So, for the RICDataDesc arraybound would = 2, * and for WMMTSPEC arraybound = 4. Everything else would * be 0. */ tANI_U16 minSize; /* The minimum size of the IE */ tANI_U16 maxSize; /* Maximum size of the IE */ tANI_U16 sig; /* This is a unique number representing the IE, used * internally for the parser. We'll see more about it * later. It's just used as a case in a huge switch * statement to get us to the right parser function */ unsigned char oui[5]; unsigned char noui; /* So along with the eid below the IE can have a OUI * which is mostly used with tag # 221 which is the * Vendor tag. If we want to support multiple tag * Number 221's each with different types we'll pass * along an OUI which is just more bytes which represent * a specific type of 221. noui is just how many OUI * bytes we should look for. */ tANI_U8 eid; /* eid is just the TLV Tag. Dunno why they didn't call it * "tag" */ tFRAMES_BOOL fMandatory; } tIEDefn;
要注意的是,对于IES_AssocResponse列表来说,它是原始数据包中的可选IE。由于其中的每部分都是我上面描述的tIEDefn。所以会举几个例子,来映射它们上面的定义,以确保你了解它是如何设置的。请注意,为了方便理解,我删除了大部分的定义,因为它太大了。
static const tIEDefn IES_AssocResponse[] = { { offsetof(tDot11fAssocResponse, SuppRates), offsetof(tDot11fIESuppRates, present), 0, "SuppRates", 0, 2, 14, SigIeSuppRates, {0, 0, 0, 0, 0}, 0, DOT11F_EID_SUPPRATES, 1, }, { offsetof(tDot11fAssocResponse, ExtSuppRates), offsetof(tDot11fIEExtSuppRates, present), 0, "ExtSuppRates", 0, 3, 14, SigIeExtSuppRates, {0, 0, 0, 0, 0}, 0, DOT11F_EID_EXTSUPPRATES, 0, }, ... ... ... { offsetof(tDot11fAssocResponse, RICDataDesc), offsetof(tDot11fIERICDataDesc, present), offsetof(tDot11fAssocResponse, num_RICDataDesc), "RICDataDesc", 2, 2, 550, SigIeRICDataDesc, {0, 0, 0, 0, 0}, 0, DOT11F_EID_RICDATADESC, 0, }, ... ... ... { offsetof(tDot11fAssocResponse, WMMTSPEC), offsetof(tDot11fIEWMMTSPEC, present), offsetof(tDot11fAssocResponse, num_WMMTSPEC), "WMMTSPEC", 4, 63, 63, SigIeWMMTSPEC, {0, 80, 242, 2, 2}, 5, DOT11F_EID_WMMTSPEC, 0, }, ... ... }
至此,我已理解了解析器使用的一些结构定义,现在,就让我来看看UnpackCore中的相关解析器代码。
while (nBufRemaining) { if (1 == nBufRemaining) { FRAMES_LOG0(pCtx, FRLOGE, FRFL("This frame reports " "only one byte remaining after it's fixed fields.n")); status |= DOT11F_INCOMPLETE_IE; FRAMES_DBG_BREAK(); goto MandatoryCheck; } pIe = FindIEDefn(pCtx, pBufRemaining, nBufRemaining, IEs);
因此,我把FindIEDefn定义的理解是这样的:
在高层次上,FindIeDefn会遍历整个可选的IE列表,在本文所举的例子中,IES_AssocResponse会查看当前索引处的缓冲区是否包含IE的标记。所以函数将从IES_AssocResponse数组的索引0开始,最终会找到代表eid的tIEDefn结构。
static const tIEDefn* FindIEDefn(tpAniSirGlobal pCtx, tANI_U8 *pBuf, tANI_U32 nBuf, const tIEDefn IEs[]) { const tIEDefn *pIe; (void)pCtx; pIe = &(IEs[0]); /* Start on the 0th index of our list of Optional IE's: * IES_AssocResponse */ while (0xff != pIe->eid) { /* While we haven't reached the end of the IES_AssocResponse list */ if (*pBuf == pIe->eid) { /* If the raw packet at the current index has the current tag */ /* If there is no extra noui bytes to check return */ if (0 == pIe->noui) return pIe; /* If the tags match and there is extra OUI data to compare let's compare */ if ( ( nBuf > (tANI_U32)(pIe->noui + 2) ) && ( !DOT11F_MEMCMP(pCtx, pBuf + 2, pIe->oui, pIe->noui) ) ) return pIe; } /* The raw packet didn't match this IE definition, let's try the next one */ ++pIe; } /* This IE isn't recognized for this type of frame, return NULL */ return NULL; }
在FindIEDefn调用之后会立即解包内核:
pIe = FindIEDefn(pCtx, pBufRemaining, nBufRemaining, IEs); /* So at this point let's just assume we have a proper pIe. * let's say we saw a tDot11fIEWMMTSPEC IE. */ /* Extract the TAG from the Packet */ eid = *pBufRemaining++; --nBufRemaining; /* Extract the Length from the packet len = *pBufRemaining++; --nBufRemaining; /* If we did find the IE Definition in the raw packet, * we'll do some validation checks. */ if (pIe && pIe->noui) { if (pIe->noui > nBufRemaining) { FRAMES_LOG3(pCtx, FRLOGW, FRFL("IE %d reports " "length %d, but it has an OUI of %d bytes.n"), eid, len, pIe->noui); FRAMES_DUMP(pCtx, FRLOG1, pBuf, nBuf); FRAMES_LOG2(pCtx, FRLOG1, FRFL("We've parsed %d by" "tes of this buffer, and show %d left.n"), pBufRemaining - pBuf, nBufRemaining); status |= DOT11F_INCOMPLETE_IE; FRAMES_DBG_BREAK(); goto MandatoryCheck; } pBufRemaining += pIe->noui; nBufRemaining -= pIe->noui; len -= pIe->noui; } /* If the TLV reports a Length greater than the amount of data * left in our buffer we will bail out. This will prevent * an OOB read. */ if (len > nBufRemaining) { FRAMES_LOG3(pCtx, FRLOGW, FRFL("IE %d reports length %" "d, but there are only %d bytes remaining in this" " frame.n"), eid, len, nBufRemaining); FRAMES_DUMP(pCtx, FRLOG1, pBuf, nBuf); FRAMES_LOG2(pCtx, FRLOG1, FRFL("We've parsed %d by" "tes of this buffer, and show %d left.n"), pBufRemaining - pBuf, nBufRemaining); status |= DOT11F_INCOMPLETE_IE; FRAMES_DBG_BREAK(); goto MandatoryCheck; } /* If our call to FindIEDefn did find an IE we recognize in * The packet let's start parsing it: if (pIe) { /* do some more size validations */ if (nBufRemaining < pIe->minSize - pIe->noui - 2U) { FRAMES_LOG3(pCtx, FRLOGW, FRFL("The IE %s must be " "at least %d bytes in size, but there are only" "y %d bytes remaining in this frame.n"), pIe->name, pIe->minSize, nBufRemaining); FRAMES_DUMP(pCtx, FRLOG1, pBuf, nBuf); status |= DOT11F_INCOMPLETE_IE; FRAMES_DBG_BREAK(); goto MandatoryCheck; } else { /* Some weird check that doesn't really do anything except * complain in dmesg. */ if (len > pIe->maxSize - pIe->noui - 2U){ FRAMES_LOG1(pCtx, FRLOGW, FRFL("The IE %s reports " "an unexpectedly large size; it is presumably " "more up-to-date than this parser, but this wa" "rning may also indicate a corrupt frame.n"), pIe->name); FRAMES_DUMP(pCtx, FRLOG1, pBuf, nBuf); } /* If our IE element is array bound, like I explained above: * If our frame type can accept multiple IE's of the same type * in the same packet we will grab the current Count of how * Many we've already parsed and store it in countOffset. * On the first iteration it will be 0, second time we see * the same IE it will be 1, etc etc. If our IE element * is not array bound countOffset will become 0. */ countOffset = ( (0 != pIe->arraybound) * ( *(tANI_U16* )(pFrm + pIe->countOffset))); switch (pIe->sig) { /* Now we switch on the signature which will take us to the correct parsing * function. * I've listed both cases for the two Array bound signatures in our * Association response below: */ case SigIeRICDataDesc: //reset the pointers back since this is a container IE and it doesn't have its own EID and Len. pBufRemaining -= 2; nBufRemaining += 2; if ( pIe && pIe->noui ) { pBufRemaining -= pIe->noui; nBufRemaining += pIe->noui; len += pIe->noui; } status |= GetContainerIesLen(pCtx, pBufRemaining, nBufRemaining, &len, IES_RICDataDesc); if (status != DOT11F_PARSE_SUCCESS && status != DOT11F_UNKNOWN_IES ) break; status |= dot11fUnpackIeRICDataDesc(pCtx, pBufRemaining, len, ( tDot11fIERICDataDesc* )(pFrm + pIe->offset + sizeof(tDot11fIERICDataDesc)*countOffset) ); break; ... ... ... /* in the case of a SigIeWMMTSPEC IE we'll call dot11fUnpackIeWMMTSPEC() */ case SigIeWMMTSPEC: status |= dot11fUnpackIeWMMTSPEC(pCtx, pBufRemaining, len, ( tDot11fIEWMMTSPEC* )(pFrm + pIe->offset + sizeof(tDot11fIEWMMTSPEC)*countOffset) ); /* Let's break this call down a bit more: * dot11fUnpackIeWMMTSPEC(pCtx, * pBufRemaining, * len, * (tDot11fIEWMMTSPEC*)(pFrm + pIe->offset + sizeof(tDot11fIEWMMTSPEC) * countOffset)); * * * The most interesting portion of this call is the last parameter. * pFrm is the start -in memory- of our Out structure tDot11fAssocResponse. * pIe->offset, like we described above is the amount of bytes * into tDot11fAssocResponse where the tDot11fIEWMMTSPEC member * lives. The second half of the math: * sizeof(tDot11fIEWMMTSPEC) * countOffset will calculate * the index into the array. Remember for the WMMTSPEC * it looks like this in the out structure: * tDot11fIEWMMTSPEC WMMTSPEC[4]; * So there are 4 slots in the array we can save. * So like I said above the first time we do this we'll save into * slot 0 because count offset will be 0. */ break; ... ... ... default: FRAMES_LOG1(pCtx, FRLOGE, FRFL("INTERNAL ERROR" ": I don't know about the IE signature %d" "-- this is most likely a 'framesc' bug.n"), pIe->sig); FRAMES_DBG_BREAK(); return DOT11F_INTERNAL_ERROR; } /* If we are array bound we'll add one to our num_* * in the case of WMMTSPEC the below math will take us * to the location in memory where the num_WMMTSPEC * inside the tDot11fAssocResponse lives and we'll add * one to it. */ if (pIe->arraybound) (++( *(tANI_U16*)(pFrm + pIe->countOffset) )); /* At this point if there is remaining IE's in the input * buffer we'll jump back up to the top of this while loop * try and find another IE defn and parse the rest of the packet. */ } } }
现在,我提供的这些信息,足以让你发现漏洞了。如果你还没有理解,我可以再给你一些提示:当我在关联响应中发送另一个wmtspec IE时,会发生什么?它将如何解析,在第二次迭代中会存储哪个索引,接下来,不断地发送WMMTSPEC IE的结果又是什么?
解析代码时,存在的问题是我从来没有验证countOffsetnum_对arraybound的影响。还记得上面我说过,countOffsetnum_表示目前已经分析的元素数,而arraybound则表示我有多少个可用的插槽。所以我可以发送15个WMMTSPEC IE,这样解析代码将继续不断地解析它们,并将它们存储在0->14的插槽中。请注意,我只有4个WMMTSPECS插槽:
typedef struct sDot11fAssocResponse{ tDot11fFfCapabilities Capabilities; tDot11fFfStatus Status; tDot11fFfAID AID; tDot11fIESuppRates SuppRates; tDot11fIEExtSuppRates ExtSuppRates; tDot11fIEEDCAParamSet EDCAParamSet; tDot11fIERCPIIE RCPIIE; tDot11fIERSNIIE RSNIIE; tDot11fIERRMEnabledCap RRMEnabledCap; tDot11fIEMobilityDomain MobilityDomain; tDot11fIEFTInfo FTInfo; tANI_U16 num_RICDataDesc; tDot11fIERICDataDesc RICDataDesc[2]; tDot11fIEWPA WPA; tDot11fIETimeoutInterval TimeoutInterval; tDot11fIEHTCaps HTCaps; tDot11fIEHTInfo HTInfo; tDot11fIEWMMParams WMMParams; tDot11fIEWMMCaps WMMCaps; tDot11fIEESERadMgmtCap ESERadMgmtCap; tDot11fIEESETrafStrmMet ESETrafStrmMet; tDot11fIEESETxmitPower ESETxmitPower; tANI_U16 num_WMMTSPEC; tDot11fIEWMMTSPEC WMMTSPEC[4]; tDot11fIEWscAssocRes WscAssocRes; tDot11fIEP2PAssocRes P2PAssocRes; tDot11fIEVHTCaps VHTCaps; tDot11fIEVHTOperation VHTOperation; tDot11fIEExtCap ExtCap; tDot11fIEOBSSScanParameters OBSSScanParameters; tDot11fIEQosMapSet QosMapSet; } tDot11fAssocResponse;
所以,如果我发送了15个WMMTSPEC IE,那多余的11个将会溢出到WscAssocRes,P2PAssocRes,VHTCaps等处,直到我在tDot11fAssocResponse结构之外进行攻击。
这个漏洞之所以如此之标准,是因为你可以针对不同类型的内存。由于该漏洞存在于IE的通用解析代码中,因此每个“out”结构都分配在不同的位置,比如,有些分配在堆栈中,有些分配在.bss中。所以你就会有很多可以溢出的地方,此时你只需要找到一个具有数组IE的802.11管理数据包,并查看“out”结构被分配的位置。你甚至可以从.bss和堆中找到这些溢出,稍后我会对此详细解释。这个漏洞对于一个真正的近端内核远程代码执行来说是一个很好的攻击目标,因为你已经控制了数据,且有多种可以溢出的位置。
以下是我在UnpackCore中偶然发现这段代码时,所出现的漏洞。
case SigIeNeighborReport: if (countOffset < MAX_SUPPORTED_NEIGHBOR_RPT) { status |= dot11fUnpackIeNeighborReport(pCtx, pBufRemaining, len, ( tDot11fIENeighborReport* )(pFrm + pIe->offset + sizeo f(tDot11fIENeighborReport)*countOffset) ); } else { status |= DOT11F_BUFFER_OVERFLOW; } break;
我很惊讶地看到,对于这个IE类型,研发人员已经发布了一个补丁来修复了内存漏洞。不过,我还是想看看这个问题在什么情况下得到了修复,为此我执行了git blame命令,并且发现了其中的问题:
可以看到,在2014年10月14日,Qualcomm曾有机会修复整个漏洞。不过,当时并没有人注意到这个IE漏洞。所以这也提醒了开发人员,如果是嵌入式系统或驱动程序或C或C ++代码,那编写代码时一定要格外注意。
我上面说过,这个漏洞之所以很标准,是因为有一些内存区域也存在溢出。这样,我就可以在BSS,堆和栈中也找到相应的问题。对于栈,我现在可以做到将其粉碎(smash the stack)并控制其IP,具体方法请点此。
但在我的结构和栈cookie之间没有任何指针或者任何有用的东西,不过,.bss中也可以发生溢出,这可能会有帮助。在一些BSS分配中,有一个巨大的VosContext结构,我可以把它溢出。另外,我还可以溢出BSS段,但还不足以深入到VoScontext。由于这种方法不会破坏什么结构,所以稍后我会在.bss和堆中复制我的溢出结构。
从上面可以看出,解析代码溢出了ar. ricdatadesc[],由于我可以控制溢出的数量,因此我也可以控制ar.num_RICDataDesc。这意味着,我可以控制有多少RicDataDescs插入&pAssocRsp->RICData [cnt],其中pAssocRsp是我分配的堆。所以使用这个简单的技巧,我可以从bss和堆中溢出受控数据。
第二个漏洞(CVE-2017-9714)和第三个漏洞:远程内核DoS(无限循环)
虽然这两个漏洞可能不会让那些专门寻找漏洞利用或内存损坏的人高兴起来,但对我来说却是非常有意义的,因为它们是整数溢出漏洞。
当我将手机置于AP模式进行网络共享时,会有一些其他的管理数据包被打开以供驱动程序解析,如关联请求,身份验证请求等。AP模式(ACCESS POINT MODE)就是无线热点模式,是无线AP的默认工作模式,这种AP模式下无线网卡能找到AP,并通过AP接入局域网中。如果手机处于AP模式,并发送关联请求,我可以发送一些名为“opaque RSN data”或“WPA Opaque data”的信息。虽然我不知道它们是什么,但是你可以把它发送出去,这正是我所关心的。
如果我发送了这些信息,UnpackCore中的80211解析代码将把它很好地放置到pAssocReq变量中,并将pAssocReq ->rsnPresent设置为1。最终,这条信息会到达/lim/limProcessAssocReqFrame.c:
tSirRetStatus sirConvertAssocRespFrame2Struct(tpAniSirGlobal pMac, tANI_U8 *pFrame, tANI_U32 nFrame, tpSirAssocRsp pAssocRsp) { /* pAssocRsp is allocated on the heap by the function that calls us */ static tDot11fAssocResponse ar; tANI_U32 status; tANI_U8 cnt =0; // Zero-init our [out] parameter, vos_mem_set( ( tANI_U8* )pAssocRsp, sizeof(tSirAssocRsp), 0 ); // delegate to the framesc-generated code, status = dot11fUnpackAssocResponse( pMac, pFrame, nFrame, &ar); ... ... ... #ifdef WLAN_FEATURE_VOWIFI_11R if (ar.num_RICDataDesc) { for (cnt=0; cnt < ar.num_RICDataDesc; cnt++) { if (ar.RICDataDesc[cnt].present) { vos_mem_copy( &pAssocRsp->RICData[cnt], &ar.RICDataDesc[cnt], sizeof(tDot11fIERICDataDesc)); } } pAssocRsp->num_RICData = ar.num_RICDataDesc; pAssocRsp->ricPresent = TRUE; } #endif }
当我打包一些RSN数据时,我将调用dot11fUnpackIeRSN并将长度/数据传递给该函数。现在,解包函数的任务就是将原始字节转换为一个c-style的结构,将结果保存到基于栈的Dot11IERSN变量中。
让我来看一下dot11fUnpackIeRSN的函数到底执行了什么内容:
你可以看到通过framesntohs调用从原始缓冲区中提取了pwise_cipher_suite_count。在我确认pwise_cipher_suite_count小于4之后,它会立即进行验证。如果它比较大,我会取消当前标志并返回DOT11F_SKIPPED_BAD_IE;,所以,如果它们被正确解析,就会返回一个漏洞,那如果没有被正确解析呢?
如果回过头来看看我如何调用dot11fUnpackIeRSN,那你会看到以下方法。
if(pAssocReq->rsn.length) { // Unpack the RSN IE dot11fUnpackIeRSN(pMac, &pAssocReq->rsn.info[0], pAssocReq->rsn.length, &Dot11fIERSN);
看到什么漏洞了吗?由于调用函数不会检查dot11fUnpackIeRSN的返回值。如果要继续使用代码,就要调用limCheckRxRSNIeMatch。
看到第一个循环,你是否能推断出为什么我可以进行无限循环?如果不知道,我可以给你一些提示,比如,什么是pwise_cipher_suite_count的类型?
由于我完全控制了pwise_cipher_suite_count且它是一个u16(我用framesntohs(frames-network-to-host-short)提取它)且在循环中的count变量(i)类型是u8,所以我可以进行无限循环。如果我把pwise_cipher_suite_count为31337 dot11funpackiersn发送,则循环将失败,所以永远不会将pwise_cipher设置为0,由于我从不检查返回值并不断进入limCheckRxRSNIeMatch。因此,一旦进入for循环,则pwise_cipher_suite_count就是u16(31337),u8最大值为255,所以u8(i)从255一直循环到0,由于宽度限制,显然永远不会达到31337。所以for循环一直在该范围循环,直到watchdog启动并在一段时间后攻击内核。
我在上面说过,你可以发送WPA不透明数据,这样,一个未检查的返回值会导致u8/u16无法与无限循环进行匹配。
第四个漏洞:堆缓冲区溢出(CVE-2017-9714)
802.11管理框架的一部分可以包括一些QoS类型信息,你可以将它们单独发送,也可以将其附加到802.11关联响应中。具体来说就是,如果我附加一个QoSMapset元素在我的关联响应中,就可能会导致一些损坏。
正如你在上面的例子中看到的那样,我设置了num_dscp_exceptions,然后验证长度小于60.一旦我确认开始使用调用链,就会以sirConvertAssocRespFrame2Struct结尾。在这个函数结束时,我会执行以下操作。
if ( ar.QosMapSet.present ) { pAssocRsp->QosMapSet.present = 1; ConvertQosMapsetFrame( pMac, &pAssocRsp->QosMapSet, &ar.QosMapSet); limLog( pMac, LOG1, FL("Received Assoc Response with Qos Map Set")); limLogQosMapSet(pMac, &pAssocRsp->QosMapSet); }
我通过调用ConvertQosMapsetFrame(pMac,&pAssocRsp->QosMapSet,&ar.QosMapSet);从一个结构转换到另一个结构。
如果光看这个函数,它是没有任何意义的,这时如果你再次查看dot11fUnpackIeQosMapSet函数,你可以看到,我可以将可以将0 -> 60字节的数据放到dscp_exception数组中。对于该数组中存储的数据,我将在pDst->num_dscp_exceptions中设置其大小。
现在再来看一下ConvertQosMapsetFrame函数:
if (dot11fIE->num_dscp_exceptions > 58) dot11fIE->num_dscp_exceptions = 58; Qos->num_dscp_exceptions = (dot11fIE->num_dscp_exceptions - 16)/2; for (i = 0; i < Qos->num_dscp_exceptions; i++)
正如你在上面的例子中看到的,如果> 58,则我会设置最大值,但是没有最小值检查。那么,如果我发送14个num_dscp_exceptions,会怎么样呢?由于会产生整数算法(integer math),所以我能得到(INT_MIN + 1)或0xfffffffe。当我将int(INT_MIN + 1)除以2时,便得到INT_MIN。当我将一个完整的32位INT_MIN放入u8时,我会得到了255(我只是取了int的底部字节( bottom byte ))。
因此,通过发送14个num_dscp_exceptions,我可以欺骗代码,将255存储到Qos->num_dscp_exceptions中,然后在其上循环。
typedef struct sSirQosMapSet { tANI_U8 present; tANI_U8 num_dscp_exceptions; tANI_U8 dscp_exceptions[21][2]; tANI_U8 dscp_range[8][2]; } tSirQosMapSet, *tpSirQosMapSet;
你可以在上面看到有42个用于dscp_exceptions的插槽,所以为了方便,我删除了这个结构的末端并将其放入临近的堆对象中。
第五个漏洞(CVE-2017-11014):另一个堆溢出
由于Qcacld驱动程序支持roaming操作框架,所以我认为它适用于IEEE 802.11k协议规范。这个操作框架旨在帮助发现具有Neighbor AP的802.11,而不必进行自我扫描。这个操作之所以很受欢迎,是因为它是为运行功率受限设备而设计的,这样在查找时它们就不必连续扫描网络,而仅仅对当前关联的Neighbor AP进行询问,并且AP将执行扫描并返回信息。
roaming文件夹是什么文件夹?
Windows系统下的roaming文件夹,是用于存放程序数据的文件夹,如QQ音乐,QQ号码的登陆信息等,在我们使用电脑程序时时,这个文件夹因为缓存程序数据的原因,体积会不断膨胀。
有趣的是,有时驱动甚至不需要询问这些数据。访问点可以只通过发送roaming操作框架,qcacld就会进行开始解析。
所以让我试着发送一个roaming操作框架,看看会发生什么?
首先我会进入limProcessActionFrame()函数:
由于我正在发送无线电测量请求,我将输入__limProcessRadioMeasureRequest:
像所有的漏洞和数据包一样,我会选择dot11fUnpack函数,将其解析为我的c-style结构。我将把下面的可选IE数组传递给UnpackCore。请注意,IE数组描述了解析器期望的可能在数据包中的元素类型。
在这个漏洞中,我将重点关注可以使用此数据包发送的APChannel报告函数。
offsetof(tDot11fIEMeasurementRequest, measurement_request.Beacon.APChannelReport), offsetof(tDot11fIEAPChannelReport, present), offsetof(tDot11fIEMeasurementRequest, measurement_request.Beacon.num_APChannelReport), "APChannelReport" , 2, 3, 53, SigIeAPChannelReport)
正如你所看到的,有一个num_APChannelReport函数,所以我可以使用这个结构中的第一个漏洞(CVE-2017-11013),但前提是它是被修复了的。根据其定义,我可以发送2个AP信道报告(字符串之后的数字是arraybound),现在看看我如何解析AP信道报告:
正如你所看到的,我将信道数量限制在50,因为pDst结构只有50个插槽。
这样我就发送了2个AP信道报告,每个信道数量限制在50。现在我将展开调用堆栈并将其返回到__limProcessRadioMeasureRequest,在这个函数中,我将调用rrmProcessRadioMeasurementRequest( pMac, pHdr->sa, &frm, psessionEntry ); ,&from包含我的解析框架,或者是在解析内核中提到的pDst。
在rrmProcessRadioMeasurementRequest里,我看到了下面的代码(为了方便介绍,我跳过了一些不相关的内容)。
你可以看到我调用了rrmProcessBeaconReportReq,并且我输入的参数之一是&rRMReq->MeasurementRequest [i]。由于我输入的结构包含了我的2个AP报告,所以现在就让我解析一下该函数(为了方便介绍,我跳过了一些代码)。
在上面的代码中,我遍历2个AP信道报告,并将信道列表存储到pChanList中。在每个列表之后,我都将指针向前进行了移动,以便更深入到目标数组中。请注意,由于每个AP信道报告发送了50个信道。所以在首次复制之后,我将把指针向上移动50个字节。看看tANI_U8 *pChanList = pSmeBcnReportReq->channelList.channelNumber;,这个信道号数组看起来如下所示。
让我看看信道号数组是如何对SIR_ESE_MAX_MEAS_IE_REQS进行数据类型定义的:
所以我将100个字节(每个ap报告50个)存储到一个配置为8字节的数组中。
第六个漏洞(CVE-2017-11015):另一个堆溢出
如果我在AP模式下向手机发送特制的身份验证框架,我将登陆到sirConvertAuthFrame2Struct,并立即调用dot11fUnpackAuthentication。对于认证数据包,我可以附加一些特定的TLV:
static const tIEDefn IES_Authentication[] = { {offsetof(tDot11fAuthentication, ChallengeText), offsetof(tDot11fIEChallengeText, present), 0, "ChallengeText" , 0, 3, 255, SigIeChallengeText, {0, 0, 0, 0, 0}, 0, DOT11F_EID_CHALLENGETEXT, 0, }, {offsetof(tDot11fAuthentication, RSNOpaque), offsetof(tDot11fIERSNOpaque, present), 0, "RSNOpaque" , 0, 8, 255, SigIeRSNOpaque, {0, 0, 0, 0, 0}, 0, DOT11F_EID_RSNOPAQUE, 0, }, {offsetof(tDot11fAuthentication, MobilityDomain), offsetof(tDot11fIEMobilityDomain, present), 0, "MobilityDomain" , 0, 5, 5, SigIeMobilityDomain, {0, 0, 0, 0, 0}, 0, DOT11F_EID_MOBILITYDOMAIN, 0, }, {offsetof(tDot11fAuthentication, FTInfo), offsetof(tDot11fIEFTInfo, present), 0, "FTInfo" , 0, 84, 222, SigIeFTInfo, {0, 0, 0, 0, 0}, 0, DOT11F_EID_FTINFO, 0, }, {offsetof(tDot11fAuthentication, TimeoutInterval), offsetof(tDot11fIETimeoutInterval, present), 0, "TimeoutInterval" , 0, 7, 7, SigIeTimeoutInterval, {0, 0, 0, 0, 0}, 0, DOT11F_EID_TIMEOUTINTERVAL, 0, }, {offsetof(tDot11fAuthentication, RICDataDesc), offsetof(tDot11fIERICDataDesc, present), offsetof(tDot11fAuthentication, num_RICDataDesc), "RICDataDesc" , 2, 2, 550, SigIeRICDataDesc, {0, 0, 0, 0, 0}, 0, DOT11F_EID_RICDATADESC, 0, }, {0, 0, 0, NULL, 0, 0, 0, 0, {0, 0, 0, 0, 0}, 0, 0xff, 0, }, };
对我来说,我最感兴趣的是挑战字串 (Challenge Text) ,不过请注意,挑战字串仅在WEP加密方案中有用。无论手机的AP是否启用了WEP,你都可以将其添加到身份验证框架中,并让驱动程序解析它,如下所示,UnpackCore会调用挑战文本的特定解析函数:
tANI_U32 dot11fUnpackIeChallengeText(tpAniSirGlobal pCtx, tANI_U8 *pBuf, tANI_U8 ielen, tDot11fIEChallengeText *pDst) { tANI_U32 status = DOT11F_PARSE_SUCCESS; (void) pBuf; (void)ielen; /* Shutup the compiler */ if (pDst->present) status = DOT11F_DUPLICATE_IE; pDst->present = 1; pDst->num_text = (tANI_U8)( ielen ); if (ielen > 253){ pDst->present = 0; return DOT11F_SKIPPED_BAD_IE; } DOT11F_MEMCPY(pCtx, pDst->text, pBuf, ( ielen ) ); (void)pCtx; return status; } /* End dot11fUnpackIeChallengeText. */
正如你所看到的,他们将挑战字串的数量限制在253个字节。当这个函数完成UnpackCore和dot11fUnpackAuthentication后,我将返回到sirConvertAuthFrame2Struct,开始执行以下操作:
// & "transliterate" from a 'tDot11fAuthentication' to a 'tSirMacAuthFrameBody'... pAuth->authAlgoNumber = auth.AuthAlgo.algo; pAuth->authTransactionSeqNumber = auth.AuthSeqNo.no; pAuth->authStatusCode = auth.Status.status; if ( auth.ChallengeText.present ) { pAuth->type = SIR_MAC_CHALLENGE_TEXT_EID; pAuth->length = auth.ChallengeText.num_text; vos_mem_copy( pAuth->challengeText, auth.ChallengeText.text, auth.ChallengeText.num_text ); }
截至目前,一切看起来都很正常,但由于pAuth是我的身份验证转换函数的一个参数,所以我需要看看这个pAuth结构是什么样的。
sirConvertAuthFrame2Struct(tpAniSirGlobal pMac, tANI_U8 *pFrame, tANI_U32 nFrame, tpSirMacAuthFrameBody pAuth)
它的类型是tpSirMacAuthFrameBody,如下所示:
typedef __ani_attr_pre_packed struct sSirMacAuthFrameBody { tANI_U16 authAlgoNumber; tANI_U16 authTransactionSeqNumber; tANI_U16 authStatusCode; tANI_U8 type; // = SIR_MAC_CHALLENGE_TEXT_EID tANI_U8 length; // = SIR_MAC_AUTH_CHALLENGE_LENGTH tANI_U8 challengeText[SIR_MAC_AUTH_CHALLENGE_LENGTH]; } __ani_attr_packed tSirMacAuthFrameBody, *tpSirMacAuthFrameBody;
最后,我需要看看SIR_MAC_AUTH_CHALLENGE_LENGTH是多大:
#define SIR_MAC_AUTH_CHALLENGE_LENGTH 128
所以挑战字串有足够的空间容纳128字节的字符数组,但是请记住,在原始数据包解析时,我使用了最大长度——253字节,所以memcpy函数如下所示。
vos_mem_copy( pAuth->challengeText, auth.ChallengeText.text, auth.ChallengeText.num_text );
可以看出,我复制的字节比结构分配多了125个字节。