246 lines
7.4 KiB
Python
246 lines
7.4 KiB
Python
import json
|
|
import os
|
|
import secrets
|
|
import string
|
|
import sys
|
|
import re
|
|
from enum import Enum
|
|
import argparse
|
|
|
|
|
|
class PSContextType(Enum):
|
|
MAIN = 0
|
|
FUNCTION = 1
|
|
NESTED_FUNCTION = 2
|
|
PARAMS = 3
|
|
COMMENTS = 4
|
|
|
|
|
|
class PSContext:
|
|
def __init__(self, name: str, ctx_type: PSContextType):
|
|
self.last_line = 0
|
|
self.ctx_type = ctx_type
|
|
self.name = name
|
|
self.content = ""
|
|
self.brackets = 0
|
|
self.first_line = 0
|
|
|
|
def open_brackets(self, nb=1):
|
|
self.brackets += nb
|
|
|
|
def close_brackets(self, nb=1):
|
|
self.brackets -= nb
|
|
|
|
def change_context(self, new_context):
|
|
self.ctx_type = new_context
|
|
|
|
|
|
class PSTree:
|
|
def __init__(self, psctx: PSContext):
|
|
self.ctx = [psctx]
|
|
|
|
@property
|
|
def current(self) -> PSContext:
|
|
if not self.is_empty():
|
|
return self.ctx[-1]
|
|
|
|
@property
|
|
def current_ctx_type(self) -> PSContextType:
|
|
if not self.is_empty():
|
|
return self.ctx[-1].ctx_type
|
|
|
|
@property
|
|
def previous(self) -> PSContext:
|
|
self.ctx.pop()
|
|
return self.current
|
|
|
|
@property
|
|
def balanced(self) -> bool:
|
|
if not self.is_empty():
|
|
return self.ctx[-1].brackets == 0
|
|
|
|
def is_empty(self):
|
|
return len(self.ctx) == 0
|
|
|
|
def close(self):
|
|
self.ctx.pop()
|
|
|
|
def add_content(self, value):
|
|
self.current.content += f"\n{value}"
|
|
|
|
def open_brackets(self, nb=1):
|
|
self.current.brackets += nb
|
|
|
|
def close_brackets(self, nb=1):
|
|
self.current.brackets -= nb
|
|
|
|
def change_context(self, ctx_name: str, ctx_type: PSContextType):
|
|
self.ctx.append(PSContext(ctx_name, ctx_type))
|
|
|
|
def extract_data(self):
|
|
var_pattern = re.compile(r'\$[\w|_]+')
|
|
matches = [match.group() for match in var_pattern.finditer(self.current.content)]
|
|
_matches = list(set(matches))
|
|
matches = []
|
|
for e in _matches:
|
|
if e.lower() in ["$true", "$false", "$null", "$_"]:
|
|
continue
|
|
matches.append(e)
|
|
matches.sort(key=len)
|
|
return matches[::-1]
|
|
|
|
def to_string(self):
|
|
return "->".join([self.ctx[i].ctx_type.name for i in range(len(self.ctx))])
|
|
|
|
|
|
def scramble(text):
|
|
new_text = ""
|
|
for char in text:
|
|
if char.islower():
|
|
new_text += secrets.choice(string.ascii_lowercase)
|
|
else:
|
|
new_text += char
|
|
return new_text
|
|
|
|
|
|
def replace_comments(text):
|
|
# Get rid of <# ... #> comments
|
|
_content = text.encode().decode(encoding="windows-1252", errors="replace")
|
|
start = False
|
|
matches = re.finditer(r"<#[^#]+#>", _content, re.MULTILINE)
|
|
for _, match in enumerate(matches, start=1):
|
|
_content = _content.replace(match.group(), "")
|
|
|
|
text = _content.split("\n")
|
|
|
|
rows = []
|
|
for nr, row in enumerate(text, start=1):
|
|
if row.find("<#") > -1:
|
|
start = True
|
|
if row.find("#>") > -1:
|
|
start = False
|
|
if not start:
|
|
rows.append(row)
|
|
|
|
_content = "\n".join(rows)
|
|
|
|
# Single Line Comments
|
|
slc_pattern = re.compile(r"#.+")
|
|
matches = slc_pattern.finditer(_content)
|
|
for _, match in enumerate(matches, start=1):
|
|
_content = _content.replace(match.group(), "")
|
|
return _content
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser(
|
|
description='Chameleon PSMapper - Helper to create obfuscated function mappings'
|
|
)
|
|
parser.add_argument('-o', '--outfile', required=True, type=str, default=None, help='Output file')
|
|
parser.add_argument('target', help='Target PS1 script')
|
|
|
|
args = parser.parse_args()
|
|
|
|
if not os.path.isfile(args.target):
|
|
print(f"[-] File {args.target} not found")
|
|
sys.exit(1)
|
|
|
|
with open(args.target, "r") as ps:
|
|
content = ps.read()
|
|
|
|
content = replace_comments(content)
|
|
content = content.split("\n")
|
|
|
|
tree = PSTree(
|
|
psctx=PSContext(name="main", ctx_type=PSContextType.MAIN)
|
|
)
|
|
|
|
mapping = {}
|
|
function = ""
|
|
|
|
for n, line in enumerate(content, start=1):
|
|
state_changed = False
|
|
see_next = False
|
|
tree.current.current_line = n
|
|
function_match = None
|
|
param_match = None
|
|
ob = 0
|
|
cb = 0
|
|
tree.add_content(line)
|
|
if tree.current_ctx_type in [PSContextType.MAIN, PSContextType.FUNCTION, PSContextType.NESTED_FUNCTION]:
|
|
function_match = re.search(r"(filter|function)\s+([\w][^\s]+)[\{\s]?", line, re.IGNORECASE)
|
|
param_match = re.search(r"param[\s|\n|\r|\(]*\(?", line, re.IGNORECASE)
|
|
|
|
if function_match:
|
|
function = function_match.groups()[1].split("(")[0]
|
|
if function.find(":") > -1:
|
|
function = function.split(":")[1]
|
|
if tree.current_ctx_type == PSContextType.MAIN:
|
|
print(f"Found new function {function} at line: {n}")
|
|
tree.change_context(ctx_name=function, ctx_type=PSContextType.FUNCTION)
|
|
see_next = True
|
|
elif tree.current_ctx_type == PSContextType.FUNCTION:
|
|
print(f"Found new nested function {function} at line: {n}")
|
|
tree.change_context(ctx_name=function, ctx_type=PSContextType.NESTED_FUNCTION)
|
|
see_next = True
|
|
ob = line.count("{")
|
|
if ob == 0:
|
|
see_next = True
|
|
else:
|
|
print(f"Function starts at line: {n}")
|
|
cb = line.count("}")
|
|
elif param_match:
|
|
tree.change_context(ctx_name=tree.current.name + "-param", ctx_type=PSContextType.PARAMS)
|
|
ob = line.count("(")
|
|
cb = line.count(")")
|
|
if ob == 0:
|
|
see_next = True
|
|
else:
|
|
print(f"Param() start at line: {n}")
|
|
else:
|
|
ob = line.count("{")
|
|
cb = line.count("}")
|
|
elif tree.current_ctx_type == PSContextType.PARAMS:
|
|
ob = line.count("(")
|
|
cb = line.count(")")
|
|
if ob == 0 and cb == 0:
|
|
see_next = True
|
|
else:
|
|
continue
|
|
tree.open_brackets(nb=ob)
|
|
tree.close_brackets(nb=cb)
|
|
|
|
if tree.balanced and not see_next:
|
|
if tree.current_ctx_type == PSContextType.PARAMS:
|
|
params = tree.extract_data()
|
|
print(f" > Found parameters: {params}")
|
|
mapping[function] = params
|
|
# Close parameters context
|
|
print(f"Param() close at line {n}")
|
|
tree.close()
|
|
elif tree.current_ctx_type == PSContextType.NESTED_FUNCTION:
|
|
# Close function context
|
|
print(f"Nested function closes at line {n}")
|
|
tree.close()
|
|
elif tree.current_ctx_type == PSContextType.FUNCTION:
|
|
# Close function context
|
|
print(f"Function closes at line {n}")
|
|
tree.close()
|
|
|
|
new_mapping = {}
|
|
for k, v in mapping.items():
|
|
new_params = {}
|
|
new_params["original"] = v
|
|
new_params["repl"] = [scramble(param) for param in v]
|
|
|
|
new_mapping[k] = {
|
|
"repl": scramble(k),
|
|
"params": new_params
|
|
}
|
|
|
|
with open(args.outfile, 'w') as outfile:
|
|
json.dump(new_mapping, outfile)
|
|
|
|
json_formatted = json.dumps(new_mapping, indent=2)
|
|
# print(json_formatted)
|