Last active
April 15, 2026 12:24
-
-
Save cmeslay/9ff499776ca2c2d84890c30a16001c20 to your computer and use it in GitHub Desktop.
Script used to automatically decrypt strings in some Covenant samples of APT28
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
| # pip install pythonnet (works with python < 3.14) | |
| # You can test this script with the following file: 69609e89b04d8d27dc47bda2971376cfd760abb40ffe325f00d0cf3303be8906 | |
| # https://bazaar.abuse.ch/sample/69609e89b04d8d27dc47bda2971376cfd760abb40ffe325f00d0cf3303be8906/ | |
| import clr,sys | |
| import binascii | |
| import re | |
| # Add dlls to reverse .NET. You could find them in REPythonNET-MCP projects (into the `dll/`` directory) | |
| clr.AddReference("dnlib") | |
| import base64 | |
| import dnlib | |
| from dnlib.DotNet import * | |
| from dnlib.DotNet.Emit import OpCodes | |
| from dnlib.DotNet.Writer import ModuleWriterOptions | |
| module = dnlib.DotNet.ModuleDefMD.Load("/tmp/input.dll") | |
| signature = ["nop", "ldloc.0", "ldarg.0", "ldloc.1", "ldelem.u1", "ldsfld", "ldloc.1", "ldsfld", "callvirt", "rem", "callvirt", "xor", "conv.u1", "callvirt", "nop", "nop", "ldloc.1", "ldc.i4.1", "add", "stloc.1", "ldloc.1", "ldarg.0", "ldlen", "conv.i4", "clt", "stloc.2", "ldloc.2", "brtrue.s"] | |
| def has_config_pattern(method): | |
| if method.HasBody: | |
| if len(method.Body.Instructions) >= len(signature): | |
| ins = [x.OpCode.Name for x in method.Body.Instructions] | |
| if len(signature) > len(ins): | |
| return False | |
| for i in range(len(ins) - len(signature) + 1): | |
| if ins[i:i+len(signature)] == signature: | |
| return True | |
| return False | |
| def xor_with_repeating_key(data: bytes, key: bytes) -> bytes: | |
| """ | |
| XOR data with key | |
| """ | |
| if not key: | |
| raise ValueError("Key empty") | |
| return bytes(data[i] ^ key[i % len(key)] for i in range(len(data))) | |
| def decrypt_str(encrypted_str, key): | |
| tmp = binascii.a2b_base64(encrypted_str) | |
| tmp = xor_with_repeating_key(tmp, key.encode("utf-8")) | |
| return tmp.decode("utf-8") | |
| XorFunction = "" | |
| KeyName = "" | |
| KeyClassName = "" | |
| for t in module.GetTypes(): | |
| for m in t.Methods: | |
| if not m.HasBody: | |
| continue | |
| if has_config_pattern(m): | |
| XorFunction = m.Name | |
| for instr in m.Body.Instructions: | |
| if instr.OpCode.Name == "ldsfld": | |
| KeyName = instr.Operand.Name | |
| KeyClassName = instr.Operand.get_DeclaringType() | |
| break | |
| print("XorFunction: {}".format(XorFunction)) | |
| print("KeyName {}".format(KeyName)) | |
| print("KeyClassName {}".format(KeyClassName)) | |
| # find the class in the assembly | |
| decryption_class = next(t for t in module.GetTypes() if t.FullName == KeyClassName.FullName) | |
| # find the class in the constructor | |
| cctor = next((m for m in decryption_class.Methods if m.Name == ".cctor"), None) | |
| # Find the key initialization and get the value | |
| found = False | |
| for instr in cctor.Body.Instructions: | |
| if found and instr and instr.OpCode == OpCodes.Stsfld and instr.Operand.Name == KeyName: | |
| break | |
| elif instr.OpCode == OpCodes.Ldstr: | |
| found = True | |
| key = instr.Operand | |
| else: | |
| found = False | |
| print("KeyValue : {}".format(key)) | |
| # Find the calling function inside the class: | |
| DecryptionFunction = "" | |
| for m in decryption_class.Methods: | |
| for instr in m.Body.Instructions: | |
| if instr.OpCode.Name == "call": | |
| if instr.Operand.Name == XorFunction: | |
| DecryptionFunction = m.Name | |
| print("DecryptionFunction: {}".format(DecryptionFunction)) | |
| # Now we can find calls to the decryption function: | |
| # - modify the opcode of the previous instruction by the decrypted string | |
| # - modify the opcode of the current instruction (call) by a nop | |
| previous_instr = None | |
| for t in module.GetTypes(): | |
| for m in t.Methods: | |
| if not m.HasBody: | |
| continue | |
| for instr in m.Body.Instructions: | |
| if instr.OpCode.Name == "ldstr": | |
| previous_instr = instr | |
| elif instr.OpCode.Name == "call" and instr.Operand.Name == DecryptionFunction: | |
| decrypted_str = decrypt_str(previous_instr.Operand, key) | |
| print("{} decrypted to {}".format(previous_instr.Operand, decrypted_str)) | |
| previous_instr.Operand = decrypted_str | |
| instr.OpCode = OpCodes.Nop | |
| else: | |
| previous_instr = None | |
| opts = ModuleWriterOptions(module) | |
| opts.Logger = None | |
| module.Write("/tmp/output.dll", opts) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment