如何防范算法求逆

假如您不幸遇到对Win32应用环境有足够了解的对手,以至于您的软件最终还是被凶悍的调试器任意蹂躏。但是您还远没有被打败,如果反调试技术(Anti-Debug)作为软件保护的第一道防线已经失守,您的对手只不过是掌握了一大堆汇编代码而已,毕竟代码和算法之间还是有相当距离的,所以您还有第二道防线可守——抗分析。在这道防线里,您有很多办法可以限制破解者掌握您的加密算法,从而阻止注册机或者破解补丁的出现。

 

一、前言

软件保护的目的是只向合法用户提供完整的功能,所以软件保护必然要包括验证用户合法性的环节,而这一环节通常采用注册码验证的方式实现。

(1)用户向软件作者提交用户码U,申请注册。

(2)软件作者计算出注册码R = f(U),回复给合法用户。

(3)用户在软件注册界面输入U和R。

(4)软件验证F(U,R)的值是否合法来判定用户的合法性。

其中一些常用术语说明如下:

(1) 用户码U:用于区别用户身份。它可能仅仅是用户自定义的一个用户名,这种形式的用户码与用户身份弱相关,假如一对合法的用户码、注册码被公开,则任何人都可以用来对软件进行注册;它也可能是用户机器的硬件特征码,这种形式的用户码与用户身份强相关,可以有效防止一次注册,多人享用的局面出现,但对于合法用户而言很不方便,一旦更换或升级机器,就必须重新申清注册码。

(2) 注册码R:用于验证用户身份。它可能与用户码具有惟一对应关系,也可能同一个用户码有若干个注册码相对应,也可能同一个注册码有若干个用户码相对应。如果用户码和注册码的取值空间非常大,即使用户码与注册码不是惟一对应关系,非法用户“碰巧”得到一对合法的用户码、注册码的概率也是微乎其微。

(3) 注册机:我们把R = f(U) 中的小f称为注册机,掌握了注册机就有能力针对任何用户码计算出相应的注册码。

(4) 验证函数:我们把F(U,R) 中的大F称为验证函数,软件使用验证函数验证注册码的合法性,即,当且仅当R = f(U) 成立时,F(U,R) 取合法值。

(5) 算法求逆:我们把破解者通过验证函数F推导注册机f的过程称为算法求逆,所以验证函数F的构造非常关键。

在软件注册保护的“初级阶段”,验证函数与注册机没有本质区别,即:F(U,R) = f(U)-R。这样做很危险,验证函数自身就包含注册机,破解者只需要跟踪软件的运行,直接将软件验证函数中计算f(U) 的汇编代码拷贝下来就可以当注册机用,甚至根本不需要了解f的算法过程。

改进的做法是先求出f的反函数f-1,使:U= f-1(R),然后令F(U,R) = f-1(R)-U。这样做安全了许多,软件本身不包含注册机f,破解者必须在充分了解f-1算法过程的基础上才能分析推导出注册机f。可能会有读者感到疑惑:假如破解者先指定R,然后直接利用验证函数计算出U = f-1(R),不就可以使用U、R来注册了吗,何必要一定要推导f呢?在实际应用中,由于U、R通常以字符串的形式给出,而验证函数通常采用数值运算,所以一般会将U、R转换成数值形式U’、R’,则注册机f及注册机的反函数f-1实际上都是复合函数,验证函数由于只需要检验U’、R’的合法性,可以不完全等于f-1

假设:

U’ = f1(U),R’ = f2(U’),R = f3(R’)

则:

f

R = f(U) = f3(f2(f1(U)))

f-1

U = f-1 (R) = f1-1(f2-1(f3-1(R)))

F

F(U,R) = f2-1(f3-1(R))-f1(U)

看起来破解者通过F(U,R) = f2-1(f3-1(R))-f1(U) 推导f-1比推导f要容易,关键在于f1通常是建立在ASCII编码表之上的一个变换,所以即使推导出f-1也没有多大价值:

(1) 在256个ASCII字符中只有95个可以使用键盘输入,假如U在8位以上,则随机选择R,利用f-1 (R) 计算出“可用”的U的概率只有区区0.00037而已。

(2) U可能还被规定为一种特定的格式,例如电子邮件地址格式,则直接利用f-1进行计算几乎等于穷举。

