现在的位置: 首页 Script >正文

VBS操作二进制数据,以及一些杂七杂八

drops和谐前看到lcx牛发表的《昨日黄花--vbscript》,里面提到了无法通过vbs写入任意二进制数据。

前几年折腾星外的时候记得写过类似的代码,翻了翻存档找到一段,正好可以满足这个要求。

文章没啥营养,不过正好可以借此科普一些关于编码的知识。


0x01 字符、编码与字符集


在解决这个问题之前,要先明白字符、编码与字符集的问题。

什么是字符?实际上,字符只是人所能识别的文字。

而文字仅仅只是图形,由人赋予其含义。

例如下面的图片,宋体、楷体和篆体的“傻逼”二字图形虽不同,却代表相同的文字,有着相同的含义。

那么,“傻逼”这两个文字,或者说能表示“傻逼”这两个汉字意义的所有图形,称为逻辑字符,即人类头脑中的抽象字符。


什么是编码?计算机并不能够理解或是储存人类所理解的逻辑字符,只能识别二进制数据。

将逻辑字符通过某种规则转换为二进制数据的过程,就是编码。这个规则的名称,叫做字符编码。进行编码操作的函数代码,称为编码器。

例如常见的UTF-8、GB2312、Unicode-16都是不同的编码规则。0xE582BBE980BC、0xC9B5B1C6、0xBB503C90分别是上述三种编码下“傻逼”这两个逻辑字符。


编码的逆操作称为解码,进行解码操作的函数代码,称为解码器。


什么是字符集?字符集就是逻辑文字的集合,代表了某种编码能够支持的逻辑字符。

在计算机初期是没有字符集这个概念的,因为包括数字和符号在内,英文的逻辑文字不过95个,使用0x20-0x7e即可完全保存,而0x00-0x1f,0x7f则被作为设备控制字符或是特殊符号使用,0x7f以上的所有字符则都认为是非法字符。这种编码称为ascii码。

可惜世界上的语言不光只有英文一种,例如欧洲部分国家的文字会包含注音。由于注音等新符号总数小于0x7f,这些符号被放置于新编码的0x80-0xff位置。根据每个国家语法上的细微差别,诞生了一批新编码,例如ibm437美国编码、ibm850西欧字符等等。

这些编码无一例外,都是用一个字节对应一个逻辑字符,所以统称为单字节编码(sbcs)。


单字节编码并不能满足所有的需求,中文韩文鬼子文等亚洲语言字符众多,有着与欧洲拼音文字完全不同的表现形式。为了表示这些字符,新的编码采取了多个字节表示一个字符的方式。例如中文gb2312编码将所有大于0x80的字节与后面一个字节进行合并处理,unicode-16编码则直接认为两个字节表示一个字符,这种使用多个字节表示一个字符的编码,叫做多字节编码(mbcs)。

而类似gb2312、euc-kr、euc-jp这种以两个字节表示一个字符的编码又有另外一个名称,叫做双字节编码(dbcs)。


显然,每种编码所支持的字符并不相同。一种编码所支持的将二进制数据解析到逻辑字符的范围,就称为这个编码的字符集。为了方便,一般使用十六进制进行表示字符集的范围。

例如ascii码的字符集为0x00-0x7f,gb2312编码的字符集为0xA1A1-0xFEFE等等。

由于字符集和编码相关,这两个词经常混用。即一般所说的字符集就是指代某种编码。


当某种字符编码的编码器进行编码时遇到了在字符集之外的字符,那么将认为这个字符是非法的,这时会使用一个默认字符进行替代。

在绝大多数编码中,这个字符是英文半角问号,即“?”。根据编码器规则不同,其可能为0x3f、0x003f、0x3f00等等。

同样的,当解码器遇到不能识别的二进制数据时,也将返回逻辑字符“?”。


如何从逻辑字符转换为人类可识别的图像?通过字体文件。

字体文件使用矢量图储存了其所支持字符集的所有字符,通过编码后的二进制值可以直接定位。

和字符集占位符类似,当字体文件遇到不支持的逻辑字符,将返回一个默认图像,绝大多数字体的默认占位字符为空白或一个方框。


结合以上信息,我们可以得出结论:

计算机通过某种规则,将人所能识别的图形文字映射为二进制;或通过相同的规则,将二进制映射为人赋予意义的逻辑字符,在字体文件中查找对应的矢量图,并显示给人类。


0x02 adodb.stream流对象


作为唯一具有完整功能的流对象,adodb.stream在vbs中的用途可谓十分广泛。这个流对象有两种模式:二进制模式和字符模式。

