Skip to content

Instantly share code, notes, and snippets.

@av-gantimurov
Last active December 4, 2020 07:13
Show Gist options
  • Save av-gantimurov/99caf6986eacf051370b7238b944454f to your computer and use it in GitHub Desktop.
Save av-gantimurov/99caf6986eacf051370b7238b944454f to your computer and use it in GitHub Desktop.
Script for decoding strings in decompiled AgentTesla samples
#!/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))
@av-gantimurov
Copy link
Author

Script for decoding xored string in new AgentTesla malware samples.
Using:

ilspycmd sample.exe > sample.cs
<script> sample.cs

If you want rewrite decompiled source code with decoded strings use '-W' option

@av-gantimurov
Copy link
Author

Sample of output decoded string in gist

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment