在采集器v1.2.2共享版中使用的是v1.2.4的zengl引擎,对脚本进行了简单的异或加密运算,v1.2.4版的下载地址如下: github项目地址为:https://github.com/zenglong/zengl_l...

    在采集器v1.2.2共享版中使用的是v1.2.4的zengl引擎,对脚本进行了简单的异或加密运算,v1.2.4版的下载地址如下:

    github项目地址为:https://github.com/zenglong/zengl_language/  选择右侧的Download Zip或Clone in Desktop

    百度盘共享链接地址:http://pan.baidu.com/s/1chaDv 这是一个zip压缩包,解压后可以看到linux和windows目录,分别为两个操作系统环境下的源码和编译器配置,windows目录中既包含用于vs2008的sln解决方案,还包含用于vc6的dsw工作空间。

    在linux中进行make之前,如果不是root用户,就请用 "su" 之类的命令提权,因为make生成libzengl.so后,会copy这个.so文件到/usr/lib目录中(该目录需要root权限才能进行写入操作),这样可执行文件zengl和encrypt才能链接和使用libzengl.so的动态链接库,详情可以查看makefile,只有libzengl.so是zengl语言的核心文件,main.c和encrypt.c生成的zengl和encrypt可执行文件都只是libzengl.so的测试C程序。

    sourceforge.net上的项目地址为:https://sourceforge.net/projects/zengl/files/ (里面有所有历史版本的压缩包,包括v1.2.4的zip包)。

    下面看下v1.2.4所做的修改(这些信息可以在readme或linux的usage.txt中查看到):

    zengl v1.2.4 Release发布版,该版本在原来vs2008的基础上增加了vc6的工作空间,两者使用相同的代码,同时为了支持VC6,修改了zengl_global.h,在VC6下zengl工程新增了一个ZL_LANG_IN_VC6预处理宏,zengl_global.h在遇到该宏时,就知道是VC6编译器,然后就会在项目中使用_vsnprintf函数而不是vsnprintf,因为VC6下的vsnprintf必须在前面添加下划线 ,vs2008和gcc则不需要。

    作者:zenglong
    时间:2013年11月6日
    官网:www.zengl.com

    zengl v1.2.4 , 该版本使用异或算法添加了简单的脚本加密功能,这种算法开销小,但是安全性低,所以只能作为一种简单的脚本伪装。

    另外添加了单目负号运算符,以前没有负号运算符时,是使用减法运算符来表示负号,但是减法运算符毕竟是双目运算符,实现起来问题比较多(实际上之前的版本负号根本没起作用),所以就为负号添加了专门的token和语法解析,负号具有和"!"取反运算符一样的优先级。

    该版本新增了几个API接口函数,如zenglApi_SetErrThenStop,这个API接口,用于在用户自定义的模块函数中设置出错信息,然后设置虚拟机停止执行,比zenglApi_Exit好的地方在于,不会长跳转直接结束,而是返回由用户决定退出的时机,有效防止外部C++调用出现内存泄漏或访问异常等,异或加密运算的API接口是zenglApi_SetSourceXorKey,在zenglApi_Run或zenglApi_Call执行前,可以将xor异或密钥通过此接口传递给虚拟机,虚拟机就会对脚本进行异或解密,异或密钥长度没有限制,所以尽量将异或密钥设置得长些,且密钥里的字符最好是随机的,当然你可以根据此思路进行扩展加密方式,或者使用第三方的加密函数,不过,过于复杂的加密方式可能会降低程序的执行性能,如果是重要的数据还是不要放在脚本里。

    需要注意的是:只有zenglApi_Run和zenglApi_Call可以使用异或密钥,zenglApi_Load则不能使用密钥,main测试项目和采集器中目前都是以zenglApi_Run或zenglApi_Call为主。

    zenglApi_AllocMemForString , zenglApi_AllocMem 接口可以在虚拟机内部为用户分配一段内存,然后由用户决定用这段内存做些什么,在用完后,最好用zenglApi_FreeMem接口将分配的内存给释放掉,当然在zenglApi_Close关闭虚拟机时也会自动释放这些内存,不过手动释放可以防止内存越滚越大。

    该版本还修复了访问类成员的BUG,之前的版本在访问类成员中的成员时会出现找不到类成员的BUG,这是由于zengl_symbol.c在递归查找类成员信息时所用的类ID信息不对造成的,在此版本中进行修复。

    作者:zenglong
    时间:2013年11月5日
    官网:www.zengl.com

    采集器v1.2.2共享版中对应的异或加密脚本的密钥为:

wxString gl_version_number = "v1.2.2"; //版本号信息
//脚本加密密钥
wxString gl_encrypt_str = "xingkekn&**&$&^^^#(*@(*#&$&*@&$*0029044*&$*@&$*&$&!!~~!!@))__*#**)#(*#(*(@#BUBUWEBudru!<!`ss`x)&idmmn!vnsme&-2-00/54-b-#i`i`!doe#(:shou!udruZ2-0\:cmuQshou@ss`x)udru(:cmuUdru@ees)'udru3-#udru3!hr!lnehgx!ho!cmuUdru@ees!i`i`#__*#**)#(*#(*(@#BU*@&$*0029044*&$*@^^^#(*@(*#&$&*@&$*00290@ees)'udru3-#uhr!lnehgx!&**&$&^^^#(*@(*";

    上面的红色部分为异或密钥,在源代码zip解压后的windows目录中有三个项目,其中的encrypt项目就是一个异或加密解密的小程式,可以使用该程式结合上面的密钥对采集器里的脚本代码进行还原,以分析zengl目前支持的一些语法结构,这些脚本都放在采集器的Module目录中,其中的图库,视频,下载模块的脚本都采用的是类对象和类函数的方式来模拟面向对象编程。

    encrypt程序使用如下命令格式:

encrypt <src> <dest> <xorkey>

    src为源脚本,dest为异或运算后的文件名,xorkey为异或密钥。

    在encrypt项目目录中有个测试脚本test.zl ,可以在该项目的配置属性 --> 调试 --> 命令参数中输入类似如下的命令:


图1

    上面之所以用xorkey_334566_hello_world作为密钥,是为了和main.c里的密钥保持一致,当然你也可以根据需要输入别的密钥。

    如果要还原采集器Module目录中的加密脚本,可以在encrypt项目生成的可执行文件encrypt.exe所在的目录如Debug中,编写一个如下的dos批处理脚本:
 

图2

    key后面的就是采集器默认的密钥,在前面已经给出。sell_inc.zl和company.zl等都是采集器里的脚本文件名。上面显示的是由源脚本到加密脚本的正向过程,只要将加密脚本和源脚本文件名参数对调,就可以实现解密过程,加密解密都是同一个密钥,因此异或加密属于对称加密,其他的加密方式像RSA这种有一对公钥和私钥的就属于非对称加密,至于异或运算的原理可以查看源代码,或者访问wiki百科等。

    在zengl动态链接库项目中,和异或密钥相关的结构体定义在zengl_global.h中:

/********************************************************************************
        下面是和虚拟机XOR异或加密运算相关的结构体和枚举等定义
********************************************************************************/


typedef struct _ZENGL_VM_XOR_ENCRYPT{
    ZL_CHAR * xor_key_str; //密钥字符串指针,由用户提供
    ZL_INT xor_key_len; //密钥长度
    ZL_INT xor_key_cur; //密钥扫描游标
}ZENGL_VM_XOR_ENCRYPT;

/********************************************************************************
        上面是和虚拟机解释器XOR异或加密运算相关的结构体和枚举等定义
********************************************************************************/

    因为zengl目前是循环使用密钥进行异或运算的,所以需要xor_key_cur游标来判断当前异或运算对应的密钥字符,当xor_key_cur到达最后一个密钥字符后,又会回到第一个密钥字符进行异或解密。当xor_key_len密钥长度为0时(默认值就是0),表示没有密钥,则从文件中读取扫描字符时,就不会进行异或运算。

    在虚拟机的结构体定义中对应添加了异或密钥成员:

/*虚拟机结构体定义*/
typedef struct _ZENGL_VM_TYPE
{
    .......................  //省略N行

    ZL_CLOCK_T total_time; //执行结束时的总时间,毫秒为单位
    ZENGL_EXPORT_VM_MAIN_ARGS * vm_main_args; //用户传递给虚拟机的一些参数
    ZL_BOOL isinApiRun; //判断是否通过zenglApi_Run运行的虚拟机
    ZL_BOOL isUseApiSetErrThenStop; //判断用户是否在模块函数中通过调用zenglApi_SetErrThenStop来停止虚拟机的
    ZENGL_VM_XOR_ENCRYPT xor_encrypt; //异或加密运算相关结构体

    /*虚拟机相关的自定义函数*/
    ZL_VOID (* init)(ZL_VOID * VM_ARG,ZENGL_EXPORT_VM_MAIN_ARGS * vm_main_args); //虚拟机初始化函数 对应 zenglVM_init
}ZENGL_VM_TYPE;
/*虚拟机结构体定义结束*/

    异或解密运算主要在zengl_main.c中:

/*
从文件中获取下一个字符
*/

ZL_CHAR zengl_getNextchar(ZL_VOID * VM_ARG)
{
    ZL_CHAR ch = ZL_STRNULL;
    ZL_UCHAR tmpch;
    ZENGL_VM_TYPE * VM = (ZENGL_VM_TYPE *)VM_ARG;
    ZENGL_COMPILE_TYPE * compile = &VM->compile;
    ZENGL_SOURCE_TYPE * source = &(compile->source);

    if(source->file == ZL_NULL)
    {
        source->file = ZENGL_SYS_FILE_OPEN(source->filename,"rb"); //一定要是rb否则二进制加密脚本解析会出错
        if(source->file == ZL_NULL)
            compile->exit(VM_ARG, ZL_ERR_FILE_CAN_NOT_OPEN ,source->filename);
    }
    if(source->needread || source->cur >= source->buf_read_len)
    {
        if((source->buf_read_len = ZENGL_SYS_FILE_READ(source->buf,sizeof(ZL_UCHAR),ZL_FILE_BUF_SIZE,source->file)) == 0)
        {
            if(ZENGL_SYS_FILE_EOF(source->file))
                return ZL_FILE_EOF;
            else
                compile->exit(VM_ARG, ZL_ERR_FILE_CAN_NOT_GETS ,source->filename);
        }
        source->needread = ZL_FALSE;
        source->cur = 0;
    }
    if(VM->xor_encrypt.xor_key_len == 0) //为0表示没有异或密钥,则不进行异或运算
        ch = (ZL_CHAR)source->buf[source->cur++];
    else
    {
        tmpch = source->buf[source->cur++];
        ch = (ZL_CHAR)(tmpch ^ (ZL_UCHAR)VM->xor_encrypt.xor_key_str[VM->xor_encrypt.xor_key_cur]);
        if((++VM->xor_encrypt.xor_key_cur) >= VM->xor_encrypt.xor_key_len)
            VM->xor_encrypt.xor_key_cur = 0;

    }
    compile->col_no++;
    return ch;
}

    用户可以通过zenglApi_SetSourceXorKey这个接口函数来设置异或密钥,如main项目的main.c中:

    char * xor_key_str = "xorkey_334566_hello_world"; //异或密钥字符串,长度没有限制

    ...........................  //省略N行代码

    zenglApi_Reset(VM);

    zenglApi_Push(VM,ZL_EXP_FAT_INT,0,1415,0);

    zenglApi_Push(VM,ZL_EXP_FAT_STR,"test second arg",0,0);

    zenglApi_SetSourceXorKey(VM,xor_key_str);

    if(zenglApi_Call(VM,"encrypt_script/test.zl","OutIn","clsTest") == -1) //编译执行zengl脚本函数
    //if(zenglApi_Call(VM,argv[1],"init","clsTest") == -1) //编译执行zengl脚本函数
        main_exit(VM,"错误:编译<test fun call>失败:%s\n",zenglApi_GetErrorString(VM));

    zenglApi_Close(VM);

    这种异或加密开销小,不过安全性很低,所以尽量将异或密钥设置得长些,且密钥里的字符最好是随机的,重要数据也尽量不要放在脚本中,网络上有很多异或加密的破解工具,一般是用重合指数率分析可能的密钥长度,然后再一位一位的去循环破解出密钥。

    既然公开了密钥和算法,为何采集器是共享版?其实是因为作者并不想做采集器,尤其是懒得去维护那么多模块的采集规则,而且作者也不希望打开网站到处是相同的内容,所以做成共享版,这样用的人自然就少了,作者只希望该采集器作为zengl的实验测试产品,另外,采集器的源代码还在整理中,所以先公开密钥,到时候再将源码发布出来。

    该版本专门做了负号的解析,在词法扫描时,当扫描到一个减号的token并将减号添加到AST抽象语法树时,会先根据CheckIsNegative函数判断该减号是否是负号,如果是负号,就将其转为负号的token,再加入到语法树中,对应zengl_parser.c里的代码如下:

