c/c++字串混淆方案總結
有過破解native程式經驗的人都知道,在大量的彙編程式碼面前不可能是從頭開始理解程式碼的,必須找到一兩個點進行突破。字串往往就是這樣的關鍵點,在程式碼中hardcode的字串會可以原封不動的在生成的binary中查詢到。所以要增加破解的難度,對字串進行混淆(或者叫加密,下面的文字可能混淆和加密混著用,在這裡沒有區別)是很重要的一步。只要字串在程式碼中出現,那麼其必然會在binary中出現,所以要想在binary中查詢不到字串,必須在程式碼進行編譯之前進行變形。所以從理論上來說,可以從下面幾個角度進行入手:
(1).編譯之前呼叫程式對字串進行處理
(2).程式碼中直接寫入混淆後的字串
(3).編譯過程中使用巨集等其他手段自動混淆
(4).對生成的binary進行處理
下面的幾個方案就是從這幾個角度提煉出來的.
方案一:直接在程式碼中寫入混淆後的字串
利用工具生成加密後的字串,在程式碼中寫入加密後的字串,在使用字串時呼叫一個函式進行解密
char* string = Decrypt("aGVsbG8=");//hello
printf("string is: %s", string);
這裡字串"aGVsbG8="是經過base64之後的,Decrypt之後還原成字串hello。
這裡加密後的內容還是字元形式的字串。字元形式的字串也會在binary中出現,如果破解者發現有大量的這種比較奇怪的字串,實際上也是破解者的一個突破口,他們可以從這裡破解出我們的加密演算法,從而將所有的字串解密出來。
對付這個問題我們有幾種方法:
1. 使用多種加密演算法,針對不同的字串使用不同的演算法
2.加密後的內容不用以0結尾的字串表示,使用binary形式,以長度表示其結尾
這種方法的有點是比較簡單,但是增加開發人員的工作比較多
方案二:字串以陣列形式初始化
比如以下寫法:
如果使用strings查詢字串,可以發現"a teststring!"可以找到,而'atest string2!'則無法找到。const char* testString = "a test string!"; printf("testString: %s\n", testString); //a test string2! char* testString2 = ((char[]){'a', ' ', 't', 'e', 's','t',' ', 's', 't', 'r', 'i', 'n', 'g', '2', '!', '\0'}); printf("testString2: %s\n", testString2);
檢視其彙編程式碼,會發現這兩種寫法的對於字串處理生成的程式碼完全不一樣。
第一種寫法,我們比較熟悉,直接從binary的.txt區域獲取字串:
.text:000000000040057B mov [rbp+var_28], 40070Ch //40070Ch 是指向binary中的地址
.text:0000000000400583 mov rax, [rbp+var_28]
.text:0000000000400587 mov rsi, rax
.text:000000000040058A mov edi, offset aTeststringS ;"testString: %s\n"
.text:000000000040058F mov eax, 0
.text:0000000000400594 call _printf
第二種寫法則完全不一樣,這些字串不會儲存在binary的.txt區域,而是在一個字元一個字元的copy到棧上面,以下gcc在ubuntu64上面生成的彙編程式碼:
.text:0000000000400599 mov [rbp+var_20], 61h
.text:000000000040059D mov [rbp+var_1F], 20h
.text:00000000004005A1 mov [rbp+var_1E], 74h
.text:00000000004005A5 mov [rbp+var_1D], 65h
.text:00000000004005A9 mov [rbp+var_1C], 73h
.text:00000000004005AD mov [rbp+var_1B], 74h
.text:00000000004005B1 mov [rbp+var_1A], 20h
.text:00000000004005B5 mov [rbp+var_19], 73h
.text:00000000004005B9 mov [rbp+var_18], 74h
.text:00000000004005BD mov [rbp+var_17], 72h
.text:00000000004005C1 mov [rbp+var_16], 69h
.text:00000000004005C5 mov [rbp+var_15], 6Eh
.text:00000000004005C9 mov [rbp+var_14], 67h
.text:00000000004005CD mov [rbp+var_13], 32h
.text:00000000004005D1 mov [rbp+var_12], 21h
.text:00000000004005D5 mov [rbp+var_11], 0
.text:00000000004005D9 lea rax, [rbp+var_20]
.text:00000000004005DD mov [rbp+var_30], rax
.text:00000000004005E1 mov rax, [rbp+var_30]
.text:00000000004005E5 mov rsi, rax
.text:00000000004005E8 mov edi, offset aTeststring2S ;"testString2: %s\n"
.text:00000000004005ED mov eax, 0
.text:00000000004005F2 call _printf
這個寫法在vc上面無法編譯通過:
char* testString2 = ((char[]){'a', ' ', 't', 'e', 's','t',' ', 's', 't', 'r', 'i', 'n', 'g', '2', '!', '\0'});
但是稍微變形一下就可以:
char testString2[] = {'t', 'h', 'i', 's', ' ', 'a', '', 't', 'e', 's', 't',' ', 's', 't', 'r', 'i', 'n', 'g', '\0'};
我看了一下生成的彙編程式碼,跟前一種寫法是一樣的。這個方案比較tricky,利用的是編譯器生成程式碼的特性。其缺點是寫字串時比較麻煩,每個字元都需要用單引號給引起來。如果字串比較多,是一個比較大的負擔。但是這種方案的好處也是十分明顯的,字串都是一個一個嵌入在程式碼裡面,要想找出來難度非常大,另外 ,字串只出現在棧上面,棧退出之後,字串就在記憶體中就找不到了,即使搜尋記憶體的方式也找不到,所以安全性非常高。
注意:這種寫法只能用在函式內部,如果testString2是一個全域性變數,字串則會儲存在binary中。
方案三:編譯前對字串進行處理
在網上發現好幾個開源專案是做這事,其基本思路是,先自定義一個自定義格式的檔案,在檔案中寫入字串,然後在編譯之前,將檔案轉換成c/c++格式,被程式碼引用。c/c++檔案中的字串則經過了我們的加密。比如這個專案:
Literalstring encryption as part of the build process
其自定義的檔案叫crx檔案,其內容如下:
////////////////////
// my .CRX file
//
// here is my password definition:
// CXRP ="SexyBeast"
//
// here are somestrings:
// my first string
constchar* pString1 = _CXR("AbcdEfg1234 blah\tblah");
// string #2
constchar* pString2 = _CXR("This is a long one, not that itshould matter...");
CXRP= "SexyBeast" 這裡定義的是加密用的祕鑰。
_CXR 則定義字串,我們會將裡面的字串加密後生成cpp檔案,如下:
///////////////////////////
#ifdef _USING_CXR
// my first string
const char* pString1 = "ab63103ff470cb642b7c319cb56e2dbd591b63a93cf88a";
#else
const char* pString1 = _CXR("AbcdEfg1234 blah\tblah"); // my first string
#endif
///////////////////////////
#ifdef _USING_CXR
// string #2
const char* pString2 = "baff195a3b712e15ee7af636065910969bb24997c49c6d0cc6a40d3ec1...";
#else
// string #2
const char* pString2 = _CXR("This is a long one, not that it should matter...");
#endif
這裡可以使用_USING_CXR巨集來對字串加密功能進行開關控制。
這裡還有一個專案:strenc
其自定義檔案叫.strenc,格式如下:
INTRO_STRING
Thisis a test of strenc()
SECOND_STRING
How's it working?
THIRD_STRING
testing"quotes" bro.
它的格式是字串名字和字串內容交叉進行,被處理後生成的是.h檔案,用巨集來表示,如下:
#ifndef STRINGS_KEY
#define STRINGS_KEY "1iCEVcHQRhf+rkybltGvodTAg6m9XMDp5WuFqxO2/jzZISUenNKL80BJP4w3as7Y"
#pragma comment(lib,"strenc")
voidStrencDecode(char* buffer,char*Base64CharacterMap);
constchar*GetDecryptedString(constchar* encryptedString)
{
char*string=newchar[1024];
strcpy(string, encryptedString);
StrencDecode(string, STRINGS_KEY);
returnstring;
}
#define INTRO_STRING GetDecryptedString("dHWjXKijXKiWRQtxXJl59Bg5XJtK6T4FfCq1")
#define SECOND_STRING GetDecryptedString("GHsJhJr5mAl5MBsKmBxU6La1")
#define THIRD_STRING GetDecryptedString("MHdLMHxU6K1uXAdeMHdLRuiuXOaU11==")
#endif
這種方法的優點是將所有的字串統一進行管理,將所有需要混淆的字串都加入到這個檔案中。我們還可以增強其加解密功能,不同的字串使用不同的加解密演算法或者不同的祕鑰。缺點是對於已經存在的程式碼,我們需要將需要混淆的字串一個一個提取出來,放在自定義格式的檔案中,如果程式碼量比較大,搜尋出所有的字串的工作量比較大。針對這個缺點,我們引入了下面一種方案。
方案四:掃描所有程式碼,將所有字串進行混淆處理
上面那個方案不錯,但是對於已經存在的大型專案要將需要混淆的字串找出來的工作量比較大,所有有了這個方案
這裡有一個實現:
StringsObfuscation System
它的實現是基於vc的,搜尋solution中的所有.h/.c/.cpp/.hpp檔案,將有字串的地方使用 __ODA__()進行替換, __ODA__的引數則為加密後的字串, __ODA__函式負責解密。它還支援uncode字串,對於unicode字串,使用函式__ODC__替換字串。它的優點就是可以很簡單的就將大型專案的所有字串都混淆。缺點是它需要在編譯時,每次都掃描一遍程式碼,將需要替換的字串替換掉。
方案五:編譯期對字串進行變換
#define PRIME 0x1000193
#define OFFSET 0x811C9DC5
struct Hash
{
template <unsigned int N, unsigned int I>
struct Helper
{
inline static unsigned int Calculate(const char(&str)[N])
{
return (Helper<N, I - 1>::Calculate(str) ^ str[I- 1]) * PRIME;
}
};
template <unsigned int N>
struct Helper<N, 1>
{
inline static unsigned int Calculate(const char(&str)[N])
{
return (OFFSET ^ str[0]) * PRIME;
}
};
template <unsigned int N>
inline static unsigned int Calculate(const char(&str)[N])
{
return Helper<N, N>::Calculate(str);
}
};
Hash::Calculate("hello")
這裡的Hash::Calculate傳入的字串會被在編譯期間計算hash值,字串在開啟優化之後(O3),在程式碼中不存在。這裡的hash可以作為index來獲取到解密後的string,定義一個這樣的巨集:
#define DECRYPT(text) GetDecryptText(Hash::Calculate(text))
GetDecryptText後面再解釋。在使用時,這樣使用就可以:printf("%s\n",DECRYPT("helloText"));
另外再準備一個工具,掃描所有帶有DECRYPT巨集的字串,生成函式GetDecryptText:
......
char str_0x12345678[12] = { 0 };
bool tag_0x12345678 = Decrypt(str_0x12345678,"[email protected]##$%^&*("); // Cipher text of "Hello World"
......
const char* GetDecryptText(int hash)
{
switch(hash)
{
......
case0x12345678: return str_0x12345678;
......
}
}
生成的程式碼可以放在一個單獨的cpp檔案中。這個方案優點實際上跟方案三差不多,都會生成一個獨立的cpp檔案。優點是對現有的程式碼結構改變較小。缺點是:
(1)需要開啟優化之後,字串才會不出現在binary中,
(2)需要編譯前對原始碼進行全部掃描
(3)依賴template,純c環境無法使用
方案六:修改編譯器,將所有的字串都加密儲存
修改編譯器,將所有的字串都加密儲存,在引用字串的地方呼叫解密函式。這個方案在網上沒有看到現成的程式碼,但是考慮到gcc是開源的,應該是可以做的。
方案七:加殼
加殼是很成熟的方法了,可以防止直接從binary中搜索字串。他的優點在於不用對程式碼做任何改動,並且不會引入任何執行時的額外效能開銷。加殼的缺點在於:加殼過的檔案比較容易認為是病毒。如果只是為了字串加密而引入加殼則有點小題大做,脫殼之後或者程式執行之後字串全部可見。
參考文獻
混淆字串
如何防止客戶端被破解
Literalstring encryption as part of the build process
StringsObfuscation System
strenc
PE檔案中隱藏明文字串
pe檔案中隱藏明文字串(續)
In-Depth:Quasi Compile-Time String Hashing