Anatomy of a VB Virus
In this article, we will look in depth at a virus written in Visual Basic. We look at various techniques used by this virus to deter the process of reverse engineering. Each technique is discussed in depth with the help of screenshots and assembly language code with comments.
Original Entry Point
This virus is written in VB (Visual Basic). Programs written in Visual Basic have an entry point that looks like the image shown below:
Learn Digital Forensics
It pushes a pointer to a Data Structure onto the Stack and then calls the ThunRTMain in MSVBVM60.dll
If we step into the code right away, we will be debugging inside MSVBVM60.dll code and that will not help us in analyzing.
Instead, we need to find out the Original Entry Point of the code section. To do this, we analyze the data structure that is pointed to by 0x004018E4.
Below we can see the data structure in memory dump.
We can see, the first two dwords in the data structure correspond to the VB marker —VB5!6, in this case.
If we trace the data structure further, we can see some memory addresses present in it, like:
An easy way to identify the OEP is by placing a breakpoint on each one of the above addresses and running it.
The code breaks at 0x0042ECC1.
By looking at the first few instructions of the code at this memory address, we can confirm that this is indeed the Original Entry Point.
After stepping into the code further, we found that it is retrieving the Win32 API addresses this way:
It calls the DllFunctionCall API exported by MSVBVM60.dll, and passes it a DWORD as an argument —0X404FF8, in this case. The value returned is the function pointer which is stored in the register, EAX (function pointer of VirtualProtectEx in this case).
This is an indirect method used by the code to invoke Win32 APIs several times. Below we can see the arguments on the stack to VirtualProtectEx():
Now, we come to the first type of Decryption Routine used.
There are several sections of code that look like:
It works this way:
- It loads a memory address into EAX.
- Then stores the DWORD 0x6E60E6B6 at EAX and 0X3221EFB8 at EAX+4.
- It loads the same memory address in ECX and now uses EAX as an offset.
- It modifies the value of EAX and then stores, 0X6C7EE7F3 at ECX+8 and 0 at ECX+C.
- In this way, it has stored 4 DWORDS at the memory address pointed to by EAX as shown in the memory dump below:
This data in the memory dump is encrypted.
- It passes the DWORD 0x1283DD to the subroutine at 0x0042C321.This DWORD value passed to the subroutine above will be used to decrypt the data written to memory above.
Execution of Subroutines using CallWindowProcW
Once we step into the subroutine above and see further, we find a call to CallWindowProcW as shown below:
The arguments on stack are:
The meaning of arguments passed to CallWindowProcW:
- 0x00182DA8 is the address of the subroutine which will be used for decryption.
- 0x00182EE8 points to the memory location where encrypted data is stored.
- 0x1283DD is the key used for decryption.
If we step over the call to CallWindowProcW, we will observe that the data at memory location, 0x00182EE8 is decrypted as shown below:
However, to analyze the decryption routine, we set a breakpoint at the first argument passed to CallWindowProcW and run the code.
Arguments on the stack to CallWindowProcW() function and the encrypted data in memory:
Here, we have encrypted data stored at 0x00182E18. We set a breakpoint at the first argument to the function, 0x00182DA8 and run the code.
We successfully break at the decryption routine:
- It stores the pointer to encrypted data in EBX.
- It stores the key in EDX.
- It will XOR the DWORD at EBX with the key stored in EDX. It will continue this loop till a null DWORD is found in the encrypted data.
In this way we can see how the code calls the decryption routines indirectly through CallWindowProcW().
After multiple calls to CallWindowProcW, it redirects the execution to a Call Table. There is a sequence of Call instructions placed to each other as shown below:
Most of these CALL instructions point to subroutines that do not perform any action. Let's step into the CALL to subroutine at 0x0043074A
Similarly, stepping into another CALL instruction in the CALL table above:
As you can see, most of these CALL instructions to subroutines have been placed only to deter the process of reverse engineering.
Locate and Write the Encrypted Malicious Code
Now we come to another important CallWindowProcW function invocation with the arguments on stack as shown below:
Explanation of the arguments:
- 0x00182800 - The subroutine used to load the encrypted code.
- 0x01610020 - The encrypted code will be loaded at this base address.
- 0x1832BC - This is the message passed to the subroutine.
We set a breakpoint at 0x00182800 and run the code.
Now, we can analyze the subroutine that is used to locate the encrypted executable code and write it to the memory address, 0x01610020:
Unlike most viruses which carry the malicious code inside the Resources Section that can be loaded using LoadResource API, this virus makes use of markers to locate the encrypted code inside it.
Below is an explanation of the subroutine along with comments:
MOV ESI,DWORD PTR SS:[ESP+4] ; Destination address (0x01610020) at which the encrypted code will be written.
MOV EDX,DWORD PTR SS:[ESP+8] ; Pointer to the marker, 0x2C51512C, it is stored in UNICODE
MOV EBX,DWORD PTR SS:[ESP+C] ; Marker will be written at this address in ASCII
MOV AL,BYTE PTR DS:[EDX] ; Read a byte from UNICODE marker
MOV BYTE PTR DS:[EBX],AL ; Store the byte at ASCII marker
ADD EDX,2 ; We increment EDX by 2 since it stores the marker in UNICODE format
LOOPDNE SHORT 00182811; Execute the loop till ECX is 0. At the end of this, we will have the ASCII marker at EBX
SUB EBX,4 ; Adjust EBX so that it points to start of marker
MOV EAX,DWORD PTR FS: ; Pointer to ProcessEnvironmentBlock
MOV EAX,DWORD PTR DS:[EAX+8] ; Get the ImageBaseAddress (00400000)
JE SHORT 00182830 ; Check if the dword, 0x2c51512c was found in the image (correct offset is 6DDB)
JMP SHORT 00182827 ; Keep executing the loop till we locate the correct offset of the marker
ADD ECX,4 ; Add 4 to the position of marker
ADD EAX,ECX ; Add the offset to image base address, 00406ddf
JMP SHORT 00182840
INC ECX ; ECX is used as an offset into the Image to locate the position of the marker, 0x2C51512C
MOV EDI,DWORD PTR DS:[EAX+ECX] ; Fetch the DWORD at offset, ECX and store at EDI
CMP EDI,DWORD PTR DS:[EBX] ; Compare with 0x2C51512C
MOV EDI,DWORD PTR DS:[EAX+ECX] ; Using ECX as offset, move DWORD from image to destination
MOV DWORD PTR DS:[ESI+ECX],EDI
JNZ SHORT 00182840
Here is an overview of how it locates the code:
- It uses a DWORD, 0x2C51512C as a marker to locate the start and end of the encrypted code in the Image.
- It locates the marker, and then copies all the encrypted code stored between the two markers to destination, 0x01610020.
Formation of the Decryption Key
The code will manually form the Decryption Key by writing one byte at a time to the memory as shown below:
As demonstrated, it is manually writing the decryption key bytes to the memory (0x5C, 0x7B, 0x0D9, 0xCD and so on).
The total length of the decryption key is 0x22 bytes and is as shown below:
Decryption of Malicious Code
After the encrypted code is written to the memory address, 0x01610020, it will be decrypted using another call to CallWindowProcW and the decryption key which was manually formed above.
Below is the subroutine used for decryption:
Here is an explanation of the code:
MOV EAX,DWORD PTR DS:[EDX+ECX] ; load the encrypted DWORD into EAX
ADD EBX,DWORD PTR SS:[EBP+10]
XOR AL,BYTE PTR DS:[EBX] ; XOR the byte from the key with AL
SUB EBX,DWORD PTR SS:[EBP+10]
JNZ SHORT 00182EA1
MOV EBX,DWORD PTR SS:[EBP+14] ; load the value, 0xFFFFFFDD into EBX once it becomes 0
MOV DWORD PTR DS:[EDX+ECX],EAX ; write the XOR'ed value back into memory
JNZ SHORT 00182E8E
Here is an overview of how the decryption routine works.
- ECX and EBX are used as negative offsets. ECX is used to index into the encrypted code and EBX is used to index into the encryption key.
- EDX and EBP+10 point to the end of encrypted code and the encryption key respectively.
- Using the negative offset, EBX, it picks a byte from the encryption key and XORs it with lower byte of the encrypted DWORD.
- It resets the value of EBX once it reaches 0.
As we step through the decryption routine above, we can see the MZ header unpacking at 0x01610020.
Execution of malicious code
After the malicious code is decrypted and stored at the memory address, 0x01610020, there is another call to CallWindowProcW with the stack arguments as shown below:
Like we did before, we set a breakpoint at the subroutine address, 0x00182870. We can see that it is passing the base address of the malicious executable as an argument, 0x01610020.
After stepping into the code further, we observe that it is building a Data Structure to store all the important information, like base addresses of newly allocated memory regions. It references this data structure later.
The subroutine at 0x001BEA58 is passed the offset into the data structure at which the values will be written.
Data Structure is shown below:
It has stored the base addresses, 0x00046D00, 0x00046E00, 0x00046F00 of the newly allocated memory regions in the above structure.
Function Name Hashes
A common method used by malwares to find the function pointers and invoke them is by calculating the hash of the function names exported by a DLL and comparing them with pre-calculated hash values.
This virus makes use of the same method several times to find function pointers and then invoke them as shown below:
Above, we can see that it passes the pre-calculated hash of the function name, 0XEC0E4EA4 to the subroutine at 0X001BEAE3.
We step into the subroutine to understand how it finds the function pointer:
Below is an explanation of the code along with comments:
MOV EBP,DWORD PTR SS:[ESP+1C] ; EBP points to base address of kernel32.dll
JE SHORT 00182D6C
MOV EAX,DWORD PTR SS:[EBP+3C] ; Get offset of PE header
MOV EDX,DWORD PTR DS:[EAX+EBP+78] ; Get Export Directory offset
ADD EDX,EBP ; Export Directory base address
MOV ECX,DWORD PTR DS:[EDX+18]
MOV EBX,DWORD PTR DS:[EDX+20] ; RVA of AddressOfNames array
ADD EBX,EBP ; Base Address of AddressOfNames array
JECXZ SHORT 00182D6C
MOV ESI,DWORD PTR DS:[EBX+ECX*4]
ADD ESI,EBP ; ESI points to the function name
LODS BYTE PTR DS:[ESI]
JE SHORT 00182D53
ROR EDI,0D ; Rotate right EDI by 13
ADD EDI,EAX ; EDI will hold the hash value of the function name
JMP SHORT 00182D47
CMP EDI,DWORD PTR SS:[ESP+20] ; Compare with the precalculated hash value
JNZ SHORT 00182D3A
MOV EBX,DWORD PTR DS:[EDX+24]
As can be seen, the function pointer retrieved in this case is LoadLibraryW API exported from kernel32.dll. It then invokes the API as shown below:
In this way, the code continues to call different APIs by first retrieving their function pointer using a pre calculated hash value and then invoking them.
Creation of a Suspended Process
In order to make the process of debugging inconvenient for the reverse engineer, it will create another instance of itself by calling CreateProcessW and the new process will be in suspended state.
Below we can see the call to CreateProcessW:
And the stack arguments:
The important argument here is the Process Creation Flag, which controls the state of the new process. In our case, the value is: 0x00000004 which corresponds to CREATE_SUSPENDED as mentioned here:
If we step over the CreateProcessW call, it will return all the important values like Process Handle, Thread Handle, Process ID and Thread ID of the newly created process and store them in a data structure at 0x04600000 (the last argument on the stack).
In the newly created process, it does not intend to execute itself. Instead, it wants to execute the embedded malicious executable which was decrypted above. So, it modifies the Process Address Space of the newly created instance.
First, it will unmap the view of section containing the base address, 0x04000000 in the new Process by calling ZwUnmapViewofSection
Below is the call:
And the stack arguments:
It requires only two arguments —the handle of the newly created process and the base address of the section to be unmapped.
Once it is unmapped, the virtual address space is not reserved and can be modified.
It makes use of multiple calls to WriteProcessMemory to copy the code malicious executable into the Process Address Space of the newly created Process.
It is important to note that it manually parses the Portable Executable Structure to find the various values like sizes of .text section, .data section, .reloc section, size of Image, Address of Entry Point and so on.
Write Malicious Code to New Process
Before it starts writing the code into the new Process' Memory, it first allocates memory using VirtualAllocEx.
PUSH 40 ; Newly Allocated Memory will have PAGE_EXECUTE_READWRITE flags.
PUSH 3000 ; MEM_COMMIT
PUSH DWORD PTR DS:[EDX+50] ; At PE Header offset 0x50, we have the SizeOfImage
PUSH DWORD PTR DS:[EDI+34] ; At PE Header offset 0x34, we have the ImageBaseAddress
PUSH DWORD PTR DS:[ECX] ; Pointer to Process handle
CALL EAX ; call VirtualAllocEx
Arguments on the stack:
Now, it starts calling WriteProcessMemory. Let us look into each of the call to WriteProcessMemory in depth.
PUSH DWORD PTR DS:[EDI+54] ; At PE Header offset 0x54 is SizeOfHeaders
PUSH ESI ; Pointer to base address of Malicious Decrypted Executable
PUSH DWORD PTR DS:[EAX+34] ; ImageBaseAddress
PUSH DWORD PTR DS:[ECX] ; Pointer to Process Handle
CALL EAX ; Call WriteProcessMemory
Arguments on the stack:
It first finds out the SizeOfHeaders by parsing the PE32 structure and then writes the same number of bytes (0x400 in this case) from malicious executable to new process' memory at base address, 0x00400000.
This call to WriteProcessMemory is used to write the contents of .text section of malicious executable to the remote process. In order to find the Address at which the data needs to be written in the new process, the size of the .text section and pointer to raw data of .text section, it manually parses the PE32 structure. These values are then used to calculate the parameters of the call to WriteProcessMemory.
Arguments on the stack:
Let us understand how it calculated these parameters. Below is the memory dump of the .text section header:
At offset, 0x10 inside the .text section header is the size of the .text section, 0x00020800 bytes.
At offset, 0xC inside the .text section header is the RVA at which it will be loaded, 0x00001000.
So, the Base Address of .text section = 0x00401000.
At offset, 0x14 inside the .text section header is the RVA at which the contents of .text section are stored, 0x00000400
Base Address of .text section contents = 0x01F20020 + 0x00000400 = 0x01F20420
So, this call to WriteProcessMemory will write 0x00020800 bytes of .text section at the base address 0x00401000 in the new process.
This call will write 0x400 bytes of .data section at the base address 0x00422000 of the new process.
The arguments on stack are:
.data section header:
This call will write 0x1800 bytes of .reloc section at the base address 0x00425000 of the new process.
The arguments on stack are:
.reloc section header:
After all these calls to WriteProcessMemory, the .text section, .data section and .reloc section of the malicious executable have been successfully written to the address space of new process.
It retrieves the ThreadContext by calling GetThreadContext as shown below:
The arguments on the stack:
The first argument is a handle to the Primary Thread in the remote process and the second argument is a pointer to the Context Structure, 0x04F20000.
It is important to note that 0x10007 is the marker of the Thread Context Structure, which is written to the Context Structure before calling GetThreadContext.
MOV ECX, DWORD PTR DS:[ECX]
MOV DWORD PTR DS:[ECX], 10007
This is how the thread context structure looks like after the call to GetThreadContext.
The two important fields in this structure which will be used by the code are:
- Process Environment Block: at 0xA4 offset in the Context Structure is the value, 7FFDA000 (PEB of new process)MOV ESI,DWORD PTR DS:[ECX] ; ESI points to Thread Context Structure
MOV ESI,DWORD PTR DS:[ESI+A4] ; At offset 0xA4 is the PEB Address.
ADD ESI,8 ; At offset 0x8 in the PEB is the ImageBaseAddress
And the ImageBaseAddress in the current process is stored at PE Header + offset 0x34.So, with a new call to WriteProcessMemory, it will fix the value of ImageBaseAddress in the new process.
Arguments on the stack:
It will write 0x4 bytes (ImageBaseAddress) in the Process Environment Block structure at offset 0x8 in the new process.
- The second important field in the Thread Context Structure is the Original Entry Point or the address of the Primary Thread in the new process which will be executed after we call ResumeThread.
- At 0xB0 offset in the Context Structure, we have the Original Entry Point.MOV ECX, DWORD PTR DS:[ECX] ; PE Header base address
MOV EDX, DWORD PTR DS:[ECX+28] ; AddressOfEntryPoint at PE Header offset, 0x28
ADD EDX, DWORD PTR DS:[ECX+34] ; ImageBaseAddress + RVA of AddressOfEntryPoint
MOV ECX,DWORD PTR DS:[ECX] ; Pointer to ThreadContext Structure
ADD ECX,0B0 ; At offset, 0xB0 in Context Structure is Original Entry Point
MOV DWORD PTR DS:[ECX],EDX ; Modify the OEP in Thread Context Structure.
After the OEP is fixed, it calls SetThreadContext to modify the Thread Context of the Primary Thread in the new process.Arguments on the stack:
Thread Context Structure in the Memory Dump:
You can see that the new value of the OEP is: 0x00416D95
After we call ResumeThread, the execution will resume at this address in the new process.
Patching the Bytes
If we execute the call to ResumeThread, the new process will execute and we will be unable to debug it.
If you try attaching a debugger to the new instance of the process before calling ResumeThread, you will observe that it is not listed in the process list.
In order to allow us to debug the new process, we patch the bytes that will be written to the OEP in the new process (at address, 0x00416D95).
Now, we need to find the Original Entry Point of the primary thread that will be executed in the newly created process after we resume its execution.
It is ImageBaseAddress + AddressOfEntryPoint
RVA, AddressOfEntryPoint is present at an offset, 0x28 inside the PE Header
The screenshot below shows that the PE Header starts at an offset, 0xD8. If we go further to offset 0x28 inside the PE Header, we have the AddressOfEntryPoint as: 0x00016D95.
So, the Original Entry Point in the new process = 0x00400000 + 0x00016D95 = 0x00416D95
We have to make sure that when we call ResumeThread, it pauses the execution, so that we can attach a debugger to the newly created process.
In order to do this, we have to patch the bytes that will be written at address, 0x00416D95 in the new process.
To do this, we need to find the location of the bytes in our current process that will be written at 0x00416d95.
In the second call to WriteProcessMemory() we can see that the bytes at position, 0x00416D95 will be overwritten. So, we need to patch before the call to WriteProcessMemory is executed.
Base address at which memory will be overwritten = 0x00401000
Original Entry Point - Base Address = 0x00416D95 - 0x00401000 = 0x15D95
This means, the bytes starting at position, 0x15D95 in the buffer will be written to the Original Entry Point of the newly created process.
Base address of the buffer = 0x01F20420
Base address of the Original Entry Point = 0x01F20420 + 0x15D95 = 0x01F361B5
We go to this address in the Disassembler and see this:
01F361B5 55 PUSH EBP
01F361B6 8BEC MOV EBP,ESP
01F361B8 83EC 10 SUB ESP,10
This is how the beginning of the subroutine looks, and will be executed when we ResumeThread.
Now, we will patch the first two bytes to EBFE which is the opcode for jmp $-2. This will result in an infinite loop, because the size of this instruction is 2 bytes. Each time it executes it will go back 2 bytes in position and execute again, which is the same instruction.
Once we have done this, we resume the Thread and attach a debugger to the new instance of the Process.
We allow it to run however since the instruction at the Original Entry Point in the new process is JMP $ - 2, it does not execute further.
So, we set a Breakpoint at 0x00416D95, which will pause the process.
We again patch the instruction at the OEP and restore the original bytes.
Now, we can begin debugging the malicious code.
After reading this article, it should help in analyzing the code of viruses which use similar techniques to make the process of reverse engineering difficult.
Learn Digital Forensics