Summary
This packer has been observed delivering a wide variety of malware families including SmokeLoader and Vidar among many others. It has been observed in the wild going back a number of years potentially to 2017. The particular variant of the packer analyzed here contains two sets of bytes with no apparent use which occur on either side of values that are integral to the decoding and unpacking process. These two byte strings are stable across many months of observed samples of this packer. What follows is a detailed analysis of the first stage of one sample of this packer which delivers a SmokeLoader payload. Until a widely recognized name or identifier can be determined for this packer, it has the designation pkr_ce1a
.
Identification
This sample has two known filenames. The first, 6523.exe
, is observed in the wild in the path component of the URL used to distribute the file.1 The second, povgwaoci.iwe
, is located in the RT_VERSION
resource within the VS_VERSIONINFO
structure in a field named InternationalName
. This field along with others in the same StringTable structure are not parsed by Exiftool or Cerbero Suite. This indicates that the structure is malformed or non-standard.
According to AV detection results, the correct identification of the unpacked file, SmokeLoader, does appear. There is also one detection based on dynamic analysis: Zenpack.
The import hash of this sample is shared by a group of other files.2 However, the large majority of imported functions are called in dead code located after opaque predicates. Therefore, the usefulness of this import hash is almost nothing. Closer analysis of the opaque predicates in this sample can be found below.
Behavioral and Code Analysis Findings
Build Analysis
According to the File Header, the timestamp of compilation is 2022-02-16T10:14:32Z. The linker version found in the PE32 Optional Header is 9.0.
The compiler is identified as Visual Studio 2008 Release according to function signatures in Ghidra that match a number of library functions in the sample. One example of this detection for the ___security_init_cookie
function is shown in the figure below.
The majority of the library functions found in the sample are from Microsoft Visual C++ 9.0.21022. This is identified in the sample’s rich signature.
Main Function
The instructions highlighted in yellow in the figure above are examples of junk code insertion. These are dummy instructions that are placed between relevant instructions with the goal of making signature development more difficult. The instructions at the end of the function highlighted in white are not executed. These highlight colorings are used throughout the screenshots of this analysis.
The first instructions in the main function calculate the size of the encoded data that contains the shellcode which is the next stage of the packer. The first part is read from a constant in the .data
section at address 0x41cc3c
. The second part is hardcoded in the main function at address 0x406573
. The size is calculated in the next instruction by adding the two parts together. The resulting size is written to a variable in the .data
section. Highlighted in the figure below are the bytes before and after the first constant. These two sets of bytes are not read during the execution of the packer, and their purpose is unknown. However, they are stable across builds of this packer going back for months at least.
The address where the encoded data is located goes through a similar process. The first part is read from the .data
section then written to a variable in the same section. The addition to the second part does not occur until later in the unpack_shellcode
function. The bytes surrounding this part are shown in the figure below.
The next set of instructions handles loading kernel32.dll
and resolving the address of LocalAlloc
.3 These instructions taken together are unique to this packer and shared across hundreds of variants. The stray instruction at address 0x406588
is part of the previous logical grouping of instructions. This is an example of interleaving code that is meant to make signature development more difficult.
The call to LocalAlloc
is obfuscated by calling it from the eax
register. This is a type of function call obfuscation. The uBytes
parameter to the function is 63072 bytes which is the result of the calculation described above.
The final instructions in the main function are calls to other malicious functions and finally a call to the entry point of the decoded shellcode. The next stage of the packer starts after that call.
Change Protection Function
This function is a wrapper around an obfuscated call to VirtualProtect
. Starting at address 0x4058e3
and continuing until the call to GetProcAddress
, the name of the function VirtualProtect
is written character-by-character, out of order, to a variable in the .data
section. Building the function name in this way is an example of variable recomposition. The address of this string is then used as the lpProcName
parameter to GetProcAddress
. Finally, a call is made to VirtualProtect
to change the protection from PAGE_READWRITE
(0x4
) to PAGE_EXECUTE_READWRITE
(0x40
) thus enabling execution in the newly allocated memory.
The flNewProtect
parameter to the VirtualProtect
function is also obfuscated by adding together 0x20
and 0x20
. This hides the PAGE_EXECUTE_READWRITE
flag of 0x40
from being located near the call to VirtualProtect
. This is a form of argument obfuscation.
Unpack Shellcode Function
This function performs three actions that are interspersed with anti-analysis code. The first action is moving the encoded data from its starting location in the .data
section to the newly allocated memory.
Note the instructions at addresses 0x405aa7
and 0x405acd
. These are both opaque predicates. An opaque predicate appears to be a conditional jump, but the conditions can only be met in one way making the jump effectively unconditional. The one at the top is basically a fake: it never jumps. The one in the middle always jumps. This one additionally encloses a block of dead code. This packer very frequently couples dead code insertion with opaque predicates that always jump over the dead code. The function calls in the dead code are included in the import table making identification via import hash of little utility.
Later on in this function is the call to the decode
function. After that is a call to a function which shifts the pointer to the decoded shellcode. The shift changes the offset from the start of this data to the location of the shellcode original execution point (OEP). Because the shellcode is position independent, this OEP is also offset zero of the shellcode. Both of these functions are analyzed in more detail below.
The shift_shellcode_oep
function is additionally wrapped in an anti-emulator loop which has a very high number of iterations. This slows processing in an emulator which can potentially cause a timeout and an analysis failure.
Note the comparison instruction at address 0x406486
. During the anti-emulation loop, the call to shift_shellcode_oep
is made once when the counter reaches 0x770e
. This is a type of anti-emulation circumvention countermeasure. A basic method for circumventing extremely long loops that target emulators is to patch out the loop. Another is to detect the loop in the emulator and then modify the counter to leave the loop. Because the shift function is called once at a point in the loop, either of these circumventions could end up not executing this function and would leave the emulator unable to execute the shellcode correctly.
The unpack_shellcode
function overall contains ten opaque predicates primarily of the same type shown above. One of them is slightly different in that it creates a dead end filled with dead code. The last instruction in the dead end is a call to terminate
. Therefore, this appears to be a location where the execution of the packer ends.
There are a total of four anti-emulation loops similar to the one analyzed above. Two of these wrap opaque predicates which in turn wrap inserted dead code. One, however, in addition to wrapping two opaque predicates, also contains two additional anti-emulator behaviors. Both of these are calls to unusual APIs: GetGeoInfoA
and GetSystemDefaultLangID
. During analysis, Qiling emulator halted with an exception because neither of these API calls have been implemented.
In the very first code block of the unpack_shellcode
function, a new and subsequently unused exception handler is registered. Chances are about even that this is an anti-emulation behavior or it is just junk code. If it is anti-emulation, it is targeting older emulators based on specific versions of Unicorn Engine which do not implement access to the Windows Thread Information Block (TIB). Moving the contents of fs:0x0
as happens at address 0x405983
would fail in that particular environment. This type of anti-emulation is an unimplemented opcode.
In the last block of the unpack_shellcode
function, before the return, the SEH is reset back to the previous handler. This occurs right after a junk call to LoadLibraryW
from which no functions are subsequently resolved.
Decode Function
The data is decoded in chunks. So this function has three basic purposes: convert the encoded shellcode size to a chunk count by dividing by 8, wrapping a loop around the call to the decode_chunk
function, and then calling that function. In the middle of this function is a very large insertion of dead code wrapped by an opaque predicate. This is highlighted in white in the figure above. It has also been truncated for the screenshot. There are two locations in this function with junk code insertion which impedes signature development: at addresses 0x4057c1
and 0x4057cc
.
The chunks are 8 bytes long, therefore at the end of the loop after the call to decode_chunk
, 8 is added to the chunk pointer then the chunk count is decremented by one.
Decode Chunk Function
The decode_chunk
function has four opaque predicates that each wrap dead code, and there are two locations with junk code insertion. In addition to these, the logic of the function is quite convoluted. However, after patching out all of the opaque predicates, dead code, and junk code, a cleaner decompilation graph can be analyzed. The figure above is that graph, fully annotated and cleaned up.
There are three tiny functions called during the decoding algorithm: load_data_keyB
, add_key4
, and, xor_mix
. These simply isolate small pieces of the algorithm to impede analysis and make signature development more difficult.
Shift Shellcode OEP Function
The shift_shellcode_oep
function is very simple. It adds 0x3bf1
to the address of the start of the decoded shellcode in allocated memory. This shifts the pointer from the start of the decoded data to the offset of the OEP. This is the address that is called in the main function.
Recommendations
This packer is polymorphic. Variants and builds share many of the same functionality, behavior, and code features. However, they are in different order with randomization of opaque predicates, junk code, and dead code. In spite of this, detection can be achieved by focusing on the two stretches of stable bytes which were identified above. The following two YARA rules match these bytes.
rule Packer_pkr_ce1a_ShellcodeSizePart
{
meta:
author = "Malwarology LLC"
date = "2022-10-14"
description = "Detects bytes surrounding the first part of the data size of the second stage shellcode in Packer pkr_ce1a."
reference = "https://malwarology.substack.com/p/malicious-packer-pkr_ce1a"
sharing = "TLP:CLEAR"
exemplar = "fc04e80d343f5929aea4aac77fb12485c7b07b3a3d2fc383d68912c9ad0666da"
address = "0x41cc3c"
packer = "pkr_ce1a"
strings:
$a = { 00699AF974[4]96AACB4600 }
condition:
$a and
uint16(0) == 0x5A4D and
uint32(uint32(0x3C)) == 0x00004550
}
rule Packer_pkr_ce1a_ShellcodeAddrPart
{
meta:
author = "Malwarology LLC"
date = "2022-10-17"
description = "Detects bytes surrounding the first part of the address of the second stage shellcode in Packer pkr_ce1a."
reference = "https://malwarology.substack.com/p/malicious-packer-pkr_ce1a"
sharing = "TLP:CLEAR"
exemplar = "fc04e80d343f5929aea4aac77fb12485c7b07b3a3d2fc383d68912c9ad0666da"
address = "0x41bc9c"
packer = "pkr_ce1a"
strings:
$a = { 0094488D6A[4]F2160B6800 }
condition:
$a and
uint16(0) == 0x5A4D and
uint32(uint32(0x3C)) == 0x00004550
}
Supporting Data and IOCs
File
Filename: 6523.exe
Filename: povgwaoci.iwe
MD5: 5663a767ac9d9b9efde3244125509cf3
SHA1: 84f383a3ddb9f073655e1f6383b9c1d015e26524
SHA25: fc04e80d343f5929aea4aac77fb12485c7b07b3a3d2fc383d68912c9ad0666da
Imphash: bc57832ec1fddf960b28fd6e06cc17ba
Timestamp: 2022-02-16T10:14:32Z
File Type: Win32 EXE
Magic: PE32 executable (GUI) Intel 80386, for MS Windows
Size: 238080
First Seen: 2022-10-14T18:37:13Z
4
Distribution URL
hxxp[://]guluiiiimnstrannaer[.]net/dl/6523.exe
Malware Behavior Catalog
DEFENSE EVASION::Software Packing [F0001]
ANTI-BEHAVIORAL ANALYSIS::Emulator Evasion::Undocumented Opcodes [B0005.002]
ANTI-BEHAVIORAL ANALYSIS::Emulator Evasion::Unusual/Undocumented API Calls [B0005.003]
ANTI-BEHAVIORAL ANALYSIS::Emulator Evasion::Extra Loops/Time Locks [B0005.004]
ANTI-STATIC ANALYSIS::Disassembler Evasion::Argument Obfuscation [B0012.001]
ANTI-STATIC ANALYSIS::Disassembler Evasion::Variable Recomposition [B0012.004]
ANTI-STATIC ANALYSIS::Executable Code Obfuscation::Dead Code Insertion [B0032.003]
ANTI-STATIC ANALYSIS::Executable Code Obfuscation::Junk Code Insertion [B0032.007]
ANTI-STATIC ANALYSIS::Executable Code Obfuscation::Interleaving Code [B0032.014]
Malware Behavior Catalog Proposed Methods
ANTI-BEHAVIORAL ANALYSIS::Emulator Evasion::Unimplemented Opcodes [B0005]
ANTI-STATIC ANALYSIS::Executable Code Obfuscation::Opaque Predicate [B0032]
ANTI-STATIC ANALYSIS::Executable Code Obfuscation::Function Call Obfuscation [B0032]
Analyses
Imphash: bc57832ec1fddf960b28fd6e06cc17ba
VirusTotal