In one of the recent episodes of “Open Analysis Live!” Sergei demonstrated how to statically unpack the Pykspa Malware using a Python script. If you haven’t seen this video yet, I recommend you to watch, it is available here – and the full series is really cool.
The video inspired me to use the same sample and demonstrate an alternative solution, applying my library, libPeConv . The advantage of using libPeConv is that you don’t have to spend time on understanding and rewriting the unpacking algorithm. Instead, you can import the original unpacking function from the original malware. It can speed up the work and be helpful also in the cases when the function of our interest is obfuscated.
bd47776c0d1dae57c0c3e5e2832f13870a38d5fd
The static analysis of this malware is already well demonstrated in the mentioned video. I will just recall the important points to which we are going to refer.
The function that is responsible for unpacking is available at RVA 0x4520. It has the following prototype:
By analyzing how it is applied, we can find out what are the arguments that should be passed:
The first one is a blob of data (BYTE*
), second – size of the blob (DWORD
), next comes the name of the file where the output will be written, and last one is some magic char
. This is how the function declaration should look:
int __cdecl *unpack_func(BYTE* blob, DWORD blob_size, LPCSTR lpFileName, char magic_val);
The function is applied twice, to decrypt two blobs of data (I call them blob1 and blob2). Important things to note are: the offsets of the blobs, their sizes and the passed magic values.
Decrypting blob1:
By following the code before the function call, we can find that the last argument (the magic char
) must have the value ‘r’.
Decrypting blob2:
Again, the magic value is ‘r’:
Now we have all the data to implement a static unpacker.
For this part you need to have Visual Studio, CMake and Git installed.
I already prepared a template that you can use to make a libPeConv-based project, so it is enough to fetch it from my Github: https://github.com/hasherezade/libpeconv_project_template
git clone --recursive https://github.com/hasherezade/libpeconv_project_template.git
Now use the CMake to generate a VisualStudio project:
The malware is 32bit, so it is important to generate a project for 32bit build, otherwise we will not be able to import the sample. Example:
Click “Finish” then “Generate” and finally you can open the project in Visual Studio.
Code of the full unpacker is very short:
https://gist.github.com/hasherezade/21f0858ee713b60070e2f33ffef44b5f
#include <stdio.h> | |
#include <windows.h> | |
#include "peconv.h" | |
// for the sample: bd47776c0d1dae57c0c3e5e2832f13870a38d5fd | |
// from: "Unpacking Pykspa Malware With Python and IDA Pro – Subscriber Request Part 1" | |
// https://www.youtube.com/watch?v=HfSQlC76_s4 | |
int (__cdecl *unpack_func)(BYTE* blob, DWORD blob_size, LPCSTR lpFileName, char r_val) = nullptr; | |
int main(int argc, char *argv[]) | |
{ | |
if (argc < 2) { | |
std::cerr << "Args: <path to the malware>" << std::endl; | |
system("pause"); | |
return 0; | |
} | |
DWORD blob1_offset = 0xC030; | |
DWORD blob1_size = 0x11000; | |
DWORD blob2_offset = 0x1D038; | |
DWORD blob2_size = 0x50000; | |
DWORD unpack_func_offset = 0x4520; | |
size_t v_size = 0; | |
LPCSTR mal_path = argv[1]; | |
std::cout << "Reading module from: " << mal_path << std::endl; | |
BYTE *malware = peconv::load_pe_executable(mal_path, v_size); | |
if (!malware) { | |
return –1; | |
} | |
std::cout << "Loaded" << std::endl; | |
ULONGLONG func_offset = (ULONGLONG)malware + unpack_func_offset; | |
unpack_func = (int (__cdecl *) (BYTE*, DWORD, LPCSTR, char)) func_offset; | |
DWORD res1 = unpack_func((BYTE*)((ULONGLONG) malware + blob1_offset), blob1_size, "blob1_unpack.bin", 'r'); | |
std::cout << "Unpacked blob1, res:" << res1 << std::endl; | |
DWORD res2 = unpack_func((BYTE*)((ULONGLONG) malware + blob2_offset), blob2_size, "blob2_unpack.bin", 'r'); | |
std::cout << "Unpacked blob2, res:" << res2 << std::endl; | |
peconv::free_pe_buffer(malware, v_size); | |
return 0; | |
} |
Firstly, we load the original malware (by a function from peconv). We need it to be loaded with all the dependencies and ready to be executed. A function that allows to achieve it is load_pe_executable
:
BYTE* peconv::load_pe_executable(LPCSTR path_to_pe, size_t &out_size);
This malware sample has no relocation table, so we not only need it loaded, but it must be loaded at it’s original base. This operation may fail on some runs, so we have to keep it in mind.
size_t v_size = 0;
BYTE *malware = peconv::load_pe_executable(mal_path, v_size);
if (!malware) return -1;
Then, using the known offset and the reconstructed declaration of the unpacking function, we are importing it from the loaded malware.
ULONGLONG func_offset = (ULONGLONG)malware + 0x4520;
unpack_func = (int (__cdecl *) (BYTE*, DWORD, LPCSTR, char)) func_offset;
We also use the known offsets of the blobs, and make pointers to the data. After we called the unpacking function with appropriate arguments, our payloads will be dumped to files with the supplied names.
DWORD res1 = unpack_func((BYTE*)((ULONGLONG) malware + blob1_offset), blob1_size, "blob1_unpack.bin", 'r');
std::cout << "Unpacked blob1, res:" << res1 << std::endl;
DWORD res2 = unpack_func((BYTE*)((ULONGLONG) malware + blob2_offset), blob2_size, "blob2_unpack.bin", 'r');
std::cout << "Unpacked blob2, res:" << res2 << std::endl;
At the end we can free the loaded malware:
peconv::free_pe_buffer(malware, v_size);
That’s all, the unpacker is ready. One last thing we can do is preparing a .bat file that will run the unpacker until the malware get loaded (remember the loading base issue caused by the missing relocation table).
Example of the batch script:
@echo off
:try_load
peconv_project.exe malware.bin
IF NOT ERRORLEVEL 0 GOTO try_load
echo.
pause
The full package (except the malware) is available here:
https://drive.google.com/file/d/1ogRJvhEB_rFV5s9wSYVl7MbAEv6xcgyU/view
Finally, let’s see it in action: