2005-06-29
Article continued from Page 2
Clearing the code, deleting the injection
Since our scanner is able to find the injected call, we can move on. Now we need to reload the original call. On other words, we need to clear the injection. To do this we should first know more information about the virus.
- The injected call flows the execution to a polymorphic decryptor, which is generated in a way that several decryption phases can occur, from 4 to 7.
- The virus must reset the "hooked" call before returning the execution to the host. Otherwise the infected application may fault. The original instruction is saved somewhere inside the virus body.
The main problem is that the virus is encrypted and the polymorphic decryptor will decrypt the full virus body several times. We need to obtain the clear virus body in order to reset the original instruction. We can't get to those bytes directly since the code is encrypted. There are a couple of solutions to clear/bypass the polymorphic decryption layers, such as using emulation and so on. Writing a full emulator is surely not a quick and easy job, however a different solution does exist. Most Windows viruses use the GetProcAddress API to obtain needed API addresses for their future execution. Lets try to set a breakpoint at GetProcAddress (of course to avoid false GetProcAddress requests. First we need to execute the virus injection, which is easy since we have located it before). This is shown below in Figure 5.

Figure 5. GetProcAddress.
The call came from 0x406AF3, which in fact points to the decrypted body. Indeed, the poly layers were bypassed! Here is the sample proof using the decrypted string, shown in Figure 6.

Figure 6. Decrypted string.
To make the disinfector able to break on GetProcAddress, we need to build a small debugger (which is likely the fastest way to do it). This is easy since Windows platform already comes with Debug APIs.
Basically, following the code debugs the virus process, modifies the original entry of GetProcAddress to 0x90 (nop), 0x90 (nop), 0xCC (int 3 breakpoint) and takes over the EXCEPTION_BREAKPOINT only if it comes from the "hooked" range:
// --- snip of scanner code ------------------------------------------------
...(snip)...
unsigned char patch[4] = { 0x90, 0x90, 0xCC };
_GetProcAddress = (DWORD) GetProcAddress(LoadLibrary("KERNEL32.DLL"), "GetProcAddress");
GetStartupInfo(&si);
if (!CreateProcess(NULL,temp_name,NULL,NULL,FALSE,DEBUG_PROCESS +
DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &si, π))
{
printf("[-] Error: cannot create process, error: %d\n",GetLastError());
goto error_di;
}
printf("\n[+] Process created, pid=0x%.08x\n",pi.dwProcessId);
printf("[+] Starting emulation engine...\n");
while (1)
{
WaitForDebugEvent(&de,INFINITE);
if (de.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT) {
printf("[!] Error: ups process exited...\n");
goto error_term;
}
if (de.dwDebugEventCode == EXCEPTION_DEBUG_EVENT)
{
if (de.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
if (de.u.Exception.dwFirstChance == TRUE)
{
printf("[+] Exception occured at: 0x%.08x, passing to
program.\n",de.u.Exception.ExceptionRecord.ExceptionAddress);
ContinueDebugEvent(de.dwProcessId,de.dwThreadId,\
DBG_EXCEPTION_NOT_HANDLED);
}
else
{
printf("[-] Hard error occured, terminating the program\n");
printf("[-] Disinfecting failed\n");
goto error_term;
}
}
if (de.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_BREAKPOINT)
{
if (fe == NULL)
{
fe = 1;
printf("[+] Reached break point at 0x%.08x\n",
de.u.Exception.ExceptionRecord.ExceptionAddress);
printf("[+] Modifing 4 bytes at host stack\n");
tc.ContextFlags = CONTEXT_CONTROL;
if (!GetThreadContext(pi.hThread, &tc))
{
printf("[-] Failed to get thread context, error: %d\n",
GetLastError());
printf("[-] Disinfecting failed\n");
goto error_term;
}
ReadProcessMemory(pi.hProcess, (void*)tc.Esp, &stack_v,4,NULL);
if (stack_v == NULL)
{
printf("[-] Error: reading from stack failed\n");
printf("[-] Disinfecting failed\n");
goto error_term;
}
tc.Esp = tc.Esp - 4;
caller += 5;
if (!WriteProcessMemory(pi.hProcess, (void*)tc.Esp, &caller, 4,
NULL))
{
printf("[-] Error: writing to stack failed\n");
printf("[-] Disinfecting failed\n");
goto error_term;
}
printf("[+] Stack modified, 0x%.08x added caller -> 0x%.08x\n", \
tc.Esp, caller);
printf("[+] Redirecting EIP to 0x%.08x...\n",where_ctx);
tc.Eip = where_ctx;
if (!SetThreadContext(pi.hThread, &tc))
{
printf("[-] Failed to set thread context, error: %d\n", \
GetLastError());
printf("[-] Disinfecting failed\n");
goto error_term;
}
VirtualProtectEx(pi.hProcess, (void*) _GetProcAddress, sizeof(patch),
PAGE_READWRITE, &oldp);
WriteProcessMemory(pi.hProcess, (void*) _GetProcAddress, &patch,
sizeof(patch), NULL);
VirtualProtectEx(pi.hProcess, (void*) _GetProcAddress, sizeof(patch),
oldp, &oldp);
printf("[+] Placed breaker at 0x%.08x\n",_GetProcAddress);
ContinueDebugEvent(de.dwProcessId,de.dwThreadId,DBG_CONTINUE);
}
if ((DWORD) de.u.Exception.ExceptionRecord.ExceptionAddress >
_GetProcAddress && (DWORD) de.u.Exception.ExceptionRecord.ExceptionAddress
< _GetProcAddress + sizeof(patch))
{
printf("[+] Virus reached the breaker at 0x%.08x\n", \
de.u.Exception.ExceptionRecord.ExceptionAddress);
tc.ContextFlags = CONTEXT_CONTROL;
if (!GetThreadContext(pi.hThread, &tc))
{
printf("[-] Failed to get thread context, error: %d\n", \
GetLastError());
printf("[-] Disinfecting failed\n");
goto error_term;
}
ReadProcessMemory(pi.hProcess, (void*)tc.Esp, &stack_v, 4, NULL);
printf("[+] Virus request captured from 0x%.08x\n",stack_v);
...(snip)...
...(snip)...
ContinueDebugEvent(de.dwProcessId,de.dwThreadId,DBG_EXCEPTION_NOT_HANDLED);
...(snip)...
// --- snip of scanner code ------------------------------------------------
Now when we have the clean virus body we can try to locate the original instructions. Since CTX.Phage doesn't modify the bits from the host code section, it has only one way to reset the original instruction by using WriteProcessMemory API (well, it could use VirtualProtect API to get write access to host code section and then write the original bytes, but it doesn't). So here is the break on WriteProcessMemory, shown in Figure 7.

Figure 7. Break on WriteProcessMemory.
As you can see, BytesToWrite is equal to 5 and Address is equal to the location found by the scanner. The only problem is that the call comes from allocated memory (the virus allocated it, copied itself and continued execution from there). But lets try to check the caller address below in Figure 8.

Figure 8. Checking the caller address.
The "const" bytes (for example those marked in the picture above) are:
Where:
- 6A 00 is push 0
- 6A 05 is push 5
- E8 05 00 00 is call $+5
- ?? ?? ?? ?? ?? is the original host bytes (wildcard)
- 50 is push eax
Here is the signature, useful to find original host bytes (there are the same in every generation), however these ones are located in the allocated memory. So the question is: does the same bytes exists somewhere inside the unencrypted body of virus, in other words, somewhere inside last section? Lets try to scan for it in Figure 9.

Figure 9. Scanning the virus.
Indeed, the same bytes were found in "native" virus location. The GetProcAddress was called by the virus from 0x406AF3, as you can see the original bytes that lay far before it. Here is the code example from the scanner which searches for the original bytes by using the signature. The same could be done by reducing 0x406AF3 by some const size, but regardless here it is:
// --- snip of scanner code ------------------------------------------------
...(snip)...
unsigned char ctx_sig[15] = { 0x6A, 0x00, 0x6A, 0x05, 0xE8, 0x05, 0x00, 0x00, 0x00,
0x90, 0x90, 0x90, 0x90, 0x90, 0x50 };
unsigned char ctx_fly[15];
ReadProcessMemory(pi.hProcess, (void*)tc.Esp, &stack_v, 4, NULL);
printf("[+] Virus request captured from 0x%.08x\n",stack_v);
printf("[+] Scanning backwards to 0x%.08x\n",upa);
while (1)
{
if (!ReadProcessMemory(pi.hProcess, (void*)stack_v, &ctx_fly,
sizeof(ctx_sig), NULL)) break;
if (stack_v <= upa) break;
found = 1;
for (int ii=0; ii < sizeof(ctx_sig); ii++)
{
if (ctx_sig[ii] != ctx_fly[ii])
{
if (ctx_sig[ii] != 0x90)
{
found = 0;
break;
}
}
}
if (found == 1)
{
printf("[+] Orginal bytes were found at 0x%.08x\n", stack_v + 9);
printf("[!] Repairing the broken instruction.\n");
ReadProcessMemory(pi.hProcess, (void*)(stack_v + 9) ,(void*) sv, 5, NULL);
printf("[!] The file was disinfected!\n");
getch();
goto error_term;
}
stack_v--;
}
if (found == 0)
{
printf("[-] Error: no signature was found.\n");
printf("[-] Disinfecting failed\n");
goto error_term;
}
...(snip)...
// --- snip of scanner code ------------------------------------------------
The full EPO heuristics scanner, together with Win32.CTX.Phage disinfector, is attached to the last section of the paper. Here is a screenshot from that application, as shown in Figure 10.

Figure 10. Screenshot of the EPO scanner.
Curtains down last words
I hope you have enjoyed this short article on EPO techniques. The disinfector discussed in this article only cancels virus injections, of course - the virus still resides in last section but fortunately it will never be executed. However, this provides an opportunity for the reader to add some kind of virus "overwriter," it is really an easy job and a good task to undertake.If you have any comments don't hesitate to contact the author. The author would also like to thank Satish Ks for moral support.
Further Reading
- http://securityresponse.symantec.com/avcenter/venc/data/w32.ctx.and.w32.cholera.html by Peter Szor and Wason Han.
- http://vx.netlux.org/29a/29a-4/29a-4.223 EPO by GriYo / 29a.
- "The Art of Computer Virus Research and Defense" by Peter Szor