/**
    将token加入AST抽象语法树
*/

ZL_VOID zengl_ASTAddNode(ZL_VOID * VM_ARG,ZENGL_TOKENTYPE token)
{
    .....................................  //省略N行代码

        case ZL_TK_PLUS:
        case ZL_TK_MINIS:
            compile->AST_nodes.nodes[compile->AST_nodes.count].tokcategory = ZL_TKCG_OP_PLUS_MINIS;
            compile->AST_nodes.nodes[compile->AST_nodes.count].tok_op_level = ZL_OP_LEVEL_PLUS_MINIS;
            if(compile->AST_nodes.nodes[compile->AST_nodes.count].toktype == ZL_TK_PLUS ||
               !compile->CheckIsNegative(VM_ARG))
            {
                break;
            }
            compile->AST_nodes.nodes[compile->AST_nodes.count].toktype = ZL_TK_NEGATIVE; //将减号转为负号单目运算符,并使用和下面的REVERSE一样的优先级
        case ZL_TK_REVERSE:
        case ZL_TK_ADDRESS:
            compile->AST_nodes.nodes[compile->AST_nodes.count].tokcategory = ZL_TKCG_OP_LOGIC;
            compile->AST_nodes.nodes[compile->AST_nodes.count].tok_op_level = ZL_OP_LEVEL_REVERSE;
            break;
    .....................................  //省略N行代码
}
   
    上面在将减号转为负号时,因为case后面没有break语句,所以就会执行case ZL_TK_REVERSE:后面的代码,从而使得负号拥有和REVERSE取反运算符一样的优先级。

    v1.2.4的版本新增了如下几个API接口函数:

/*API接口,用于在用户自定义的模块函数中设置出错信息,然后设置虚拟机停止执行,比zenglApi_Exit好的地方在于,不会长跳转直接结束,而是返回由用户决定退出的时机,有效防止外部C++调用出现内存泄漏或访问异常*/
ZL_EXPORT ZL_EXP_VOID zenglApi_SetErrThenStop(ZL_EXP_VOID * VM_ARG,ZL_EXP_CHAR * errorStr, ...);

/*API接口,用户通过此接口设置脚本源代码的XOR异或运算加密密钥*/
ZL_EXPORT ZL_EXP_VOID zenglApi_SetSourceXorKey(ZL_EXP_VOID * VM_ARG,ZL_EXP_CHAR * xor_key_str);

/*API接口,用户通过此接口将字符串拷贝到虚拟机中,这样在C++中就可以提前将源字符串资源给手动释放掉,而拷贝到虚拟机中的新分配的资源则会在结束时自动释放掉,防止内存泄漏*/
ZL_EXPORT ZL_EXP_CHAR * zenglApi_AllocMemForString(ZL_EXP_VOID * VM_ARG,ZL_EXP_CHAR * src_str);

/*API接口,用户通过此接口在虚拟机中分配一段内存空间*/
ZL_EXPORT ZL_EXP_VOID * zenglApi_AllocMem(ZL_EXP_VOID * VM_ARG,ZL_EXP_INT size);

