ollvm反混淆學習
看了@無名俠大佬發的一篇關于使用unicorn模擬執行還原ollvm的貼子受到了很大的啟發, 自己也基于這個思路做了些樣本學習,下面來探討一下。
ollvm原理
Ollvm大致可分為 bcf(虛假塊), fla(控制流展開), sub(指令膨脹), Split(基本塊分割)
bcf:
克隆一個真實塊,并隨機替換其中的一些指令,然后用一個永遠為真的條件建立一個分支。克隆后的塊是不會被執行的。
Fla:
將所有的真實塊使用一個switch case結構包裹起來,每個真實塊執行完畢后都會重新賦值switch var,對于有分支的塊會使用select指令,并跳轉到switch起始代碼塊(分發器)上,根據switch var來執行下一個真實塊。
Sub:
指令膨脹,將一條運算指令,替換為多條等價的運算指令。
Split:
利用隨機數產生分割點,將一個基本塊分割為兩個,并使用絕對跳轉連接起來。
關于ollvm具體的實現,可參考源碼。
還原思路
網上有很多還原ollvm的腳本,但是只能還原特征很明顯的ollvm,或者說只是debug版的ollvm。在debug版中ollvm的特征非常明顯,一個分發器,和引用了這個分發器的真實塊。但經過編譯器優化后,分發器可能會變成多個,基本塊會合并造成虛假塊也可能會和真實塊合并,等等。
現實情況是,你基本上碰不到簡單的ollvm,所以那些東西個人感覺意義不是很大,還是需要靠自己。
談下還原思路
Bcf:
Bcf塊是執行不到的塊,所以說當使用unicorn 跑過一遍函數后,其中沒有執行到的塊肯定有包括bcf塊,我們只需要將它挑出來標記下就好。
但函數中可能存在分支,只跑一遍函數是無法覆蓋到所有分支的,所以要想辦法找到函數的所有分支。一開始采用的是無名俠大佬的方法,當碰到csel指令時人工干預讓其覆蓋所有分支,但整個函數經常陷入死循環,分析過后發現虛假塊的跳轉也有可能使用csel指令。
后來想到了在二進制漏洞挖掘中的思路fuzz(模糊測試),即變異函數的參數傳遞給函數,來覆蓋更多的分支。這樣做也不能說能夠找到函數的所有分支。影響一個函數的分支執行大概有三種情況,參數,全局變量,內部函數調用的返回值。后兩種情況的話留意下模糊執行的trace應該能找到些蛛絲馬跡,可能會比較麻煩。
Fla
這個環節會產生控制流塊,我們只需要將這些塊挑出來標記,找出所有的真實塊,并通過模擬執行還原真實塊之間的關系就好。
控制流塊的剔除采用了無名俠大佬對基本塊簽名的方法。
Sub:
指令膨脹的還原,使用llvm的pass優化效果還可以,但目前一些ir翻譯工具對arm64的支持不怎么樣。
Split:
基本塊分割更多是用來增加bcf和fla效果的。
總結整體思路:
(1)利用模擬執行和fuzz技術,找出bcf塊并剔除。
(2)使用基本塊簽名剔除控制流塊。
(3)將剩余的塊標記為真實塊,并使用模擬執行找出對應關系。
(4)根據對應關系,重構cfg。
實戰
自己編譯的一個樣本如下:
void HexDump(char *buf,int len,int addr)__attribute((__annotate__(("split"))))__attribute((__annotate__(("fla"))))__attribute((__annotate__(("bcf")))){ int i,j,k; char binstr[80]; for (i=0;i if (0==(i%16)) { sprintf(binstr,"%08x -",i+addr); sprintf(binstr,"%s %02x",binstr,(unsigned char)buf[i]); } else if (15==(i%16)) { sprintf(binstr,"%s %02x",binstr,(unsigned char)buf[i]); sprintf(binstr,"%s ",binstr); for (j=i-15;j<=i;j++) { sprintf(binstr,"%s%c",binstr,('!''~')?buf[j]:'.'); } printf("%s",binstr); } else { sprintf(binstr,"%s %02x",binstr,(unsigned char)buf[i]); } } if (0!=(i%16)) { k=16-(i%16); for (j=0;j sprintf(binstr,"%s ",binstr); } sprintf(binstr,"%s ",binstr); k=16-k; for (j=i-k;j sprintf(binstr,"%s%c",binstr,('!''~')?buf[j]:'.'); } printf("%s",binstr); }}

