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