Introduction
The sample we are going to analyze comes within a DLL. It is flagged by Intezer to be related to TA428 (for general information on the file, check this link). I can’t say for sure if it is Poison Ivy or a variant or something else so let’s keep things fuzzy here.
I intend to divide this analysis in two parts, as it would be way too lengthy otherwise.
Shellcode analysis
After some operations (out of scope of our analysis) the malicious DLL creates a new heap object (HeapCreate). It then allocates it for 17068 bytes (0x42AC) with HeapAlloc before copying the shellcode on that newly allocated memory space and transfering control to it.
After some initialization, the shellcode retrieves the InInitializationOrderModuleList in order to get kernel32.dll base image address. Indeed, we can see at offset 0x3F, that it is comparing a string, which is added an offset of 12, to the DWORD 0x00320033. Windows encodes strings in UTF-16 internally. Then we understand that by adding 12 to the string pointer it is actually moving by 6 characters and that the second operand is “32” in UTF-16.

Parsing PE header to retrieve kernel32.dll base address
After having obtained the base address of kernel32, the program pushes it on the stack along with two other values, 0 and 4402890Eh, before calling a function. This latter value is in fact the CRC32 of the string “VirtualAlloc“.
Instead of storing strings in the executable, the author(s) just calculated a ‘hash’. This technique known as API hashing is a common way of calling API functions in malware.
– But hold on second! When I compute the CRC32 of the string ‘VirtualAlloc’ I am not finding
4402890Ehbut09CE0D4Ah. How do you explain that?
The author of this code included the nullbyte in the calculation. I can’t say if it was intentional or if it was an implementation error. But anyway, the algorithm is exactly following CRC32 algorithm (but is complicating the implementation).
NOTE:
I may be doing a post on a crypto/hashing related subject really soon in which I may be talking about this implementation. When it will be done there will be a link to this, right here.
I won’t go into the details of the get_procedure_in_dll function (the name I gave to it is self-explanatory) because it is not really interesting, but here is a summary of what is done:
- First the DLL base address given as argument is parsed to retrieve the VAD of 4 elements in the following order:
NumberOfNames,AddressOfFunctions,AddressOfNames,AddressOfNamesOrdinals. - Then it iterates over every exported function (using a counter) and for each of them, it computes the CRC32 with the reversed polynomial (
EDB88320h) until the hash given as parameter is obtained. - Then it retrieves the VAD of the function by using the counter,
AddressOfNamesOrdinalsandAddressOfFunctions.
NOTE 1:
I have extracted the CRC32 algorithm from the shellcode and turned it into a NASM program. I have precomputed the results of all functions exported bykernel32.dll,user32.dll,urlmon.dll,WS32_32.dll,wininet.dllandntdll.dll(libraries used in this malware as we’ll see later on). You can find those lists as well as the algorithm in assembly on my Github (make sure to read the README file to understand the content of the folder 😉).
It then calls VirtualAlloc to allocate 65536 bytes.

From here, things are getting a bit weirder. Like we can see the shellcode retrieves the number of ticks with rdtsc. Then it enters a loop.

This loop starts by multiplying the just retrieved number of ticks (the higher part of it specifically) by an immediate value of 0AC564B05h and adding 1 to the result. It then stores the result in what looks like an array of DWORD.
The address of the table, [esi + 8D9h], might be important later on, so let’s keep it in our mind.
Things are keeping strange. Now it stores 3 values on its dynamically allocated space: 1.0, 0 and 50h and then calls a function at offset 0x3A0C, 31 times.
The purpose of these operations is pretty cryptic at this point but let us see.

The function is operating on two bytes in the previous created array. It is performing some transformations on each byte and then swapping them.

Because of the adjacency of these values, I wonder if they are not in fact QWORDs. But whatever, we may have an answer to that later.
Then it retrieves the address of LoadLibrary (CRC32 = 4134D1ADh) using the same trick as before and stores it at [ESI + 9Dh]. To keep track of all these addresses in its virtual space, we will keep a table up-to-date, as we encounter new values. Here is the content of this table so far:
| Offset (ESI + X == Base + A000h + X) | Value | Comments |
|---|---|---|
| 8D9h – 960h | D/QWORD table | ? |
| 9Dh | LoadLibrary |
Now something interesting happens:

x86 trick that leads to overlapping code
IDA just gets confused. Indeed, the target of the call at offset 3A64h is strange: it aims at the middle of an instruction (3A72h). This is an example of overlapping code. So despite what IDA shows, the instruction begins at 3A72h (not at 3A71h) which correspond to these bytes FF 96 9D 00 00 00. Zydis tells us that this is a call dword ptr ds:[esi+0x9D] . Thanks to the table, we can see that it is the address of LoadLibrary that is stored at esi + 0x9D. The shellcode is then making a call to this procedure.
– But wait a minute!
LoadLibraryneeds the library name right? Where is it?
Well, they used a little assembly “trick” to push its address on the stack:
If we look at the previous instruction, we can see that the following opcodes are “unexpected”.
The disassembler is getting confused: it just outputs some gibberish instructions. If we pay close attention, we can see that some of these “opcodes” really looks like ASCII chars. And indeed, it is the ASCII representation of “advapi32” (terminated by a null byte). So, as we know, a call instruction in x86 pushes the following address on the stack before transfering execution: in our example it is pushing 3A69h which is the address of the DLL name. This is how the string is transfered to LoadLibrary. Because of that, I’m sure that this code was fully written in assembly. A typical compiler wouldn’t mix up data and code first. In the source code something like: “db 'advapi32',0” was written.

Like we can see at offset 3A78h the address of advapi32.dll is stored at two places. We update the table accordingly.
Actually, the same operation is going to occur over and over in order to load other libraries, namely: kernel32.dll (2 times), advapi.dll, ntdll.dll, user32.dll, urlmon.dll, WS32_32.dll and WinInet.dll. When done with that, it retrieves the address of the current executable file (with a call to GetModuleFileNameA) and stores it. Here is an IDA disassembly of some of these operations, after some manual rectifications:

The disassembly is clearly easier to read now. Let’s update the array with the new information:
| Offset (ESI + X == Base + A000h + X) | Value | Comments |
|---|---|---|
| 8D9h – 960h | D/QWORD table | ? |
| 9Dh | LoadLibrary | |
| ABBh | kernel32.dll | |
| AD3h | advapi32.dll | |
| – A000h (Base) | advapi32.dll | |
| ADBh | ntdll.dll | |
| – 9FFCh | ntdll.dll | |
| ABFh | user32.dll | |
| – 9FF7h | user32.dll | |
| – 9FF4h | urlmon.dll | |
| – 9FF0h | WS2_32.dll | |
| – 9FE0h | WinInet.dll | |
| – 9FECh | Kernel32.dll | |
| 5B2h | ptr to current executable filepath |
Now, the shellcode is pushing an other CRC32 value (4DCF392h). This corresponds to GetModuleFileNameA. This function receives as argument a size of 255, a pointer to the address that will store the module filename and lastly NULL which indicates that we want the name of the executable of the current process. Then it transfers execution to another location, 3DC0h, by doing a call.

But when we check the target of the call (loc_3DC0), we see that it corresponds to a pop edi… So why is it ‘poping’ the return address ? I bet you already know the answer: if we go back to the call (at offset 0x3B47) and inspect the 4 following bytes (20B943E7), we can notice that they correspond to another CRC32. This time it identifies the function CreateMutexA. So, as it is always doing, we know that it will invoke get_procedure_in_dll and it does as expected:

Then, it stores the address of the function in its virtual address and increments edi by 8, before going back for another round. It seems that an array of structure is stored at the address pointed to by edi (3B4Ch). These structure looks to be of 8 bytes. Here is the potential format (based on what we’ve just seen):
struct s {
u32 procedure_crc32;
u16 dll_offset;
u16 procedure_offset;
};
Let’s declare this structure in IDA and cast data to this type.

Here is the code before the declaration of the structure:

And that is the code (a part of it) after the declaration of the structure:

This look waaay better. A logical step here is to update our table, but there is so much entry that I find this to be cumbersome. I’m sure that it is possible to combine IDA scripting capabilities with the program I made to create a quick and convenient way to keep track of which function is stored at which address but I didn’t give enough time to do it, so we’ll proceed manually I guess 😉 .
Oh and actually, there is a second table just like this one but much shorter (we can notice that the indexation was normalized: the offset A000h was removed. Why? Idk.

Then, the same trick is used again: the address of something is pushed on the stack. If we compute the address it gives us 3E74h + 323h = 419Ch.

Let’s have a look at what is there:

At first, we can think that it is CRC32 hashes. But it is not. If we look closely at the values, some of them falls in the ASCII alphabet range. Let’s IDA cast this data to strings for us:

Ok our guess was right, there is some strings but they are mixed up with bytes… Some of them are really interesting because they are providing juicy information. For instance, the probable C2 IP address, some registry path and other strings: “hold“, “99x7nmpWW|“, “3&Uw9f*lZ>!MIQ“. If we look even more closely to the bytes between the strings, we can notice that the bytes preceding each string matches exactly their sizes (if we take them as WORD values) ! We can assume (not confidently though) that this is yet another kind of structure, which is storing a string, its length and… a third value:
struct s {
u16 unknown_field;
u16 length;
char* string;
};
Same thing as before, let’s declare this structure in IDA and cast these bytes according to it:

Ok my guess may not be accurate, at least for the string part. Indeed, I was expecting that each IP address that appears in the previous screenshots would have its own structure, but it seems that it is not the case.
Our assumptions may be corrected by going further in the analysis. If we go back to the code that referenced this data, more specifically to offset 3E83h, we can see that the first field of the structure is moved in eax. This validates a part of our assumption: indeed the movsx is used because WORD values are taken from memory. So the first 2 values of the structure are indeed WORDs.
Then, the code is preparing the argument of a function call. As we can see on the image, this is a call to RtlMoveMemory which has the following signature (the “IN” and “OUT” are just SAL annotations):
VOID RtlMoveMemory(
Out VOID UNALIGNED *Destination,
In const VOID UNALIGNED *Source,
In SIZE_T Length
);

So what it is obviously doing is simply copying the strings from the table into its virtual space at the location indicated by the first value of the structure, which is an offset. It is doing that until a 0 WORD is encountered. This helps understanding the structure of the table at offset 419Ch and correct our hypothesis: the third argument is not always a “natural” ASCII string. Let us redefined it with what we know so far:
struct s {
u16 offset;
u16 length;
void* blob; // it can point to a string or number
};
When done with its parsing, in enters another loop which is doing the same thing, but now without requiring a call to RtlMoveMemory… But the thing is that it has reached the end of the shellcode ! So maybe this code is used in/with other variants/extensions of this malware…

Moving on, advapi.dll is loaded in order to choose one function between GetLastError and RtlGetLastWin32Error. The shellcode also checks if the executable has admin privileges by calling IsNTAdmin (and stores that result).

Finally, it checks if a mutex called “99x7nmpWW” exists. If an error occured during this check or if it already exists it stops its execution (ExitProcess). Else, it computes 81h before transfering control to another procedure which we will analyze in the next part.

Summary
So far we’ve covered the beginning of the shellcode. This part seems to be a setup part: it has initialized its virtual address space with various addresses, structures and so on. Finally, it is checking if the executable has admin rights and then it verifies it has already infected this machine.
The next part, I hope, will be more fun as we will dive into the malware capabilities.
1 Some of these pre-computed values for kernel32.dll and user32.dll are also available at the following links but as they are quite old, they are incomplete:
