我遇到个需求:需要给离线程序加一道锁,防门外汉的那种。因为离线程序遇到高手,肯定是会被破解的。像IntelliJ全家桶这样超大型软件,都无法防止用户的暴力破解。何况我这小打小闹的玩意。所以,目的就是防止一些普通的程序员也能轻易破解软件。
实现效果
假设已经开发了一个桌面应用“test.exe”(该软件不联网),那么每次安装程序的时候,都需要输入验证码(激活码)才能安装,该激活码需要向软件所有者索要。从而达到了防止程序被随意拷贝安装使用的目的。
那么验证码如何才能实现实时更新且由软件所有者提供呢?
实现方法
我想到的方法是根据时间,来生成一个验证码。为什么要根据时间呢?因为离线程序和软件所有者,所拥有的相同的元素,可能就是时间了。
当然验证码校验的方式肯定是:离线程序和软件所有者拥有一样的验证码生成算法,这样才能保证验证码能通过验证。
所以这种方案的缺陷便是:只要验证码生成算法被破解,那么防御就失效。而破解这个算法难度也不会很大,只要对程序代码进行分析,找到加密所在的代码,即可破解。毕竟离线程序的代码都在计算机本地,高手自然有手段对代码进行逆向,再分析。
不过程序员都知道,分析别人写的代码是一件很恶心的事,特别是拿到的还不是源码(没有注释,可能变量名也被混淆了),只是逆向出来的代码。可读性很差。所以破解离线程序的代价也是很大的。所以稍微加一点防御措施,也能让不少人望而却步。
好了,目的和意义交代完了,进入主题。
验证码生成算法
验证码有以下特点:
- 无规律
- 有时效性
上面说到,我能想到的离线程序和软件所有者之间的共有要素只能想到“时间”。时间是随时在变化的。对时间加以处理,便可以得到无规律的验证码。
验证码生成的时间点
我们时间常用的精度是秒。如果我们每秒都生成一个验证码的话,那么验证码的量很大,且时效性太短,没有意义,所以我选择以分钟为单位来生成验证码。当然,也可以根据需要,以小时,天为单位生成验证码。
import time
timeStamp = int(time.time()) # 取时间戳,并转化为整数
minTime = timeStamp - timeStamp % 60 # 取余60得到当前秒数,减去秒数即得到分钟级别的时间戳
python默认获取的时间戳是秒级,且后面带有小数点,所以这两行就是先取整,然后把秒去掉,保留分钟级别的时间戳。
验证码的时效性
通常我们收到验证码时,都会提示验证码的有效时间为10分钟。
我为了实现类似的效果(总之就是保证验证码是在一段时间内有效的),想到了一个蠢办法:
将当前时间的前后15分钟范围内的验证码都生成出来,与软件所有者提供的验证码比对,只要有一个比对成功,就认为验证码正确。
这样做的目的是因为不联网的电脑的时间经常不准,差个几分钟很正常,所以我取正负15分钟,在大多数情况下确保软件所有者提供的验证码包含在这个范围内。
理解了这个思路,那么就可以根据自己的实际情况调整验证码的有效期。
for j in range(-15, 15):
minTimeRange = minTime + 60 * j # 加60秒即一分钟,每次加一分钟
保证验证码无规律
为了无规律,可以以时间戳为seed,生成随机数,这样基本也无规律,但是它有一个问题,它不能跨编程语言,也就是相同的seed,不能保证python和java生成的随机数相同。这就会为以后验证码校验制造困难。
你想想,软件所有者难道时时刻刻坐在电脑面前?索要验证码的人可不管你在不在电脑面前,他也不想等你回到电脑面前。这种时候,搞个app在手机上,用同样的算法实现,那么生成的验证码也能保证一致。安卓平台使用java开发的,桌面软件通常不用java,所以用随机函数不靠谱。
我就想到一个方法:
- 将前面获取的分钟级的时间戳转化为list,每个数字一个坑(这个时候注意最后一位数始终是0,所以实际处理时可以把尾数忽略掉)
- 初始化result=0,再初始化seed=131073(为保证验证码的位数为6位数,所以取刚迈进6位数的二进制数“100000000000000001”,换成别的数也可以达到一样的效果,只是获取的验证码位数不固定而已)
- 每次循环从list取一位数,将seed用位运算符“<<”移位list[i],然后再与result做异或运算。最终输出result
完整代码如下:
import time
checkCode = 530915 # 实际由软件所有者提供输入
flag = False
timeStamp = int(time.time()) # 取时间戳,并转化为整数
minTime = timeStamp - timeStamp % 60 # 取余60得到当前秒数,减去秒数即得到分钟级别的时间戳
seed = 131073 # 二进制100000000000000001 #为保证验证码的位数为6位数,所以取刚迈进6位数的二进制数,换成别的数也可以达到一样的效果。
# 由于正式环境的机子时间不一定准确,但也不考虑非常不准确的情况,通常与准确时间的差值不会超过10分钟
# 取[-15,15)范围是因为保证验证码是在一定范围内都是有效的。即正式环境的当前时间的正负15分钟涵盖了验证码生成的那个时间,验证码即可校验正确
for j in range(-15, 15):
minTimeRange = minTime + 60 * j # 加60秒即一分钟,每次加一分钟
numList = list(map(int, str(minTimeRange))) # 将时间戳每一位数转化为list
result = 0
for i in range(0, len(numList) - 1): # 末尾数都为0,所以我不处理他
result = result ^ (seed << numList[i])
if result % 1000000 == checkCode: # 取结果的末六位数与验证码比对
flag = True
break
print(flag)
代码很短。主要是提供个思路。
上面还得注意一点,用seed=131073可以保证result>100000(即保证有6位数及以上),但是 result % 1000000却不能保证得到的验证码一定是6位数,因为如果末六位的前几位数为0,则取余的结果就不是6位数了。要想保持6位数,需要在前面补0。或者干脆转换成字符串再去取末六位。比对的时候也拿字符串进行比对就没问题。我这里对验证码位数没要求,所以就无所谓了。
PS:对于代码中的seed=131073的设计是否是最优的,我觉得不是,只是这个数达到了我想要的效果,所以我就用它了。