/*API接口,将AllocMem分配的资源手动释放掉,防止资源越滚越大*/
ZL_EXPORT ZL_EXP_VOID zenglApi_FreeMem(ZL_EXP_VOID * VM_ARG,ZL_EXP_VOID * ptr);

    上面是该版本新增的5个API接口,第一个zenglApi_SetErrThenStop主要在嵌入到C++程序时用的多些,因为C++里经常要用到对象,在初始化了对象后,如果没有释放对象就直接使用zenglApi_Exit接口进行长跳转退出的话,就会发生内存泄漏。如采集器v1.2.2共享版里的代码片段(代码以后会开源出来):

    wxString tmpContent = content;
    if(ex.Compile(pattern,flags))
    {
        MatchCount = ex.GetMatchCount();
        if(index >= MatchCount)
        {
            return zenglApi_SetErrThenStop(VM_ARG,"bltRegexMatches函数的第二个元组参数无效,正则表达式中共有%d个元组,而你要访问第%d个元组,元组索引值必须小于总元组数" ,MatchCount,index);

    如上面C++代码中tmpContent对象使用content进行了初始化,而且ex也是wxWidgets里的一个正则表达式对象,这两个都必须正常退出释放才能确保内存不会发生泄漏,如果直接用zenglApi_Exit接口长跳转退出的话,这两个对象就释放不了,可能有人会说,可以在zenglApi_Exit前手动释放这些对象,但是手动释放一方面很麻烦,另一方面,像上面代码中的tmpContent这个对象在content值很短时,如果手动释放就会报错,只有content值比较长时才需要释放,这样就不好判断了。所以这里就可以用zenglApi_SetErrThenStop接口设置好出错信息,并在内部设置虚拟机的停止标志,然后返回,在return退出C++程序时,C++会自动对这些对象进行释放工作。

    所以在没什么对象需要释放的情况下,可以用zenglApi_Exit函数,在有对象需要释放的情况下,就用zenglApi_SetErrThenStop接口。

    其他几个接口都好理解,就不多说了。

    该版本还修复了访问不到类成员的BUG,主要是zengl_symbol.c中类ID信息引用错误造成的:

/*
    扫描类引用时的节点,将类成员转为数组的索引压入栈中。
    如test.test2[3].name.val
    如果test2在test类中的索引为1,name在test2类里的索引为2,val在name类的索引为3则
    表达式相当于test[1,3,2,3],第一个1代表test2成员,第二个3是test2[3]里的索引3
    第三个2代表name,第四个3代表val
    然后就按数组的方式将1,3,2,3依次压入栈作为test数组的索引值,所以test类本质上就是
    一个数组。
    
    类引用是采用点运算符来连接的,所以该函数采用了堆栈扫描法
    先在点运算符的第一个子节点中查找类id,然后使用该类id找到第二个子节点即类成员的
    索引,接着将索引PUSH压入栈。
*/

ZL_VOID zengl_SymScanDotForClass(ZL_VOID * VM_ARG,ZL_INT nodenum)
{
    ............................................  //省略N行代码

    //classid = compile->SymClassMemberTable.members[classMemberIndex].classid; //使用该类成员的类id,为获取后面的成员的索引做准备
    classid = compile->SymClassMemberTable.members[classMemberIndex].cls_stmt_classid; //应该使用声明该成员时所使用的类ID信息,而非该成员所在的类结构,因为该成员的成员是声明类里的成员!

    ............................................  //省略N行代码
}

    代码中的解释有点绕口,看个具体的例子,下面是采集器v1.2.2中优库视频模块规则.zl里的代码片段(共享版的需要先解密还原才能看的到):

class ykVideoPattern
    lstPages;
    contentLinks;
    title;
   .........................  //省略N行代码
endclass

class ykVideo
    ykVideoPattern p;
    .........................  //省略N行代码
    fun InitPatterns(obj)
        ykVideo obj;
        obj.p.lstPages = '(/search_video/q_.*?_orderby_1_page_.*?)"';
        .............................  //省略N行代码
    endfun
endclass

    上面的脚本代码中,类ykVideo中定义了一个p成员,该成员是使用ykVideoPattern类进行声明的,当obj.p.lstPages要访问p里的成员lstPages时,显然应该在ykVideoPattern的类信息中进行查找,但是原来的zengl版本中却是在ykVideo类信息中进行查找的,当然就找不到lstPages成员了,所以在zengl_symbol.c的zengl_SymScanDotForClass查找类成员函数中,应该使用声明该成员的类ID信息,而不是该成员所在类结构的类ID信息。

    最后在main测试项目中,有测试脚本:test.zl ,test2.zl,test3.zl ,要进行测试需要在main项目的命令行参数中输入test.zl :


图3

    有关v1.2.4版本的介绍就到这里。

    有人说作者更新慢,没办法,又不能靠这些吃饭 (这些开发都只是纯粹的个人兴趣),先要生存,才能谈别的。。。

    OK,到这里,休息,休息一下 o(∩_∩)o~~
上下篇

下一篇: zengl v1.2.5 RC4加密,64位系统及Mac系统编译,Api调用位置规范,内建模块函数

上一篇: zengl v1.2.2 正式发布版及智能采集器源代码

相关文章

zengl v1.4.1 及zengl_SDL新增的Android工程

zengl v1.3.0 位运算符 字符串脚本解析 缓存 Bug修复

zengl v1.3.2 编译静态库 Bug和向下兼容处理

zengl v1.9.0 增加self特殊类名,增加zenglApi_SetDefLookupHandle等接口

zengl编程语言v0.0.21汇编级调试

zengl编程语言v1.0.5 编译,执行,源码调试一体化,开发俄罗斯方块