易语言编译器逆向
关键词:易语言、E-Debug、编译原理、KCTF
首先,先思考一个问题,这有关于易语言的编译架构设计:为什么C++、Python语言都有相应的CLI(命令行接口)进行编译,而易语言却查无资料?
答案是因为易语言仅在运行期间通过主动和怠性的俩种方式进行代码的Parsing,主动Parsing也就是在GetMessage、PeekMessage之类的消息循环中不断地解析代码文件,也可以将其理解为定时解析。
怠性解析也就是等待触发某一事件时再执行,比如当用户手动点击编译时或者在保存代码文件时进行一次代码的Parsing。
由于仅在GUI运行期间或者说用户编码阶段进行代码的Parsing,这就导致了易语言没有提供CLI以对代码进行命令行的编译操作,同时也没有内置CLI对代码进行编译操作。
如何证明?仅根据是否提供CLI来猜测语言的功能是不太严谨的。接下来,我通过正常的编码操作、内存分析以及分析控制流的方式来尝试验证。并借此机会,对易语言的编译流程进行逆向,尝试编写一个CrackMe供大家娱乐玩耍。
注:易语言版本为 5.8
首先,我们编写如图 1所示函数代码。值得注意的是,如果是手动编码,那么应该可以发现代码被自动格式化了。

图 1
接下来,我们在上述代码的基础上,为函数的调用参数添加一个“test,我是非法参数”并按下回车进行测试。如图 2所示,易语言发生了编译报错,我们将此类报错忽视掉,按下确定。
图 2
可以看到如图 3所示的情况,易语言自动添加了俩个变量,分别为“test”和“我是非法参数”。同时,我们可以发现出现了新的报错“语法错误:错误(39),为某支持库命令提供了过多的参数”。
这直接证明了,易语言在GUI运行期间,或者说用户编码期间进行了代码的Parsing。接下来,我们尝试通过内存分析的方式来判断一下易语言程序是否仅在用户编码阶段进行代码的Parsing。

图 3
首先,我们可以发现易语言在代码行末尾按下回车时,触发了格式化,所以我们将其列出三种情况,如表 1所示:
易语言代码的三种情况
代码情况
注释
原始代码
即未格式化的代码
格式化代码
如“ ”-> “ ”
填充了参数的格式化代码
信息框( , 0 , , )
表 1
接下来尝试寻找在内存中的代码,基于上述的代码,不断修改字符串“我是Parsing验证代码行”并利用Cheat Engine 定位变量地址。定位结果如图 4所示,仅有1个搜索结果,到了此番境地,我们不难假设:如果该变量(token)对应着代码行相关代码,且代码行相关内存发生格式化,即被Parsing了。且程序内存上下文当中没有“明文”的代码。便认为“易语言程序”仅在GUI运行期间或用户编码期间进行代码的Parsing,所以没有提供CLI进行命令行的编译操作。(为排除易语言有内置CLI但并未对外公开的可能)
图 4
为了便于验证,对代码进行了修改,添加了一行对照组代码,如图 5所示。
图 5
程序内存数据如图 6所示,我们不难发现存在着尚未解析的代码,还有用于对照,发生了Parsing的测试行代码。

图 6
经过测试分析,发现“6A C0”为“信息框”函数的调用号,“(36 1A)0F”为字符串参数的长度,为进一步验证易语言有无内置CLI但并未对外公开的可能。我们通过修改字符串的长度,让易语言的Parsing发生异常,通过堆栈的上下文来判断是否有内置CLI的可能。
将字符串长度“0F”修改为“00”,并在易语言展开函数参数解析,如图 7所示:

图 7
此时程序发生异常,如图 8所示,转到相应代码,同时检查堆栈:
图 8
通过检查堆栈,发现疑似代码提示的相关函数,如图 9所示。接下来,我们仅需验证该函数的调用参数是否尚未格式化的原始代码,即可推断其是否是内置的CLI。

