首页
论坛
专栏
课程

[商业保护] [原创]实时混淆在线编译器

2012-12-4 16:54 15792

[商业保护] [原创]实时混淆在线编译器

2012-12-4 16:54
15792
12月6日修改:接受大家意见,原来的DEMO是二进制代码,不能直接运行,调试起来比较麻烦,改成了用可执行文件来演示,见17楼

来由:

前些时候,接到一个比较特殊的需求,要求软件向服务器回传数据,数据是加密的,密匙实时从服务器获取,把数据加密后上传给服务器,密匙是一次一密,有效时间为30分钟,要求密匙和算法不能被用户取得,否则客户端那里可以伪造数据,然后用密匙加密后并上传给服务器.

首先密匙肯定会被获得,而加密算法也肯定会被分析出来,那怎么保证在密匙有效时间内不被利用呢?

考虑了把加密程序和密匙作为一段2进制代码形式的,让在需要加密时,客户向服务器请求到的不是密匙,而是含有密匙的加密代码段,客户下载到后,申请一块内存运行,这样可以避免被很简单的直接通过网络数据包得到密匙,但这段代码形式毕竟是固定的,如果dump下来分析,很容易定位到密匙,最好是每次代码都发生变化,这个时候,突然想到了刘涛涛大神数年前的<<扭曲变换加密>>(附录1),仔细再次阅读后,发现可以做一个不同的处理,基本可以符合这个要求...

于是...就有了现在的这个小玩具.

现在这个实时混淆在线编译器,目前仅支持纯的nasm语法源程序(不支持伪指令,宏只支持%define),仅支持x86指令集. 输出是2进制代码.

演示如下:

原始代码:
[bits 32]
%define Key 1234
%define dwFlag 12345678

mov edi,dwFlag
mov eax,dword [edi]
cmp eax,Key
jz @1
mov eax,0
jmp @2
@1:
mov eax,Key
@2:
mov dword [edi],eax


这段demo代码很简单,从一个内存地址,取出一个dword,和Key进行比较,如果不相符,把此内存地址置为0,相符则内存地址置为Key

用nasm编译后,长度为:28个字节,反编译或调试,有经验的大概只要10秒就看懂了它的逻辑并可以修改了.

现在我们把它放到我们的玩具上,会生成一个输出链接地址:

http://www.unest.org/outputs.php?pid=51e5aec538e3c232b2876a891b3b2c93&dwFlag=4199408&Key=3435973836&UN_maxsize=50000

GET参数:
dwFlag/Key 就是原代码中$define部分,可以在提交时实时指定,UN_maxsize则可以控制编译后的最大体积(注:最终生成体积一般在最大体积设置的90% - 98%范围内,这个demo系统已限制50KB为最大.也可以把UN_maxsize调的很小,比如50byte,,但又容易被理解了,因为体积小了,大部分混淆也就无法实现了.),

可以发现,每次下载时,编译成可执行代码前都进行了混淆处理,下载的2进制代码都大不相同,但实现的功能却是和原代码一样的,这样应用到我们开头的需求中后,我们就可以保证密匙在有效期内不被破解出来(200多byte原代码混淆成200KB),如果超过有效期被破解,也没用了,因为下次请求,新的密匙,又被全新的混淆代码所包围.一次一混淆,一次一编译.

现在我们的混淆/多态模板并不多,也很简单,在分析后可能可以被部分压缩,去扰,但已经留了donate接口,可以手动提交,添加和应用,随着模板的增加,最终生成程序的复杂度将呈指数型上升.

最后,如果您

对这个小玩具感兴趣
愿意帮助我们改进她
和其他使用者共享混淆/多态模板
发现BUG第一时间通知我们

欢迎PM我, 索取注册邀请码 (注:数量不多,送完即止)

如有建议或意见,加群闲聊: 24470013

附录1:
扭曲变换加密

作者:刘涛涛 me@liutaotao.com
网址:http://liutaotao.com/nqby.txt

一,一般来讲,加密就是加壳

我们经常考虑,一个可执行文件,怎么样加密才能安全呢?

