整合到 Visual Studio C++ 專案中的字串混淆系統
Michael Haephrati, CodeProject MVP 2013
下載原始碼 - 16 KB
簡介
混淆器的一般目的是隱藏程式程式碼、流程和功能。如果您的程式使用某個作為商業秘密的演算法,混淆將使其更難被反向工程並揭示該商業秘密。但是隱藏可執行檔案中的資料呢?有人說這幾乎是不可能實現的。由於問題需要能夠讀取此資料,因此資料必須存在,如果存在,則最終可以被揭示。在我看來,一個精心混淆的程式可以使其幾乎不可能猜到資料儲存(加密)的位置,即使找到資料,也很難使用強加密對其進行混淆。
解決方案
本文將介紹的工具集,是為應用程式中的字串加密目的而開發的。
通常,一個應用程式即使無需反向工程,也能透露大量關於自身的資訊。當您使用十六進位制編輯器(或記事本作為文字編輯器)開啟 Calc.exe 這樣的應用程式時,您可以找到類似這樣的字串
![]()
如果不加密地儲存,會帶來風險的字串示例是密碼。如果您的軟體連線到 Internet 服務、簡訊閘道器或 FTP 伺服器併發送密碼,則任何使用文字編輯器開啟您應用程式可執行檔案的人都可以看到此密碼。
背景
我閱讀了 Chris Losinger 的 出色文章,並希望透過建立更強的加密(AES-256)並支援更多變體和字串型別,包括 UNICODE、雙位元組和單位元組字串,將其提升到一個新的水平。
此工具的目的是專業用途,而不僅僅是概念驗證。
原始碼
加密/解密機制整合在兩個單獨的專案中,需要將它們新增到您的 Visual Studio 專案中。這兩個專案位於主資料夾中
a. obfisider 專案。
b. obfuscase 專案。
Obfisider 和 Obfuscate 專案
Obfisider 專案包含 AES 加密部分。Obfuscate 專案包含掃描解決方案及其包含的每個專案的必要部分,並加密找到的字串。
掃描解決方案解決方案透過 parseSolution 呼叫另一個名為 parseProject 的函式進行掃描。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45static strList parseSolution( const char * solName ) { strList result; static char drive[_MAX_DRIVE]; static char somepath[_MAX_PATH]; static char buffer[_MAX_PATH]; static char path[_MAX_PATH]; static char ext[_MAX_EXT]; _splitpath( solName, drive, somepath, buffer, ext ); FILE * f = fopen( solName, "r" ); if( NULL == f ) { printf("ERROR: Solution %s is missing or unavailable.\n", solName ); exit(1); } while( !feof(f) ) { char * res = fgets( buffer, sizeof(buffer), f ); if( NULL == res ) continue; if( NULL != strstr(buffer, "Project(") ) { char * ptrName = strchr( buffer, '=' ); char * ptrFile = strchr( ptrName, ',' ); *ptrFile++ = 0; char * ptrEnd = strchr( ptrFile, ',' ); *ptrEnd++ = 0; while( ('=' == *ptrName) ||(' ' == *ptrName) ||('"' == *ptrName) ) ptrName++; if( '"' == ptrName[strlen(ptrName)-1] ) ptrName[strlen(ptrName)-1] = 0; while( (' ' == *ptrFile) ||('"' == *ptrFile) ) ptrFile++; if( '"' == ptrFile[strlen(ptrFile)-1] ) ptrFile[strlen(ptrFile)-1] = 0; _makepath( path, drive, somepath, ptrFile, NULL ); result.push_back( std::string(path) ); } } fclose(f); return result; }
parseProject 函式從給定專案中提取相關檔案。相關檔案表示:.c、.cpp、.h 和 .hpp 檔案。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43/** * Parse project and extract fullpath source filename from project. */ static strList parseProject( const char * projName ) { strList result; static char drive[_MAX_DRIVE]; static char somepath[_MAX_PATH]; static char buffer[_MAX_PATH]; static char path[_MAX_PATH]; static char ext[_MAX_EXT]; _splitpath( projName, drive, somepath, buffer, ext ); FILE * f = fopen( projName, "r" ); if( NULL == f ) { printf("ERROR: Project %s is missing or unavailable.\n", projName ); exit(1); } while( !feof(f) ) { char * res = fgets( buffer, sizeof(buffer), f ); if( NULL == res ) continue; if( (NULL != strstr(buffer, "<ClInclude Include=")) ||(NULL != strstr(buffer, "<ClCompile Include=")) ) { char * ptrName = strchr( buffer, '=' ); char * ptrName1 = strstr( buffer, "/>" ); if( NULL != ptrName1 ) *ptrName1 = 0; while( ('=' == *ptrName) ||(' ' == *ptrName) ||('"' == *ptrName) ) ptrName++; while( ('"' == ptrName[strlen(ptrName)-1]) ||(' ' == ptrName[strlen(ptrName)-1]) ||('\n' == ptrName[strlen(ptrName)-1])) ptrName[strlen(ptrName)-1] = 0; _makepath( path, drive, somepath, ptrName, NULL ); result.push_back( std::string(path) ); } } fclose(f); return result; }
AES_Encode 函式
此函式處理使用 AES-256 加密字串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36/* -------------------------------------------------------------------------- */ int AES_encode_a( unsigned int key_start, const wchar_t * plainString, unsigned char * outbuf, unsigned outlen ) { unsigned char key[32]; aes_key key_context = {0}; int i; unsigned char offset; /** Calculate required size */ int retval = (wcslen(plainString) + 1); /** Round to 16 byte over */ retval = ((retval + 15)&(~0xF)) + 4; /** Memory request */ if( NULL == outbuf ) return -retval; /** Not enought memory */ if( outlen < retval ) return 0; /** Prepare output buffer */ memset( outbuf, 0, retval ); // wcscpy( (char*)(outbuf+4), plainString ); WideCharToMultiByte( CP_ACP, 0, plainString, -1, (outbuf+4), retval-sizeof(unsigned),NULL, NULL); *((unsigned*)outbuf) = key_start; /** Prepare key */ srand(key_start); for( i = 0; i < sizeof(key); i++ ) key[i] = rand(); aes_prepare( &key_context, key ); memset( key, 0, sizeof(key) ); for( i = 4; i < retval; i += 16 ) { aes_encrypt_block( &key_context, &outbuf[i] ); } memset( &key_context, 0, sizeof(key_context) ); return retval; } /* -------------------------------------------------------------------------- */
AES_Decode 函式
此函式處理將字串解密回來
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25/* -------------------------------------------------------------------------- */ static int AES_decode( const unsigned char * inbuf, unsigned inlen, void *plainString, unsigned stringSize ) { unsigned char key[32]; aes_key key_context = {0}; int i; BYTE * outbuf = (BYTE*)plainString; if( NULL == plainString ) return -inlen; if( stringSize < inlen ) return 0; /** Prepare output buffer */ memcpy( outbuf, inbuf, inlen ); /** Prepare key */ for( i = 0; i < sizeof(key); i++ ) key[i] = rand(); aes_prepare( &key_context, key ); memset( key, 0, sizeof(key) ); for( i = 0; i < inlen; i += 16 ) { aes_decrypt_block( &key_context, &outbuf[i] ); } memset( &key_context, 0, sizeof(key_context) ); return inlen; }
將字串解密回來
ASCII 字串使用以下函式( __ODA__ )解密
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/* -------------------------------------------------------------------------- */ char* __ODA__( const char * enc_str ) { int i, size = strlen( enc_str )/2; unsigned char * inBuff = NULL; unsigned key = 0; PDECODED_LIST ptr = &charList; char * result = a_text_err; while( NULL != ptr->next ) { if( ptr->org_str == enc_str ) return ((char*)ptr+sizeof(DECODED_LIST)); ptr = ptr->next; } if( NULL == (inBuff = (unsigned char*)malloc( size )) ) return result; // a_text_error if( NULL == (ptr->next = (PDECODED_LIST)malloc( size + sizeof(DECODED_LIST) )) ) { free( inBuff ); return result; // a_text_error } ptr = ptr->next; ptr->
當字串是 UNICODE 時,使用以下函式 (__ODC__)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/* -------------------------------------------------------------------------- */ wchar_t* __ODC__( const char * enc_str ) { int i, size = strlen( enc_str )/2; unsigned char * inBuff = NULL; unsigned key = 0; PDECODED_LIST ptr = &wcharList; wchar_t * result = w_text_err; while( NULL != ptr->next ) { if( ptr->org_str == enc_str ) return (wchar_t*) ((char*)ptr+sizeof(DECODED_LIST)); ptr = ptr->next; } if( NULL == (inBuff = (unsigned char*)malloc( size )) ) return result; // w_text_error if( NULL == (ptr->next = (PDECODED_LIST)malloc( size + sizeof(DECODED_LIST) )) ) { free( inBuff ); return result; // w_text_error } ptr = ptr->next; ptr->
如何使用
設定專案依賴關係,以便您的解決方案生成的主要可執行檔案或 DLL 依賴於“obfinsider”專案。
‘obfinsider’專案必須依賴於另一個專案——‘obfuscate’專案。這將自動包含 obfinsider.lib,但如果您進行了可能破壞此依賴關係的更改,請手動新增 obfisider.lib。
工作原理
過程
首先構建 'obfuscate',構建完成後,將執行一個構建後事件。構建後事件會呼叫 obfuscate 並將整個解決方案檔案作為其引數。
檔案掃描Obfuscate 掃描給定解決方案並處理其中的每個相關檔案。當前版本要求路徑中不存在空格,如果存在空格,呼叫 "obfuscate" 的正確方式應該是
"$(TargetPath)" "$(SolutionPath)"
"obfuscate" 掃描解決方案中的所有專案檔案,但處理以下檔案型別:.c、.cpp、.h 和 .hpp。
工作流程
對於每個檔案,會進行以下檢查
a. 跳過已混淆的檔案
b. "obfuscate" 不處理自身,因此會跳過其自身的檔案。
c. 跳過註釋。這包括
// 這是註釋
和
/* 這是另一個註釋 */
d. #include 和 #pragma 宣告將被跳過。
e. 初始化全域性字串將被忽略。如果您檢視以下示例
Static char c[]="test string";
f. 對於所有其他字串,“obfuscate”會查詢原始宣告,並用對解密函式的呼叫替換它,並將加密的字串作為引數。
為了方便維護,原始行將作為註釋行保留在新行的上方。
ASCII 與 Unicode
系統區分 ASCII 和 Unicode 文字。為每種型別使用兩組單獨的函式。
以下語句
wcscpy(mystring, "my text");
或
wcscpy(mystring, _T("my text"));
將被識別為 Unicode 型別,並替換為對 __ODC__ 的呼叫,而類似的 ASCII 語句
strcpy(mystring, "my text");
將被識別為這樣,並替換為對 __ODA__ 的呼叫。
加密和編碼方案
1. 每個字串都使用一個臨時生成的加密金鑰單獨加密。2. 此金鑰是隨機生成的,而用於隨機數生成的 SEED 值在應用程式開始時設定。
3. 所有字串都用 NULL 字元填充以使其長度匹配 AES-256 加密方案所需的完整加密塊數量。
4. 結果是二進位制形式,並使用以下演算法表示為可列印字元集
- 每個位元組都分成兩半。先編碼高位半位元組,然後是第二半位元組。例如:0xAF 分成:0xA 和 0xF。
- 編碼值是前一個值和新值之間的差值(初始值為“A”)。
例如:值 0x3 是透過從字元“A”和“D”(0x40+0x3==0x43)移位而得到的。
5. 當移位值達到 0x7E 時,將從該值中減去初始值“A”。
例如:如果最後一個字元是'z'(程式碼 0x7A),編碼值為 0xF,那麼新值將編碼為字元'+'(程式碼 0x2B == 0x7A + 0xF - (0x7E-0x20))。示例
以下語句
wprintf(L"This is a test\n" );
將被替換為以下行
1
2/* wprintf( L"This is a test\n" );*/ wprintf( __ODC__("IPPXXXXXbmm|\"$%.=KXfgpx#-;DPZiw}$$*0=APR[\\epy##$.27EKXXdhq}#/00>DEOVVW]";
侷限性
不可能涵蓋 C/C++ 專案中字串出現的所有變體,儘管我已經盡力涵蓋了大多數。因為人們可以透過指定來初始化一維字元陣列
例如:用大括號括起來的逗號分隔的常量列表,每個常量都可以包含在一個字元中 字串常量(常量周圍的大括號是可選的)
static char a[]="some_string";
當陣列大小未設定時,無法加密預定義的內容,因為在編譯時不知道實際大小。
另一個此類系統無法加密的例子是所謂的“隱藏合併”
1
2
3#define PRODUCT_NAME "MyProduct" #define PRODUCT_FOLDER "MyFolder" #define WORK_PATH PRODUCT_NAME "/" PRODUCT_FOLDER
許可證
本文以及任何相關的原始碼和檔案,均根據 CDDL(Common Development and Distribution License) 獲得許可。
關於作者
Michael N. Haephrati 是一位企業家、發明家和音樂家。Haephrati 參與了許多專案,從 HarmonySoft 開始,設計了 Rashumon,這是 Amiga 計算機的第一個圖形多語種文字處理器。在 1995-1996 年期間,他在加州庫比蒂諾擔任 Apple 的合同工。在一家研究所工作,邁出了在以色列開發信用評分領域的第一步。他創立了 Target Scoring,並基於地理統計資料開發了一個名為 ThiS 的信用評分系統,參與了 VISA CAL、Isracard、Leumi 銀行和 Discount 銀行(Target Scoring,作為一家大型以色列機構的業務發展副總裁)。
2000 年,他創立了 Target Eye,並開發了第一個名為 Target Eye 的遠端 PC 監控系統。
其他專案包括:資料清理(作為 DataTune 系統的一部分,該系統已在許多組織中實施)。
關注 @haephrati
關注 Twitter、Google、LinkedIn
文章頂部
附件:[SourceCode.zip]