看雪学院 09月24日 19:25
深入解析易语言的编译机制
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入剖析了易语言的编译架构设计,重点探讨了其与C++、Python等语言在编译方式上的差异。文章通过实际编码、内存分析和控制流分析,验证了易语言仅在GUI运行期间进行代码解析,因此缺乏命令行编译接口。作者详细分析了易语言的变量数据结构(局部变量与全局变量)、语法树的指令表以及前缀表达式的构建过程,并对花指令的定位、清除与扩展进行了探讨。此外,文章还分析了机器指令的生成机制,并提供了调整链接器设置以增强用户体验的建议。最后,对易语言的市场应用进行了简要调研。

易语言的编译机制独特,仅在GUI运行期间进行代码解析,这解释了其缺乏命令行编译接口的原因。文章通过多种技术手段验证了这一特性,包括对代码解析触发机制(如消息循环和事件触发)的分析,以及通过内存和堆栈分析来排除内置CLI的可能性。

文章详细阐述了易语言的变量数据结构,区分了局部变量和全局变量,并分析了它们的内存存储方式和结构。通过Cheat Engine等工具,作者展示了如何定位和分析变量的ID、类型、大小等关键信息,为后续的代码优化和混淆奠定了基础。

通过对代码进行内存分析和控制流分析,文章揭示了易语言的语法树是如何通过一系列指令表构建的,并最终转化为前缀表达式。指令表涵盖了流程控制、算术运算、逻辑比较等多种操作,理解这些指令对于逆向分析和代码还原至关重要。

文章深入探讨了易语言的花指令功能,分析了其插入级别、模板以及清除和扩展花指令的方法。通过Hook特定函数和分析机器指令,作者展示了如何实现更强大的代码混淆效果,以增强程序的安全性。

最后,文章还提及了易语言链接器的相关配置,如添加ASLR和调整IMAGE_BASE等参数,以及分析了机器指令的生成过程,为理解易语言的底层编译和执行机制提供了更全面的视角。

zZhouQing 2025-09-24 17:59 上海

看雪论坛作者ID:zZhouQing

首先,先思考一个问题,这有关于易语言的编译架构设计:为什么C++Python语言都有相应的CLI(命令行接口)进行编译,而易语言却查无资料?

答案是因为易语言仅在运行期间通过主动和怠性的俩种方式进行代码的Parsing,主动Parsing也就是在GetMessagePeekMessage之类的消息循环中不断地解析代码文件,也可以将其理解为定时解析。

怠性解析也就是等待触发某一事件时再执行,比如当用户手动点击编译时或者在保存代码文件时进行一次代码的Parsing

由于仅在GUI运行期间或者说用户编码阶段进行代码的Parsing,这就导致了易语言没有提供CLI以对代码进行命令行的编译操作,同时也没有内置CLI对代码进行编译操作。

如何证明?仅根据是否提供CLI来猜测语言的功能是不太严谨的。接下来,我通过正常的编码操作、内存分析以及分析控制流的方式来尝试验证。并借此机会,对易语言的编译流程进行逆向,尝试编写一个CrackMe供大家娱乐玩耍。

注:易语言版本为 5.8

1.证明易语言没有提供CLI

正常编码方式验证

首先,我们编写如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 1A0F”为字符串参数的长度,为进一步验证易语言有无内置CLI但并未对外公开的可能。我们通过修改字符串的长度,让易语言的Parsing发生异常,通过堆栈的上下文来判断是否有内置CLI的可能。

将字符串长度0F”修改为“00”,并在易语言展开函数参数解析,如7所示:

7

此时程序发生异常,如8所示,转到相应代码,同时检查堆栈:

8

通过检查堆栈,发现疑似代码提示的相关函数,如9所示。接下来,我们仅需验证该函数的调用参数是否尚未格式化的原始代码,即可推断其是否是内置的CLI

 

9

在函数头下断点,做些简单的测试。发现函数的调用似乎仅受PeekMessage影响,同时函数的数据流当中也并未出现明文的原始代码。9所示的EDI寄存器为Parsing后的变量指针,可通过其特征定位所有函数。

至此,我们基本可以认为易语言程序仅在用户编码阶段进行了程序的Parsing,没有提供CLI进行命令行编译。

2.变量数据结构

局部变量分析通过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 000D 00 00 00

01 03 00

80

00

00

00

00

CM_001

002

00 00 000D 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 000D 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 000D 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。在后续编译与变量操作相关的地方会用到。同时,变量ID4byte存储,意味着最多存在4,294,967,295个变量。

12

全局变量分析定位全局变量数据结构的方式很简单,通过搜索“当前全局变量数量”即可定位到“全局变量上下文”的基地址+4的位置。

在本案例中,存储全局变量的基地址在e.exe+1CB2D8,其内存如13所示。

