Last active
December 4, 2020 07:13
-
-
Save av-gantimurov/99caf6986eacf051370b7238b944454f to your computer and use it in GitHub Desktop.
Script for decoding strings in decompiled AgentTesla samples
This file contains 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 | |
# -*- coding: utf-8 -*- | |
""" | |
Script for decoding string in AgentTesla source code for samples from Oct2020 | |
Searches for specific class name. Class name may be defined by command arg. | |
Author: Gantimurov Alexander | |
Date: 2020-12-04 10:13 | |
""" | |
import argparse | |
import json | |
import logging | |
import re | |
logger = logging.getLogger(__name__) | |
CLASS_DEF_NAME = "namespace <PrivateImplementationDetails>" | |
XOR_KEY = 170 | |
def prepare_argparse(): | |
parser = argparse.ArgumentParser( | |
description='[Add module description]') | |
parser.add_argument('-v', '--verbosity', | |
action="count", | |
default=0, | |
help="Increase output verbosity") | |
parser.add_argument('--key', | |
dest='key', | |
type=int, | |
default=XOR_KEY, | |
help='xor key for decrypting strings (default 0x{:X})' | |
.format(XOR_KEY)) | |
parser.add_argument('-D', '--debug', | |
dest='debug', | |
help='Debug', | |
action="store_true") | |
parser.add_argument('file', | |
help='File or directories to parse', | |
metavar='FILE' | |
) | |
parser.add_argument('--namespace', | |
dest='ns', | |
help='Name of class for search, default {!r}' | |
.format(CLASS_DEF_NAME), | |
default=CLASS_DEF_NAME) | |
parser.add_argument('-W', '--write', | |
dest='write', | |
help='Output rewritten string of source file', | |
metavar='FILE' | |
) | |
return parser | |
def read_str_tuples(lines, namespace_prefix, debug=False): | |
""" | |
Trying to find tuples of strings like | |
public static string A() | |
{ | |
return 5C3A5EFF-0EBA-40BD-AA04-F848E6988197.[0] ?? (0, 0, 0); | |
} | |
public static string a() | |
{ | |
return 5C3A5EFF-0EBA-40BD-AA04-F848E6988197.[1] ?? (1, 0, 2); | |
} | |
public static string B() | |
{ | |
return 5C3A5EFF-0EBA-40BD-AA04-F848E6988197.[2] ?? (2, 2, 19); | |
} | |
""" | |
is_ns_found = False | |
is_class_found = False | |
method_prefix = "public static string" | |
class_prefix = "internal class" | |
pattern = re.compile(r"\?\?\s*\w*\((\d+),\s*(\d+),\s*(\d+)\);") | |
methods = {} | |
method = "" | |
namespace_name = "" | |
class_name = "" | |
for ind, line in enumerate(lines): | |
line = line.strip() | |
# if debug: | |
# print("{}: {} --> '{}'".format(ind, repr(line), line)) | |
if not is_ns_found: | |
if line.startswith(namespace_prefix): | |
ns_found = re.findall(r"{([0-9A-Fa-f-]+)}", line) | |
if ns_found: | |
namespace_name = ns_found[0] | |
is_ns_found = True | |
if debug: | |
print("found namespace {}" | |
.format(namespace_name)) | |
continue | |
if not is_class_found and line.startswith(class_prefix): | |
class_name = line.replace(class_prefix, '', 1).strip() | |
if debug: | |
print("found class {}".format(class_name)) | |
continue | |
if line.startswith(method_prefix): | |
is_class_found = True | |
method = line.replace(method_prefix, '', 1).strip() | |
if debug: | |
print("found method name {!r} --> {!r}" | |
.format(line, method)) | |
continue | |
if method: | |
found = pattern.findall(line) | |
if found: | |
methods[method] = tuple(tuple(tuple(map(int, found[0])))) | |
if debug: | |
print("Found method {} --> {}".format(method, found[0])) | |
return methods, namespace_name, class_name | |
def rewrite_source(source, class_name, strings, debug=False): | |
""" | |
Try rewrite sources with decrypted strings for later analysis | |
Using invoke method and crypted index, in each sample they different | |
""" | |
print("Searching invoke methods '{}'".format(class_name)) | |
rep_cnt = 0 | |
new_lines = [] | |
for line in source: | |
if not line.strip(): | |
new_lines.append(line) | |
continue | |
old_line = "" | |
for method, string in strings.items(): | |
repl = "{}.{}".format(class_name, method) | |
if repl in line: | |
if not old_line: | |
old_line = line | |
line = line.replace(repl, json.dumps(string)) | |
rep_cnt += 1 | |
if debug and old_line: | |
print("old: {!r}".format(old_line)) | |
print("new: {!r}".format(line)) | |
new_lines.append(line) | |
print("Made {} replaces".format(rep_cnt)) | |
return new_lines | |
def read_bytes(lines, namespace_prefix, debug=False): | |
""" | |
Read bytes array of crypted strings | |
""" | |
is_ns_found = False | |
is_array_found = False | |
is_array_byte_found = False | |
prefix_array = re.compile(r"internal\s+static\s+byte\[\]\s+") | |
pattern_byte = re.compile(r"(\d+),") | |
array = bytearray() | |
for ind, line in enumerate(lines): | |
line = line.strip() | |
# if debug: | |
# print("{}: {} --> '{}'".format(ind, repr(line), line)) | |
if line.startswith(namespace_prefix): | |
is_ns_found = True | |
continue | |
if not is_ns_found: | |
continue | |
if prefix_array.search(line): | |
is_array_found = True | |
if debug: | |
print("found crypted array at {}".format(ind)) | |
if is_array_found: | |
found = pattern_byte.findall(line) | |
if found: | |
array.append(int(found[0]) & 0xFF) | |
is_array_byte_found = True | |
# if debug: | |
# print("add byte {} to array".format(found[0])) | |
elif is_array_byte_found: | |
if debug: | |
print("all bytes found, finished at {}, total {} bytes" | |
.format(ind, len(array))) | |
break | |
return array | |
def decrypt_strings(crypted_array, marks, key, debug=False): | |
str_b = bytearray((ind ^ val ^ key) & 0xFF | |
for ind, val in enumerate(crypted_array)) | |
strs = {} | |
for method in marks: | |
# print("{} {} {}".format(num, start, length)) | |
# if method == "class" or method == "namespace": | |
# continue | |
if isinstance(marks[method], tuple): | |
num, start, length = marks[method] | |
strs[method] = str_b[start:start + length].decode('utf-8') | |
if debug: | |
print("{met} --> {s!r}".format(met=method, | |
i=num, | |
s=strs[method])) | |
return strs | |
if __name__ == '__main__': | |
parser = prepare_argparse() | |
args = parser.parse_args() | |
lines = [] | |
if args.verbosity > 0: | |
print(args) | |
with open(args.file, 'r') as f: | |
lines = f.readlines() | |
if lines: | |
marks, ns_name, class_name = read_str_tuples(lines=lines, | |
namespace_prefix=args.ns, | |
debug=args.debug) | |
print("Found namespace {!r}".format(ns_name)) | |
print("Found class {!r}".format(class_name)) | |
print("Found {} crypted strings".format(len(marks))) | |
cr_array = read_bytes(lines=lines, | |
namespace_prefix=args.ns, | |
debug=args.debug) | |
print("Found crypted array with {} bytes".format(len(cr_array))) | |
str_dict = decrypt_strings(debug=args.debug, | |
key=args.key, | |
crypted_array=cr_array, | |
marks=marks) | |
print("Decrypted {} strings".format(len(str_dict))) | |
if str_dict: | |
if args.write: | |
with open(args.write, 'w') as fw: | |
out = rewrite_source( | |
source=lines, | |
class_name=class_name, | |
strings=str_dict, | |
debug=args.debug) | |
fw.write(''.join(out)) | |
print("Decrypted source saved to {}".format(args.write)) | |
else: | |
print("Extracted {} strings".format(len(str_dict))) | |
for m in sorted(str_dict.values()): | |
print("{!r}".format(m)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Sample of output decoded string in gist