VBScript の難読化と解除

# VBスクリプトの難読化と解除


あまり見たことのない `*.vbe` というファイルを見かけました。

調べてみると、VBスクリプトの難読化をしたファイルみたい。

昔は「Windows Script Encoder」というツールが、Microsoftから提供されていたらしいが、今では提供されておらず、coreとなるDLLを直接呼び出す難読化用のスクリプトが使われているとのこと。

どこが一次情報源かわからなかったけど、とりあえず以下のサイトを参考にしました。

* [Encode and Decode a VB script](https://gallery.technet.microsoft.com/Encode-and-Decode-a-VB-a480d74c)

スクリプトをちょっと引用させてもらいます。

## エンコード（難読化）

エンコード側は結構短め。

```vb:encode.vbs
Option Explicit 
 
dim oEncoder, oFilesToEncode, file, sDest 
dim sFileOut, oFile, oEncFile, oFSO, i 
dim oStream, sSourceFile 
 
set oFilesToEncode = WScript.Arguments 
set oEncoder = CreateObject("Scripting.Encoder") 
For i = 0 to oFilesToEncode.Count - 1 
    set oFSO = CreateObject("Scripting.FileSystemObject") 
    file = oFilesToEncode(i) 
    set oFile = oFSO.GetFile(file) 
    Set oStream = oFile.OpenAsTextStream(1) 
    sSourceFile=oStream.ReadAll 
    oStream.Close 
    sDest = oEncoder.EncodeScriptFile(".vbs",sSourceFile,0,"") 
    sFileOut = Left(file, Len(file) - 3) & "vbe" 
    Set oEncFile = oFSO.CreateTextFile(sFileOut) 
    oEncFile.Write sDest 
    oEncFile.Close 
Next
```

## デコード（難読化の解除）

デコード側は結構長いですが、ほとんどがファイルの読み込みとチェックで、ポイントは `Decode` 関数ですね。


```vb:decode.vbs
Option Explicit

Const BIF_NEWDIALOGSTYLE = &H40
Const BIF_NONEWFOLDERBUTTON = &H200
Const BIF_RETURNONLYFSDIRS = &H1

Const FOR_READING = 1
Const FOR_WRITING = 2


Const TAG_BEGIN1 = "#@~^" 
Const TAG_BEGIN2 = "==" 
Const TAG_BEGIN2_OFFSET = 10 
Const TAG_BEGIN_LEN = 12
Const TAG_END = "==^#~@" 
Const TAG_END_LEN = 6

Dim argv
Dim wsoShellApp
Dim oFolder
Dim sFolder
Dim sFileSource
Dim sFileDest
Dim fso
Dim fld
Dim fc
Dim bEncoded
Dim fSource
Dim tsSource
Dim tsDest
Dim iNumExamined
Dim iNumProcessed
Dim iNumSkipped

Function Decode(Chaine)
 Dim se,i,c,j,index,ChaineTemp
 Dim tDecode(127)
 Const Combinaison="1231232332321323132311233213233211323231311231321323112331123132"

 Set se=WSCript.CreateObject("Scripting.Encoder")
 For i=9 to 127
  tDecode(i)="JLA"
 Next
 For i=9 to 127
  ChaineTemp=Mid(se.EncodeScriptFile(".vbs",string(3,i),0,""),13,3)
  For j=1 to 3
   c=Asc(Mid(ChaineTemp,j,1))
   tDecode(c)=Left(tDecode(c),j-1) & chr(i) & Mid(tDecode(c),j+1)
  Next
 Next
 
 tDecode(42)=Left(tDecode(42),1) & ")" & Right(tDecode(42),1)
 Set se=Nothing

 Chaine=Replace(Replace(Chaine,"@&",chr(10)),"@#",chr(13))
 Chaine=Replace(Replace(Chaine,"@*",">"),"@!","<")
 Chaine=Replace(Chaine,"@$","@")
 index=-1
 For i=1 to Len(Chaine)
  c=asc(Mid(Chaine,i,1))
  If c<128 Then index=index+1
  If (c=9) or ((c>31) and (c<128)) Then
   If (c<>60) and (c<>62) and (c<>64) Then
    Chaine=Left(Chaine,i-1) & Mid(tDecode(c),Mid(Combinaison,(index mod 64)+1,1),1) & Mid(Chaine,i+1)
   End If
  End If
 Next
 Decode=Chaine
End Function

Sub Process (s)
 Dim bProcess
 Dim iTagBeginPos
 Dim iTagEndPos


 iNumExamined = iNumExamined + 1

 iTagBeginPos = Instr(s, TAG_BEGIN1)

 Select Case iTagBeginPos
 Case 0
  MsgBox sFileSource & " does not appear to be encoded.  Missing Beginning Tag.  Skipping file."
  iNumSkipped = iNumSkipped + 1

 Case 1
  If (Instr(iTagBeginPos, s, TAG_BEGIN2) - iTagBeginPos) = TAG_BEGIN2_OFFSET Then
   iTagEndPos = Instr(iTagBeginPos, s, TAG_END)

   If iTagEndPos > 0 Then
    Select Case Mid(s, iTagEndPos + TAG_END_LEN)
    Case "", Chr(0)
     bProcess = True

     If fso.FileExists(sFileDest) Then
      If MsgBox("File """ & sFileDest & """ exists.  Overwrite?", vbYesNo + vbDefaultButton2) <> vbYes Then
       bProcess = False
       iNumSkipped = iNumSkipped + 1
      End If
     End If

     If bProcess Then
      s = Decode(Mid(s, iTagBeginPos + TAG_BEGIN_LEN, iTagEndPos - iTagBeginPos - TAG_BEGIN_LEN - TAG_END_LEN))

      Set tsDest = fso.CreateTextFile(sFileDest, TRUE, FALSE)
      tsDest.Write s
      tsDest.Close
      Set tsDest = Nothing

      iNumProcessed = iNumProcessed + 1
     End If

    Case Else
     MsgBox sFileSource & " does not appear to be encoded.  Found " & Len(Mid(s, iTagEndPos + TAG_END_LEN)) & " characters AFTER Ending Tag.  Skipping file."
     iNumSkipped = iNumSkipped + 1
    End Select

   Else
    MsgBox sFileSource & " does not appear to be encoded.  Missing ending Tag.  Skipping file."
    iNumSkipped = iNumSkipped + 1
   End If

  Else
   MsgBox sFileSource & " does not appear to be encoded.  Incomplete Beginning Tag.  Skipping file."
   iNumSkipped = iNumSkipped + 1
  End If

 Case Else
  MsgBox sFileSource & " does not appear to be encoded.  Found " & (iTagBeginPos - 1) & "characters BEFORE Beginning Tag.  Skipping file."
  iNumSkipped = iNumSkipped + 1
 End Select
End Sub

Set argv = WScript.Arguments

sFileSource = ""
sFolder = ""
iNumExamined = 0
iNumProcessed = 0
iNumSkipped = 0

Select Case argv.Count
Case 0
 Set wsoShellApp = WScript.CreateObject("Shell.Application")

 On Error Resume Next
 set oFolder = wsoShellApp.BrowseForFolder (0, "Select a folder containing files to decode", BIF_NEWDIALOGSTYLE + BIF_NONEWFOLDERBUTTON + BIF_RETURNONLYFSDIRS)
 If Err.Number = 0 Then 
  If TypeName(oFolder) = "Folder3" Then Set oFolder = oFolder.Items.Item
  sFolder = oFolder.Path
 End If
 On Error GoTo 0

 Set oFolder = Nothing
 Set wsoShellApp = Nothing

 If sFolder = "" Then
  MsgBox "Please pass a full file spec or select a folder containing encoded files"
  WScript.Quit 
 End If

Case 1
 sFileSource = argv(0)

 If InStr(sFileSource, "?") > 0 Then 
  MsgBox "Pass a full file spec or no arguments (browse for a folder)"
  WScript.Quit
 End If

Case Else
 MsgBox "Pass a full file spec, -?, /?, ?, or no arguments (browse for a folder)"
 WScript.Quit 
End Select

Set fso = WScript.CreateObject("Scripting.FileSystemObject")

If sFolder <> "" Then
 On Error Resume Next
 Set fld = fso.GetFolder(sFolder)
 If Err.Number <> 0 Then 
  Set fld = Nothing
  Set fso = Nothing
  MsgBox "Folder """ & sFolder & """ is not valid in this context"
  WScript.Quit 
 End If
 On Error GoTo 0

 Set fc = fld.Files

 For Each fSource In fc
  sFileSource = fSource.Path

  Select Case LCase(Right(sFileSource, 4))
  Case ".vbe"
   sFileDest = Left(sFileSource, Len(sFileSource) - 1) & "s"
   bEncoded = True
 
  Case Else
   bEncoded = False
  End Select

  If bEncoded Then
   Set tsSource = fSource.OpenAsTextStream(FOR_READING)
   Process tsSource.ReadAll
   tsSource.Close
   Set tsSource = Nothing
  End If
 Next

 Set fc = Nothing
 Set fld = Nothing

Else
 If Not fso.FileExists(sFileSource) Then
  MsgBox "File """ & sFileSource & """ not found"
 Else
  bEncoded = False

  Select Case LCase(Right(sFileSource, 4))
  Case ".vbe"
   sFileDest = Left(sFileSource, Len(sFileSource) - 1) & "s"
   bEncoded = True
    Case Else
   MsgBox "File """ & sFileSource & """ needs to be of type VBE or JSE"
   bEncoded = False
  End Select

  If bEncoded Then
   Set tsSource = fso.OpenTextFile(sFileSource, FOR_READING)
   Process tsSource.ReadAll
   tsSource.Close
   Set tsSource = Nothing
  End If
 End If
End If

Set fso = Nothing

MsgBox iNumExamined & " Files Examined; " & iNumProcessed & " Files Processed; " & iNumSkipped & " Files Skipped"
```

## 実際にエンコード（難読化）

まずは、適当なスクリプトを作成してみます。

```vb:hello.vbs
WScript.echo "Hello, World."
```

難読化する前に、動作確認のため実行してみます。

```
D:\temp>cscript hello.vbs
Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.

Hello, World.
```

このファイルをエンコード（難読化）して、実行してみます。

```
D:\temp>encode.vbs hello.vbs

D:\temp>type hello.vbe
#@~^IAAAAA==    Um.bwDR+1tK~J_+sVK~~    KDV9 J@#@&igkAAA==^#~@
D:\temp>cscript hello.vbe
Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.

Hello, World.
```

`#@~^IAAAAA==    Um.bwDR+1tK~J_+sVK~~    KDV9 J@#@&igkAAA==^#~@` というのが難読化した結果ですね。

今度は、デコード（難読化解除）してみます。

```
D:\temp>ren hello.vbs hello_org.vbs

D:\temp>decode.vbs hello.vbe

D:\temp>type hello.vbs
WScript.echo "Hello, World."
```

問題なく、デコードされています。

## 日本語が混じったスクリプトのデコード（難読化解除）

ググると、日本語が混在しているスクリプトはデコードがうまくいかないという話がありました。
試してみます。

```vb:hello2.vbs
' 世界に向かって挨拶する
WScript.echo "Hello, 世界."
```

まずは難読化する前に、動作確認します。

```
D:\temp>cscript hello2.vbs
Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.

Hello, 世界.
```

次にエンコード（難読化）して、実行してみます。

```
D:\temp>encode.vbs hello2.vbs

D:\temp>type hello2.vbe
#@~^LgAAAA==v,世界に向かって挨拶する@#@&q?1DkaYcnm4W~J_+ssK~P世界cE@#@&4AcAAA==^#~@

D:\temp>cscript hello2.vbe
Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.

Hello, 世界.
```

いよいよデコード（難読化解除）します。

```
D:\temp>ren hello2.vbs hello2_org.vbs

D:\temp>decode.vbs hello2.vbe

D:\temp>type hello2.vbs
' 世界に向かって挨拶する
I^Ntix%.Pchf,/+emlP,{世界4"
```

想定通り、デコードに失敗しています。

エンコード結果を見るとわかるように、ASCIIの範囲外の文字はそのままで難読化対象外のようです。

そういう観点で見直すと `Decode` 関数で気になる点があります。

```vb
 For i=1 to Len(Chaine)
  c=asc(Mid(Chaine,i,1))
  If c<128 Then index=index+1
  If (c=9) or ((c>31) and (c<128)) Then
   If (c<>60) and (c<>62) and (c<>64) Then
    Chaine=Left(Chaine,i-1) & Mid(tDecode(c),Mid(Combinaison,(index mod 64)+1,1),1) & Mid(Chaine,i+1)
   End If
  End If
 Next
```

index をインクリメントしている個所が `c<128` になっています。
これは、おそらく ASCII 範囲だけインクリメントしたいのだろうと思いますが、このままでは負数の場合におかしいことになりそうです。

試して見ると `"Hello, 世界."` のコードは `[72, 101, 108, 108, 111, 44, 32, -28510, -30139, 46]` のようになります。

```vb:test.vbs
Dim str, cd, ch

str = "Hello, 世界."
For i=1 to Len(str)
 cd=Mid(str,i,1)
 ch=asc(cd)
 WScript.echo ch & " " & cd
Next
```

```
D:\temp>cscript test.vbs
Microsoft (R) Windows Script Host Version 5.812
Copyright (C) Microsoft Corporation. All rights reserved.

72 H
101 e
108 l
108 l
111 o
44 ,
32
-28510 世
-30139 界
46 .
```

VBScript の公式ドキュメントがどこかわかりませんが、.NetやVBAのドキュメントページで `Asc` の戻り値を調べると以下のような記載がありました。

> 戻り値の範囲は、非 DBCS システムでは 0～255 ですが、DBCS システムでは -32768～32767 です。

（元のドキュメントは範囲をenダッシュで記載していますが、わかりにくかったので「～」に置き換えています）

一般的な日本の Windows では CP932 という DBCS(Double Byte Character Set) なので、負の値が返ってくることがありえるわけですね。

そこで、元の `Decode` 関数を以下のように修正して、新しい `decode2.vbs` を作ってみました。

```diff_visual_basic
  For i=1 to Len(Chaine)
   c=asc(Mid(Chaine,i,1))
-  If c<128 Then index=index+1
+  If 0<=c and c<128 Then index=index+1
   If (c=9) or ((c>31) and (c<128)) Then
    If (c<>60) and (c<>62) and (c<>64) Then
     Chaine=Left(Chaine,i-1) & Mid(tDecode(c),Mid(Combinaison,(index mod 64)+1,1),1) & Mid(Chaine,i+1)
    End If
   End If
  Next
```

再度、デコード（難読化解除）してみます。

```
D:\temp>del hello2.vbs

D:\temp>decode2.vbs hello2.vbe

D:\temp>type hello2.vbs
' 世界に向かって挨拶する
WScript.echo "Hello, 世界."
```

デコード（難読化解除）成功です。

## Python でのデコード（難読化解除）

ソースを見る限り、変換テーブルさえ作れれば、どの言語でも難読化の解除ができそうな感じなので、とりあえず Python でデコードしてみました。

```python:decode.py
#!/usr/bin/env python3
# decode for Windows Script Encoder

import sys
import re

COMBINATIONS = [ord(c) - ord('1') for c in \
    list("1231232332321323132311233213233211323231311231321323112331123132")]

DECODE_TABLE = {
    '\t': 'Wn{', ' ': '.-2', '!': 'Gu0', '"': 'zR!',
    '#': 'V`)', '$': 'Bq[', '%': 'j^8', '&': '/I3',
    "'": '&\\=', '(': 'IbX', ')': 'A}:', '*': '4)5',
    '+': '26e', ',': '[ 9', '-': 'v|\\', '.': 'rzV',
    '/': 'C\x7fs', '0': '8kf', '1': '9cN', '2': 'p3E',
    '3': 'E+k', '4': 'hhb', '5': 'qQY', '6': 'Ofx',
    '7': '\tv^', '8': 'b1}', '9': 'DdJ', ':': '#Tm',
    ';': 'uCq', '=': '~:`', '?': '^~S', 'A': 'wEB',
    'B': "J,'", 'C': 'a*H', 'D': ']tr', 'E': '"\'u',
    'F': 'K71', 'G': 'oD7', 'H': 'NyM', 'I': ';YR',
    'J': 'L/"', 'K': 'PoT', 'L': 'g&j', 'M': '*rG',
    'N': '}jd', 'O': 't9-', 'P': 'T{ ', 'Q': '+?\x7f',
    'R': '-8.', 'S': ',wL', 'T': '0g]', 'U': 'nS~',
    'V': 'kGl', 'W': 'f4o', 'X': '5xy', 'Y': '%]t',
    'Z': '!0C', '[': 'd#&', '\\': 'MZv', ']': 'R[%',
    '^': 'cl$', '_': '?H+', '`': '{U(', 'a': 'xp#',
    'b': ')iA', 'c': '(.4', 'd': 'sL\t', 'e': 'Y!*',
    'f': '3$D', 'g': '\x7fN?', 'h': 'mPw', 'i': 'U\t;',
    'j': 'SVU', 'k': '|si', 'l': ':5a', 'm': '_ac',
    'n': 'eKP', 'o': 'FXg', 'p': 'X;Q', 'q': '1WI',
    'r': 'i"O', 's': 'lmF', 't': 'ZMh', 'u': 'H%|',
    'v': "'(6", 'w': '\\Fp', 'x': '=Jn', 'y': '$2z',
    'z': 'yA/', '{': '7=_', '|': '`_K', '}': 'QOZ',
    '~': ' B,', '\x7f': '6eW' }

def decode(chaine):
    in_chaine = chaine.replace("@&", "\n"). \
        replace("@#", "\r").replace("@*", ">"). \
        replace("@!", "<").replace("@$", "@")
    out_chaine = ""
    index = -1
    for c in list(in_chaine):
        if 0 <= ord(c) <= 127:
            index += 1
        out_chaine += DECODE_TABLE[c][COMBINATIONS[index%64]] \
            if c in DECODE_TABLE else c
    return out_chaine

def process(str):
    result = re.match(r'#@~\^......==(.*)......==\^#~@\s*', str)
    if result:
        chaine = result.group(1)
        return decode(chaine)
    else:
        raise Exception("parse failed. " + str)

if __name__ == '__main__':
    if len(sys.argv) != 2 or not sys.argv[1].lower().endswith(".vbe"):
        raise Exception("usage: decode.py FILE_NAME.vbe")
    with open(sys.argv[1], encoding="cp932") as f:
        str = f.read()
    source = process(str)
    print(source)
```

記号がごちゃごちゃしていてわかりにくいですが。
結果は、標準出力に表示するだけです。

```shell-session
$ ./decode.py hello2.vbe
' 世界に向かって挨拶する
WScript.echo "Hello, 世界."
```

文字コード(CP932)と、改行コード(CRLF)に注意すれば大丈夫そう。