【摘要】 我们在Move IR编译器中发现了一个漏洞,其内联注释可以伪装成可执行代码。这是因为move-IR解析器无法识别内联注释末尾的一些unicode换行符。特别是代码在解析公
我们在Move IR编译器中发现了一个漏洞,其内联注释可以伪装成可执行代码。这是因为move-IR解析器无法识别内联注释末尾的一些unicode换行符。特别是代码在解析公共的\r\n和\r\n时,无法正确解析。另外,其他有效的unicode换行符完全被解析器忽略。
该漏洞的潜在影响可能有很大的不同:
1)每个特定模块的业务逻辑及其用例;
2)Move IR语言的当前和将来功能;
3)用于向libra网络提交bytecode的开发平台;
我们可能想到的一些潜在利用场景是:
· 制造资产(Libra Coins或Libra网络上的任何其他资产)以换取费用的水龙头可以部署一个恶意模块,该模块收取费用,但实际上不会向用户提供制造此类资产的可能性。
· 宣称冻结存款并在一段时间后释放的钱包实际上可能永远不会释放这些资金。
· 一个支付拆分器模块拆分部分资产并将资产转发给多方用户,但实际上可能永远不会将拆分的部分资产发送给相应的用户。
· 获取敏感数据并应用某种加密操作来隐藏它的模块(例如哈希或加密操作)实际上可能永远不会应用这种操作。
恶意模块发布者通过在内联注释结尾处创建具有不同于\n的特定换行符的模块,可以欺骗信任其明显源代码的用户。这在区块链环境中尤为重要,因为在区块链环境中,信任被可审核性所取代。我们的团队之前在2018年11月1日的Solidity编译器审核中发现了一个相关漏洞。
目前该漏洞已被修复,为了让大家更好理解,下面给出一个详细的讲解。
漏洞细节展示
我们首先解释漏洞的位置,然后介绍两个关键方案。在该方案下,可以利用它来将嵌入式注释伪装为可执行代码。 该漏洞利用依赖于以下事实:不同的文本编辑器以不同的方式解释和渲染换行符。有权在Libra网络中发布Move模块以欺骗用户的恶意行为者可以利用这一点来欺骗用户,然后用户将与行为异常的模块进行交互。
虽然此功能目前仅限于一组受信任的用户,但将来它可能是开放的、公开的和无需权限的。在漏洞利用部分,我们展示了一个简单的概念验证Move模块,其中包含单个资源声明。 在处理有价值资产的模块中,可以想到更危险的情况。
漏洞
在strip_comments函数中,提交8ea3329处的IR编译器代码,解析器模块尝试使用简单的正则表达式来标识内联注释,该正则表达式旨在匹配以\ r,\ n或\ r \ n结尾的注释:
fn strip_comments(string: &str) -> String
{ // Remove line comments let line_comments = Regex::new(
r"//.*(\r\n|\n|\r)").unwrap(); line_comments.replace_all(
string, "$1").into_owned()}
我们确定了解析器失败的两种情况:
1. 上面的函数在检测用\r字符标记的内联注释的结尾时失败。
2. 上面的函数将忽略其他几个合法换行符的Unicode字符。
案例1
move IR解析器使用regex rust crate中的正则表达式来检测move模块代码中的内联注释。 它尝试使用以下命令匹配以\r \n,\ n或\r结尾的内联注释:
Regex::new(r"//.*(\r\n|\n|\r)")
但是正则表达式实际上没有按预期方式运行,因为它实际上没有检测到\ r换行符所标记的内联注释的结尾。
为了理解错误正则表达式的正确行为,我们分别解释其每个组件:
1. 双引号前面的r是一个用于标识“原始字符串文本”的rust特性,它被用作编写regex字符串的方便方法,而不必转义特殊字符。
2. //:匹配“Move IR内联注释”语法的开头。
3. .*:匹配换行符\n以外的零个或多个字符。这是由于历史原因,因为正则表达式最初是在基于行的工具中使用的。 该行为记录在正则表达式Rust箱子中,如下所示:“ [The character]。 将匹配\n“以外的任何有效UTF-8编码的Unicode标量值。
4. (\r \n | \n | \r):是一个与字符\r \n,\n或\r匹配的匹配组,尝试标识嵌入式注释的结尾。
通常正则表达式引擎会遍历整个正则表达式,尝试将正则表达式中的下一个标记与字符串中的下一个字符进行匹配。 如果找到匹配项,引擎将通过正则表达式和主题字符串前进。 在我们的案例中,Move IR内联注释中的每个字符都将首先由.*表达式匹配,但\n除外,如第3点中所述。最重要的是,\r字符将位于与.*匹配的字符之中。当引擎最终在字符串中找到\n时,它将停止使用其他字符。
这意味着解析器将仅将\n标识为内联注释的末尾,而没有将\r标识为行终止符。 换句话说,与其他常规字符一样,\r被视为注释的一部分,而不是其结尾。
在“案例1”部分中,我们展示了一个恶意模块的概念验证案例,该模块利用了主要编辑器呈现\r字符的方式。
案例2
Move IR解析器处理最常见的换行符CR(\r),LF(\n)和CRLF(\r \n)。除了未能正确解析\r之外,如漏洞–情况1中所述,还有其他几个Unicode字符表示解析器没有检测到的换行符。完整的Unicode换行符集是:
LF(\n或十六进制的0x0A)
VT(\v或十六进制的0x0B)
FF(十六进制的0x0C)
CR(\r或十六进制的0x0D)
CRLF(\r \n或十六进制的0x0D0A)
NEL(十六进位0xC285)
LS(十六进制的0xE280A8)
PS(十六进制的0xE280A9)
字符LF,CR和CRLF是最广泛使用的换行符。但是其中一些流行的Visual Studio IDE等代码编辑器仍可以正确识别一些不太常见的换行符,例如NEL,LS或PS。与案例1一样,这可能导致难以检测到恶意move模块。在案例2中展示了此类示例。
漏洞利用
作为概念证明,我们现在介绍两种恶意模块案例,分别利用“漏洞”部分中描述的两种案例。此操作中引用的所有测试文件都可以在专用存储库中找到:https://github.com/openzeppelin/move-compiler-vulnerability。
案例1: \r
几个代码编辑器和渲染器(例如Visual Studio Code,Sublime Text,Gedit,GitHub)认为\r字符(十六进制为0x0d)是有效的换行符。这意味着当用a\r分隔时,这些编辑器将在两个不同的行中显示内联注释和以下指令。 例如GitHub提供的以下Move模块在声明资源。
但是内嵌注释的末尾有一个隐藏的\r换行符,可以通过检查文件的hexdump来检测到:
$ xxd Module_CR.mvir00000000: 6d6f 6475 6c65 204d 207b 0a20 2020 202f
module M {. /00000010: 2f20 536f 6d65 2063 6f6d 6d65 6e74 0d20 /
Some comment. 00000020: 2020 2072 6573 6f75 7263 6520 417b 783a
resource A{x:00000030: 2075 3634 7d0a 7d0a u64}.}.
如漏洞-案例1部分所述,上述模块实际上将被编译为空模块。
在使用MOVE IR编译器输出进行验证部分,我们将通过更深入地比较为常规和恶意move模块生成的编译器字节码来演示该场景。
案例2:其他换行符
最常用的换行符是\n,\r和\r \n。 但是正如我们之前说过的,文本编辑器呈现换行符的方式因编辑器而异。这是Visual Studio IDE编辑器(由软件开发人员广泛使用)如何呈现一个非常简单的Move模块的方法,其中内联注释和资源声明之间使用常见的\n换行符:
但是此编辑器不仅将LF解释为有效的换行符,还将CRLF,NEL,LS和PS解释为有效的换行符。
接下来,我们演示Visual Studio IDE如何显示相同的简单模块,但分别具有PS,NEL和LS换行符:
可能会诱使用户使用Visual Studio IDE从最后三个文件中的任何一个读取move模块的代码,以为m模块正在声明一个资源,而Move IR编译器会将该指令视为内联注释的一部分。
正如图片文件夹中包含的屏幕截图所示,unix命令行cat和gnome的gedit也显示了类似的行为,与其他主要编辑器(如visual studio代码、atom、sublime文本或github在线编辑器)不同。
验证Move IR编译器的输出
为了简单起见,我们使用两个简单的概念验证模块来展示编译器的输出,每个模块都对应于前面描述的案例1和2。 但是对于Move IR解析器无法检测到的所有其他换行符,可以重现以下内容。 我们使用的编译器是从最新提交8ea33298678117748f1c75112f35a9fbc05b2172生成的。
首先,使用内嵌注释和资源声明(其中所有换行符均为\n)编译一个简单的非恶意Move模块(请参见Module_LF.mvir):
$ ./compiler Module_LF.mvir --module --output lf-output
这种情况下的输出是:
$ xxd -ps lf-output4c49425241564d0a01000801530000000200000002550000000
40000000b59000000020000000d5b00000002000000055d00000006000000046300000
02000000008830000000400000009870000000300000000000001010001020300014d0
1410178000000000000000000000000000000000000000000000000000000000000000
000020100000200
现在编译一个没有资源声明的非恶意Move模块(请参阅Module_Commented.mvir):
$ ./compiler Module_Commented.mvir --module --output commented-output
$ xxd -ps commented-output4c49425241564d0a010004012f000000020000000d31
0000000200000005330000000200000004350000002000000000000300014d00000000
00000000000000000000000000000000000000000000000000000000
现在让我们编译两个不同的恶意Move模块,它们将嵌入式注释伪装为可执行代码。 首先与案例1对应,内联注释和资源声明之间的换行符将是CR字符(\r或十六进制的0x0D;请参见Module_CR.mvir):
$ ./compiler Module_CR.mvir --module --output cr-output
输出
$ xxd -ps ps-output4c49425241564d0a010004012f000000020000000d310000000
200000005330000000200000004350000002000000000000300014d000000000000000
0000000000000000000000000000000000000000000000000
请注意,在两种情况下生成的bytecode与在没有资源声明的模块情况下生成的bytecode相同。这表明编译器确实遗漏了模块中的资源声明,其中注释以CR和PS换行符结尾。
代码更改
在提交5fb715e中,Libra团队对受漏洞影响的代码行进行了更改,并修改了用于识别strip_comments_function中的内联注释的正则表达式。 改进的代码以更简洁的方式匹配以换行符(\n)结尾的注释,但是仍然容易将内联注释伪装为可执行代码。 让我们仔细看一下代码:
fn strip_comments(string: &str) -> String { // Remove line comments
let line_comments = Regex::new(r"(?m)//.*$").unwrap();
line_comments.replace_all(string, "$1").into_owned()}
当使用\n作为换行符时,用于匹配内联注释的正则表达式可以很好地工作,但是,与原始表达式一样,它无法检测\r或其他Unicode标记的内联注释结尾的能力。
为了理解新的错误正则表达式的确切行为,我们分别解释其每个组件:
1. 双引号前面的r是一个用于标识“原始字符串文本”的rust特性,它被用作编写regex字符串的方便方法,而不必转义特殊字符。
2. (?m)标志表示多行模式,这意味着^和$不再仅匹配输入的开头/结尾,而是行的开头/结尾。
3. //:匹配“Move IR内联注释”语法的开头。
4. .*:匹配换行符\n以外的零个或多个字符。
5. $是与换行符(\n)匹配的特殊字符。是否在正则表达式的文档中明确说明此字符与其他Unicode换行符(包括\ r换行符)不匹配。
总而言之,此正则表达式与上一个正则表达式完全一样,除了\n字符以外,不匹配任何其他换行符,从而基于相同的Move IR模块,产生了与原始报告相同的攻击向量和利用用于说明漏洞。
漏洞修复
在提交7efb022中,Libra团队通过引入strip_comments_and_verify函数修复了该漏洞,该函数依次在实际剥离注释之前调用verify_string函数。此函数通过is_permitted_printable_char和is_permitted_newline_char函数确保字符串中的所有字符均为允许的字符。这些功能通过确保字符位于整个字符集的有限范围内来验证字符。特别是明确允许的唯一换行符是\n(0x0A),而\r和其他更深奥的换行符不在is_permitted_printable_char函数定义的范围内。
我们已经使用所有新的行字符测试用例验证了此修复程序。除了\n情况可以正常工作之外,所有其他测试模块现在都引起编译时解析器错误。
结论
我们证明,由于语言解析器中的漏洞,Move模块可以将内联注释伪装为可执行代码。有权在Libra网络中发布Move模块的恶意行为者可能会欺骗与模块进行交互的行为与预期不同的用户。我们得出的结论是,只要不修复漏洞,没有深入分析文件的实际内容或编译后产生的实际bytecode,就不能信任带有内嵌注释的Move IR模块文件来达到预期的效果。
研究了两种情况,最关键的一种是move-ir解析器的源代码如何欺骗开发人员和审计人员,使他们相信广泛使用的换行符被认为是有效的内联注释结尾。为了修复此漏洞,我们建议Move的开发团队修改strip_comments函数中的regex,以便正确解析该字符以及表示换行符的所有其他Unicode字符。为了提高软件的透明度,还建议考虑使用带有所有不同换行符的Move模块进行彻底和广泛的单元测试。(链三丰)