(3) 用户不一定能够自由选择U,例如U为机器特征码,在这种情况下, f-1则完全没有直接利用价值。

当然,既然破解者在将U’转换成U时会遇上“不可显示,无法输入”字符的麻烦,所以软件作者在注册机中将R’转换成R时会遇上同样的麻烦。通常可以直接将R用数字表示,另一个常用的办法是“进制查表法”,例如将19760510转换成36进制后会变成一个五位数:11 27 19 11 2,如果用0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ表示36进制下的数字,则19760510就被转化成为“BRJB2”。

以上是软件注册算法的一些基本概念,而要防范破解者对注册算法求逆,还需要专门的手段:

1. 巧妙构造f、f-1,使得通过f-1求f在计算上不可行。

2. 巧妙构造F,使F与f、f-1都不直接相关。

下面我们将一起来研究如何运用“堡垒战术”、“游击战术”和“陷阱战术”三大战术来实现以上手段。

 

二、堡垒战术

事实上,在通信领域人们很早就开始了身份验证的研究,并发展出了散列加密和非对称加密等优秀的密码学算法,其中的MD5算法和RSA算法非常适合在软件注册算法中运用。

MD5散列算法的原理是:

(1) 定义四个32位常量:A=0x01234567,B=0x89abcdef,C=0xfedcba98,D=0x76543210

(2) 将明文文本按一定的拆分和填充规则变成32位分组信息,设组数为n

(3) 将A、B、C、D与各组分组信息进行n轮非线性变换,最终得到A’、B’、C’、D’。

(4) 合并A’、B’、C’、D’为一个128位的消息。

 

MD5算法的特点是:

(1) 任意长度的消息最终都可变换成一个被称为“消息摘要”的固定长度(128位)的散列值。

(2) 相同的消息其消息摘要固定不变,不同的消息造成相同消息摘要的情况称为“冲突”,这种情况肯定存在,但至今还没有实例发现。

(3) 由消息摘要无法逆推消息。

MD5算法通常并不被直接用来对消息进行加密,因为不存在逆算法,所以加密之后无法解密,这样的加密是没有应用价值的,MD5算法的用途在于数字签名。例如甲和乙进行通信,甲仅仅将明文A加密为B传送给乙是不够的,因为即使密文B的加密强度再高也只能防止在传输的过程中被解密而泄漏内容,却不能防止破坏者直接篡改密文B。乙收到B后解密得到A,也无法确定A所包含的内容的真实性。所以甲在发送B的同时通常会在A之后署名然后计算C = MD5(A),将B、C一起发送给乙。乙收到B、C后,首先解密B得到A,然后同样计算C = MD5(A),若计算出的C与收到的C相同,则可确定A真实可靠。

同样MD5算法也不适合直接被用来做注册机,假如使用R = MD5(U) 做注册机,由于MD5不存在反函数,验证函数将不得不包含注册机。但是我们用以下办法使用MD5算法:

(1) 设注册机f为:R= f(U)。

(2) 设MD5(a) = b。

(3) 令验证函数F为:F(U,R)= MD5 [ f-1(R)-U + a ]

显然F的合法值应该为b,只要U、R满足R=f(U),F一定等于b。由于MD5不可逆,破解者无法通过b获知f-1(R)-U+ a应该等于a,也就无法获得f-1的准确表达式,更无法获得注册机f。至于f-1(R)-U+ a表达示中虽然含有a,但破解者无法判断a和f-1的关系。例如:U = f-1(R) = R * 5 + 19,a= 7,则:F(U,R) = MD5(R*5+26-U),a和f-1融为一体,叫破解者如何分得清?

实际上利用MD5算法还有很多方法可以构造F,让破解者根本看不出F和f、f-1的关系,就更谈不上求逆了。这里只是举了一个极端简单的例子而已,相信读者完全能够进行精彩的发挥。MD5算法使用非常广泛,也几乎没有什么算法实现上的漏洞,各位可以通过google搜索到大量的实现MD5算法的函数库,按照文档说明直接调用。

当然,MD5算法也有它的缺陷,正因为MD5完全不可逆,所以a必须为常数,一旦破解组织获得了一对合法的U、R,他们就可以跟踪到合法的a、b值,从而获得f-1,并进一步推导出注册机f。当然,前提是他们必须先设法获得一对合法的U、R。

RSA非对称算法的原理是(其证明过程使用数论学中的费马定理和欧拉推论,有兴趣的朋友请自行查阅,这里从略):

(1) 选择两个互不相同的素数p、q,令n=pq,m=(p-1)(q-1)

(2) 选择素数e<n

(3) 计算d满足edmod m = 1

(4) 对于任意A<n,若B=Aemod n,则A=Bd mod n

可见在机密通信开始之前,收信方可随机生成n、e和d,然后将n和e作为公钥发送给发信方;发信方将机密信息A加密为密文B=Aemod n;收信方收到密文B后用私钥d进行解密得到明文A=Bd mod n,当然机密信息A必须<n,否则可将A拆分为<n的片断Ai再进行加密发送。

如果通信过程被第三者监听,由于d不在通信过程中出现,监听者最多可获取n、e和密文B,在不知道d的情况下无法获取明文A=Bd mod n,如果监听者希望通过n、e推导d,由d的生成过程可知,他需要对n进行质因数分解获得p、q,众所周知,计算n=pq很容易,但计算pq=n则困难得多。假如n足够大,例如1024bit,则对n进行质因数分解的代价按照现有的计算水平测算,需要动用上千万美元的计算机系统耗时一年的时间!

RSA算法很容易在软件保护中进行应用:

(1) 软件作者使用:R = Ud mod n作为注册机

(2) 软件使用:U = Re mod n作为验证函数

破解者即使跟踪软件运行的全过程,也不到d,无法写出注册机,当然,RSA算法并不容易编程实现,它具有几个难点:

(1) 如何处理大数运算,目前主流RSA 算法都建立在1024位的大数运算之上。而大多数的编译器只能支持到64位的整数运算,这远远达不到RSA 的需要。

(2) 如何快速进行模幂运算B = Ae mod N,这里面包含大量的数学技巧,其中蒙哥马利算法甚至可以让这一运算过程根本不出现最耗时的除法运算。

(3) 如何快速求解同余方程 XY mod M = 1。

(4) 如何快速获取指定长度的随机素数。

事实上,这些问题并不是各自独立的,它们互有关联,环环相套。假如各位有兴趣自己动手来实现RSA算法,相信随着这些问题逐一迎刃而解,您也会逐渐意识到RSA算法真的是一种“优美”的算法。

有很多开放源码函数库实现了RSA算法,如crypto++、miracl、freelip、rsaref等,软件作者可以利用这些函数库对自己的软件进行RSA保护。这些函数库俺都有过研究,代码效率的确是很不错的,运行速度很快,但感觉要么使用的数据结构过于复杂,要么编码风格杂乱无章,俺的水平和耐心都实在有限,以至于无法读懂这些东西。由于RSA算法原理比较复杂,限于篇幅,这里不能加以全面介绍,有兴趣的朋友可以到看雪论坛(www.pediy.com)查阅在下拙文《RSA与大数运算》,包含了详细的原理介绍和简单易懂的MFC源代码。

基本上进行RSA算法保护的软件可以说是建立了一座坚不可摧的堡垒,但是我们都知道马其顿防线的故事,由于RSA算法众所周知,再加上软件作者通常会采用公共函数库来实现RSA算法,所以使用RSA算法也存在一些风险:

1、RSA算法本身虽然足够坚固,但使用者往往全盘采用第三方公用代码。这些代码可能含有漏洞,而全世界有大量的资深破解者在研究这些代码的漏洞,一旦发现漏洞,软件的安全性就可能成为陪葬品,因为每一种公开的固定代码都必然包含一些特征串,通过搜索特征串可以轻松获知哪些软件使用了含有缺陷的代码。

 

2、RSA算法的使用者往往并不了解算法细节,可能因错误使用RSA而在不知不觉之中遭遇非常规手段的攻击,例如在不同的软件作品中使用不同的e、d但使用相同的n而遭到“公共模组攻击”,或在电子邮件等通信加密领域使用e、d、n而遭到“选择密文攻击”等等。

3、另外RSA算法原理看起来加解密过程可以互换,但在通信领域由于密文的解密过程并不暴露给窃听者,所以其加解密过程虽然同为模幂运算,但实际实现过程往往并不一致,在解密端通常会采用“中国剩余定理”进行加速,而中国剩余定理中包含对原始数据p、q的引用。这一类函数库在软件保护中使用时要非常小心,一旦软件作者选择了错误的RSA库函数,在验证函数中使用了中国剩余定理,则会导致RSA防线如同虚设。

4、某些函数库在生成随机素数时,采用“伪随机数产生器”,即在完全相同的初始条件下会产生完全相同的“随机数”序列。破解者如果得到该函数库,就可以根据n值推断p、q生成过程,从而攻破RSA防线。

5、RSA算法中存在若干由某些特殊素数构造而成的“弱密钥”,某些函数库在生成随机素数时,没有淘汰这些特殊素数,导致RSA防线在数论高手面前虚弱无力。

虽然对软件实施RSA保护存在一定的风险,但是毕竟RSA算法非常复杂,可以乐观地估计, RSA算法足以让90%以上的破解者面对晦涩的汇编代码好一阵子找不到北。

这一节基本上都借用了通信领域的密码学知识,事实上密码学知识在软件保护上大有用武之地,各位如果潜心加以研究,一定还会发现“堡垒”其实还有很多种建造方法。

 

三、游击战术

游击战第一宗旨:化整为零。对付强大的对手非常有效。而软件保护中的游击战术就是将验证函数F“肢解”成多个互不相同的Fi,然后将这些Fi尽可能藏到隐蔽的程序角落。

通过任意一个Fi的验证都只是注册码合法的必要条件,而非充分条件,真正合法的注册码能够通过所有的Fi的验证。破解者找到Fi其中的任一个或任意几个,只要不能将所有的Fi一网打尽,他就无法一睹F的全貌,无法进行算法求逆,无法得到注册机。

当然,将F分解成一系列必要非充分的Fi也不是一件容易的事情,需要较专业的数学知识,但是我们至少可以使用分段函数来简单地实现这一目标:

(1) 将R切分成多段Ri

(2) 构造不同的f算法使得:Ri = fi(U)

(3) 令Fi=fi的反函数fi-1

这样做虽然有点麻烦,但绝对是值得的。例如我们可以让F1使用MD5算法,F2使用RSA算法,F3使用自定义不知名算法,在用户输入注册码后仅仅使用F1进行验证,并将注册码以密文形式写入自定义格式的数据文件,然后如果验证通过(不妨假定破解者总是有办法让它通过的)就恭喜注册成功。另外两个验证函数藏起来,只有使用者执行特定的操作时才被调用,例如在用户进行存档操作或使用某些高级功能的时候将注册码读出来再次验证,俺甚至碰到过一个软件在软件被关闭时响应窗口Destory消息来调用一个验证函数。一旦任何一个验证函数发现注册码非法,就清除注册码并将软件恢复为未注册状态,甚至可以更极端地选择“自杀”。

游击战第二宗旨:虚虚实实。对于破解者来说,遇到游击战术会非常被动,除非他找到的验证函数已经能够将U、R形成一对一的对应关系,否则永远不能确定软件中是否还埋藏着其他的验证函数,而事实上软件作者根本没有必要让U、R形成一对一的对应关系,验证函数个数的不确定性的确很容易让试图制作注册机的破解者懊恼不已。

假如运用一点简单的线性代数的知识,我们可以将Ri的其中几个(注意只是其中几个,而不是全部)和Fi关联起来:

设:Ra = 3U,Rb= 5U,Rc = 7U,则:

 

Fa = 7Ra + 11Rb + 5Rc – 111U

Fb = 11Ra + 7Rb + 3Rc – 89U

Fc = 5Ra + 3Rb + 11Rc – 107U

这样破解者找到Fa、Fb、Fc中的任意一个,甚至无法求出R哪怕是小小的一段,一个更好的主意是让参与到线性方程组中的Ri的个数稍稍大于使用线性方程的验证函数的个数,软件作者手里持有线性方程的某一组特定解作为注册机,而破解者则无法了解验证函数到底有几个,就像鬼子永远搞不清楚八路军到底有多少,以至于最后歇斯底里地见人就认为是“八路的干活”。

如果将一对U、R作为纵横坐标,看作平面上的一点,将注册机f看作由合法U、R连成的一段平面曲线,我们还可以构造多个空间曲面方程作为验证函数F,条件是f落在这些空间曲面之上,如果稍稍了解空间解析几何的知识,相信各位可以构造出无数个曲面方程作为验证函数,甚至还可以考虑使用参数方程,这样即使破解者获得了所有的Fi,也要有精深的解几功力才能求出f。

我们必须反复强调数学知识的重要性,不管是数论、代数、线性代数、几何、解析几何,还是微积分(俺个人认为利用傅立叶变换作验证函数将会非常有趣)、概率论,都可以拿来作为软件保护的武器。请相信,如果你具备了3成的数学功力,就足以将6成功力以下的对手折磨致死,因为你和他掌握着不对称的信息量。

游击战第三宗旨:战略转移。游击队也经常会被鬼子扫荡,因为游击队的行踪经常会被汉奸告密。我们的游击战术的致命弱点在于,每一个验证函数都必须访问注册码,而注册码的源头只有一个:用户从注册界面输入的那一个。破解者会跟踪程序从注册界面读入注册码的过程,并监控存放注册码的内存地址,一旦验证函数访问这一地址就会泄漏行踪,这样注册码实际上成为了破解者寻找验证函数的一把钥匙,理论上他只要牢牢地抓着这把钥匙不放,就一定会找到所有的验证函数。应对的办法就是大规模的转移,游击战嘛,就是要能“跑”,将鬼子拖死、累死,软件必须不停的将注册码“搬家”,搬家的方法要多样化:

(1) 内存拷贝,这种常规做法容易被破解者的bpm内存监控断点识破。

(2) 写入注册表或文件,然后在另一处代码中再读入到另一个内存地址,这种办法会被破解者的注册表、文件监视工具识破。

(3) 一次将注册码拷贝到多个地址,让破解者无法确定哪一个地址是注册码的新家,当然如果敌人坚忍不拔,个个都追,至少也可以消耗敌人大量的精力。

(4) 在反复使用同一个函数搬家后突然使用另一个前半部分代码相同而后半部分不同的函数进行搬家。这种方法很容易让疲惫的敌人一不小心就将注册码跟丢。

(5) 将注册码肢解后一部分一部分地“偷运”到不同的地址然后再组装,这种办法很容易让敌人疏于防备。

(6) 将以上方法反复使用,如果仅仅靠copy&paste就可以将对手逼成神经病,何乐而不为呢?

事实上主动权永远掌握在你的手里,你还可以自创多如牛毛的搬家大法来对付可怜的破解者。可能您会一不小心遇上一个精力过人孜孜不倦神勇无比的对手最终将您的游击队一一肃清,但是请相信,只要您认真贯彻了游击战术的三大宗旨,灵活结合使用,一定会让90%以上的破解者像无头苍蝇一样乱转一气之后恼羞成怒关机投降。当然您可能会受到很多口头“关照”,如“变态”、“#$%$*^”等等,但是这只能说明您的对手缺乏风度,并不会对您造成任何实质性的影响。

 

四、陷阱战术

所谓“陷阱”,是要让破解者误入歧途,陷入困惑之中无法自拔,我们不应该一味试图编制让破解者读不懂的代码,那不是一件容易的事情,而且抱着这样的目标很容易犯自以为是、低估对手的错误。我们的任务最好是引诱对手犯自以为是的错误,让他在并没有完全弄懂验证过程时却自以为懂了。当然,将要介绍的“陷阱”,只是俺个人的一些研究思路,或许只能博高手一笑,但希望至少能够对各位有所启发,起到抛砖引玉的作用。

第一个陷阱俺称之为“随机陷阱”,原理是准备多个验证函数,程序每次运行时仅仅随机调用其中的某一个。其中随机数的产生可以放在程序头,由于应用程序通常会有大量的数据初始化工作,再加上随机数产生函数代码本身也比较晦涩难懂,夹杂在其中生成一个随机数应该是很隐蔽的。这样破解者在跟踪程序之后通常会自以为程序只有一个验证函数,于是放心地发布了注册机,而其他人使用该注册机进行注册时,由于程序生成了不同的随机数,调用了不同的验证函数,注册自然失败。

不幸的是您无法判断您的对手的功力和习惯,有些功力一般的菜鸟(像俺这样的)或者是某些有特殊习惯的高手往往会将您的程序跟踪好几遍,甚至几十遍,您的随机陷阱就会被发现。所以俺强烈建议您不妨将其中一个验证函数被随机调用的概率控制在1%左右。这样做的坏处是会出现一些并不完整的注册机,却能够让您的软件在注册状态下运行好多次。好处是根据心理学家的研究,人类总是对自己关系越深却又无法控制的东西最感兴趣,一旦某人千方百计找来一个不完整的注册机注册了您的软件,他会认为“哈哈,现在这东西是俺的了!”那种感觉当然会很好,他和您的软件的关系在心理上的亲密程度一般绝不会比他掏腰包买来的软件差,而一旦哪天他突然发现您的软件识破了他的非法身份,他会很失落,那感觉肯定会很不好,极有可能他会忍不住将“自己的东西”拿回来的冲动,哪怕是掏腰包。这样的话,恭喜您获得了一个分外的注册用户。您还会获得附带的收获:您的对手,破解者的声誉受到了打击,事实上众多的破解高手并不靠破解生活,他们在乎的其实就是所谓的声誉,您的这一次打击很可能会让他灰心丧气,失去了继续破解的兴趣,这样的话,恭喜您除掉了一个难缠的敌人。

第二个陷阱俺称之为“整数陷阱”,由于通常验证函数都将用户码与注册码转换成整数进行运算,我们可以利用破解者的这一经验作一些手脚。例如我们将用户码拆分成两个整数U1、U2,注册码也拆分成两个整数R1、R2,注册机为R1=U1+115,R2=U2+351,验证函数有两个,其中F1为:[(R1-U1)2+ (R2-U2)2]1/2 =369,F2为:(R2-U2)/(R1-U1)=3,我们将F1暴露,将F2隐藏。这样破解者找到F1后首先会发现验证过程包含了U、R的完整信息,而且F1显然是一个勾股方程,查表可得其唯一整数解为R1-U1=81,R2-U2=360,很容易自以为是的认为注册机为R1=U1+81,R2=U2+360。然而偏偏我们使用的不是F1的整数解,却在计算机的计算下恰好满足F1。其实可以将U看作平面坐标系上的一个点,满足F1的是以U为圆心,369为半径的圆;满足F2的是经过U且斜率为3的直线;F1和F2的交点才是合法的R。我们所学习的数学函数绝大多数都是连续函数,而计算机处理的却是离散值,所以这一类的陷阱很容易构造,但是破解者要识破,就不见得有那么容易了。

第三个陷阱俺称之为“函数陷阱”,破解高手通常对自己的分析归纳能力很自信,当他们在跟踪程序的过程中遇到函数调用时,喜欢分几次传递不同的参数给函数,试图从返回值的变化中分析归纳出函数的作用,我们可以投其所好构造陷阱。首先我们暴露F1,F1读入

R作为参数,返回值为R’并且R’与R有一定的对应规律,紧接着验证R’=U,让破解者误以为F1的功能仅仅是F1(R)=R’。而其实F1的功能是明修栈道,暗渡陈仓,在返回R’之前将R’写入了一个变量地址。在另一处埋伏的F2会将该变量地址中的R’还原成R并进行完全不同的验证。这种陷阱是很容易让猛兽跌跟斗,对付乱蹦乱跳的小兔子却往往并不见效,破解菜鸟们总是看见call就下意识地按F8跟进去,就像打CS的菜鸟看见敌人就扣住扳机不肯撒手一样,拦都拦不住,那也没有办法。

以上几个例子都有一定的发挥空间,也可以结合使用,归根结底,陷阱战术的精髓在于利用对手的经验误导对手。俺认为各位有必要研究研究破解者的习惯和爱好,设计出更强悍的独门陷阱来对付越来越强壮的crackers。

 

五、结语

对算法求逆的防范可以阻止注册机的传播,但是不能有效地阻止破解补丁的流毒,各位还需将防范暴力破解等技术用上。

 

转自---------------看雪论坛

posted @ 2018-09-06 21:09  Sendige  阅读(268)  评论(0编辑  收藏  举报