Author:leonnewton
DEXLabs發表過題為《Detecting Android Sandboxes》的博客,文章提出了一個檢測Android沙箱的方法,并附了PoC。本文對原文的內容進行介紹,并加上筆者自己實際實驗的結果及討論。
看下百度百科對二進制翻譯的解釋:
二進制翻譯(Binary Translation)是一種直接翻譯可執行二進制程序的技術,能夠把一種處理器上的二進制程序翻譯到另外一種處理器上執行。
Qemu使用二進制翻譯技術把要翻譯的native代碼,比如ARM下的代碼,翻譯到host系統下一樣或者不同的指令集,比如x86指令集。跟指令集模擬技術相比,二進制翻譯運行速度更快,因為已經翻譯的代碼塊可以進行cache然后直接執行。下面是Qemu二進制翻譯過程的流程圖:
從圖中可以看出,當遇到分支指令的時候,就會對后面的代碼進行處理。后面的代碼地址可以在分支指令里面找到,所以會先去找是否有代碼的cache。當命中的時候,代碼塊已經有cache了,代碼已經翻譯過,所以直接執行就行了。當沒有命中的時候,翻譯函數就會去翻譯代碼,一直翻譯到遇到下一個分支指令。然后把翻譯好的代碼放到cache中接著執行。
物理CPU會在每執行一條指令以后,就把程序計數器加一,程序計數器永遠都是最新的值。那么對于一個虛擬的CPU來說,也應該是在每執行完代碼中的一條指令然后將程序計數器加一,這樣來保證程序計數器是最新的值。然而,由于被翻譯的代碼是在本地執行的,也就是模擬出來的CPU,所以只有在原來代碼需要訪問程序計數器的時候,才需要返回一個最新的正確的值(因為這不會影響host系統上面正常的運行)。Qemu在遇到需要返回最新值的時候會更新程序計數器,其他時候不會每次都更新,來作為一種優化措施。因此,程序計數器會指向一個代碼塊的最開始位置,因為每次進行分支跳轉的時候才需要更新程序計數器。
試想下在多任務的操作系統中,當有中斷發生的時候,上面的優化會導致什么事?由于程序計數器不是每執行一次就更新的,那么虛擬CPU在執行一個代碼塊的時候是不能被中斷的,被中斷后就沒法恢復正確的程序計數器值。所以在運行一個代碼塊的時候,一般不會發生任務調度的情況。整個過程如下圖所示:
上面說不能中斷,不能任務調度,那么實驗就人為制造任務調度的情況,然后考察任務調度的情況。記錄發生任務調度的地址,然后統計查看這些地址的分布情況。那么在一個真機的環境中,這些地址應該近似是相等的次數,均勻的分布。每個地址發生調度情況的可能性是相等的。在Qemu虛擬的環境中,應該是在一個代碼塊的最后才會發生任務調度,所以地址分布也不是均勻的,而是變化很大。
具體的實驗是通過2個線程。一個線程在一個代碼塊中對一個全局變量不斷地加1,每次循環全局變量都賦值為1。另一個線程也是循環,每次都去讀前面的那個全局變量。然后統計讀出來每個數字的次數。流程如下:
不斷加1的線程代碼如下:
#!cpp
void* thread1(void * data){
for(;;){
__asm__ __volatile__ ("mov r0, %[global];"
"mov r1, #1;"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
"add r1, r1, #1;" "str r1, [r0];"
:
:[global] "r" (&global_value)
:
);
}
}
一共是從2一直加到32。
另一個線程直接讀全局變量,并且把讀出來的值進行統計
#!cpp
for(i=0;i<50000;i++)
count[global_value]++;
然后計算count數組統計出來的值的方差,看看分布情況的離散程度,這里我嘗試了3個計算方式。
對所有數據除以最大值,然后計算方差和標準差;
,進行離差標準化以后,計算方差和標準差。
一共是循環讀取50000次。
在模擬器里,3種方式計算出來的方差和標準差如下:
每個數字統計的次數如下:
除了count[32]
其他都是0。
在真機中,3種方式計算出來的方差和標準差如下:
每個數字統計的次數如下:
雖然count[32]
比其他各個地方都多,但是中間基本都是均勻分布的。
那么實驗的含義是什么呢。其實就是在一個線程加1的過程中,另一個線程在什么地方打斷了它,然后從讀出來的值看是在哪里打斷了,也就是進行了任務調度。因此從結果來看,確實符合上面的分析,模擬器只在代碼塊的最后,有分支指令的時候進行了任務調度。而在真機中,實驗中發現也是在這里調度最多,但在上面其他位置是基本均勻分布的。大家可以自己設計反應離散程度的指標來判斷是否運行在模擬器。