13

其结构与局部变量是一样的,区别在于前者存储在.data”区段,而后者存储在“Heap”(堆)当中。

Cache分析经过分析,发现存在变量Cache,其内存结构没有发生变化。触发条件为赋值粘贴同一代码,其代码地址位于004E5000,如14所示,内存数据如15所示。(Cache变量影响指令的生成)

14

15

小结为了便于理解,给出C样式的变量数据结构。变量上下文的结构如16所示。动态变量结构如17所示。

16

17

3.语法树分析

指令表在上文中,我们提到易语言通过主动和怠性的俩种方式进行代码的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

相除

整除

求余数

相加

6A 14 00 00 00

6A 15 00 00 00

6A 16 00 00 00

6A 17 00 00 00

相减

取符号

取绝对值

6A 18 00 00 00

6A 19 00 00 00

6A 1A 00 00 00

6A 1B 00 00 00

取整

绝对取整

四舍五入

求次方

6A 1C 00 00 00

6A 1D 00 00 00

6A 1E 00 00 00

6A 1F 00 00 00

求平方根

求正弦

求余弦

求正切

6A 20 00 00 00

6A 21 00 00 00

6A 22 00 00 00

6A 23 00 00 00

求反正切

求自然对数

求反对数

是否运算正确

6A 24 00 00 00

6A 25 00 00 00

6A 26 00 00 00

6A 27 00 00 00

置随机数种子

取随机数

等于

不等于

6A 28 00 00 00

6A 29 00 00 00

6A 2A 00 00 00

6A 2B 00 00 00

小于

大于

小于或等于

大于或等于

6A 2C 00 00 00

6A 2D 00 00 00

6A 2E 00 00 00

6A 2F 00 00 00

近似等于

并且

或者

取反

6A 30 00 00 00

6A 31 00 00 00

6A 32 00 00 00

6A 33 00 00 00

位取反

位与

位或

位异或

6A 7E 02 00 00

6A 7F 02 00 00

6A 80 02 00 00

6A 81 02 00 00

左移

右移

合并整数

合并短整数

6A 35 00 00 00

6A 37 00 00 00

6A 38 00 00 00

6A 39 00 00 00

连续赋值

重定义数组

取数组成员数

取数组下标

6A 3A 00 00 00

6A 3B 00 00 00

6A 3C 00 00 00

6A 3D 00 00 00

复制数组

加入成员

插入成员

删除成员

6A 3E 00 00 00

6A 3F 00 00 00

6A 31 02 00 00

6A 4C 00 00 00

清除数组

数组排序

数组清零

取文本长度

6A C0 00 00 00

 6A B0 00 00 00

6A 3C 02 00 00

6A 4D 00 00 00

信息框

运行

取系统语言

取文本左边

4

至此,我们已可尝试编写解析器,自行解析指令流,还原出源代码,并对其进行操作。

前缀表达式易语言的Parsing最终会将代码转化为前缀表达式,接下来给出示例并进行分析,示例如5所示。

aa123456123

6A3400 00 00 00 00 00 00 00 00 00 00 00 00 00 0036 1D 3804 00 01 2537 21 13 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 36 21 14 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 36 17 00 00 00 00 00 C0 5E 40 17 00 00 00 00 00 80 7C 40 01 17 00 00 00 00 00 C0 5E 40 01 01

5

按照字节顺序解读部分标志位,34 00 00 00代表赋值指令,这在4有提到,36代表left标志,1D 38是类型,04 00 01 25代表的是赋值对象(aa),也就是变量的ID号,37代表的是right21代表有嵌套的expr13 00 00 00代表的是相加操作。36代表着left21代表着有嵌套的expr14 00 00 00代表着相减操作。36代表着left17代表着操作符对应的操作数,00 00 00 00 00 C0 5E 40代表着Double类型的12317代表着操作符对应的操作数,00 00 00 00 00 80 7C 40代表着Double类型的45601代表着当前操作的结束。接下来的17代表着操作符对应的操作数,00 00 00 00 00 C0 5E 40代表着Double类型的12301代表着操作的结束。最后一个01代表着最后一个操作的结束。

我们将指令流尝试转化为表达式(leftrightend标志不计入),如6所示。

= aa + - 123 456 123

6

不难发现,易语言最终将代码转化为前缀表达式,关于前缀表达式的内容,笔者写在附录当中。

小结我们拿到了指令表、并发现易语言的语法树通过前缀表达式进行构建。至此,结合变量数据的相关信息我们可以进行代码优化、编译时混淆等内容。

4.花指令

定位模板易语言有提供花指令插入的功能,分3个级别。分析花指令的目的在于扩展其功能,易语言程序本身所提供的花指令能被DejunkOllydbg插件轻易地清除,而对花指令插入的功能进行分析,则能够达到更加强大的混淆效果。