一般用的手段,是加壳。加壳工具的工作原理,就是把可执行文件的代码与
数据都进行加密变换,作为数据存放。生成的目标文件入口代码是加壳软件
准备好的防跟踪代码。经过漫长的防跟踪代码后,会把原始可执行文件的代码
与数据段恢复,然后跳转到原来的入口处,继续运行。这样做的缺点是,不管
你的加密多强,防跟踪代码多牛,只要一运行,在内存中就全部恢复了。只要
把内存映象dump下来,反汇编一下,就清清楚楚了。甚至有工具可以直接把
dump下来的内存映象存为可执行文件。这样加密就彻底失败了。

简单加壳是不安全的,这大家都知道了。我们一般把上述简单的加壳方式叫“压缩壳”。
所以现在的加壳软件都在上述“压缩壳”的基础上,多做了一些工作,比如:

* 防止内存被 dump 。这实际上是不可能做到的。因为Windows操作系统就不是
一个安全系统,你怎么可能做到内存不被dump呢?曾有一个壳,我用了多种方法
dump都不成功。但最后还是找到了一个方法成功dump了。我这才惊叹dump原来有
这么多种方法,真是防不胜防。

* 修改文件入口代码。因为一般软件都是用常用的几种编译器编译生成的。如果
加壳软件知道你是用什么编译器编的(这很容易),把入口代码破坏掉,用另外一
段功能类似的代码替换它。这样dump下来的代码就比较难找到正确的入口,直接
被存为一个EXE的可能性就小多了。但还是会被反汇编的。

* 还有一些加壳软件,支持对一个或几个重点函数加密。甚至使用了虚拟机。但他们
都只能重点加密少数几个函数,不可能把所有函数都加密。而且对这个函数还有很多
要求。这可以想象。如果用汇编写一个函数,不加ret它可能连函数结束地址都找不到,
怎么可能加密呢

******

尽管加壳软件可以使用以上多种技术防止被跟踪,分析,还原,但我认为,它们仍然没
没摆脱“壳”的这个中心思想。以上的这些技术不过是在“壳”的大前提下所做的一些
小的插曲。它仍然是不安全的

二,扭曲编译的思想

做个比喻。加壳保护就好比是你桌上有宝贝,为了保护它,你在屋外围了一圈铁丝网。只
要有人突破了这道铁丝网,进入你的屋子,一眼就看到了桌上的宝贝。这当然不安全。

重点函数加密的思想,就好比是,我屋外围了一圈铁丝网,我还把宝贝放进了
保险箱里。如果有人突破了铁丝网,进入屋子,一眼就看到了保险箱。虽然保险箱不会被轻
易打开,但他如果把保险箱搬走,慢慢分析呢?这也不够安全。

最安全的,就是进了屋子,却什么也找不着。没有目标,这才是最让人头疼的。

现在的编译器,都是追求生成高效率的运行代码。这些代码的模式基本一成不变。有经验
的程序员看反汇编代码简单跟看源码一样,毫无秘密可言。如果我们有一个编译器,它的编译
目标不是为了高效,而是为了防止被读懂,那该多好啊!我有C++源码,我能看懂。一旦编译,
谁也别想通过反汇编看懂我想做什么,或者很难。遗憾的是,这样的编译器还没有。

如果我们自己编一个这样的编译器呢?不现实。工作量太大了。即使是找一个开源的C++编译器
来改工作量也不得了。

直接做一个会加密的编译器行不通。而一旦编译连接生成EXE后,就只能加壳了。难道就没有办法
了吗?我想出一个主意,就是加密编译的中间文件OBJ,输出ASM文件,用ML编译成OBJ,然后再link连接!

这个方法有几个好处:

* OBJ文件格式相对简单。不象处理C++源文件那么工作量大。
* OBJ文件中保留了很多源文件的信息,比如符号名,代码与数据,标号等等。方便加密。这些信
息很多在LINK的过程中被丢掉了。所以LINK为EXE后再处理就极不方便了。
* 这是一个全新的思想!对代码的加密已经不限于加壳,而是加密每一个函数,每一条指令。再也
没有一目了然的汇编了。
* 可以很容易设定加密的强度。可以根据需要,对一部分代码轻量级加密,而对另一部分代码重点
加密。
* 可以嵌套加密。重复使用几种加密变换,无限制地使代码膨胀
* 因为是加密OBJ文件,所以不管DLL还是EXE都可顺利加密,驱动程序也可以

基于这个思想,我们的加密软件就要出台了!我们暂时叫它扭曲变换器 1.0

