破译“致得E6协同文档管理系统” 3.2.847版

破解“致得E6协同文档管理系统” 3.2.847版

此文原来被删除过,但现在致得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++编译成二进制之后破解难度就大些了。

睡觉了,改天再讨论软件加密的问题吧。