二进制模式自然保存了原始二进制数据。由于二进制模式的[adodb.stream]::write方法需要一个Byte()类型的数组,而这个类型在VBS中不能定义,只能通过某些对象的属性或方法来获取,直接向二进制模式的流写入数据是不可能的。

字符模式的流对象中保存了由charset属性指定的字符集编码的字符数据,写入文件则是简单的将其保存的数据直接写入到文件中,不会再次进行编码/解码操作。


所以思路也就很简单了,创建一个字符流,并将其charset属性设置为字符集为0x00-0xff的编码名称,最后将二进制字节作为字符写入流中即可。

这里又涉及到一次转换的问题:由于流本身是一个字符流,那么写入的数据只能是字符,意味着我们要先把字节转换为字符。

vbs有三个字符转换函数:chr、chrw、chrb。

chr使用ansi字符集,即当前系统的默认字符集。由于操作系统的不同,这个字符集也有所不同。所以被排除在外,

chrw使用unicode-16-LE字符集,属于可以使用的标准编码。

chrb内部仅仅返回输入的第一个字节。和VB类似,由于vbs内部传参为BSTR,需要配合chrb(0)将其合并为一个合法的unicode字符。

最终均使用unicode编码,所以输入字节所对应的逻辑字符就确定了,即为unicode-16-LE编码中0x00-0xff所代表的逻辑字符。


为了保证数据无误,这个逻辑字符必须同时在流编码和写入时的编码中对应同一字节。所以现在的问题就变成了:哪种字符集和unicode-16-LE字符集在0x00-0xff字符区间具有相同的逻辑字符?

首先unicode本身是被排除掉的,因为作为dbcs字符集,其每个逻辑字符用两个字节保存,即0x61表示的字符"a"实际上将解码为0x6100。同样道理可以排除其他的dbcs字符集。

同属windows,.net和vbs在编码处理上是相同的。于是在msdn [System.Text.Encoding]::IsSingleByte方法的示例中找到了所有的sbcs字符集,链接为:https://msdn.microsoft.com/zh-cn/library/system.text.encoding.issinglebyte(v=vs.80).aspx

经测试,iso-8859-1编码可以达到此效果。经查询资料得知,iso-8859-1编码又称Latin-1编码,unicode编码对其进行了兼容。同时此编码使用了完整的0x00-0xff字符区间,没有占位符,也就不会导致数据丢失。

所有的问题已经解决,剩下的就是编写代码了。


0x03 测试与利用


明白了原理,构造代码就不难了。首先创建一个流对象,将其模式设置为文本,字符集设置为iso-8859-1,最后通过chrb(byte)&chrb(0)或者chrw(byte)向其中写入数据即可。

测试代码如下:

xorfile "1.bin","2.bin",1
xorfile "2.bin","3.bin",1
hex2file "0102030405060708097f8081828384858687888990e9eaebecedeff0f1f2f3f4f5f6f7fefafdff","4.bin"
msgbox "ok"

function xorfile(infile,outfile,xornum)
  dim ins,outs
  set ins=createobject("adodb.stream")
  set outs=createobject("adodb.stream")
  with outs
    .charset="iso-8859-1"
    .type=2
    .mode=3
    .open
  end with
  with ins
    .type=1
    .mode=3
    .open
    .loadfromfile(infile)
    .position=0
      'DONOT use midb(data_of_stream_read_method,i,1) in the FOR statement,it's very very slow
      'use stream.read(1) to get single bytes,for store them,create a VBS VARIANT array(arr=Array(stream.size)) and fill it
    for i=1 to ins.size
      outs.writetext chrw(ascb(.read(1)) xor xornum)  'chrb(ascb(.read(1)) xor xornum) & chrb(0)
    next
    .close
  end with
  outs.savetofile outfile,2
  set ins=nothing
  set outs=nothing
end function

function hex2file(strhex,outfile)
  dim stm
  set stm=createobject("adodb.stream")
  with stm
    .type=2
    .mode=3
    .charset="iso-8859-1"
    .open
    .position=0
    for i=1 to len(strhex) step 2
      .writetext chrw(cbyte("&h"&mid(strhex,i,2)))
    next
    .savetofile outfile,2
    .close
  end with
  set stm=nothing
end function

脚本会读取同目录下的1.bin,异或后保存为2.bin,再读取2.bin经异或还原为3.bin,最后将0x0102030405060708097f8081828384858687888990e9eaebecedeff0f1f2f3f4f5f6f7fefafdff二进制序列写入4.bin。

