可怕!CPU暗藏了這些未公開(kāi)的指令!
大家好,我是軒轅。
我們知道,我們平時(shí)編程寫的高級(jí)語(yǔ)言,是經(jīng)過(guò)編譯器編譯以后,變成了CPU可以執(zhí)行的機(jī)器指令:
而CPU能支持的指令,都在它的指令集里面了。
很久以來(lái),我都在思考一個(gè)問(wèn)題:
CPU有沒(méi)有未公開(kāi)的指令?
或者說(shuō):
CPU有沒(méi)有隱藏的指令?
為什么會(huì)有這個(gè)問(wèn)題?
平常我們談?wù)摼W(wǎng)絡(luò)安全問(wèn)題的時(shí)候,大多數(shù)時(shí)候都是在軟件層面。談應(yīng)用程序的漏洞、后端服務(wù)的漏洞、第三方開(kāi)源組件的漏洞乃至操作系統(tǒng)的漏洞。
但很少有機(jī)會(huì)去觸及硬件,前幾年爆發(fā)的熔斷和幽靈系列漏洞,就告訴我們,CPU也不是可信任的。
要是CPU隱藏有某些不為人知的指令,這是一件非常可怕的事情。
如果某一天,某些國(guó)家或者某些團(tuán)體組織出于某種需要,利用這些隱藏的指令來(lái)發(fā)動(dòng)攻擊,后果不堪設(shè)想。
雖然想到過(guò)這個(gè)問(wèn)題,但我一直沒(méi)有付諸實(shí)踐去認(rèn)真的研究。
直到前段時(shí)間,極客時(shí)間的一位老師分享了一份PDF給我,解答了我的疑惑。
這份PDF內(nèi)容是2017年頂級(jí)黑客大會(huì)Black Hat上的一篇報(bào)告:《us-17-Domas-Breaking-The-x86-ISA》,作者是大神:@xoreaxeaxeax,熟悉匯編的同學(xué)知道這名字是什么意思嗎?
這份PDF深度研究了x86架構(gòu)CPU中隱藏的指令,原報(bào)告因?yàn)槭怯⑽模雌饋?lái)有些晦澀,這篇文章,我嘗試用大家易懂的語(yǔ)言來(lái)給大家分享一下這篇非常有意思的干貨。
有些人會(huì)問(wèn):真的會(huì)有隱藏指令的存在嗎,CPU的指令集不是都寫在指令手冊(cè)里了嗎?
我們以單字節(jié)指令為例,單字節(jié)的范圍是0x00-0XFF,總共256種組合,Intel的指令手冊(cè)中是這樣介紹單字節(jié)指令的:
橫向?yàn)閱巫止?jié)的高四位,縱向?yàn)閱巫止?jié)的低四位,順著表格定位,可以找到每一個(gè)單字節(jié)指令的定義。比如我們常見(jiàn)的nop指令的機(jī)器碼是0x90,就是行為9,列為0的那一格。
但是不知道你發(fā)現(xiàn)沒(méi)有,這張表格中還有些單元格是空的,比如0xF1,那CPU拿到一個(gè)為0xF1的指令,會(huì)怎么執(zhí)行呢?
指令手冊(cè)沒(méi)告訴你。
這篇報(bào)告的主要內(nèi)容就是告訴你,如何去尋找這些隱藏的指令。
指令集的搜索空間
想要找到隱藏的指令,得先明確一個(gè)問(wèn)題:一條指令到底有多長(zhǎng),換句話說(shuō),有幾個(gè)字節(jié),我們應(yīng)該在什么樣的一個(gè)范圍內(nèi)去尋找隱藏指令。
如果指令長(zhǎng)度是固定的,比如JVM那樣的虛擬機(jī),那問(wèn)題好辦,直接遍歷就行了。
但問(wèn)題難就難在,x86架構(gòu)CPU的指令集屬于復(fù)雜指令集CISC,它的指令不是固定長(zhǎng)度的。
有單字節(jié)指令,比如:
90 nop
CC int 3
C3 ret
也有雙字節(jié)指令,比如:
8B C8 mov ecx,eax
6A 20 push 20h
還有三四節(jié)、四字節(jié)、五字節(jié)···最長(zhǎng)能有十幾個(gè)字節(jié),比如這條指令:
指令:lock add qword cs:[eax + 4 * eax + 07e06df23h], 0efcdab89h
機(jī)器碼:2e 67 f0 48 818480 23df067e 89abcdef
一個(gè)字節(jié)、兩個(gè)字節(jié),甚至三個(gè)四個(gè)遍歷都還能接受,4個(gè)字節(jié)最多也就42億多種組合,對(duì)于計(jì)算機(jī)來(lái)說(shuō),也還能接受。
但越往后,容量是呈指數(shù)型增長(zhǎng),這種情況再去遍歷,顯然是不現(xiàn)實(shí)的。
指令搜索算法
這份報(bào)告中提出了一種深度優(yōu)先的搜索算法:
該算法的指導(dǎo)思想在于:快速跳過(guò)指令中無(wú)關(guān)緊要的字節(jié)。
怎么理解這句話?
比如壓棧的指令push,下面幾條雖然字節(jié)序列不同,但變化的只是數(shù)據(jù),其實(shí)都是壓棧指令,對(duì)于這類指令,就沒(méi)必要花費(fèi)時(shí)間去遍歷:
- 68 6F 72 6C 64 push 646C726Fh
- 68 6F 2C 20 77 push 77202C6Fh
- 68 68 65 6C 6C push 6C6C6568h
第一個(gè)字節(jié)68就是關(guān)鍵字節(jié),后面的四個(gè)字節(jié)都是壓入棧中的數(shù)據(jù),就屬于無(wú)關(guān)緊要的字節(jié)。
如果能識(shí)別出這類,快速跳過(guò),將能夠大面積減少需要遍歷的搜索空間。
(PS:本文來(lái)自公眾號(hào):編程技術(shù)宇宙)
上面只是一個(gè)例子,如何能夠系統(tǒng)化的過(guò)濾掉這類指令呢?報(bào)告中提出了一個(gè)方案:
觀察指令中的有意義的字節(jié),它們對(duì)指令的長(zhǎng)度和異常表現(xiàn)會(huì)產(chǎn)生沖擊。
又該怎么理解這句話?
還是上面那個(gè)例子,當(dāng)嘗試修改第一個(gè)字節(jié)68的時(shí)候,這一段二進(jìn)制序列可能就完全變成了別的指令,甚至指令長(zhǎng)度都會(huì)發(fā)生變化(比如把68改成90,那就變成了一個(gè)字節(jié)的nop指令),那么就認(rèn)為這第一個(gè)字節(jié)是一個(gè)有意義的字節(jié),修改了它會(huì)對(duì)指令的長(zhǎng)度產(chǎn)生重要影響。
反之,如果修改后面字節(jié)的數(shù)據(jù),會(huì)發(fā)現(xiàn)這仍然是一條5個(gè)字節(jié)的壓棧指令,長(zhǎng)度沒(méi)變化,也沒(méi)有其他異常行為表現(xiàn)與之前不同,那么就認(rèn)為后面幾個(gè)字節(jié)是無(wú)關(guān)緊要的字節(jié)。
在這個(gè)指導(dǎo)思想下,我們來(lái)看一個(gè)例子:
從下面這一段數(shù)據(jù)開(kāi)始出發(fā):
我們從兩個(gè)字節(jié)的指令開(kāi)始遍歷:
把最后那個(gè)字節(jié)的內(nèi)容+1,嘗試去執(zhí)行它:
發(fā)現(xiàn)指令長(zhǎng)度沒(méi)有變化(具體怎么判斷指令長(zhǎng)度變沒(méi)變,下一節(jié)會(huì)重點(diǎn)討論),那就繼續(xù)+1,再次嘗試執(zhí)行它:
一直這樣加下去,直到發(fā)現(xiàn)加到4的時(shí)候,指令長(zhǎng)度發(fā)生了變化,長(zhǎng)度超過(guò)了2(但具體是多少還不知道,后文會(huì)解釋):
那么在這個(gè)基礎(chǔ)上,長(zhǎng)度增加1位,以指令長(zhǎng)度為3的指令來(lái)繼續(xù)上面的探索過(guò)程:從最后一位開(kāi)始+1做起。
隨著分析的深入,梳理一下指令搜索的路徑圖:
當(dāng)某一條的最后一個(gè)字節(jié)遍歷至FF時(shí),開(kāi)始往回走(就像遞歸,不能一直往下,總有回去的時(shí)候):
往回走一個(gè)字節(jié),將其+1,繼續(xù)再來(lái):
按照這個(gè)思路,整個(gè)要搜索的指令空間壓縮到可以接受遍歷的程度:
如何判定指令長(zhǎng)度
現(xiàn)在來(lái)解答前面遺留的一個(gè)問(wèn)題。
上面這個(gè)算法能夠工作的一個(gè)重要前提是:
我們得知道,給末尾字節(jié)+1后,有沒(méi)有影響指令的長(zhǎng)度。
要判斷某個(gè)字節(jié)是不是關(guān)鍵字節(jié),就得知道這個(gè)字節(jié)的內(nèi)容變化,會(huì)不會(huì)影響到指令長(zhǎng)度,所以如果無(wú)法判斷長(zhǎng)度有沒(méi)有變化,那上面的算法就無(wú)從談起了。
所以如何知道長(zhǎng)度有沒(méi)有變化呢?報(bào)告中用到了一個(gè)非常巧妙的方法。
假設(shè)我們要評(píng)估下面這一串?dāng)?shù)據(jù),前面開(kāi)頭到底多少個(gè)字節(jié)是一條完整指令。
可能第一個(gè)字節(jié)0F就是一條指令。
也可能前面兩個(gè)字節(jié)0F 6A是一條指令。
還可能前面五個(gè)字節(jié)0F 6A 60 6A 79 6D是一條指令。
到底是什么情況,我們不知道,讓我們用程序來(lái)嘗試推導(dǎo)出來(lái)。
準(zhǔn)備兩個(gè)連續(xù)的內(nèi)存頁(yè)面,前面一個(gè)擁有可執(zhí)行的權(quán)限,后面一個(gè)不能執(zhí)行。
記?。寒?dāng)CPU發(fā)現(xiàn)指令位于不可執(zhí)行的頁(yè)面中時(shí),它會(huì)拋異常!
現(xiàn)在,在內(nèi)存中這樣放置上面的數(shù)據(jù)流:第一個(gè)字節(jié)放在第一個(gè)頁(yè)面的末尾位置,后面在字節(jié)放在第二個(gè)不可執(zhí)行的頁(yè)面上。
然后JMP到這條指令的地址,嘗試去執(zhí)行它,CPU中的譯碼器開(kāi)始譯碼:
譯碼器譯碼發(fā)現(xiàn)是0F,不是單字節(jié)指令,還需要繼續(xù)分析后面的字節(jié),繼續(xù)取第二個(gè)字節(jié):
但注意,第二個(gè)字節(jié)是位于不可執(zhí)行的頁(yè)面,CPU檢查發(fā)現(xiàn)后會(huì)拋出頁(yè)錯(cuò)誤異常:
如果我們發(fā)現(xiàn)CPU拋了異常,并且異常的地址指向了第二個(gè)頁(yè)面的地址,那么我們可以斷定:這條指令的長(zhǎng)度肯定不止一個(gè)字節(jié)。
既然不止一個(gè)字節(jié),那就往前挪一下,放兩個(gè)字節(jié)在可執(zhí)行頁(yè)面,從第三個(gè)字節(jié)開(kāi)始放在不可執(zhí)行頁(yè)面,繼續(xù)這個(gè)過(guò)程。
繼續(xù)上面這個(gè)過(guò)程,放三個(gè)字節(jié)在可執(zhí)行頁(yè)面:
四個(gè):
當(dāng)放了四個(gè)字節(jié)在可執(zhí)行頁(yè)面之后,事情發(fā)生了變化:
指令可以執(zhí)行了!雖然也拋了異常(因?yàn)樘熘肋@是個(gè)什么指令,會(huì)拋什么異常),但頁(yè)錯(cuò)誤的地址不再是第二個(gè)頁(yè)面的地址了!
有了這個(gè)信號(hào),我們就知道,前面4個(gè)字節(jié)是一條完整的指令:
挖掘隱藏指令
現(xiàn)在核心算法和判斷指令長(zhǎng)度的方法都介紹完了,可以正式來(lái)開(kāi)挖,挖出那些隱藏的指令了!
以一臺(tái)Intel Core i7的CPU為目標(biāo),來(lái)挖一挖:
挖掘成果,收獲頗豐:
這些都是Intel指令集手冊(cè)中未交待,但CPU卻能執(zhí)行的指令。
然后是AMD Athon的CPU:
挖掘成果:
那這些隱藏的指令是做什么的呢?
有些已經(jīng)被逆向工程分析了。
還有的就是毫無(wú)記錄,只有Intel/AMD自己人知道了,誰(shuí)知道它們用這些指令是來(lái)干嘛的?
軟件即便是開(kāi)源都能爆出各種各樣的問(wèn)題,何況是黑盒一樣的硬件。
CPU作為計(jì)算機(jī)中的基石,它要是出了問(wèn)題,那可是大問(wèn)題。
我不是陰謀論,害人之心不可有,但防人之心不可無(wú)。
看完這些,我對(duì)國(guó)產(chǎn)、安全、自主可控這幾個(gè)字的理解又加深了一層。
各位朋友,你對(duì)這些隱藏指令怎么看?歡迎評(píng)論區(qū)分享你的觀點(diǎn)。
最后,歡迎大家加入我的知識(shí)星球,新一輪的學(xué)習(xí)活動(dòng)就快要開(kāi)始了。