UM Virus Detecting: Part 1 - Imports

Good Morning! My name is seb and welcome to my blog. We'll start off talking about how to detect usermode viruses. It's very simple, but we won’t be getting into EDR (endpoint detection and response) yet, just the basics. I'm heavily researching Windows Internals, but starting off with something basic to build up from.


Unfortunately, for my safety, I will use a nickname for this antivirus. This nickname will be "Easy SuperInfector" When I first started analyzing usermode viruses, I jumped into IDA to check what imports these viruses were using. Imports give you a solid way to fingerprint malware because certain APIs are commonly abused by viruses.

For example, here’s a list of suspicious functions I pulled from analyzing free virus samples:

const std::vector<std::string> freeFuncs = {
    oxorany("PeekNamedPipe"),
    oxorany("LeaveCriticalSection"),
    oxorany("CreateRemoteThread"),
    oxorany("ReadProcessMemory"),
    oxorany("WaitForMultipleObjects"),
    oxorany("GetFileSizeEx"),
};

These are classic APIs for manipulating other processes or doing IPC in sneaky ways.

And here’s a list from paid or more advanced malware samples from Easy SuperInfector:

const std::vector<std::string> paidFuncs = {
    oxorany("CreateDXGIFactory"),
    oxorany("InternetCheckConnectionW"), // important, it’s on all premiums
    oxorany("GetCurrentProcessId"),
    oxorany("IsDebuggerPresent"),
    oxorany("GetStartupInfoW"),
    oxorany("GetModuleHandleW"),
    oxorany("GetCurrentProcess"),
};

I keep these lists separate to track different malware tiers. I also remind myself to cut these down later if malware authors change their imports.


Next, I needed to open the file and map it into memory so I could inspect its contents.

This opens the binary file in read mode with shared read access. If it fails, I return false because I can’t analyze a file that’s not open.

Then I map it into memory:

Mapping the file as an image lets me read its memory as if it were loaded in a process. PAGE_READONLY | SEC_IMAGE flags make sure the mapping treats it as a valid executable image.

If mapping fails, I close the file handle and return false.


Now I create a view of this mapped file:

This gives me a pointer to the file data in memory. Without this, I can’t read the PE headers or imports.


Next, I validate the PE headers to make sure the file is legit:

The DOS header has to start with 'MZ' (IMAGE_DOS_SIGNATURE). If not, this is not a valid PE file.

Then:

This checks the NT header signature ('PE\0\0'). If this fails, the file’s corrupt or invalid.


Now for the important part, finding the import directory RVA:

If this is zero, the file has no imports, so no suspicious API usage to scan.


Then I start iterating the import descriptors:

This loops over each imported DLL, reading the imported functions one by one.

Inside this loop, I access the thunk data, getting pointers to the imported functions:

This gives me the actual function names or ordinals.


While scanning the imported functions:

I get the function name, lowercase it for easier comparison, then check if it’s in my suspicious lists.

If yes, I add it to the found sets.


Finally, I clean up all the handles and mapped views:


And return true if the binary imports all functions in either the free or paid suspicious lists:

Basically, if the binary is importing every function from one of those lists, it’s a huge red flag.

Last updated