我的输入文件是通过winhex填充功能生成的随机加密数据,大小为1M,包含了0x00-0xff全部的字节。对比原始文件和经二次异或的文件,完全相同。

注意注释部分,我没有使用[adodb.stream]::read()方法读取全部的数据并在循环中通过midb(data_of_stream_read_method,i,1)函数取得每个字符,因为midb是字符串函数,vbs处理字符串是非常慢的(在我的测试中,完成同样的功能要花费将近10分钟的时间)。

虽然可以创建一个数组,然后使用[adodb.stream]::read(1)每次读取一个字节并填充数组,但显然不如直接把流当成数组用更简单。


至于利用,可以像lcx牛说的用在DVE上,也可以用在其他不得不用vbs的地方,比如说注入点echo vbs来释放执行getpassword等等。

由于都是组件调用,也可以用在其他任何能调用com组件的地方,例如sqlserver。


0x04 另外两种写入二进制数据的方法


原文提到了使用XMLDOMElement对象将base64字符串转换为二进制的方法,这种方法由于广为周知,其原理不做过多叙述。

由于base64字符串不能一一对应到字节,导致无法通过此方法直接操作二进制数据。

但实际上,XMLDOMElement对象的datatype不光有bin.base64一种。datatype属性表示一个XMLDOMElement的XDR数据类型,参考https://msdn.microsoft.com/en-us/library/ms256121,可以看到除了bin.base64之外,还有bin.hex类型可用。

很显然,这是一个将hex字符串转换为字节的方式,而且由于十六进制字符可以和字节一一对应,操作这个十六进制字符串并进行转换就能达到操作二进制数据的目的。

由于使用十六进制字符串作为中间值,导致效率很低。以下测试代码可以完成与上面脚本相同的功能,但耗时相对较长:

xorfile "1.bin","2.bin",1
xorfile "2.bin","3.bin",1
hex2file "0102030405060708097f8081828384858687888990e9eaebecedeff0f1f2f3f4f5f6f7fefafdff","4.bin"
msgbox "ok"

function xorfile(infile,outfile,xornum)
  dim ins,dom,elm,stm,tmp
  set ins=createobject("adodb.stream")
  set dom=createobject("microsoft.xmldom")
  set elm=dom.createelement("z")
  elm.datatype="bin.hex"
  set stm=createobject("adodb.stream")
  with stm
    .mode=3
    .type=1
    .open
  end with
  with ins
    .type=1
    .mode=3
    .open
    .loadfromfile(infile)
    .position=0
    for i=1 to ins.size
      tmp=hex(ascb(.read(1)) xor xornum)
      elm.text=tmp
      stm.write elm.nodetypedvalue
    next
    stm.savetofile outfile,2
    .close
  end with
  stm.close
  set ins=nothing
  set stm=nothing
  set elm=nothing
  set dom=nothing
end function

function hex2file(strhex,outfile)
  dim dom,elm,stm
  set dom=createobject("microsoft.xmldom")
  set elm=dom.createelement("z")
  elm.datatype="bin.hex"
  elm.text=strhex
  set stm=createobject("adodb.stream")
  with stm
    .mode=3
    .type=1
    .open
    .write elm.nodetypedvalue
    .savetofile outfile,2
    .close
  end with
  set stm=nothing
  set elm=nothing
  set dom=nothing
end function

原文同样提到了另外一种常见做法,使用十六进制字符串结合adodb.recordset组件进行转换。

由于这里直接采用了十六进制字符串作为输入,效仿前面的代码部分,不难得到以下代码。

xorfile "1.bin","2.bin",1
xorfile "2.bin","3.bin",1
hex2file "0102030405060708097f8081828384858687888990e9eaebecedeff0f1f2f3f4f5f6f7fefafdff","4.bin"
msgbox "ok"

function xorfile(infile,outfile,xornum)
  dim ins,rs,stm,tmp
  set ins=createobject("adodb.stream")
  set rs=createobject("adodb.recordset")
  with rs
  .fields.append "z",205,1
  .open
  .addnew
  end with
  set stm=createobject("adodb.stream")
  with stm
    .mode=3
    .type=1
    .open
  end with
  with ins
    .type=1
    .mode=3
    .open
    .loadfromfile(infile)
    .position=0
    for i=1 to ins.size
      tmp=hex(ascb(.read(1)) xor xornum)
      if len(tmp)=1 then 
        tmp="0" & tmp
      end if
      rs("z")=tmp & chrb(0)
      rs.update
      stm.write rs("z").getchunk(1)
    next
    stm.savetofile outfile,2
    .close
  end with
  stm.close
  set ins=nothing
  set rs=nothing
  set stm=nothing
end function

function hex2file(strhex,outfile)
  dim rs,stm
  set rs=createobject("adodb.recordset")
  rs.fields.append "z",205,len(strhex)/2
  rs.open
  rs.addnew
  rs("z")=strhex & chrb(0)
  rs.update
  set stm=createobject("adodb.stream")
  with stm
    .mode=3
    .type=1
    .open
    .write rs("z").getchunk(len(strhex)/2)
    .savetofile outfile,2
    .close
  end with
  rs.close
  set rs=nothing
  set stm=nothing
end function

同样的,由于使用了字符串作为中间值,效率依然不高。对同样1M大小的输入文件进行测试,设置编码的方法耗时6秒,xmldom耗费12秒,recordset耗费26秒。

和使用midb读取[adodb.stream]::read()返回的二进制流类似,如果将所有的十六进制字符拼接为一个大字符串再进行写入,会慢的让人无法忍受,处理同样的输入文件耗时在10分钟以上。


0x05 一些杂七杂八


vbs,或和jscript统称的windows script已经度过了将近二十年,和其衍生的asp一样,已经逐渐退下神坛直至没落。

虽说更新换代是趋势,单并不意味着VBS没有作用。比如说现在用于替代vbs的ps有一个非常大的坑,在渗透中使用是非常容易被察觉的(当然作为桌面脚本要远远强过vbs)。

至于其他脚本如python,在渗透中经常被目标系统的环境所限制,而且就单windows而言,这些linux系的脚本总显得不伦不类。

系统提供了方便的功能和接口函数,为什么非要用庞大繁琐的第三方库。


而且就我个人而言,并不认为vbs有任何做不到的事情。

vbs加载dll有至少十余种方式,例如公开的的[shell.application]::controlpanelitem就是一例。

当一个自定义dll被加载到进程中,我们就可以通过这个dll完全控制这个进程。不要忘了,vbs的宿主是wscript/cscript/w3wp/ie/mshta/rundll32,这是带着微软签名的可信任程序。

一个能任意控制的、带着微软签名的可信任程序有多么可怕?典型例子就是IE DVE,还有比这更优雅的bypass么?

再举两个应用层面的例子,比如说干掉星外:

或是uacbypass:

这些都是13年的发现,在随后14年的IE DVE中,或许有人找到了类似的利用方式。


既然提到DVE,那么多说一句,IE DVE是VBS引擎的漏洞,实际上与IE无关。

DVE使用了修改COleScript.SafetyOption来跳出IE组件安全限制这样一个偷懒的做法,但实际上,这是个任意内存读写的漏洞。

这也就意味着,一个通过wscript/cscript/mshta执行的VBS脚本,借助IE DVE漏洞的代码可以读写整个进程的内存,并做到任意控制。

这是什么?一个shellcode loader。wscript/cscript + IE DVE = shellcode loader。

如果放到某些不打补丁的服务器上,那么通过修改w3wp就可以完全跳出一切安全防护,无论是禁用组件,或是第三方的狗与盾。w3wp + IE DVE = execute bypass。

从VBS跳到逆向,是不是很有意思?windows是一个整体,把几个点结合起来利用有着无穷的威力。

这些都是第三方脚本所无法做到的。


windows是个了不起的发明,windows的互用性和兼容性是其中最了不起的一点。

然而正是因为互用性,使得某些正常的功能调用可以达到意想不到的后果。兼容性则是通用的保障。

也正验证了那句话:越是老旧和基础的模块,出现问题的后果越严重。

把一切结合起来用,vbs/jscript就不再是windows script,而是windows God。

当vbs超脱出vbs,足以击穿大部分的常见防护。

“用(windows script)脚本控制系统”,微软是这么说的,也确实实现了。


0x06 附件


附件中包含了前面的三个脚本,以及一个1M的测试文件。

让大家失望了,没有0x05中所述的几个脚本,因为现在看来这些脚本滥用会造成不小的危害。

部分脚本在合适的时候会放出来,还是有不少有用的地方。

老外也在一直研究这方面,好逼不能都让他们装了。

在此之前还需要几篇文章铺垫,请各位看官稍安勿躁。


下载地址:       vbs_write_binary.zip

百度网盘:http://pan.baidu.com/s/1c2kcA1y

解压密码见注释。