三,扭曲变换器

有了思想,就开始动手编码。原以为OBJ文件格式是有文档的,工程进度应该很快。没想到其中还是
有很多内容需要考虑。每每说这是最后一个问题,解决了就没事了,却总是后延。前前后后居然写
了差不多半年时间。

主要遇到的技术问题:
* 汇编器ML会把所有的代码放到一个段中,这是不可以的。CL则通常是一个函数一个段。
* 汇编器ML不能生成 COMDAT 段。尽管文档中讲它支持COMMON,但加了这个关键字无效果。
* 汇编器ML不支持 WEAKEXTERN
* 汇编器ML只支持 defaultlib 这一个 drectve 关键字,其它 export, include 等关键字不支持.

总之,CL编译的OBJ其中有很多属性是ML无法生成的。微软的masm真的该升级了。

还有一些问题:
* 分不清代码与数据。数据段中肯定是数据,但代码段中却有可能不是代码,是数据。这时如果你试图
反汇编它,就会出错。
* ?????

不管怎样,这些问题都一一解决了(别问我怎么做的)。

采用的代码扭曲方法:

* 用 JMP 把代码打乱。这已经不是什么新鲜的招数了,但它依然有效。

* 用 JMP 把多个函数缠绕在一起。这样可以让分析者找不到函数从什么地方开始,到什么地方结束。

* 把 call 改掉。破解者对 call 是极敏感的,这举可以让他找不到一个 call。比如,我可以把
        call sub1
    改为:
        mov eax, offset sub1 + 3
        push offset @1
        sub eax, 3
        jmp eax
    @1:
* 把 ret 改掉。破解者对 ret 是极敏感的,这举可以让他找不到一个 ret。比如,我可以把ret写作
        push ecx
        mov ecx, [esp+4]
        add esp,8
        jmp ecx
* 改条件跳。条件跳也是极敏感的指令,比如我们可以把
        cmp reg1, reg2
        jge L_DST     
    L_NEXT:
写作:
        push eax
        mov eax, reg1
        sub eax, reg2
        shr eax, 1fh
        neg eax
        and eax, L2 - L1
        add eax, L1
        jmp eax
    L1:
        pop eax
        jmp L_DST
    L2:
        pop eax
    L_NEXT:
再看这个,你能看懂是什么意思吗
push offset @@L - offset L_3 + 23h
jmp L_1
L_2:
        jz L_3
        ret 4
L_3:      
        add dword ptr [esp+4], offset L_3 - 23h
        add esp,4
        ret

L_1:
call L_2
        ...
        这里出现了call和ret,但又不是一般所期望的那种。这里的call不代表你发现了一个函数调用。
这里的ret不代表是一个函数的结束。

* 使用堆栈代替寄存器。比如:
        MOV     EAX, DWORD PTR [ECX+0AD8h]
        PUSH    EAX
        MOV     ECX, DWORD PTR [EAX]
可以写作:
        PUSH    EAX
        PUSH    ECX
        MOV     EAX, DWORD PTR [ESP]
        ADD     EAX, 0AD8h
        MOV     EAX, DWORD PTR [EAX]
        MOV     DWORD PTR [ESP+04h], EAX
        PUSH    DWORD PTR [ESP+04h]
        MOV     EAX, DWORD PTR [ESP]
        MOV     DWORD PTR [ESP+08h], EAX
        MOV     EAX, DWORD PTR [ESP]
        MOV     EAX, DWORD PTR [EAX]
        MOV     DWORD PTR [ESP+04h], EAX
        MOV     EAX, DWORD PTR [ESP]
        MOV     ECX, DWORD PTR [ESP+04h]
        ADD     ESP, 08h
你能看懂吗?很明显,这个变换是不可逆变换。因为它本来使用了哪个寄存器,已经没有办法知道了。

* ……还可以想出很多扭曲变换的方法。化繁为简只有一种方法,化简为繁可以有无穷多种方法。

还有一些功能:
* 在C语言中,使用 #pragma code_seg(".code$curve_NoChange") 来指示后面的代码不做任何加密。因
为有时候有些代码含有大量的循环,加密它会严重影响效率。

* 在C语言中,使用 #pragma code_seg(".code$curve_Max") 来指示后面的代码重点加密。比如后面是
与注册算法相关。

现在的扭曲变换器我叫它1.0版,已经非常稳定了。我用它把VC6的库文件LIB都处理了一遍,再用LIB.exe工具
写回LIB文件中,我们就有了一套加密后的库。如果用这套库来LINK你的软件,分析者就很难从汇编中找到哪
个是printf哪个是strcpy,IDA也无法识别MFC静态链接的库函数了。能把VC6的库都加密一遍不出错,我
相信它已经很强壮很稳定了。

用它来加密我们做的一个共享软件,就再也没人写出注册机了。去读懂大量的经过以上变换的代码是不可
想象的。但还是有人暴破了,真佩服他。我会不断丰富加密方法,让暴破者都放弃。

现在的扭曲变换器还只支持VC6使用的COFF格式的OBJ,下一步,要分析一下VS2005的OBJ格式,尽快支持它。

我经常喜欢反汇编一下,分析点什么东西。我有不少朋友也经常在做反汇编或破解的工作。我不希望扭曲变换
器在网上一公布,被广泛使用。有一天我想分析点什么却无法下手。所以,这软件暂时还不提供下载,也不出售。
如果你有一段小程序想测试一下,可以把OBJ发给我,我加密一下给你。如果你有一个商业项目需要安全地加密,
也可以谈谈。

附上一个为CCG写的crackme,用扭曲变换器加密,带部分源码,供参考。
        http://liutaotao.com/CrackMe.zip

        LiuTaoTao        2006.7.7

[公告]安全服务和外包项目请将项目需求发到看雪企服平台:https://qifu.kanxue.com

最新回复 (22)
hrpirip 1 2012-12-4 17:20
2
0
一个字,“
两个字,“真牛
三个字,“真是牛!
mark,收藏。
viphack 4 2012-12-4 17:50
3
0
谢谢 啊 mark
cooolie 2012-12-4 18:06
4
0
support~
cntrump 13 2012-12-4 19:36
5
0
调试起来是个问题。
blueapplez 14 2012-12-4 19:43
6
0
200byte ->200kb
只要搞清楚Client对这200kb的处理方式,还有必要去逆向这200kb的数据吗?
boywhp 12 2012-12-4 20:29
7
0
我感觉不太靠谱,楼主应该画一个详细的流程图,大家才好帮你分析啊,你一上来就搞一堆汇编
对大伙思路是一个考验啊,至少我看得云里雾里,不知道你具体是怎么搞的
Osris 2 2012-12-5 08:32
8
0
这个输入的是nasm源代码,输出混淆后的二进制代码,开发时仍然本地进行,开发完成后,把需要混淆的部分代码上传到服务器,给最终用户前从服务器得到实时混淆的代码,插入到主要程序相应的位置,完成交付即可.
Osris 2 2012-12-5 08:34
9
0
有必要的,前面我举的例子没有表述清楚,因为用户的项目具体内容不便透露,我另外想了一个应用场景,请指正.

用网格计算进行密码破解的场景,客户机有空闲时间时向服务器申请,服务器从字典文件中抽若干个发给客户机,客户机进行
运算,如果运算符合了,直接把结果告诉服务器,运算完所有,也告诉服务器,服务器按客户机运算的次数给客户积分.

一开始,是直接明码通讯的,很快,管理员发现有邪恶的人申请后不进行运算,伪造运算完所有的结果发给服务器,骗得积分.
于是管理员改进了,在通讯时包含一个密匙,结果用密匙加密后再发回给服务器.

然后,发现有更邪恶的人直接破解了程序并加了PATCH,程序收到任务和密匙后,不执行,直接用密匙加密伪造的结果后发回服务器.

再然后,管理员把运算计分部分和对结果进行加密部分从主程序提取出来,在客户申请的时候连代码+数据发过去,写入内存后执行,
...时间不长,实时patch出来了...

现在我们把运算计数部分和对结果进行加密部分在发给用户前,用我们的小玩具混淆起来,从200byte -> 200KB(或更大,越大越能保证不在密匙有效期内被破解.)

我们知道要伪造有2个条件,1个是获得密匙,2是知道在用密匙加密前,计分结果的存放的地方(某内存地址或某寄存器).

而要获得这2个条件,都必须分析被混淆后的200kb的代码,

