Skip to content

Commit 5aee68b

Browse files
authored
Merge pull request #4 from GDATAAdvancedAnalytics/ratmalware
Add deobfuscator for string encoding seen in an XWorm sample
2 parents 47540e7 + 4672bd3 commit 5aee68b

3 files changed

Lines changed: 307 additions & 0 deletions

File tree

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using de4dot.blocks;
5+
using dnlib.DotNet;
6+
using dnlib.DotNet.Emit;
7+
8+
namespace de4dot.code.deobfuscators.RATMalware
9+
{
10+
public class DeobfuscatorInfo : DeobfuscatorInfoBase {
11+
internal const string THE_NAME = "RATMalware";
12+
public const string THE_TYPE = "rat";
13+
private const string DEFAULT_REGEX = DeobfuscatorBase.DEFAULT_ASIAN_VALID_NAME_REGEX;
14+
15+
public DeobfuscatorInfo()
16+
: base(DEFAULT_REGEX) {
17+
}
18+
19+
public override string Name => THE_NAME;
20+
public override string Type => THE_TYPE;
21+
22+
public override IDeobfuscator CreateDeobfuscator() {
23+
return new Deobfuscator(new DeobfuscatorBase.OptionsBase {
24+
RenameResourcesInCode = false, ValidNameRegex = validNameRegex.Get()
25+
});
26+
}
27+
}
28+
29+
/**
30+
* Deobfuscator for a type of string obfuscation seen in XWorm RAT samples.
31+
* It uses double base64 encoding and XOR string decryption function for strings considered critical (like IP).
32+
*/
33+
internal class Deobfuscator : DeobfuscatorBase {
34+
private int _score;
35+
private StringDecrypter _stringDecrypter;
36+
37+
public Deobfuscator(OptionsBase options)
38+
: base(options)
39+
{
40+
}
41+
42+
public override string Type => DeobfuscatorInfo.THE_TYPE;
43+
public override string TypeLong => DeobfuscatorInfo.THE_NAME;
44+
public override string Name => TypeLong;
45+
46+
protected override int DetectInternal() => _score;
47+
48+
private static readonly string[] CallChain = new[] {
49+
"System.Byte[] System.Convert::FromBase64String(System.String)",
50+
"System.String System.Text.Encoding::GetString(System.Byte[])",
51+
"System.Byte[] System.Convert::FromBase64String(System.String)",
52+
"System.String System.Text.Encoding::GetString(System.Byte[])"
53+
};
54+
55+
protected override void ScanForObfuscator() {
56+
_stringDecrypter = new StringDecrypter(module);
57+
_stringDecrypter.Find();
58+
/* Don't wanna cause false detections in other binaries
59+
if (_stringDecrypter.Detected) {
60+
_score += 10;
61+
}*/
62+
63+
foreach (var type in module.Types) {
64+
if (!type.HasMethods)
65+
continue;
66+
67+
foreach (var m in type.Methods) {
68+
if (m.Body == null) continue;
69+
70+
// Check for ldstr followed by a bunch of calls.
71+
int state = -1;
72+
foreach (var instr in m.Body.Instructions) {
73+
if (state == -1) {
74+
if (instr.OpCode.Code == Code.Ldstr)
75+
state = 0;
76+
}
77+
else if (instr.OpCode.Code is Code.Call or Code.Callvirt
78+
&& instr.Operand is IMethod calledMethod
79+
&& calledMethod.FullName == CallChain[state]) {
80+
if (++state == CallChain.Length) {
81+
_score += 50;
82+
return;
83+
}
84+
}
85+
else // no call after ldstr
86+
state = -1;
87+
}
88+
}
89+
}
90+
}
91+
92+
/**
93+
* Gets the value of a static string field for a given type.
94+
*/
95+
private static string FindStaticFieldAssignment(TypeDef typeDef, string name) {
96+
var cctor = typeDef.FindStaticConstructor();
97+
var blocks = new Blocks(cctor);
98+
DecodeDoubleBase64(blocks);
99+
100+
string currentStr = null;
101+
foreach (var block in blocks.MethodBlocks.GetAllBlocks()) {
102+
foreach (var instr in block.Instructions) {
103+
if (instr.OpCode.Code == Code.Ldstr) {
104+
currentStr = instr.Operand as string;
105+
}
106+
else if (instr.OpCode.Code == Code.Stsfld && instr.Operand is IField field && field.Name == name) {
107+
return currentStr;
108+
}
109+
}
110+
}
111+
112+
return null;
113+
}
114+
115+
public override void DeobfuscateBegin() {
116+
base.DeobfuscateBegin();
117+
118+
foreach (var decrypterMethod in _stringDecrypter.StringDecrypters) {
119+
var blocks = new Blocks(decrypterMethod);
120+
DecodeDoubleBase64(blocks);
121+
_stringDecrypter.ObtainKey(decrypterMethod, blocks);
122+
123+
staticStringInliner.Add(decrypterMethod,
124+
(method, gim, args) => {
125+
string str;
126+
if (args[0] is FieldDef fieldDef) {
127+
str = FindStaticFieldAssignment(fieldDef.DeclaringType, fieldDef.Name);
128+
if (str == null)
129+
throw new Exception("Unable to find assignment to " + fieldDef.FullName);
130+
}
131+
else
132+
str = (string)args[0];
133+
134+
return _stringDecrypter.Decrypt(method, str);
135+
});
136+
}
137+
DeobfuscatedFile.StringDecryptersAdded();
138+
}
139+
140+
public override IEnumerable<int> GetStringDecrypterMethods() => new List<int>();
141+
142+
public override void DeobfuscateMethodBegin(Blocks blocks)
143+
{
144+
base.DeobfuscateMethodBegin(blocks);
145+
146+
DecodeDoubleBase64(blocks);
147+
}
148+
149+
/**
150+
* Decodes string literals that have been encoded with base64 twice.
151+
*/
152+
private static void DecodeDoubleBase64(Blocks blocks) {
153+
/*
154+
nop
155+
call class [mscorlib]System.Text.Encoding [mscorlib]System.Text.Encoding::get_UTF8()
156+
nop
157+
call class [mscorlib]System.Text.Encoding [mscorlib]System.Text.Encoding::get_UTF8()
158+
ldstr "BASE64-LITERAL"
159+
call uint8[] [mscorlib]System.Convert::FromBase64String(string)
160+
callvirt instance string [mscorlib]System.Text.Encoding::GetString(uint8[])
161+
call uint8[] [mscorlib]System.Convert::FromBase64String(string)
162+
callvirt instance string [mscorlib]System.Text.Encoding::GetString(uint8[])
163+
*/
164+
foreach (var block in blocks.MethodBlocks.GetAllBlocks()) {
165+
int state = 0;
166+
for (int i = 1; i < block.Instructions.Count; i++) {
167+
var instr = block.Instructions[i];
168+
if (instr.OpCode.Code is Code.Call or Code.Callvirt
169+
&& instr.Operand is IMethod calledMethod
170+
&& calledMethod.FullName == CallChain[state]) {
171+
if (++state == CallChain.Length) {
172+
// Check for ldstr above calls.
173+
if (block.Instructions[i - CallChain.Length].OpCode.Code == Code.Ldstr) {
174+
var b64 = (string)block.Instructions[i - CallChain.Length].Operand;
175+
var decoded = Encoding.UTF8.GetString(
176+
Convert.FromBase64String(Encoding.UTF8.GetString(Convert.FromBase64String(b64))));
177+
block.Replace(i - CallChain.Length, 5, OpCodes.Ldstr.ToInstruction(decoded));
178+
i -= CallChain.Length - 1;
179+
// Not sure if the nops between get_UTF8 are always present, so we handle it separately.
180+
i -= KillGetUtf8(block, i - 2);
181+
}
182+
183+
state = 0;
184+
}
185+
}
186+
else {
187+
state = 0;
188+
}
189+
}
190+
}
191+
}
192+
193+
/**
194+
* Removes a sequence of nop/call Encoding.UTF8 going upwards from the start index.
195+
* Returns the number of removed instructions.
196+
*/
197+
private static int KillGetUtf8(Block block, int startIndex) {
198+
for (int i = startIndex; i >= 0; i--) {
199+
var instr = block.Instructions[i];
200+
if ((instr.OpCode.Code == Code.Call && instr.Operand is IMethod { FullName: "System.Text.Encoding System.Text.Encoding::get_UTF8()" })
201+
|| instr.OpCode.Code == Code.Nop) {
202+
if (i == 0) {
203+
block.Remove(0, startIndex + 1);
204+
return startIndex + 1;
205+
}
206+
continue;
207+
}
208+
209+
if (startIndex - i > 0) {
210+
block.Remove(i + 1, startIndex - i);
211+
}
212+
return startIndex - i;
213+
}
214+
215+
return 0;
216+
}
217+
}
218+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using dnlib.DotNet;
5+
using dnlib.DotNet.Emit;
6+
using de4dot.blocks;
7+
8+
namespace de4dot.code.deobfuscators.RATMalware {
9+
class StringDecrypter {
10+
readonly ModuleDefMD _module;
11+
12+
readonly MethodDefAndDeclaringTypeDict<StringDecrypterInfo> _stringDecrypterMethods = new();
13+
14+
private class StringDecrypterInfo {
15+
public readonly MethodDef Method;
16+
public string Key;
17+
public StringDecrypterInfo(MethodDef method) => Method = method;
18+
}
19+
20+
public bool Detected => _stringDecrypterMethods.Count > 0;
21+
22+
public IEnumerable<MethodDef> StringDecrypters => _stringDecrypterMethods.GetKeys();
23+
24+
public StringDecrypter(ModuleDefMD module) => _module = module;
25+
26+
public void Find() {
27+
foreach (var type in _module.GetTypes()) {
28+
FindStringDecrypterMethods(type);
29+
}
30+
}
31+
32+
void FindStringDecrypterMethods(TypeDef type)
33+
{
34+
foreach (var method in DotNetUtils.FindMethods(type.Methods, "System.String",
35+
new[] { "System.String" })) {
36+
if (!DotNetUtils.CallsMethod(method, "System.Char Microsoft.VisualBasic.Strings::Chr(System.Int32)")
37+
|| !DotNetUtils.CallsMethod(method, "System.Int32 Microsoft.VisualBasic.Strings::Asc(System.Char)")
38+
|| !DotNetUtils.CallsMethod(method, "System.Byte[] System.Convert::FromBase64String(System.String)"))
39+
continue;
40+
41+
// We don't look for the key string here right away, because it might still be encoded.
42+
var info = new StringDecrypterInfo(method);
43+
_stringDecrypterMethods.Add(info.Method, info);
44+
Logger.v("Found string decrypter method " + Utils.RemoveNewlines(info.Method));
45+
}
46+
}
47+
48+
/**
49+
* This callback is called for each call to one of the string decrypter methods.
50+
*/
51+
public string Decrypt(MethodDef method, string str) {
52+
var info = _stringDecrypterMethods.Find(method);
53+
if (info == null)
54+
throw new ArgumentException("Passed method is not a string decrypter");
55+
56+
var key = info.Key;
57+
58+
var bytes = Convert.FromBase64String(str);
59+
var chrArr = new char[bytes.Length];
60+
int i = 0, keyIndex = 0;
61+
foreach (byte b in bytes) {
62+
chrArr[i++] = (char)(b ^ (byte)key[keyIndex]);
63+
keyIndex = (keyIndex + 1) % key.Length;
64+
}
65+
66+
return string.Intern(new string(chrArr));
67+
}
68+
69+
/**
70+
* Should be called to associate a decrypter method with its key, once it has been decrypted.
71+
* We assume the key is simply the first ldstr instruction.
72+
*/
73+
public void ObtainKey(MethodDef method, Blocks blocks) {
74+
var info = _stringDecrypterMethods.Find(method);
75+
if (info == null) throw new ArgumentException("Passed method is not a string decrypter");
76+
77+
foreach (var block in blocks.MethodBlocks.GetAllBlocks()) {
78+
foreach (var instr in block.Instructions.Where(instr => instr.OpCode.Code == Code.Ldstr))
79+
{
80+
info.Key = instr.Operand as string;
81+
return;
82+
}
83+
}
84+
85+
throw new Exception("Could not obtain key for " + method.FullName);
86+
}
87+
}
88+
}

de4dot.cui/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ static IList<IDeobfuscatorInfo> CreateDeobfuscatorInfos() {
8888
new de4dot.code.deobfuscators.MPRESS.DeobfuscatorInfo(),
8989
new de4dot.code.deobfuscators.Obfuscar.DeobfuscatorInfo(),
9090
new de4dot.code.deobfuscators.Phoenix_Protector.DeobfuscatorInfo(),
91+
new de4dot.code.deobfuscators.RATMalware.DeobfuscatorInfo(),
9192
new de4dot.code.deobfuscators.Rummage.DeobfuscatorInfo(),
9293
new de4dot.code.deobfuscators.Skater_NET.DeobfuscatorInfo(),
9394
new de4dot.code.deobfuscators.SmartAssembly.DeobfuscatorInfo(),

0 commit comments

Comments
 (0)