图 9
在函数头下断点,做些简单的测试。发现函数的调用似乎仅受PeekMessage影响,同时函数的数据流当中也并未出现明文的原始代码。图 9所示的EDI寄存器为Parsing后的变量指针,可通过其特征定位所有函数。
至此,我们基本可以认为易语言程序仅在用户编码阶段进行了程序的Parsing,没有提供CLI进行命令行编译。
通过Cheat Engine 搜索相应的变量字符串,搜索访问的指针即可进行局部变量的分析。
定位到图 10所示函数,对其进行分析,其数据流如注释所示。
图 10
对图 11所框选的内存范围进行测试并将测试结果可视化如表 1所示,相关测试代码内容如表 2所示。

图 11
样本
0-4
5-8
9
A
B
C-F
CM_ID
(注释)
LENGTH
TYPE
STATIC
PUBLIC
ARRAY_FLAG
LIST_SIZE
001
00 00 00 0D 00 00 00
01 03 00
80
00
00
00
00
CM_001
002
00 00 00 0D 00 00 00
01 03
00
80
01
00
00
00
CM_002
003
00 00 00 10 00 00 00
01 03
00
80
00
00
00
00
CM_003
004
00 00 00 0D 00 00 00
01 03
00
80
00
00
01
EA
CM_004
005
00 00 00 1D 00 00 00
3E 00 01 41
00
00
00
00
CM_005
006
00 00 00 0D 00 00 00
01 03 00
80
00
01
00
00
CM_006
表 2
CM_ID
注解
CM_001
无注释,整数类型,非静态,非数组
CM_002
无注释,整数类型,静态,非数组
CM_003
有注释,整数类型,非静态,非数组
CM_004
无注释,整数类型,非静态,数组
CM_005
无注释,文本类型,非静态,非数组
CM_006
全局,无注释,整数类型,非静态,非数组
表 3
同时,如图 12所示的框选数据,也就是变量类型地址的上方,这串数据表示变量的ID。在后续编译与变量操作相关的地方会用到。同时,变量ID用4个byte存储,意味着最多存在4,294,967,295个变量。
图 12
定位全局变量数据结构的方式很简单,通过搜索“当前全局变量数量”即可定位到“全局变量上下文”的基地址+4的位置。
在本案例中,存储全局变量的基地址在e.exe+1CB2D8,其内存如图 13所示。

图 13
其结构与局部变量是一样的,区别在于前者存储在“.data”区段,而后者存储在“Heap”(堆)当中。
经过分析,发现存在变量Cache,其内存结构没有发生变化。触发条件为赋值粘贴同一代码,其代码地址位于004E5000,如图 14所示,内存数据如图 15所示。(Cache变量影响指令的生成)

图 14
图 15
为了便于理解,给出C样式的变量数据结构。变量上下文的结构如图 16所示。动态变量结构如图 17所示。
图 16
图 17
在上文中,我们提到易语言通过主动和怠性的俩种方式进行代码的Parsing,怠性Parsing又可分为如下几种情况:回车格式化代码、单击编译、Ctrl+S保存代码等。
因此,我们通过定位在内存中的代码,并尝试通过Ctrl+S保存代码进行内存数据分析。定位到图 18所示位置。其中edi指向的是当前的函数名,[edi+bc]+8指向的是Parsing后的AST树。

图 18
编写脚本和设计代码,进行数据分析,如图 19所示:
图 19
经过测试,字节Offset:0-4表示指令类型,如6A 34 00 00 00表示“赋值”。其中第一个字节具有特殊意义,一般表示是第一个expr,如果有expr嵌套,后续字节不会出现。同时,字节01代表一段指令的结束。(01的存在,可减少指令类型的描述,使得同操作指令可优化内存占用。)
接下来给出部分的指令类型表,如表 4所示,包含流程控制、算数运算、逻辑比较、位运算、变量操作、数组操作、文本操作、系统操作等内容。
6A 34 00 00 00
6A 6E 02 00 00
6B 00 00 00 00
6C 01 00 00 00
赋值
判断
如果
如果真
70 03 00 00 00
70 05 00 00 00
70 07 00 00 00
70 09 00 00 00
判断循环首
循环判断首
计次循环首
变量循环首
6A 0C 00 00 00
6A 0D 00 00 00
6A 0E 00 00 00
6A 0F 00 00 00
跳出循环
返回
结束
相乘
6A 10 00 00 00
6A 11 00 00 00
6A 12 00 00 00
6A 13 00 00 00
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2025-7-30 21:21
被zZhouQing编辑
,原因: 我去,参考资料打错了