在易语言->“工具”->“系统配置”->“安全”的对话框当中可以看到“花指令插入级别”。默认为0,将其修改为3后,记得点击确定保存下修改的结果再进行搜索,否则Cheat Engine搜索到的是对话框所提供的临时变量。搜索结果如20所示,存储花指令插入级别的变量地址为e.exe+1CAFE0

20

e.exe+1CAFE0(插入花指令级别)下硬件访问断点,点击“编译”->“静态编译”,定位到函数0042FAA0,发现共有四个花指令模板,如7所示。

21

地址

字节码

长度

0x5B5A48

EB 01

2

0x5B5A4C

F9 72 01

3

0x5B5A50

F8 73 01

3

005B5A54

E8 00 00 00 00 83 04 24 06 C3 00 00

A

7

清除花指令清除花指令的一般过程如22所示,在编写花指令清楚工具的过程当中需要保持一个原则“优先匹配长度较长的花指令”,以免出现优先清理长度较短的花指令而部分花指令清理失败导致的程序运行错误情况。

22

扩展花指令在花指令插入函数当中有这样一段代码,用于插入实际的花指令,如23所示。通过Hook这个地址,即可达到扩展花指令的目的。

23

花指令的目的在于混淆视听,笔者个人认为重点在于量,所以花指令模板的来源优先从花指令清除插件当中获取。如OllydbgDejunk、花指令清除工具等等。

24

小结在本章节当中,我们对易语言花指令插入的功能做了简单的分析,并分别尝试给出了清除和扩展花指令的方案。结合机器指令的分析,能够跟进一筹。

5.机器指令

23当中,我们看到TokenSet_Operate函数的调用,其中ecx/edi所指向的this指针是机器码的Baseecx+8的位置为生成的机器码流指针。由于易语言会在每行指令编译后进行花指令的插入,如25所示。我们可借此机会定位其生成机器指令的function

25

通过回溯堆栈,发现如26所示代码段,函数头机器码0x5B5A44内容如27所示。

26

27

小结至此,我们可以发现无论是花指令模板的生成还是函数头或其他汇编的生成,所调用的函数为TokenSet_Operate,我们可Hook这个函数,统计出语法树所对应的机器码表,并基于此Diy易语言的机器指令。由于时间较为仓促,过段时间一并附上。

Diy的思路可借鉴笔者24KCTF所写的栈混淆。

6、链接器

在易语言的安装目录下,有个tools/link.ini文件,其内容如28所示。关注到extra_args,我们可为其添加俩个参数“/DYNAMICBASE”、“/BASE:0x12345000”、“/MAP”,功能分别为开启ASLR、调整IMAGE_BASE和生成MAP文件。

28

再次编译程序,可发现目录下多了个map文件,同时IMAGE_BASE也发送了变化。

29

小结由于易语言是闭源开发软件,通过调整链接器设置,可使新手更加友好地学习语言。

 

7.附录

前缀表达式前缀表达式(也称为波兰表达式)是一种算术表达式表示方式,其中运算符写在操作数之前,没有括号。它的特点是运算顺序由表达式的写法自然决定,不需要像中缀表达式那样考虑运算符优先级和括号问题。

例如,中缀表达式1 - (2 + 3)的前缀表达式是:

- 1 + 2 3

同时,也存在着后缀表达式(也成为逆波兰表达式)。

后缀表达式(逆波兰表达式)运算符写在操作数之后,如1 2 3 + 4 × + 5 -。计算时从左向右扫描,用栈处理,简单高效。

Parsing函数在本案例当中的易语言Parsing函数头位置应该是004CC939,笔者没有更加具体的分析。感兴趣的读者可自行尝试。

30

控件名混淆 

 

当下易语言市场调研根据笔者这段时间的观察,易语言所涉及的产业如31所示。

 图31

参考资料1. 段钢.加密与解密(第4版)电子工业出版社, 2018.

2.Xdbg.Diy-易语言花指令引擎与编译结果打乱码,2023.

看雪ID:zZhouQing

https://bbs.kanxue.com/user-home-960900.htm

*本文为看雪论坛精华文章,由 zZhouQing 原创,转载请注明来自看雪社区

1.25折门票即将售罄

看雪·第九届安全开发者峰会(SDC 2025)

# 往期推荐

无"痕"加载驱动模块之傀儡驱动 (上)

为 CobaltStrike 增加 SMTP Beacon

隐蔽通讯常见种类介绍

buuctf-re之CTF分析

物理读写/无附加读写实验

球分享

球点赞

球在看

点击阅读原文查看更多

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

易语言 编译原理 逆向工程 反汇编 语法树 花指令 E Language Compilation Principles Reverse Engineering Disassembly Syntax Tree Obfuscation
相关文章