<-- home

Clipper.2 Writing a static .NET config extractor in Python

Look at that, working perfectly (is proud)

After reverse engineering a generic Clipper I decided to write a static config extractor for it and, as it turns out, coding a config extractor for .NET malware isn’t straight-forward.

The best two ways I’ve found are the following:

The first method loads the binary as .NET assembly (without executing the entry point) and gets the different class members through Reflection. The second method reads and parses the assembly without loading it.

Thus, this blog post will showcase a basic but adaptable Python extractor script using dnlib and pythonnet. The full extractor script is available at the bottom of the page.

Extractor logic #

The different addresses, mutex value, C2 URL and configuration data are stored unencrypted in the Addresses class.

As all the class members are static they are initialised by a call to .cctor (the static/class/type constructor) when the Addresses class is first instantiated.

The malware's config

All that’s needed to extract the malware’s config is to get a reference to Addresses::.cctor and then to parse its CIL.

>>> mod = dnlib.DotNet.ModuleDefMD.Load('./clipper/crack.exe.malware')
>>> cctor = mod.Types[6].Methods[0]
>>> for inst in list(cctor.Body.Instructions):
...     print(inst)
IL_0000: ldstr "0x9e60ca775c5c6c65e900795782be58e0de549615"
IL_0005: stsfld System.String Crypto.Crypto.Addresses::ethereum
IL_000A: ldstr "8AFcmXsQttSXuBeYCL9fpa2rn5JrDwwoihMerrwF48V7Ar1EKNTZyGa6G2tMFMhEZNEReroTLe2gPSMQw6VZLSD65AyBqzD"
IL_000F: stsfld System.String Crypto.Crypto.Addresses::xmr
IL_0014: ldstr "pqdXXeEmLRGXHCg1"
IL_0019: stsfld System.String Crypto.Crypto.Addresses::Mutexx
IL_001E: ldstr "yes"
IL_0023: stsfld System.String Crypto.Crypto.Addresses::startup
IL_0028: ldstr "1H8M6uYCSAquJuZjTjy33ruXs23hZy72E9"
IL_002D: stsfld System.String Crypto.Crypto.Addresses::btc
IL_0032: ldstr "http://www.example.com/log.php"
IL_0037: stsfld System.String Crypto.Crypto.Addresses::url
IL_003C: ldstr "yes"
IL_0041: stsfld System.String Crypto.Crypto.Addresses::ethereumE
IL_0046: ldstr "yes"
IL_004B: stsfld System.String Crypto.Crypto.Addresses::xmrE
IL_0050: ldstr "yes"
IL_0055: stsfld System.String Crypto.Crypto.Addresses::btcE
IL_005A: nop
IL_005B: ret

The method alternates between Ldstr and Stsfld OpCodes. First Ldstr pushes a reference to the string and then Stsfld affects this value to a class member. All that’s left to do is to loop over each pair of lines to generate the config.

Full extractor script #

#!/usr/bin/env python

# Installation
# 1. install pythonnet
#		pip3 install pythonnet
# 2. install the dotnet SDK
#		sudo apt install -y dotnet-sdk-6.0
# 3. build dnlib and copy it to local dir
#		git clone https://github.com/0xd4d/dnlib && cd dnlib
#		dotnet build
#		cp ./Examples/bin/Debug/net6.0/dnlib.dll ..

from pythonnet import load

import clr

import dnlib
from dnlib.DotNet import *

import sys, json, os

def extract(file: str) -> dict:
	mod = dnlib.DotNet.ModuleDefMD.Load(file)
	config = {}

	# v------------ extraction logic goes here ------------v

	insts = list(mod.Types[6].Methods[0].Body.Instructions)

	for val, name in zip(insts[::2], insts[1::2]):
			config[name.Operand.name.ToString()] = val.Operand

	# ^------------ extraction logic goes here ------------^

	return config

if __name__ == '__main__':

	# Take files from both ARGV and STDIN
	files = sys.argv[1:]
	if not sys.stdin.isatty():
		files += [f.strip() for f in sys.stdin]

	if not files:
		print('Usage:\n	Files : ARGV + STDIN\n	Config: STDOUT')

	for file in files:
		# Resolve the actual path of the file
		file = os.path.abspath(os.path.expanduser(os.path.expandvars(file)))

			print('Processing,', file, file=sys.stderr)
			# Write the config to STDOUT
		except Exception as e:
			print('Failed to extract from', file, file=sys.stderr)