ScoringMathTea Analysis: Inside the Lazarus Group’s Flagship Cyber Espionage Malware


Immersive’s Container 7 research team has been investigating a complex piece of malware linked to Operation DreamJob. It’s used by Lazarus, a North Korean state-sponsored threat actor, to target and steal information from European defense companies, as well as a British industrial automation company.
This blog post briefly covers the wider geopolitical implications of a North Korean cyber espionage campaign targeting the West, before delving into Lazarus’s ScoringMathTea remote access trojan (RAT), reverse-engineering it, and identifying its custom cipher used to obfuscate crucial indicators of compromise (IoCs).
Key findings
- ScoringMathTea is a complex RAT supporting around 40 commands.
- Prototypes of this malware were identified in 2023 and named ForestTiger by Microsoft as a memory-resistant backdoor.
- The malware uses a rolling substitution cipher with a custom alphabet to encrypt its configuration data.
- The malware has enumeration and anti-analysis commands written into it to check the environment, before initiating command and control (C2) communication.
- The malware uses a .dat file sent back and forth from the attackers’ C2 server to issue commands.
- ScoringMathTea has been written with an emphasis on anti-analysis, so static analysis tools don’t identify export functions to hook onto. This makes analysis more complex.
Overview
The Lazarus Group, also known as Hidden Cobra, Labyrinth Chollima, and Guardians of Peace, is a highly prolific, state-sponsored advanced persistent threat (APT) group believed to be run by the Reconnaissance General Bureau (RGB) of the North Korean government. While the group is headquartered in Pyongyang, its operators frequently work remotely.
Lazarus is unique among state-backed groups for its heavy focus on financial crime, which generates billions in illicit revenue for the regime. This includes cyber extortion and substantial heists against banks and cryptocurrency exchanges, such as the $81 million Bangladesh Bank robbery and the $625 million Ronin Network hack.
The group also conducts traditional cyber espionage campaigns, such as the 2014 Sony Pictures Entertainment attack and the spread of the WannaCry ransomware in 2017.
Lazarus has a history of being a highly aggressive adversary in its effort to further North Korean state objectives, whether it’s stealing money or data relating to the operation of Western military assets.
North Korea’s cyber strategy
The North Korean government views its sophisticated cyber capability as a critical component of its asymmetric warfare strategy, designed to counter the conventional military superiority of its adversaries. The Reconnaissance General Bureau (RGB) primarily orchestrates its operations and oversees specialist cyber units, such as the Lazarus Group.
Given North Korea's limited domestic internet infrastructure, a large number of these state-sponsored hackers often operate from abroad in countries like China and Russia to evade detection and access more reliable connections.
This centralized strategic planning, coupled with decentralized execution, enables Pyongyang to conduct high-impact, low-cost operations that are difficult to attribute definitively. They support the primary goal of regime survival and the advancement of its strategic programs.
The diagram below shows our best understanding of how the North Korean government and its associated cyber strategy are organized. Solid lines represent confirmed connections based on Mandiant's best assessment; dotted lines represent suspected connections. Lazarus is working under the RGB.

This diagram shows our best understanding of how the North Korean government and cyber strategy works. Kim Jong-Un sits at the top as the supreme leader; the diagram then splits off into three branches. The leftmost branch contains the "Central Committee of the Workers; Party of Korea", comprising IT workers and the united front department. The center branch contains the general staff department and the "Reconnaissance General Bureau" with multiple threat actor groups sat under it, including Lazarus. The rightmost branch comprises the Ministry of State Security, with other state-sponsored groups sitting under it.
North Korea's cyber strategy is driven by three main strategic objectives: generating revenue, conducting espionage, and causing disruption.
Due to stringent international sanctions, the regime relies heavily on illicit cyber activity to finance its military and, in particular, its nuclear and ballistic missile programs.
This financial component involves large-scale cyber heists. These often target global financial institutions and, increasingly, cryptocurrency exchanges, resulting in the theft of billions of dollars. Simultaneously, their espionage efforts target military, government, and high-tech sectors globally to steal critical scientific, technological, and defense information.
Finally, the element of disruption aims to sow political and social instability in adversary nations, as seen in the destructive attacks against South Korean networks and high-profile ransomware campaigns. These multi-faceted operations demonstrate a pragmatic and evolving approach to cyber warfare, positioning it as a core instrument of North Korean foreign policy.
Operation DreamJob
Operation DreamJob is a long-running, persistent cyber espionage campaign conducted by the Lazarus Group. The operation is characterized by its reliance on sophisticated social engineering to gain initial access, using the lure of a "dream job" at a prestigious company to trick high-value targets.
How Operation DreamJob works:
- Social engineering: The core of the operation involves attackers engaging with targets – predominantly individuals in the aerospace, defense, engineering, and technology sectors – by offering seemingly lucrative but fake job opportunities.
- Infection method: The victim is persuaded to download a lure document (such as a job description), which is opened with a bundled, trojanized open-source application (like a modified PDF reader or software plugin). This malicious application is used for dynamic link library (DLL) side-loading to deploy the initial stages of the attack.
- Payload deployment: Once executed, a series of custom droppers and loaders inject the main payload – in this case, ScoringMathTea RAT.
- Strategic objectives (espionage and revenue): The primary goal of Operation DreamJob is cyber espionage, specifically the theft of sensitive data, intellectual property, and proprietary manufacturing information. This aligns with North Korea's broader strategy to advance its military capabilities, recently focusing on unmanned aerial vehicle (UAV) technology to help perfect its domestic drone program.
ScoringMathTea
During the latest campaign attributed to Lazarus as part of Operation DreamJob, Lazarus targeted multiple defense companies in Europe.
Researchers at ESET reported that attacks against several European UAV companies can be tied back to North Korea's geopolitical objectives to scale up its drone program. Lazarus's priority intelligence requirements (PIRs) from Pyongyang would suggest the theft of proprietary information to improve its drone manufacturing process.
ScoringMathTea is Lazarus's “flagship” malware that the group has used in some of its most prolific and successful campaigns. It’s a highly complex, remote access trojan (RAT) and has been seen in a number of attacks, including:
- A British industrial automation company in October 2023
- A Polish defense company in March 2023
- An Indian technology company in January 2023
ESET provides more details on how ScoringMathTea is moved into the victim environment via loaders. The next section of this blog will analyze ScoringMathTea itself.
Analysis
Given that the malware is a DLL file, the most obvious first action was to identify the exports directory to see what functions it exposes and what ordinal it’s associated with.
In this case, the DLL didn’t appear to have any exports, which isn’t too strange. The absence of a public export directory is normal in many DLL files, including ones produced by Microsoft. What this means is that instead of being called through system utilities like rundll32.exe, this malware relies on a preceding loader to load the DLL's entry point (specifically during the DLL_PROCESS_ATTACH event).
Using Capa to get the initial metadata of the DLL also provides the base address and some insight into the malware's malicious functionality. By passing a folder containing Capa rules as an argument, it's also possible to obtain the specific offsets where the suspected malicious functionality resides.
analysis static
extractor VivisectFeatureExtractor
base address 0x180000000
rules C:/Users/windo/Desktop/Tools/capa-rules-master
function count 458
library function count 636
total feature count 35607
contain obfuscated stackstrings (9 matches)
namespace anti-analysis/obfuscation/string/stackstring
scope basic block
matches 0x180001CBC
0x180001DB4
0x180001E97
0x180001EC6
0x180001F18
0x1800035D0
0x18001153C
0x180011F93
0x18001602C
Anti-analysis functionality immediately becomes the most interesting aspect, as understanding how it can be defeated early on makes later analysis easier. The next step is to load the sample into Ghidra and start exploring the above offsets.
After navigating to the first offset in the list (leading to FUN_180001CBC) and scrolling through, there are some local variables with hex bytes associated with each. This array of hex bytes decodes to kernel32.dll.
/* kernel32.dll */
local_30 = DAT_180060010 ^ (ulonglong)auStack_f8;
local_68[0] = 0x6b;
local_68[1] = 0x65;
local_68[2] = 0x72;
local_68[3] = 0x6e;
local_68[4] = 0x65;
local_68[5] = 0x6c;
local_68[6] = 0x33;
local_68[7] = 0x32;
local_68[8] = 0x2e;
local_68[9] = 100;
local_68[10] = 0x6c;
local_68[0xb] = 0x6c;
local_68[0xc] = 0;
if ((ProcessEnvironmentBlock != (void *)0x0) &&
(*(longlong *)((longlong)ProcessEnvironmentBlock + 0x18) != 0)) {
puVar10 = (undefined8 *)(*(longlong *)((longlong)ProcessEnvironmentBlock + 0x18) + 0x10);
for (puVar5 = (undefined8 *)*puVar10; puVar5 != puVar10; puVar5 = (undefined8 *)*puVar5) {
puVar11 = (ushort *)puVar5[0xc];
puVar12 = local_68;
do {
uVar7 = *puVar11;
puVar11 = puVar11 + 1;
if ((ushort)(uVar7 - 0x41) < 0x1a) {
uVar7 = uVar7 + 0x20;
}
This technique of hex encoding strings is repeated throughout the function, so let's cover exactly what the function is doing.
Dynamic API resolution
This entire function, FUN_180001CBC, is the bootstrap loader for the Lazarus implant, setting up the foundation for all subsequent dynamic operations.
Its primary purpose is to manually locate the base address of kernel32.dll by iterating through the operating system's Process Environment Block (PEB) loaded modules list and performing a case-insensitive string comparison with the hardcoded module name.
Once located, it identifies the addresses of the most critical Windows functions, specifically GetProcAddress (the key function used to locate all other APIs by name), LoadLibraryA, and memory management APIs such as VirtualAlloc and VirtualProtect.
Finally, it allocates a new data structure using GlobalAlloc and populates this structure with the resolved function pointers, effectively creating a custom Import Address Table (IAT).
This entire routine serves as a stealth mechanism to ensure the malware's core capabilities are available, without leaving easily visible function calls in the binary's static import tables. It's designed to bypass static string detection from the likes of Windows Defender antivirus and diligent analysts.
/* GetProcAddress */
psVar1 = (short *)puVar5[6];
if (psVar1 != (short *)0x0) {
local_88 = 0x50746547; // PteG
local_84 = 0x41636f72; // Acor
local_80 = 0x65726464; // erdd
local_7c = 0x7373; // ss
local_7a = 0;
pcVar2 = (code *)FUN_180001c08(psVar1,(longlong)&local_88);
/* GlobalAlloc */
local_c8 = 0x626f6c47;
local_c4 = 0x6c416c61;
local_c0 = 0x636f6c;
local_d8 = 0x626f6c47;
local_d4 = 0x72466c61;
/* Custom_GetProcAddr Function */
if (pcVar2 == (code *)0x0) {
pcVar2 = FUN_180001c08;
}
local_d0 = 0x6565;
local_ce = 0;
pcVar3 = (code *)FUN_180001c08(psVar1,(longlong)&local_c8);
lVar4 = FUN_180001c08(psVar1,(longlong)&local_d8);
if (((pcVar3 != (code *)0x0) && (lVar4 != 0)) &&
(puVar5 = (undefined8 *)(*pcVar3)(0x40,0x40), puVar5 != (undefined8 *)0x0)) {
*puVar5 = pcVar2;
local_48 = 0x6c75646f4d746547;
uStack_40 = 0x41656c646e614865;
puVar5[6] = pcVar3;
puVar5[7] = lVar4;
All the API functions are hex encoded and in little endian, as below, which resolved to GetProcAddress:
/* GetProcAddress */
local_88 = 0x50746547; PteG
local_84 = 0x41636f72; Acor
local_80 = 0x65726464; erdd
local_7c = 0x7373; ss
local_7a = 0;
After all this logic completes, another function is referenced as pcVar2. It’s responsible for navigating the data structures of the loaded DLL (specifically, its export directory table) to find the memory address of an exported function given its name.
This function, FUN_18001c08 (shown in the code block below), is a hand-crafted implementation of GetProcAddress and serves as the Lazarus malware's engine for dynamic API resolution. It first validates that the DLL is a genuine Windows Portable Executable (PE) file by checking the magic bytes MZ (0x5A4D) and PE (0x4550) headers. Upon validation, it traverses the DLL's export directory table, specifically iterating through the list of exported function names.
It then performs a byte-by-byte comparison between those names and the requested API name. Once a match is found, it uses the corresponding index to look up the function's actual memory address in the Export Address Table, adds the DLL's base address, and returns the final execution pointer.
This routine is designed to bypass static analysis tools that rely on the standard IAT, so you won't find any data in the IAT when you check PEStudio, CFF Explorer, or PEBear.
/* Checking 5A4D - Dynamic API Resolution */
/* Strings are in reverse because of little endian */
if ((((param_1 != (short *)0x0) && (param_2 != 0)) && (*param_1 == 0x5a4d)) &&
((*(int *)((longlong)*(int *)(param_1 + 0x1e) + (longlong)param_1) == 0x4550 &&
(uVar3 = *(uint *)((longlong)*(int *)(param_1 + 0x1e) + 0x88 + (longlong)param_1), uVar3 != 0)
))) {
uVar9 = 0;
puVar6 = (uint *)((ulonglong)*(uint *)((longlong)param_1 + (ulonglong)uVar3 + 0x20) +
(longlong)param_1);
if (*(int *)((longlong)param_1 + (ulonglong)uVar3 + 0x18) != 0) {
do {
pbVar5 = (byte *)((ulonglong)*puVar6 + (longlong)param_1);
lVar7 = param_2 - (longlong)pbVar5;
do {
bVar1 = pbVar5[lVar7];
bVar2 = *pbVar5;
pbVar5 = pbVar5 + 1;
if (bVar1 == 0) {
iVar4 = -(uint)bVar2;
goto LAB_180001c83;Rolling substitution cipher
A rolling substitution cipher is an encryption method where the key used to scramble each character isn’t static or repeating, but changes dynamically throughout the message.
This mechanism begins with an initial key to find a character's position in a custom alphabet. Immediately after, the key "rolls" by calculating a new value based on a property of the decrypted character – in the case of the Lazarus malware, by adding the character's ASCII value to the old key.
This technique hinges on a custom alphabet. Here’s an explanation of the process for obtaining it.
Normal Alphabet:
- ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890.-
Lazarus Custom Alphabet:
- pB1Q5ZyneCb6sR03u2OxfK8vVMkEaow-ciSDYIUmlF4hq9XLPJNzThGGr.WtdA7j
To begin, the function FUN_18002544 is responsible for performing the decryption routine. This function references various DAT (static values) throughout that are assigned to variables, such as passkey (renamed for ease of understanding).
input_string_cpy = _strdup(input_string);
if (input_string_cpy != (char *)0x0) {
ptr_input_str_cpy = *input_string_cpy;
uVar2 = 0;
if (ptr_input_str_cpy != '\0') {
bVar4 = 0xb;
pcVar3 = input_string_cpy;
do {
uVar1 = 0;
passkey = &DAT_180062b70;
do {
if (ptr_input_str_cpy == *passkey) {
ptr_input_str_cpy = (&DAT_180062b70)[uVar1 - bVar4 & 0x3f];
*pcVar3 = ptr_input_str_cpy;
bVar4 = bVar4 + ptr_input_str_cpy & 0x3f;
break;
}
uVar1 = uVar1 + 1;
passkey = passkey + 1;
} while (uVar1 < 0x40);
uVar2 = uVar2 + 1;
pcVar3 = input_string_cpy + uVar2;
ptr_input_str_cpy = *pcVar3;
} while (ptr_input_str_cpy != '\0');
}
}
*param_1 = input_string_cpy;
if (input_string_cpy == (char *)0x0) {
_snwprintf_s((wchar_t *)(param_1 + 1),0x80,0xffffffffffffffff,L" ");
}
else {
_snwprintf_s((wchar_t *)(param_1 + 1),0x80,0xffffffffffffffff,L"%S",input_string_cpy);
}
return param_1;
}
By reviewing how the DAT values are cross-referenced, such as DAT_18002b70, you'll be able to identify the other function that uses the decryption routine, specifically FUN_180011ca0, which contains the custom alphabet.
local_38 = DAT_180060010 ^ (ulonglong)auStack_128;
local_78 = local_78 & 0xffffff00;
_DAT_180062b70 = 0x6e795a3551314270;
uRam0000000180062b78 = 0x3330527336624365;
_DAT_180062b80 = 0x76384b66784f3275;
uRam0000000180062b88 = 0x2d776f61456b4d56;
_DAT_180062b90 = 0x6d55495944536963;
uRam0000000180062b98 = 0x4c5839716834466c;
_DAT_180062ba0 = 0x674748547a4e4a50;
uRam0000000180062ba8 = 0x6a37416474572e72;
*(undefined4 *)(param_1 + 0x868) = 2;
*(undefined4 *)(param_1 + 0x86c) = 0x1e;
*(undefined4 *)(param_1 + 0x870) = 0xc000;
*(undefined4 *)(param_1 + 0x874) = 1;
local_108[0] = (undefined8 ******)0x0;
local_f8 = 0;
local_f0 = 0;
The 64-character alphabet used by the rolling cipher is stored in memory as an array of eight-byte chunks. Since the malware is running on an x86-64 (little-endian) architecture, the bytes are stored in reverse order within those chunks.
When viewing the data in a disassembler or hex editor, the character sequence must be read backwards to reconstruct the correct, functional lookup table used by the cipher. This reversal is a necessary step to translate the raw bytes into the intended ASCII key.
The code block below shows a set of two eight-byte values, which totals the first 16-byte segment of the 64-character alphabet.
/* Hex encoded and in little endian order */
uRam0000000180062b78 = 0x3330527336624365; // nyZQ1Bp
_DAT_180062b80 = 0x76384b66784f3275; // 0Rs6bCe
Using the alphabet as a reference, the next step was to identify the encrypted strings within the binary that would use this alphabet for decryption. This led to FUN_1800021D8.
[cut for brevity]
uStack_20 = 0x180002201;
local_28 = DAT_180060010 ^ (ulonglong)auStack_16f8;
FUN_180002544(local_16d8,"I7xfQeEu4Ehv");
FUN_180002544(local_15d0,"6nb3ciOS0");
FUN_180002544(local_14c8,"mlmSmr35w3-6");
FUN_180002544(local_13c0,"EstuM0lMFK");
FUN_180002544(local_12b8,"qEA.J.wS6dsr");
FUN_180002544(local_11b0,"FDN5dPFOQxj");
FUN_180002544(local_10a8,"vF0PI4td9AF");
FUN_180002544(local_fa0,"FDNp9qo9M");
FUN_180002544(local_e98,"hUUM5sBEsae");
FUN_180002544(local_d90,"lSXmsLhaqV");
FUN_180002544(local_c88,"SCwYq24bt6g");
FUN_180002544(local_b80,"1Xg0Qo0wb");
FUN_180002544(local_a78,"OBMXz5Eu4Ehv");
FUN_180002544(local_970,"vFXz-NmBN1X");
FUN_180002544(local_868,"lSXk3Fk48");
FUN_180002544(local_760,"qT3Tr5CkEsae");
FUN_180002544(local_658,"FA1ZbENmBN1X");
FUN_180002544(local_550,"EstuOGTsAR.");
FUN_180002544(local_448,"F0y_GhZGyN");
FUN_180002544(local_340,"FDNWCru1zQL");
FUN_180002544(local_238,"ErkZlBiOS0");
FUN_180002544(local_130,"FDNInak6En");
uVar7 = 0;
[cut for brevity]
This function is essentially responsible for converting strings, such as I7xfQeEu4Ehv, into normal, usable strings using the custom alphabet identified earlier.
That said, simply referencing each letter of the string against the alphabet identified earlier isn’t enough to break the cipher. See the code below for an example of why this won’t work:

A CyberChef window with a normal A-Z alphabet, and the custom Lazarus alphabet in the Recipe section. The input section contains the string “I7xfQeEu4Ehv”, the output section contains “eATSui5JW5YN”, representing a failed decryption.
If this were a standard substitution cipher, the decryption would work perfectly. However, since it's a rolling substitution cipher, additional steps are required to break it.
The cipher's reliance on its rolling mechanism is balanced by a fixed starting point. Before processing any encrypted string, the logic inside FUN_180002544 (the decryption function discussed earlier) hardcodes the rolling key to 0x0B (11 decimal).
bVar4 = 0xb; // 11 Decimal
This value serves as the initial key, determining the exact shift required to decode the first character of a string. Critically, this initial key must be known to start the chain, as an incorrect shift here would result in a corrupted first character. This would then feed a corrupted ASCII value into the key roll logic, causing the rest of the decryption to fail.
It begins with the subtractive decryption, where the current rolling key (e.g., the initial key – 11 decimal) is used as the shift amount to successfully decode the first encrypted character (I to k). Immediately following this, the key is updated for the next character: the ASCII value of the newly decrypted plaintext character (k, which is 107 in decimal) is added to the old key (11) modulo 64.
This calculation (11 + 107) modulo 64 yields 54, making 54 the new shift key for the second character. This dependence on the plaintext output means the key constantly shifts, creating a dynamic chain of decryption that’s difficult to break without knowing this specific ASCII value addition rule.
In short, this works as below:
- Add the old key (11) and the ASCII value of the decoded character (k = 107)
- 11 + 107 = 118
- Divide by modulus by dividing the sum (118) by the alphabet size (64)
- 118 ÷ 64 = 1 (with a remainder of 54)
- Return the remainder, which is the result of the modulo operation
- 118 (mod 64) = 54
This 54 then becomes the new key for the next character's decryption, keeping the key value within the cipher's required range of 0 to 63. This process repeats itself with each decoded character in order, until the entire process is completed.
The code block below shows the complete set of decrypted strings from FUN_1800021D8.
ENCRYPTED: I7xfQeEu4Ehv -> DECRYPTED: kernel32.dll
ENCRYPTED: 6nb3ciOS0 -> DECRYPTED: psapi.dll
ENCRYPTED: mlmSmr35w3-6 -> DECRYPTED: advapi32.dll
ENCRYPTED: EstuM0lMFK -> DECRYPTED: user32.dll
ENCRYPTED: qEA.J.wS6dsr -> DECRYPTED: imaGl5TYT2L3
ENCRYPTED: FDN5dPFOQxj -> DECRYPTED: winhttp.dll
ENCRYPTED: vF0PI4td9AF -> DECRYPTED: shlwapi.dll
ENCRYPTED: FDNp9qo9M -> DECRYPTED: winmm.dll
ENCRYPTED: hUUM5sBEsae -> DECRYPTED: crypt32.dll
ENCRYPTED: lSXmsLhaqV -> DECRYPTED: oleacc.dll
ENCRYPTED: SCwYq24bt6g -> DECRYPTED: version.dl
ENCRYPTED: 1Xg0Qo0wb -> DECRYPTED: GaE6Lf7z
ENCRYPTED: OBMXz5Eu4Ehv -> DECRYPTED: netapi32.dll
ENCRYPTED: vFXz-NmBN1X -> DECRYPTED: shell32.dll
ENCRYPTED: lSXk3Fk48 -> DECRYPTED: ole32.dll
ENCRYPTED: qT3Tr5CkEsae -> DECRYPTED: iphlpapi.dll
ENCRYPTED: FA1ZbENmBN1X -> DECRYPTED: wtsapi32.dll
ENCRYPTED: EstuOGTsAR. -> DECRYPTED: userenv.dll
ENCRYPTED: F0y_GhZGyN -> DECRYPTED: ws232.dll
ENCRYPTED: FDNWCru1zQL -> DECRYPTED: wininet.dll
ENCRYPTED: ErkZlBiOS0 -> DECRYPTED: urlmon.dll
ENCRYPTED: FDNInak6En -> DECRYPTED: winsta.dll
Hardcoded C2
Looking back at FUN_180011CA0 (the function that contained the 64-character custom alphabet) and scrolling down, there are additional details that allude to a C2 domain.
There’s a large section of local variables containing hex strings that, when decoded, reveal a domain: hxxps://www[.]mnmathleague[.]org/cke (defanged).
/* hxxps://www[.]mnmathleague[.]org/cke */
std::basic_string<>::_Tidy_deallocate((basic_string<> *)local_108);
local_b8[0] = 0x68;
local_b8[1] = 0x74;
local_b8[2] = 0x74;
local_b8[3] = 0x70;
local_b8[4] = 0x73;
local_b8[5] = 0x3a;
local_b8[6] = 0x2f;
local_b8[7] = 0x2f;
local_b8[8] = 0x77;
local_b8[9] = 0x77;
local_b8[10] = 0x77;
local_b8[0xb] = 0x2e;
local_b8[0xc] = 0x6d;
local_b8[0xd] = 0x6e;
local_b8[0xe] = 0x6d;
local_b8[0xf] = 0x61;
local_b8[0x10] = 0x74;
local_b8[0x11] = 0x68;
local_b8[0x12] = 0x6c;
local_b8[0x13] = 0x65;
local_b8[0x14] = 0x61;
local_b8[0x15] = 0x67;
local_b8[0x16] = 0x75;
local_b8[0x17] = 0x65;
local_b8[0x18] = 0x2e;
local_b8[0x19] = L'o';
local_b8[0x1a] = 0x72;
local_b8[0x1b] = 0x67;
local_b8[0x1c] = 0x2f;
local_b8[0x1d] = 99;
local_b8[0x1e] = 0x6b;
local_b8[0x1f] = 0x65;
If aligning to Lazarus’s tactics, techniques, and procedures (TTPs), this could be a legitimate domain used as a watering hole, or a C2 domain, and part of Lazarus’s infrastructure.
Going further through the function shows an additional set of characters to be appended to the suspected C2, hex encoded, little endian, and stored in wide char format:
local_78 = 0x690064; id
local_74 = 0x6f0074; ot
local_70 = 0x2f0072; /r
local_6c = 0x640061; da
local_68 = 0x700061; pa
local_64 = 0x650074; et
local_60 = 0x730072; sr
local_5c = 0x69002f; i/
local_58 = 0x64006e; dn
local_54 = 0x780065; xe
local_50 = 0x70002e; p.
local_4c = 0x700068; ph
Concatenating the characters in the order they were initialized (local_78 to local_4c) yields the complete path string:
id + ot + /r + da + pa + et + sr +i/ + dn + xe + p. + ph
Final Path: /?id=ot/redir/tp/extend.php
After this, they’d need to be manually reordered to correct the little-endian string formation. This would make the entire domain:
hxxps://www[.]mnmathleague[.]org/cke/?id=ot/redir/tp/extend[.]phpCommands
ScoringMathTea is a RAT, meaning it has the capability to accept and run commands based on an index table. This process is facilitated by FUN_18001153C.
The function performs three actions:
- Sets up global parameters
- Hardcodes a full path to C:\Windows\System32\cmd.exe into the malware’s internal data structure, so commands run with the SYSTEM shell
- Dynamically builds a command dispatch table (at DAT_180062d40)
The function is shown in the code block below:
/* C:\windows\system32\cmd.exe */
local_48 = 0x3a0043;
local_44 = 0x57005c;
local_40 = 0x6e0069;
local_3c = 0x6f0064;
local_38 = 0x730077;
local_34 = 0x53005c;
local_30 = 0x730079;
local_2c = 0x650074;
local_28 = 0x33006d;
local_24 = 0x5c0032;
local_20 = 0x6d0063;
local_1c = 0x2e0064;
local_18 = 0x780065;
local_14 = 0x65;
wcscpy_s((wchar_t *)&DAT_180065912,0x104,(wchar_t *)&local_48);
FUN_1800208c0_mem_write((undefined1 (*) [32])&DAT_1800652d3,0,0x600);
FUN_1800208c0_mem_write((undefined1 (*) [32])&DAT_180063800,0,0x18f6);
puVar1 = DAT_1800652a3;
_DAT_1800658d7 = 0xffffffff;
_DAT_1800658db = 2;
DAT_1800658dd = 0;
_DAT_1800658de = 0;
*(undefined8 *)DAT_1800652a3[1] = 0;
puVar1 = (undefined8 *)*puVar1;
while (puVar1 != (undefined8 *)0x0) {
puVar2 = (undefined8 *)*puVar1;
thunk_FUN_180027a8c(puVar1);
puVar1 = puVar2;
}
*DAT_1800652a3 = DAT_1800652a3;
DAT_1800652a3[1] = DAT_1800652a3;
_DAT_1800652ab = 0;
DAT_180062d40 = (undefined8 *)operator_new(0x7f8);
*DAT_180062d40 = FUN_180012764;
DAT_180062d40[1] = FUN_180012764;
DAT_180062d40[2] = FUN_180012e04;
DAT_180062d40[3] = FUN_180012e54;
DAT_180062d40[4] = FUN_180012f98;
DAT_180062d40[5] = FUN_1800136e0;
DAT_180062d40[6] = FUN_180013a44;
DAT_180062d40[7] = FUN_180013e3c;
DAT_180062d40[8] = FUN_180014740;
DAT_180062d40[9] = FUN_180014a48;
DAT_180062d40[10] = FUN_180014b28;
DAT_180062d40[0xb] = FUN_180014af0;
DAT_180062d40[0xc] = FUN_180014bb8;
DAT_180062d40[0xd] = FUN_180014c68;
DAT_180062d40[0xe] = FUN_180014e24;
DAT_180062d40[0xf] = FUN_180014fd8;
DAT_180062d40[0x10] = FUN_180015390;
DAT_180062d40[0x11] = FUN_180015bb8;
DAT_180062d40[0x12] = FUN_18001602c;
DAT_180062d40[0x13] = FUN_1800163e0;
DAT_180062d40[0x14] = FUN_1800167c0;
DAT_180062d40[0x15] = FUN_1800169fc;
DAT_180062d40[0x16] = FUN_180016de8;
DAT_180062d40[0x17] = FUN_180016bc8;
DAT_180062d40[0x18] = FUN_1800173ac;
DAT_180062d40[0x19] = FUN_1800173cc;
DAT_180062d40[0x1a] = FUN_18001746c;
DAT_180062d40[0x1b] = FUN_180017564;
DAT_180062d40[0x1c] = FUN_180017a34;
DAT_180062d40[0x1d] = FUN_180017b0c;
DAT_180062d40[0x1e] = FUN_18001810c;
DAT_180062d40[0x1f] = FUN_180018420;
DAT_180062d40[0x20] = FUN_18001884c;
DAT_180062d40[0x21] = FUN_18001897c;
DAT_180062d40[0x22] = FUN_180018a04;
DAT_180062d40[0x23] = FUN_180018ef8;
DAT_180062d40[0x24] = FUN_1800191cc;
DAT_180062d40[0x25] = FUN_1800196d0;
DAT_180062d40[0x26] = FUN_180019820;
DAT_180062d40[0x27] = FUN_180019ae4;
lVar3 = 0x140;
The command dispatch table is an array of function pointers that maps the numeric Command ID that could be issued from the C2 server (via a file called q.dat). Each of these points to an individual function that handles the task.
For example, index ID [7]points to FUN_180013E3C, which is responsible for:
- Requesting a payload from the C2 via FUN_1800BD04
- Assembling the payload in an allocated memory buffer
- Reflectively loading the payload by mapping the downloaded binary into executable memory via FUN_18001AB4
- Executing that payload's EntryPoint (its exported function) as exportfun00
To discover most of the commands in the index table would largely require dynamic analysis. Since there’s no clear export function in this RAT, it would require the RAT's loader.
It’s highly likely that a legitimate Microsoft executable was used to load the ScoringMathTea DLL. The malicious DLL could be renamed to a legitimate library (like dinput.dll, for example) and placed in the same directory as the legitimate Microsoft executable. When the executable runs and calls for dinput.dll, the malicious payload will run instead of the one stored in $PATH (which would be the legitimate dinput.dll).
Detection and mitigation
The Container 7 research team has provided some tools to assist in detecting and decrypting the ScoringMathTea RAT:
Indicators of compromise
Below are the indicators of compromise (IoCs) relating to ScoringMathTea. The in-the-wild sample analyzed in this blog can be found on VirusTotal.

MITRE ATT&CK

Conclusion
ScoringMathTea is one of Lazarus’s most complex malware samples and demonstrates the effort being put into obfuscation and encryption.
With Lazarus focusing on attacking defense organizations and conducting cyber espionage, it's reasonable to suggest that more malware like this will make its way into the wild using highly sophisticated techniques to evade detection.
If you want to get hands-on with this malware, try out our dedicated lab: Lazarus Cyber Espionage Campaign: Analysis.
Trusted by top
companies worldwide
Ready to Get Started?
Get a Live Demo.
Simply complete the form to schedule time with an expert that works best for your calendar.
.webp)







.webp)


