此文原来被删除过,但现在致得E6也已经升级到5.5了,应该没什么影响,再次贴出来作为技术讨论。
声明:本文纯粹出于学习目的,如果用于商业目的造成法律责任后果自负。
最近研究软件加密问题,看“致得E6协同文档管理系统”(http://www.zhidesoft.com/a-5d4c6d9330535cae0131755be0325e59.shtml) 不错,就试试是否可以破解。免费版限制了一些功能,主要是把用户限制到了5个,对于一般的企业都是没法用的,破解的话最起码要把用户数修改为更大的值。
1. 人官方网站下载并安装最新版的软件,安装目录默认为%:zdecm。值得一提的是安装程序会自动将软件安装到非系统盘分区,防止由于重装系统可能造成的数据丢失,我装时是被装到D盘的,也不知道会不会跟皮皮一样自动找一个比较大点儿的分区。
安装完目录主要包括:
home 数据目录,在这里保存日志、备份、数据库等运行过程中产生的数据
tomcat 这里包括了运行的所有程序,包括JRE、Tomcat以及致得的软件,其软件就安装在webapps/ROOT目录下
2. 看到tomcat就成功了一半了,肯定是用java写的,而且还是WEB,如果包含JSP页面更好,即便没有JSP页面而是预编译的JAR包也没有太大关系,反正是字节码又不是二进制,分析起来要简单得多。
在WEB-INF的classes目录下有很多目录,但没有任何.class文件,肯定是被打成jar包放到了某个路径中,最合理的就是WEB-INF/lib了,接下来就需要分析到底是哪个JAR包了。lib目录下jar包很多,但很多都是很熟悉的三方库,即便不熟悉的,后面也都有版本号,很规范,那些应该不是,只有几个文件没有带版本号,估计应该就是其中的一个或几个。一看到czd.jar这个文件名起得很可疑,一开始就盯上它了。
3. 下载反编译工具Java Decompiler,简称JD
http://java.decompiler.free.fr/?q=jdgui
还是GUI界面,用起来很方便,跟.Net Reflector一样好用。
4. 使用JD打开czd.jar文件,主要就是搜用户相关的东西,使用user当关键词,开始是想找到一些SQL语句,这样改起来就简单了,但是这个软件用了struts和Hibernate,只用了一些配置文件将模型和数据库表映射了一下,而SQL语句是自动生成的,这样如果细看代码就费劲了。
用任务管理器找到mqsql的进程,然后在命令行netstat -ao,查看该进程监听的端口是13307,数据库用的是MySQL,管理员账号是默认的ROOT用户,没有密码,然后用MySQL的工具一连就上去了。试着用tdms_user的几个字段名找一下相关SQL语句没找到,这些操作全都是通过Hibernate处理的。
5. 既然能进数据库又尝试了一下通过数据库表直接添加用户,可tdms_user这个表有个ID字段,在源代码模型User类中没有找到生成ID的代码,可能也是Hibernate自动生成的,于是我就照着以前的几个用户自己填了一个,手动添加了一个用户,但界面上没有显示出来,又添加了组的对应记录后才显示出来。
6. 在点来点去的时候,发现在管理界面中的系统信息会显示已授权的用户数量为5,于是又开始顺着系统信息的页面去找线索,很快找到了org.apache.jsps.admin.systemInfo_jsp,也就是系统信息页面对应的JSP编译后的代码,但与授权相关的信息是通过PageContextImpl.proprietaryEvaluate传递的,在这时没有直接获得授权信息的代码。
7.接着查找与systemInfo相关的一些信息,终于在zd.dms.action.admin.SystemInfoAction里找到了读取信息的方法
this.infos.add(
new NameValuePair("软件名称", SystemConfigManager.getInstance()
.getProductNameAndVersion()));
而下面的代码
this.infos.add(
new NameValuePair("授权用户数",
String.valueOf(SDU.coc)));
更让人高兴,肯定SDU.coc就是了,找到SDU这个类,JD就是帅,直接点一下就到了,就是zd.dms.e.SDU,而coc只是一个静态的字段
public class SDU
{
public static int coc = 5;
一开始太高兴了,直接把5改了,然后重新编译这个类,生成SDU.class文件,然后将czd.jar解压(用rar或jar、ZIP软件都可以),用新生成的SDU.class替换原来的文件,然后重新用JAR打包,放回原处,却发现仍然不好用。
后来又仔细看SystemInfoAction这个类,原来还有SDU其它几个字段都有用
if (SDU.fk) {
StringBuilder sb = new StringBuilder();
sb.append(" ");
sb.append("<span class='adminTextFloatLeft'>");
sb.append(String.valueOf(ECMLoader.getSDU().ki()));
sb.append("</span>");
if (ECMLoader.getSDU().cd())
sb.append("<a href='javascript:void(0)' class='btnNormal' onclick='showWindow(\"激活加密锁\", \"/admin/activate.html\");'><span>再次激活加密锁</span></a>");
else {
sb.append("<a href='javascript:void(0)' class='btnNormal' onclick='showWindow(\"激活加密锁\", \"/admin/activate.html\");'><span>激活加密锁</span></a>");
}
this.infos.add(new NameValuePair("加密锁号", sb.toString()));
特别是SDU.fk,再仔细看SDU.cd( )的代码
public boolean cd()
{
coc = 5;
OnlineUser.limitOnlineUserToTrial();
return false;
}
不难理解,coc开始初始化虽然改了,但后来又被改回去了,干脆把cd( )里的代码全去掉,直接返回true。
然后在package zd.dms.service.config.SystemConfigManager里看到这么一段
public String getProductNameAndEdition() {
String productAndEdition = "致得E6协同文档管理系统";
ECMLoader.getSDU(); if (SDU.fk) {
if (ECMLoader.getSDU().wdp())
productAndEdition = productAndEdition + "(专业版) ";
else if (ECMLoader.getSDU().wdw())
productAndEdition = productAndEdition + "(图文版) ";
else if (ECMLoader.getSDU().wdu())
productAndEdition = productAndEdition + "(旗舰版) ";
else
productAndEdition = productAndEdition + "(标准版) ";
}
else {
productAndEdition = productAndEdition + "(免费版) ";
}
return productAndEdition;
}
这就是SDU.fk和wdp()、wdw()、wdu()的作用了,分别表示不同的版本,也不管各个版本啥功能了,感觉应该“旗舰版”最好吧,直接冲着“旗舰版”去就行了,根据上面一段代码,修改相应的几个函数
public String ki()
{
return "";
}
public boolean cd()
{
return true;
}
public boolean dv()
{
return false;
}
public boolean wdt()
{
return false;
}
public boolean wdp()
{
return false;
}
public boolean wdw()
{
return false;
}
public boolean wdu()
{
return true;
}
public boolean fk()
{
return true;
}
在初始化时修改几个字段的初始化
public static int coc = 99999;
public static boolean fk = true;
private static boolean itd = true;
说到这里也就一下致得代码写得比较差的地方,既然fk都定义成public了,为啥还写一个函数fk( )?
然后重新编译,再替换SDU.class,一切正常,在添加用户的时候居然还增加了登录IP限制的功能。
搞定,睡觉。
C#、Java开发起来真是快,但软件加密、限制方面做起来就麻烦了,不记得谁说的了,有一句真理“源码面前没有密秘”。其实C#和Java的通常说的编译器只是翻译器而己,翻译成一种通用的中间语言,很容易就被翻译过来,要想真正的加密,还是得下一番工夫,要与C++结合才行。
顺便提示一下致得软件的开发者,下次发布之前先用混淆器混淆一下,特别是在重命名的时候将一些变量使用非可见字符来命名,这样反编译的代码读起来就有点儿难度了。
致得里面还做了一些复杂的运算,比如
public String eci()
{
if (StringUtils.isNotBlank(hid)) {
return hid;
}
String cpuId = HI2.getCPUId();
String biosSN = HI2.getBiosSN();
String hardId = cpuId + ":" + biosSN;
byte[] encoded = Base64.encodeBase64(hardId.getBytes());
hid = new String(encoded);
return hid;
}
还搞什么CPUID、BIOSID,没有任何意义,要从原理上把语言搞清楚,考虑一下如何与C/C++结合来保护软件版权,用C/C++编译成二进制之后破解难度就大些了。
睡觉了,改天再讨论软件加密的问题吧。