先找出所有的基本塊(以跳轉指令結尾的塊)
這里需要注意下由于編譯器優化的關系,基本塊會合并,有些基本塊并不是以跳轉指令結尾,就如這樣:

這些情況,是因為兩個基本塊同時引用了這個塊,所以需要將這個塊拷貝一份,并將另一個塊的引用修改為新拷貝的塊,不然還原關系的時候會亂掉。

我這里占用了main函數的空間。
找出所有的基本塊后開始fuzz執行,并統計所有被執行到的塊。這里fuzz采用了,先使用peach編寫規則生成參數的語料庫保存到文件中,然后讀取文件中的內容當作參數傳遞給函數, 當然如果不關心函數的其他分支,fuzz的步驟感覺可以跳過,例如一些純算法函數。
經過幾十輪fuzz后,共統計到如下被執行了的塊。

這些塊中肯定是包含了控制流塊的,所以現在用簽名法來過濾掉控制流塊。

過濾后還剩下169個塊,這些塊就是真實塊了,為了保險起見我還人工過濾了一下,基本沒什么問題。
接下來開始模擬執行找出他們之間的對應關系了,當碰到一個真實塊時記錄下它上一個執行的真實塊,并保存起來。
傳遞給函數的參數也需要使用上面fuzz使用的參數,這樣才能執行到每一個塊。
模擬執行后,基本塊之間的關系如下:

如果數組中只有一個基本塊的話,那么他們是一個順序關系,如果有兩個的話則是分支關系, 如果2個以上則有三種情況:
(1)漏了真實塊;
(2)該塊不是一個真實塊;
(3)該塊是一個分支共用塊。
經排查這里是第三種情況,如下:


9e8這個塊被兩個基本塊引用,并兩個基本塊都是一個分支塊,所以會出現這種情況。具分析其中一個塊的分支對應的是bcf,不會被執行到,所以數組中是3個基本塊而不是4個。對于這個情況也需要將9e8這個塊copy一份,將兩個基本塊中的其中一個引用修改為copy后的塊。


修改完畢后,記得將copy塊添加到真實塊中,并重試。

可以看到問題解決了。
找出對應關系后需要接著還原分支關系,當條件為真時跳到那個塊,為假時跳轉到那個塊。因為每個分支塊都會有一條cmp 和csel指令, 如果找到的分支塊中沒有這兩條指令,那么就是漏了真實塊。

還原他們的關系,只需要在模擬執行時,記錄cmp的返回值,和返回值對應的真實塊即可,這里會比較麻煩,需要手動找到cmp的地址, 左右值, 和比較關系。

模擬分支塊的關系如下:

我這里根據記錄的條件,翻譯成了匯編。
最后根據這些真實塊之間的關系patch即可, 注意在patch分支塊時需要注意csel和cmp的關系,像這種:

如果我們如果在基本塊的最后patch b.ne xxx b xx, 那么標志位就會被上面的一個cmp干擾,所以需要將上面 一個cmp也patch掉。
好了現在大功告成,直接來看偽代碼。

把偽代碼拿出來編譯測試:

寫在最后
目前還在測試大概還原了5 6個樣本,可能還有一些細節方面未考慮到,所以發出來希望聽下大佬的意見。
之所以沒貼代碼出來是因為代碼太雜了和篇幅太大了,實在是不太方便,有需要的話可以參考無名俠大佬的帖子和源碼,我都是基于他之上的。
如果大家感覺以上有不妥或者不理解的地方,歡迎和我一起探討一起學習。