您的位置:首页 > 编程语言 > C语言/C++

c/c++字符串混淆方案总结

2015-04-29 19:14 190 查看
有过破解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形式,以长度表示其结尾
这种方法的有点是比较简单,但是增加开发人员的工作比较多
方案二:字符串以数组形式初始化
比如以下写法:
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);
如果使用strings查找字符串,可以发现"a teststring!"可以找到,而'atest string2!'则无法找到。
查看其汇编代码,会发现这两种写法的对于字符串处理生成的代码完全不一样。
第一种写法,我们比较熟悉,直接从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
http://www.codeproject.com/Articles/2724/Literal-string-encryption-as-part-of-the-build-pro
其自定义的文件叫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
https://code.google.com/p/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
http://www.codeproject.com/Articles/502283/Strings-Obfuscation-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)
)
{
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)
)
{
return (OFFSET ^ str[0]) * PRIME;
}
};

template <unsigned int N>
inline static unsigned int Calculate(const char(&str)
)
{
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,"!@##$%^&*("); // 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中搜索字符串。他的优点在于不用对代码做任何改动,并且不会引入任何运行时的额外性能开销。加壳的缺点在于:加壳过的文件比较容易认为是病毒。如果只是为了字符串加密而引入加壳则有点小题大做,脱壳之后或者程序运行之后字符串全部可见。
参考文献
混淆字符串
/article/2564727.html
如何防止客户端被破解
http://tanqisen.github.io/blog/2014/06/06/how-to-prevent-app-crack/
Literalstring encryption as part of the build process
http://www.codeproject.com/Articles/2724/Literal-string-encryption-as-part-of-the-build-pro
StringsObfuscation System
http://www.codeproject.com/Articles/502283/Strings-Obfuscation-System
strenc
https://code.google.com/p/strenc/
PE文件中隐藏明文字符串
http://a.vifix.us/blog/pe%E6%96%87%E4%BB%B6%E4%B8%AD%E9%9A%90%E8%97%8F%E6%98%8E%E6%96%87%E5%AD%97%E7%AC%A6%E4%B8%B2
pe文件中隐藏明文字符串(续)
http://a.vifix.us/blog/pe%E6%96%87%E4%BB%B6%E4%B8%AD%E9%9A%90%E8%97%8F%E6%98%8E%E6%96%87%E5%AD%97%E7%AC%A6%E4%B8%B2%EF%BC%88%E7%BB%AD%EF%BC%89
In-Depth:Quasi Compile-Time String Hashing
http://www.gamasutra.com/view/news/127915/InDepth_Quasi_CompileTime_String_Hashing.php
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: