Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 218 additions & 0 deletions de4dot.code/deobfuscators/RATMalware/Deobfuscator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
using System;
using System.Collections.Generic;
using System.Text;
using de4dot.blocks;
using dnlib.DotNet;
using dnlib.DotNet.Emit;

namespace de4dot.code.deobfuscators.RATMalware
{
public class DeobfuscatorInfo : DeobfuscatorInfoBase {
internal const string THE_NAME = "RATMalware";
public const string THE_TYPE = "rat";
private const string DEFAULT_REGEX = DeobfuscatorBase.DEFAULT_ASIAN_VALID_NAME_REGEX;

public DeobfuscatorInfo()
: base(DEFAULT_REGEX) {
}

public override string Name => THE_NAME;
public override string Type => THE_TYPE;

public override IDeobfuscator CreateDeobfuscator() {
return new Deobfuscator(new DeobfuscatorBase.OptionsBase {
RenameResourcesInCode = false, ValidNameRegex = validNameRegex.Get()
});
}
}

/**
* Deobfuscator for a type of string obfuscation seen in XWorm RAT samples.
* It uses double base64 encoding and XOR string decryption function for strings considered critical (like IP).
*/
internal class Deobfuscator : DeobfuscatorBase {
private int _score;
private StringDecrypter _stringDecrypter;

public Deobfuscator(OptionsBase options)
: base(options)
{
}

public override string Type => DeobfuscatorInfo.THE_TYPE;
public override string TypeLong => DeobfuscatorInfo.THE_NAME;
public override string Name => TypeLong;

protected override int DetectInternal() => _score;

private static readonly string[] CallChain = new[] {
"System.Byte[] System.Convert::FromBase64String(System.String)",
"System.String System.Text.Encoding::GetString(System.Byte[])",
"System.Byte[] System.Convert::FromBase64String(System.String)",
"System.String System.Text.Encoding::GetString(System.Byte[])"
};

protected override void ScanForObfuscator() {
_stringDecrypter = new StringDecrypter(module);
_stringDecrypter.Find();
/* Don't wanna cause false detections in other binaries
if (_stringDecrypter.Detected) {
_score += 10;
}*/

foreach (var type in module.Types) {
if (!type.HasMethods)
continue;

foreach (var m in type.Methods) {
if (m.Body == null) continue;

// Check for ldstr followed by a bunch of calls.
int state = -1;
foreach (var instr in m.Body.Instructions) {
if (state == -1) {
if (instr.OpCode.Code == Code.Ldstr)
state = 0;
}
else if (instr.OpCode.Code is Code.Call or Code.Callvirt
&& instr.Operand is IMethod calledMethod
&& calledMethod.FullName == CallChain[state]) {
if (++state == CallChain.Length) {
_score += 50;
return;
}
}
else // no call after ldstr
state = -1;
}
}
}
}

/**
* Gets the value of a static string field for a given type.
*/
private static string FindStaticFieldAssignment(TypeDef typeDef, string name) {
var cctor = typeDef.FindStaticConstructor();
var blocks = new Blocks(cctor);
DecodeDoubleBase64(blocks);

string currentStr = null;
foreach (var block in blocks.MethodBlocks.GetAllBlocks()) {
foreach (var instr in block.Instructions) {
if (instr.OpCode.Code == Code.Ldstr) {
currentStr = instr.Operand as string;
}
else if (instr.OpCode.Code == Code.Stsfld && instr.Operand is IField field && field.Name == name) {
return currentStr;
}
}
}

return null;
}

public override void DeobfuscateBegin() {
base.DeobfuscateBegin();

foreach (var decrypterMethod in _stringDecrypter.StringDecrypters) {
var blocks = new Blocks(decrypterMethod);
DecodeDoubleBase64(blocks);
_stringDecrypter.ObtainKey(decrypterMethod, blocks);

staticStringInliner.Add(decrypterMethod,
(method, gim, args) => {
string str;
if (args[0] is FieldDef fieldDef) {
str = FindStaticFieldAssignment(fieldDef.DeclaringType, fieldDef.Name);
if (str == null)
throw new Exception("Unable to find assignment to " + fieldDef.FullName);
}
else
str = (string)args[0];

return _stringDecrypter.Decrypt(method, str);
});
}
DeobfuscatedFile.StringDecryptersAdded();
}

public override IEnumerable<int> GetStringDecrypterMethods() => new List<int>();

public override void DeobfuscateMethodBegin(Blocks blocks)
{
base.DeobfuscateMethodBegin(blocks);

DecodeDoubleBase64(blocks);
}

/**
* Decodes string literals that have been encoded with base64 twice.
*/
private static void DecodeDoubleBase64(Blocks blocks) {
/*
nop
call class [mscorlib]System.Text.Encoding [mscorlib]System.Text.Encoding::get_UTF8()
nop
call class [mscorlib]System.Text.Encoding [mscorlib]System.Text.Encoding::get_UTF8()
ldstr "BASE64-LITERAL"
call uint8[] [mscorlib]System.Convert::FromBase64String(string)
callvirt instance string [mscorlib]System.Text.Encoding::GetString(uint8[])
call uint8[] [mscorlib]System.Convert::FromBase64String(string)
callvirt instance string [mscorlib]System.Text.Encoding::GetString(uint8[])
*/
foreach (var block in blocks.MethodBlocks.GetAllBlocks()) {
int state = 0;
for (int i = 1; i < block.Instructions.Count; i++) {
var instr = block.Instructions[i];
if (instr.OpCode.Code is Code.Call or Code.Callvirt
&& instr.Operand is IMethod calledMethod
&& calledMethod.FullName == CallChain[state]) {
if (++state == CallChain.Length) {
// Check for ldstr above calls.
if (block.Instructions[i - CallChain.Length].OpCode.Code == Code.Ldstr) {
var b64 = (string)block.Instructions[i - CallChain.Length].Operand;
var decoded = Encoding.UTF8.GetString(
Convert.FromBase64String(Encoding.UTF8.GetString(Convert.FromBase64String(b64))));
block.Replace(i - CallChain.Length, 5, OpCodes.Ldstr.ToInstruction(decoded));
i -= CallChain.Length - 1;
// Not sure if the nops between get_UTF8 are always present, so we handle it separately.
i -= KillGetUtf8(block, i - 2);
}

state = 0;
}
}
else {
state = 0;
}
}
}
}

/**
* Removes a sequence of nop/call Encoding.UTF8 going upwards from the start index.
* Returns the number of removed instructions.
*/
private static int KillGetUtf8(Block block, int startIndex) {
for (int i = startIndex; i >= 0; i--) {
var instr = block.Instructions[i];
if ((instr.OpCode.Code == Code.Call && instr.Operand is IMethod { FullName: "System.Text.Encoding System.Text.Encoding::get_UTF8()" })
|| instr.OpCode.Code == Code.Nop) {
if (i == 0) {
block.Remove(0, startIndex + 1);
return startIndex + 1;
}
continue;
}

if (startIndex - i > 0) {
block.Remove(i + 1, startIndex - i);
}
return startIndex - i;
}

return 0;
}
}
}
88 changes: 88 additions & 0 deletions de4dot.code/deobfuscators/RATMalware/StringDecrypter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Linq;
using dnlib.DotNet;
using dnlib.DotNet.Emit;
using de4dot.blocks;

namespace de4dot.code.deobfuscators.RATMalware {
class StringDecrypter {
readonly ModuleDefMD _module;

readonly MethodDefAndDeclaringTypeDict<StringDecrypterInfo> _stringDecrypterMethods = new();

private class StringDecrypterInfo {
public readonly MethodDef Method;
public string Key;
public StringDecrypterInfo(MethodDef method) => Method = method;
}

public bool Detected => _stringDecrypterMethods.Count > 0;

public IEnumerable<MethodDef> StringDecrypters => _stringDecrypterMethods.GetKeys();

public StringDecrypter(ModuleDefMD module) => _module = module;

public void Find() {
foreach (var type in _module.GetTypes()) {
FindStringDecrypterMethods(type);
}
}

void FindStringDecrypterMethods(TypeDef type)
{
foreach (var method in DotNetUtils.FindMethods(type.Methods, "System.String",
new[] { "System.String" })) {
if (!DotNetUtils.CallsMethod(method, "System.Char Microsoft.VisualBasic.Strings::Chr(System.Int32)")
|| !DotNetUtils.CallsMethod(method, "System.Int32 Microsoft.VisualBasic.Strings::Asc(System.Char)")
|| !DotNetUtils.CallsMethod(method, "System.Byte[] System.Convert::FromBase64String(System.String)"))
continue;

// We don't look for the key string here right away, because it might still be encoded.
var info = new StringDecrypterInfo(method);
_stringDecrypterMethods.Add(info.Method, info);
Logger.v("Found string decrypter method " + Utils.RemoveNewlines(info.Method));
}
}

/**
* This callback is called for each call to one of the string decrypter methods.
*/
public string Decrypt(MethodDef method, string str) {
var info = _stringDecrypterMethods.Find(method);
if (info == null)
throw new ArgumentException("Passed method is not a string decrypter");

var key = info.Key;

var bytes = Convert.FromBase64String(str);
var chrArr = new char[bytes.Length];
int i = 0, keyIndex = 0;
foreach (byte b in bytes) {
chrArr[i++] = (char)(b ^ (byte)key[keyIndex]);
keyIndex = (keyIndex + 1) % key.Length;
}

return string.Intern(new string(chrArr));
}

/**
* Should be called to associate a decrypter method with its key, once it has been decrypted.
* We assume the key is simply the first ldstr instruction.
*/
public void ObtainKey(MethodDef method, Blocks blocks) {
var info = _stringDecrypterMethods.Find(method);
if (info == null) throw new ArgumentException("Passed method is not a string decrypter");

foreach (var block in blocks.MethodBlocks.GetAllBlocks()) {
foreach (var instr in block.Instructions.Where(instr => instr.OpCode.Code == Code.Ldstr))
{
info.Key = instr.Operand as string;
return;
}
}

throw new Exception("Could not obtain key for " + method.FullName);
}
}
}
1 change: 1 addition & 0 deletions de4dot.cui/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ static IList<IDeobfuscatorInfo> CreateDeobfuscatorInfos() {
new de4dot.code.deobfuscators.MPRESS.DeobfuscatorInfo(),
new de4dot.code.deobfuscators.Obfuscar.DeobfuscatorInfo(),
new de4dot.code.deobfuscators.Phoenix_Protector.DeobfuscatorInfo(),
new de4dot.code.deobfuscators.RATMalware.DeobfuscatorInfo(),
new de4dot.code.deobfuscators.Rummage.DeobfuscatorInfo(),
new de4dot.code.deobfuscators.Skater_NET.DeobfuscatorInfo(),
new de4dot.code.deobfuscators.SmartAssembly.DeobfuscatorInfo(),
Expand Down
Loading