因为是一次一乱的.即使手上有完整的混淆前的代码,也必须分析这混淆后的代码才能实现伪造.

通过这个小玩具想做到的是一次一混淆,程序员开发一次,而用户得到的完整副本或副本的关键位置代码却是各式各样
Osris 2 2012-12-5 08:36
10
0
这个小玩具其实就想做一件事,把代码变得更大,更乱,混淆后连他妈(开发者)都认不出他.当然,代码的原功能是不变的.
思路其实刘涛涛大神的 扭曲变形 一文 (1楼附录1) 已经表述清楚了,区别是1:我们目前只支持纯的nasm代码,还不支持obj们,
2:我们的混淆是在线实时进行的,一次一编译,一次一混淆,也符合现在流行的云吧 :),不再是开发者混淆好了发拷贝给用户,而是可以让用户每次取得都不同.

1楼的DEMO 中的链接可以把参数dwFlag值改成一个有效可读写的内存地址(堆栈之类),
然后下载的2进制码可以直接在调试器里开跑,会发现它实现的就是DEMO中原始代码的功能,当目标内存地址和Key相同时,不变,否则目标内存地址就被置0
elianmeng 1 2012-12-5 08:44
11
0
得了 你还是尽量简单点吧 从服务器传个key就可以了 升级太麻烦了 感觉不实用
Fido 2012-12-6 07:52
12
0
特殊需求,特殊对待...

学习了..

有时间膜拜一下你们的神器..看看到底有多屌
boywhp 12 2012-12-6 08:38
13
0
结合你假设的场景我又看了一遍,貌似是可行的,关键是那个key一定要足够混淆才行,而且地址应该确保能够随机,否则:
1、我先加载你的混淆后的代码到内存一个基地址Base
2、我在你发送数据前调用的某个API函数,你总要用socket发送数据吧?
3、执行混淆的代码,断下来后,你的key是否内存位置是随机的?如果不是就可以获取到你的key
4、如果你要调用外部实现的加密算法的话,那key基本就等于明白的告诉破解者了

另外我认为刘滔滔的这种类型的加密其实理论上是可以还原的:
假设你一共有M1、M2、M3、Mn个变形模版,输入为S_in, 输出为S_out = M3(M2(S))...【其中的模版序列相当与密钥】
对于模版类型的加密会留下一个很明显的特征:就是最后一次的模版必然会暴露,还有就是单一模版一定存在一个对应的解谜模版
例如M1一定存在一个解密的M1'模版。
如果破解者能够通过大量的样本统计【或者开发人员泄密】获取到所有的M1,M2...Mn变形模版
1、对S_out依次检查M1-Mn,如果有模版匹配,对其执行对应的解密模版
2、重复1,直到没有模版匹配
海风月影 17 2012-12-6 11:17
14
0
分布式计算,要有一部分冗余计算的
客户端算没算对,是可以校验的,哪有客户端发上来的结果,不经过校验就接受的?
校验得适当,客户端不加密,也无法作弊的

你这个应用场景的架构本身就有问题,被作弊是必然的
Osris 2 2012-12-6 14:17
15
0
首先,感谢您看完我的大段文字.

key是被放在代码里,作为代码被多态混淆

如最简单的XOR加密
xor [123],key
会被多态为:
xor [123],key ^ rand
xor [123],rand
这样即使知道key(实际是不可能知道的),也不能直接搜索代码部分定位到.

至于解密部分,理论上是可以的,特别在模板比较少的情况下,但我们这个多态混淆模板用户可以自己添加并互相共享,每添加一个,复杂度都会上一个指数级,在庞大后不但不可能被恢复成原来的代码.想精简到原代码的程度都会比较困难.

这个工具的目的不是保护程序无法被破解,而是要一次一混淆,你破解了一个copy,OK ,但也就代表这个copy被破而已.
Osris 2 2012-12-6 14:22
16
0
您指的冗余计算是指比如把相同要求发给不同人,然后比较是否结果相同来判断是否有人作弊吗?

但冗余计算无法检测。。他们实际做没做这个事情,比如分布式计算某密码,每人发了100个字典,都不匹配,结果一样,但有人就根本没有计算,直接返回,那就用冗余计算无法识别了,如果只有匹配了才算积分,那冗余计算是可以的,但如果要按客户机消耗运算能力来计积分,冗余计算就无能为力了。
Osris 2 2012-12-6 14:23
17
0
接受大家意见,原来的DEMO是二进制代码,不能直接运行,调试起来比较麻烦,改成了用可执行文件,方便大家加载调试,查看效果

多态混淆前原代码和一楼相同:
[bits 32]
%define Key 1234
%define dwFlag 12345678

mov edi,dwFlag
mov eax,dword [edi]
cmp eax,Key
jz @1
mov eax,0
jmp @2
@1:
mov eax,Key
@2:
mov dword [edi],eax


加载后如下图:


为方便测试,dwFlag的位置固定为程序入口处前4个字节,Key固定为0cccccccch
运行效果:
当把dwFlag位置修改为非0cccccccch后,运行后会被置0,不改变dwFlag位置(原为0cccccccch),运行后不变。
这个效果和上面的原代码一样。

但重点是:你会发现每次下载的代码都是被实时混淆多态的,每次下载都不一样哦!虽然它们做的工作是一样的。

DEMO下载地址:
http://unest.org/demo/demo.php
上传的附件:
lingyu 1 2012-12-6 14:48
18
0
设要求云用户查询数A是否为1-100000区间内MD5的值。
选择一个4字节随机数设为R = 12345678,随机选择一个要加载的bit,设为第0位。
根据R生成发往用户端的数据,
M[0]为A;
从R的第1至31位:
若此位为1,则随机选择一个1-100000区间内MD5的值(设为A1);
若此位为0,则随机选择一个随机数(设为R1)。
发给用户查询的数据应该为M[32]={A,R1,A1...Rn..An}

用户端计算MD5(1-1000000)是否为M[i],若相等则对应第i位标记为1。
最后将计算的结果发往服务器。

若A是1-100000区间内MD5的值用户将返回12345679(R + 1),否则返回12345678,返回其他数据则是伪造的。
用代码描述应该是很简单的,这个过程可以证明用户若不能破解MD5,则必须计算MD5(1-1000000),否则会被服务器发现。
密码字典等其他算法也是类似的,只需要加入一些已经在字典中的hash看用户是否能正确识别出来就行了。
达文西 2012-12-6 15:18
19
0
我只是来占楼的
海风月影 17 2012-12-6 18:10
20
0
这个还是你算法设计的不好,18楼的密码学版主已经解答过了。

不要把Compare过程放在客户端,要把Compute过程放在客户端

我再通俗地给你把算法改造一下

计算某密码,假设计算过程 FN(D, K) = M

你的需求可能是,下发D,下发K1 - K100,然后算出相应的M1 - M100,

把M1 - M100传到服务器上来,判断K1- K100计算结果是否满足要求

本地只有计算过程,没有比较过程,这样客户端算错了,很容易处理
boywhp 12 2012-12-6 19:59
21
0
[QUOTE=lingyu;1123680]设要求云用户查询数A是否为1-100000区间内MD5的值。
选择一个4字节随机数设为R = 12345678,随机选择一个要加载的bit,设为第0位。
根据R生成发往用户端的数据,
M[0]为A;
从R的第1至31位:
若此位为1,则随机选择一个1-100000区间内MD5的值(设为A1...[/QUOTE]

来一个通俗易懂的:
发送给用户的数据中有部分数据是已知的,用来校验客户端是否恶意计算结果
服务端生成一个随机【客户端不可知,服务端知道】序列:
校验数据1、计算1、计算2、校验数据2。。。
然后客户端就傻了,因为不知道那个数据是校验数据,如果校验算错,结果将不可信,只能全部计算一遍,
服务端只需要测试对应序列所有的随机位是否是1,很容易就知道是否伪造数据了
Osris 2 2012-12-6 20:58
22
0
感谢lingyu,海风月影,boywhp三位,明白了冗余计算的原理,确实是非常巧妙的设计,但它也有自身的限制,它需要服务器能预先知道结果,才能用来作为判断客户端是否说谎的标准,遇到服务器无法预知结果的情况就无法适用了。不过确实,在我假设的这个场景中,冗余计算比代码混淆来得合理的多。
nnhy 3 2012-12-10 22:02
23
0
可以研究一下SSL,非对称加密就是解决这个问题
游客
登录 | 注册 方可回帖
返回