Last active
June 20, 2023 10:33
-
-
Save notdodo/3d5ac56cd837c3f79d2c687f3e75cac1 to your computer and use it in GitHub Desktop.
Deobfuscate a powershell script with re-ordering obfuscation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# | |
# AUTHOR: Edoardo Rosa notdodo https://github.com/notdodo | |
# https://twitter.com/_d_0_d_o_ | |
# | |
# Sample: ("{0}{1}{4}{3}{5}{2}" -f 'CONv','er','G','R','tTo-SecURest','In') | |
# Decoded output: CONvertTo-SecURestRInG | |
# | |
try: | |
from pathlib import Path | |
import argparse | |
import re | |
import sys | |
except ModuleNotFoundError: | |
print("Please install pathlib, argparse") | |
DEBUG = False | |
description = "Clean an obfuscated string re-ordered powershell script" | |
charset = r"\w|\d|\n|\s|,|.|\-|=|/|:|#|_|{|}|\[|\]" | |
# Find format reoder pattern | |
regex_finder = r"\(\"([{\d+}]+)\"\s*-f\s*(['" + charset + "',]+)\)" | |
# Extract positions | |
regex_position = r"{(\d+)}" | |
# Extract payload strings | |
regex_content = r"'([" + charset + "]+)'" | |
# Match custom placeholder | |
regex_placeholder = r"({#subs_\d+})" | |
# Match [char]\d | |
regex_char = r"\[char\](\d+)" | |
# Match concatenation of strings | |
regex_concat = r"\((('|\"[\w|\s|\$]+'|\"\+|.)+)\)" | |
def decode(source, destination): | |
# Open file and return the content in string | |
def open_ps1(path): | |
try: | |
with open(str(path), "r") as ps1: | |
ps1_content = ps1.read() | |
except: | |
print("Error opening file") | |
sys.exit(-1) | |
return ps1_content | |
# Create a copy of the file decoded | |
def save_to_file(path, content): | |
try: | |
with open(path, "w+") as dec: | |
dec.write(content) | |
except: | |
print("Error on saving the file") | |
sys.exit(-1) | |
# Remove known issues | |
def clean_before(ps1_content): | |
matches = re.finditer(regex_char, ps1_content, re.IGNORECASE) | |
for _, word in enumerate(matches): | |
total = word.group() | |
letter = int(word.groups()[0], 10) | |
ps1_content = ps1_content.replace(total, "'" + chr(letter) + "'") | |
ps1_content = ps1_content.replace("`", "") | |
return ps1_content | |
# For each pattern found resolve it a regenerate the content | |
def parse_content(ps1_content): | |
ps1_content = clean_before(ps1_content) | |
count_matches = 0 | |
occurrences = {} | |
while re.search(regex_finder, ps1_content, re.IGNORECASE): | |
matches = re.finditer(regex_finder, ps1_content, re.IGNORECASE) | |
for _, word in enumerate(matches): | |
if DEBUG: | |
print(word.groups()[0]) | |
print(word.groups()[1]) | |
# Array of indexes (in string) | |
positions = re.findall( | |
regex_position, | |
word.groups()[0].replace("\n", "").strip(), | |
re.IGNORECASE, | |
) | |
# Array of string segments | |
contents = re.findall( | |
regex_content, | |
word.groups()[1].replace("\n", "").strip(), | |
re.IGNORECASE, | |
) | |
if len(positions) == len(contents): | |
# out = ''.join([contents[int(p)].strip() for p in positions]) | |
out = "" | |
for p in positions: | |
# Resolve second level expressions | |
if re.match(regex_placeholder, contents[int(p)]): | |
out += occurrences["'" + contents[int(p)] + "'"] | |
else: | |
out += contents[int(p)].strip() | |
placeholder = "'{#subs_" + str(count_matches) + "}'" | |
# Resolve and save expression or sub-expression | |
occurrences[placeholder] = out | |
# Put a placeholder for later substitution | |
ps1_content = ps1_content.replace(word.group(), placeholder) | |
count_matches += 1 | |
else: | |
print("Did you include all chars in 'charset' var?") | |
print("[i] EXCEPTION OCCURRED IN: " + word.group()) | |
sys.exit(-1) | |
# Print all resolved 1st level/basic expressions | |
subs = re.finditer(regex_placeholder, ps1_content, re.IGNORECASE) | |
for _, word in enumerate(subs): | |
ps1_content = ps1_content.replace( | |
"'" + word.group() + "'", occurrences["'" + word.group() + "'"] | |
) | |
return ps1_content, occurrences | |
new_content, occurrences = parse_content(open_ps1(source)) | |
save_to_file(destination, new_content) | |
print("Decoded {} occurrences:".format(len(occurrences))) | |
print(destination) | |
if __name__ == "__main__": | |
parser = argparse.ArgumentParser(description=description) | |
parser.add_argument("file", help="Input file") | |
args = parser.parse_args() | |
ps1 = Path(args.file) | |
if ps1.is_file(): | |
ps1 = Path(args.file).resolve() | |
decode_file = str(ps1.parent) + "/decoded_" + str(ps1.name) | |
decode(ps1, decode_file) | |
else: | |
print(ps1) | |
print("Not a valid file!") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment