标题: VBS字符串的内部实现
作者: Demon
链接: https://demon.tw/programming/vbs-string-internal.html
版权: 本博客的所有文章,都遵守“署名-非商业性使用-相同方式共享 2.5 中国大陆”协议条款。
最近对 VBS 字符串 Chr(0) 注①截断讨论得比较多,看来有必要介绍一下 VBS 字符串的内部实现。Demon 友情提示:本文需要一些 C 语言和 Windows 编程的知识,VBScript 初学者慎入。
VBS 是基于微软的 ActiveX/COM 技术实现的,而 COM 对象为了做到支持任何语言,定义了一系列通用的数据类型,微软称之为自动化对象类型(Automation data types),其中之一就是 BSTR。VBS 在内部是以 BSTR 来表示字符串的,BSTR 在 WTypes.h 中定义:
typedef wchar_t WCHAR; typedef WCHAR OLECHAR; typedef OLECHAR *BSTR;
从定义可以看出,BSTR 是指向 wchar_t 类型(也就是 C 语言中的 Unicode)的指针,但是 BSTR 并不是普通的 wchar_t 指针。标准 BSTR 指向一个有长度前缀和 NUL 结束符的 wchar_t 数组。BSTR 的前4字节是一个表示字符串长度的前缀。BSTR 长度域的值是字符串的字节数,并且不包括 NUL 结束符。常用的 BSTR 处理函数请参考 MSDN 文档。
理论说的有点抽象,下面用代码来说明:
str = "Hello" & Chr(0) & "world"
这是一句很简单的 VBS 代码,但是 VBScript 解释器在内部做了什么呢?其实就是初始化了一个 BSTR 变量(不考虑字符串连接过程):
/* 仅仅为了演示,实际代码肯定不是这样的 */ BSTR str = SysAllocStringLen(L"Hello\0world", 11);
为了更清楚地了解 BSTR 的结构,我们换一种写法:
/* BSTR 包含长度前缀,但是却实际指向第一个字符 */ wchar_t arr[] = {22,0,'H','e','l','l','\0','w','o','r','l','d','\0'}; BSTR str = &arr[2];
这个 BSTR 在内存中的结构为:
00000000 16 00 00 00 48 00 65 00 6C 00 6C 00 6F 00 00 00 00000010 77 00 6F 00 72 00 6C 00 64 00 00 00
橙色表示四个字节的长度前缀。红色高亮表示 BSTR 指针的当前指向,蓝色高亮表示字符串中的 Chr(0) 字符,绿色高亮表示 BSTR 的结束字符 NUL(该字符是 SysAllocStringLen 函数加上去的,因为是 Unicode,所以要占两个字节)。也就是说,如果不考虑前面四个字节,BSTR 就是 C 语言中的 null-terminated string。
再看一段 VBS 代码:
MsgBox Len(str)
用 MsgBox 来显示刚才定义的字符串长度,VBScript 解释器内部又做了什么呢?是不是像 C 语言标准库函数 strlen 一样,遍历整个字符串,以 NUL 作为字符串结束的标识呢?
/* C语言 strlen 函数的简单实现 */ size_t strlen (const char * str) { const char *eos = str; while( *eos++ ) ; return( (int)(eos - str - 1) ); }
答案显然是否定的,因为字符串中含有 Chr(0),如果像 strlen 这样实现,那么就会被 Chr(0) 截断,Len 函数应该返回5才对,然而实际上返回的是11这个正确的数字。
VBS 的 Len 函数内部应该是这么实现的:
/* 同上,仅为演示 */ size_t Len(const BSTR str) { return SysStringLen(str); }
或者不调用 Windows API,由于 BSTR 前4个字节前缀表示字符串的字节数(不包括结尾的 BUL 字符),所以只要移动一下指针就行了:
/* 强制转换成int指针减一后读取,然后除以2(一个Unicode字符两字节) */ size_t Len(const BSTR str) { return *((int *)str - 1) / 2; }
可以看出,由于 BSTR 的长度可以通过前缀取得,并不需要以 NUL 来作为字符串结束符,也就是说,VBS 字符串是 binary safe (二进制安全)的。
那么为什么下面的代码只能显示 Hello 呢?
MsgBox str
这看起来好像和上面说的矛盾,其实不然。VBS 字符串的确是兼容 Chr(0) 字符的,MsgBox 之所以会被 Chr(0) 截断,是因为 MsgBox 在内部调用了 MessageBox 函数,而该函数是以 NUL 作为字符串结束符的。
/* 简单起见只实现一个参数 * MessageBox 的第二个参数是以 NUL 作为结束符的 * Pointer to a null-terminated string that contains the message to be displayed. * 所以 VBS 字符串中包含的 Chr(0) 会把字符串截断 */ int MsgBox(const BSTR str) { return MessageBoxW(NULL, str, L"", 0); }
也就是说,如果 VBS 内置的函数或者 COM 组件的某些方法在其内部实现中调的 Windows API 的字符串参数是以 NUL 作为结束符的话,就会被 Chr(0) 字符截断。
现在再去看《ASP/VBScript中CHR(0)的由来以及带来的安全问题》、《ASP上传漏洞之利用CHR(0)绕过扩展名检测脚本》、《ASP缺陷—-一个特殊字符chr(0)》、《用Python脚本写ASP页面》,应该就不会有疑问了吧。
时间关系就不再展开了,如果你想了解更多关于 COM 组件的知识,我推荐你拜读一下 Jeff Glatt 的神作《COM in plain C》。
仅以此文回答雨中风铃的问题。
注①:本文中 Chr(0) 和 NUL 交替使用,表示同一个意思。
赞赏微信赞赏支付宝赞赏
随机文章:
文章已拜读,非常感谢你的认真解答。
[…] 在《VBS字符串的内部实现》中谈到了 VBS 字符串在内部是以 Unicode 的形式来保存的,然而在外部,VBS 脚本文件的编码却不一定是 Unicode,本文主要探讨一下 VBS 文件编码与 Unicode 的关系。 […]
VBS中怎样避免被chr(0)截断字符串?
[…] 与VBS一样,VB的字符串内部是用BSTR实现的,详见《VBS字符串的内部实现》,这里不赘述。 […]