為了保護程序虛擬機是最常用的一種手段,并不是說用了多復雜的算法。主要是會花費大量的時間來把程序還原為本來的樣子。使用OLLVM項目中的控制流平坦化、虛假控制流、指令替換等混淆策略會進一步加大分析的難度。
Ollvm是什么
llvm是一個底層虛擬機,OLLVM(Obfuscator-LLVM)是瑞士西北應用科技大學安全實驗室于2010年6月份發起的一個項目,這個項目的目標是提供一個LLVM編譯套件的開源分支,能夠通過代碼混淆和防篡改,增加對逆向工程的難度,提供更高的軟件安全性。目前,OLLVM已經支持LLVM-4.0.1版本;所使用的編譯器是clang。
llvm保護的大概方法是:程序主要使用一個主分發器來控制程序基本塊的執行流程進而模糊每個模塊之間的聯系。
源代碼如下:
#include <stdio.h>
int check(int a)
{
if(a>0)
return 3;
else
return 0;
}
int main()
{
int a;
scanf("%d",&a);
if (check(a)==3)
{
puts("good");
}
else
{
puts("wrong");
}
return 0;
}
正常編譯如下
使用llvm編譯
./build/bin/clang main.c -o test -mllvm -fla
程序流程圖明顯復雜很多。
看一下check函數
原程序
signed __int64 __fastcall check(__int64 a1)
{
char v1; // bl@2
signed __int64 result; // rax@8
int i; // [sp+1Ch] [bp-14h]@1
srand(0x64u);
for ( i = 0; i < strlen((const char *)a1); ++i )
{
v1 = *(_BYTE *)(i + a1);
*(_BYTE *)(i + a1) = v1 + rand() % 5;
}
if ( *(_BYTE *)a1 != 97 || *(_BYTE *)(a1 + 1) != 98 || *(_BYTE *)(a1 + 2) != 99 || *(_BYTE *)(a1 + 3) != 100 )
result = 0LL;
else
result = 4LL;
return result;
}
平坦化程序
__int64 __fastcall check(__int64 a1)
{
size_t v1; // rax@13
signed int v2; // ecx@13
char v3; // ST04_1@16
signed int v4; // eax@18
signed int v5; // eax@21
signed int v6; // eax@24
signed int v7; // eax@27
signed int v9; // [sp+44h] [bp-1Ch]@1
int v10; // [sp+48h] [bp-18h]@1
signed int v11; // [sp+5Ch] [bp-4h]@0
srand(0x64u);
v10 = 0;
v9 = 706310565;
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
while ( v9 == -2109444161 )
{
v7 = 1322041670;
if ( *(_BYTE *)(a1 + 3) == 100 )
v7 = 867560817;
v9 = v7;
}
if ( v9 != -2069803162 )
break;
++v10;
v9 = 706310565;
}
if ( v9 != 306556692 )
break;
v4 = 1322041670;
if ( *(_BYTE *)a1 == 97 )
v4 = 564228819;
v9 = v4;
}
if ( v9 != 341947172 )
break;
v6 = 1322041670;
if ( *(_BYTE *)(a1 + 2) == 99 )
v6 = -2109444161;
v9 = v6;
}
if ( v9 != 564228819 )
break;
v5 = 1322041670;
if ( *(_BYTE *)(a1 + 1) == 98 )
v5 = 341947172;
v9 = v5;
}
if ( v9 != 682671478 )
break;
v3 = *(_BYTE *)(a1 + v10);
*(_BYTE *)(a1 + v10) = rand() % 5 + v3 - 97 + 97;
v9 = -2069803162;
}
if ( v9 != 706310565 )
break;
v1 = strlen((const char *)a1);
v2 = 306556692;
if ( v10 < v1 )
v2 = 682671478;
v9 = v2;
}
if ( v9 != 867560817 )
break;
v11 = 4;
v9 = 1000770411;
}
if ( v9 == 1000770411 )
break;
if ( v9 == 1322041670 )
{
v11 = 0;
v9 = 1000770411;
}
}
return (unsigned int)v11;
}
如果分析下來,難度會比較大。
嘗試進行函數的恢復
按照如下思路進行去平坦化:
- 函數的開始地址為序言的地址
- 序言的后繼為主分發器
- 后繼為主分發器的塊為預處理器
- 后繼為預處理器的塊為真實塊
- 無后繼的塊為retn塊
- 剩下的為無用塊
獲取相關的塊列表地址,就能夠通過angr定義規則,來約束函數模塊。
(按照miasm功能介紹,可以獲取反編譯后的模塊地址,但因為報錯太多就放棄了。)
之后便找到了bird大佬寫的腳本,將上面報錯部分修改之后拿來用。
程序恢復結果:
2018 X-NUCA 使用腳本去平坦化
初始
恢復之后
但程序還使用了”指令替換” -subv 里面還有指令替換,好在程序的邏輯并不復雜。
第一部分比較復雜:
第二部分就比較清晰了:
根據后面的來觀察執行的操作,函數加密分為兩部分。每部分各16個字符,后面16個進行的是三次異或運算。所有運算的結果都儲存在6130D0中,動態調試的過程中要時刻注意其中的結果。(取巧的方法:因為我們可以確定flag的前七位是X-NUCA{ ,通過結果計算,這樣會更容易找到加密的位置)
str = '012345abcdefghijklmnopqrstuvwxyz'
a=[0x68,0x1C,0x7C,0x66,0x77,0x74,0x1A,0x57,0x06,0x53,0x52,0x53,0x02,0x5D,0x0C,0x5D]
b=[0x04,0x74,0x46,0x0E,0x49,0x06,0x3D,0x72,0x73,0x76,0x27,0x74,0x25,0x78,0x79,0x30]
flag1=""
flag2=""
j=0
for i in range(16):
flag1+=chr(a[i]^ord(str[i]))
for i in range(16):
j=i+16
flag2+=chr(b[i]^ord(str[j])^a[i]^ord(str[i]))
print flag1+flag2
總結:符號執行對于虛擬機的分析作用很大,且二進制分析工具了解太少,如果沒有bird 大佬的腳本,現在也不能成功去平坦成功一次。而且也看到了,對于指令替換便需要重新寫一新的腳本。 miasm的功能很強大,應當盡快掌握。
文章文件鏈接:https://pan.baidu.com/s/1CEXUZ-Yb_t__3770WL51cw
提取碼:a0wt
文章參考:
bird@tsrc?https://paper.seebug.org/192/
Deobfuscation: recovering an OLLVM-protected program