Thursday 9 December 2010

Pulling the digital rug out from under the malware's feet

Usermode processes are limited in terms of what they can do by the API platform provided by the OS and the non-privileged instructions that they can execute. This means they have to abide by certain rules and regulations put in place to protect system integrity and security. Usermode processes cannot directly communicate with the BIOS or peripheral hardware, nor kernel objects - they must talk to them via drivers and other kernel-mode code. The model of communication roughly looks like this:

Process <-> Service <-> Driver <-> Kernel

The service is not a traditional Windows service that runs as a process - it's a service representing the kernel mode driver (KMD) that sits in the kernel layer. The process communicates with the service using DeviceIoControl and other associated APIs. The service acts as a bridge between user- and kernel-space. The driver then interacts with system APIs, structures and mechanisms in order to perform its tasks. In the case of a hardware driver, the driver uses objects that are provided by the Hardware Abstraction Layer (HAL). The HAL acts as the bridge between software by providing software access to physical device memory. For example, the HAL might inform a KMD the address at which the memory for a graphics card's display buffer is mapped in virtual memory.

The model of isolated kernel- and user-mode is convenient because it alleviates the need for software developers to implement their own low level mechanisms such as spinlocks and memory isolation. It also allows the system much greater control over how everything is running and therefore massively improves stability and security. In a system that doesn't distinguish between user and kernel memory, a process could write over important kernel code or data. This could either be because the process is malicious, or it has a bug. In systems like that, a crashing process often crashes the whole system.

If you put all of this in the context of malware, it's like having a virtual machine running with the malware process in it. To put it bluntly: you can trash the crap out of user memory, but kernel memory remains intact. This means in the case of disaster (e.g. a critical system service such as lsass.exe dies) the kernel can finish important tasks before throwing a bugcheck and halting the system to prevent further damage.

What makes a great weapon in the fight against malware is a KMD-based anti-malware solution. The kernel can in fact hide critical anti-malware processes and hook important calls (e.g. VirtualProtectEx, WriteProcessMemory, etc) to make sure that the anti-malware processes stay intact. It can also access any user object (process, thread, memory, file, etc) regardless of its user-level protection. It's like having the ability to walk through walls. As the title of this post said, you can pull the digital rug out from under the malware's feet without it being able to stop you.

One important thing anti-malware solutions need to do is monitor the execution of processes. In a KMD it is possible to register a callback function that is called whenever a new process is created. This can be achieved in Windows with the PsSetCreateProcessNotifyRoutine system API. The same thing can be done for threads with PsSetCreateThreadNotifyRoutine. You simply pass it a pointer to a callback function and that function gets called whenever a new process is created. Of course it might be more prudent to hook an API responsible for loading executable binaries using the System Service Dispatch Table (SSDT) so that you can check the executable file for known malware before allowing the creation of the process to continue.

What we must remember is that whilst a malware KMD could technically be loaded and attack our kernel-mode code. Whilst this is somewhat unlikely, it is still a scenario that could occur. There's not much we can do about this. At the point when malware infects the kernel, the system can (pretty much) be considered irreparably comprimised.

For now this post is simply a conglomeration of my personal knowledge and a great deal of research. I am currently considering whether it would be feasible to introduce a KMD into my project.

Wednesday 8 December 2010

Integrity checking

In order to make sure anti-malware software itself isn't comprimised, we can take a few precautions.

The first mechanism is assembly signing. Using a certificate we can make sure that the CLI will refuse to load the binary if it is modified by a 3rd party. It not only provides this security, but also proves the identity of the individual or group that produced the assembly. This means that we get security and assurance of authenticity, which is good for real security and user confidence. Visual Studio provides the ability to sign executables when they are produced. Certain tools are available that strip signatures from an assembly, so it is a good idea to programatically check that the assembly is signed at startup. We can also maintain a read/write lock on critical files to help make sure they are not modified.

An issue that is much more difficult to tackle is runtime manipulation. Process memory has been safe from direct manipulation since Windows NT4 due to the introduction of isolated process memory, but the WriteProcessMemory API still allows us to modify the memory of a remote process. It is also possible to create a thread in another process, allowing execution of arbitrary code in that process's context.

There are a few tricks we can use to check if our binary has been patched in memory. One thing we can do is perform a read operation on our own memory (the memory allocated for .code) and compare it to a hash to check that no code was modified. This is an annoyance for malware writers, but it could easily be patched by writing over the check code or modifying the hash. Whilst the system could be considered irreparebly comprimised if malware could write memory to a process running as SYSTEM, it's not to say that the situation could not be quite easily recovered from manually.

Another trick is memory protection scanning. If we remove the write flag from the protection value on all memory in our process that doesn't need to be read (e.g. the code) the malware must modify that memory's protection in order to write to it. This can be done in usermode by calling VirtualProtectEx. One could write a kernel-mode driver (KMD) to hook such calls and block them, but this is complex and likely outside the scope of this project. In our case, we can simply iterate all pages in our process' memory and check the protection constants match up.

Sunday 5 December 2010

Type scanning

Files are usually identifiable by their extension. Whilst we'd expect .jpg to be a JPEG image file, it might actually contain text or executable data. This is a problem in malware scanning because it's trivial to modify the registry to make any file extension to be treated as an executable.

HKEY_CLASSES_ROOT\.abc
    (Default) = "exefile"
    Content Type = "application/x-msdownload"
HKEY_CLASSES_ROOT\.abc\PersistentHandler
    (Default) = "{098f2470-bae0-11cd-b579-08002b30bfeb}" 

I could then rename file.exe to file.abc and run it, and it'd run like a normal program. In the context of anti-malware software, we have to scan every file's data to check that files are in fact what they say they are. To shorten this process, we can use file type signatures. For example, an extremely simple signature for Win32 executable files might be that the file starts with the ASCII characters 'MZ' and also contains the ASCII string 'This program cannot be run in DOS mode'.

There is a project called TrID that aims to do exactly this. It comes with a large list of file type definitions that were generated by comparing hundreds of samples of the same file type. This data can be easily re-generated and compiled into a single database file.

Friday 3 December 2010

Removal technique using CSharpCodeProvider

Defining a full set of rules as simple information (e.g. formatted as XML) is difficult because of the complex nature of a malware removal script. It might involve loops, conditional code and all manner of calls to system APIs to find out information before removal. This makes a rigid format nearly impossible to create and maintain. Instead, Aether will use removal scripts written in C# to remove detected malware. As it turns out, this is not nearly as difficult as it may at first seem.

All .NET source code (C#, VB.NET, F#, etc) is translated into Common Intermediate Language (CIL) bytecode at compile time, which is then placed into a .NET binary executable[ref]. At runtime, the bytecode is translated by the Common Language Runtime (CLR) into instructions native to the processor. The framework not only allows some interesting runtime manipulation of this process, but also allows a program to call on the compiler itself at runtime to compile .NET source code into a managed assembly for use in the program. This assembly can be written directly out to a file, or kept in memory for use via reflection.

.NET runtime compilation diagram

We can use this technique to write malware removal definitions. The raw source code of the removal process is stored in the definitions file (obviously kept secure using a digital signature) and is then loaded into memory and compiled. The program then uses reflection to discover the assembly's entry point and begin execution. The code executes inside the host program's memory space and does not need a separate process.

The actual compilation code is not complex or difficult. It can be accomplished in around 20 lines of code, using the CSharpCodeProvider and CompilerParameters classes. Using reflection on the produced assembly, we can then execute the removal code. It is also trivial to pass parameters to methods in the assembly, for example the location of the infected file.

To save overhead, compiled assemblies will be cached in memory for the duration of the scan. Simple removals (kill process, delete files) have a global script that is compiled automatically when a scan begins.

Controlling services with a GUI using unmanaged memory IPC

Running a security program as a service has some great benefits:
  • Its start and stop events, along with any errors, are automatically logged as system events.
  • It is trivial to make the process run under the local system account.
  • Failure recovery policies can be defined and managed by the operating system itself.
However, the biggest problem is that services cannot communicate with the desktop window manager and therefore cannot directly own any window handle. All control of the service must be done through other mechanisms. Whilst a skilled user might be able to interact with the service control manager directly using the command line, it is not user friendly and provides a limited experience. To really provide a useful user interfact, the service must accept some form of inter-process communication (IPC) that accepts incoming commands and parameters and returns status messages.

In this project, I have decided to use unmanaged memory IPC. This is a form of IPC in which blocks of memory in the service and GUI frontend application are allocated and then used to communicate between the two programs. The initial setup of this method requires a handshake in which the address of a memory block can be passed to the other process. To do so we can send a custom command to the service using the service control manager.

The following is a simple example of how unmanaged memory IPC can be set up:
  1. Client program uses VirtualAlloc to allocate two blocks of memory - one for parameters, one for return values. It fills the memory blocks with the pattern 0x1337D00D. The memory pages for the return value block are marked as PAGE_READWRITE. The memory pages for the parameters block are marked as PAGE_READONLY. Giving execute permissions would be bad for security.
  2. Client program sends an AETHER_CLIENT_PID message to the service.
  3. Service begins waiting for the process ID of the client program.
  4. Client program sends its process ID to the service as a message.
  5. Service stores the process ID.
  6. Client program sends an AETHER_ADDR_PARAM message to the service.
  7. Service begins waiting for parameter memory address.
  8. Client program encodes the address of the parameter memory block as an integer and sends it to the service as a message.
  9. Service stores the memory address.
  10. Client program sends an AETHER_ADDR_RETN message to the service.
  11. Service begins waiting for return value memory address.
  12. Client program encodes the address of the return value memory block as an integer and sends it to the service as a message.
  13. Service stores the memory address.
  14. Client program sends an AETHER_IPC_TEST message to the service.
  15. Service uses OpenProcess to get a handle to the client program's process, then uses ReadProcessMemory to check that all buffers read 0x1337D00D. It then uses WriteProcessMemory to fill the memory blocks with 0x00000000.
  16. Client program waits 100ms and checks the memory blocks to make sure the pattern was written correctly.
  17. If either test (15 or 16) failed, throw an error message.
At this point, the client program can write parameters into the parameters memory block (it alters the protection constant to PAGE_READWRITE temporaraly) and send a service control message to the service asking it to perform an operation. The service can then read the parameter using ReadProcessMemory. It performs some initial parameter checking, and returns a status code back to the return value message block to state whether or not the task has started. If everything is OK, it then performs the operation, and returns any necessary return values to that block.

To prevent problems such as overlap (e.g. a return value for one operation being sent when a return value for another is expected) each operation is designated a unique identification number. Furthermore, the code that manages IPC is designed such that only one operation can be communicated about at one point in time. To do this, I will use the Mutex class.

Parameters and return values will be encoded as binary serialized structures (using the BinaryFormatter class) with a prepended length value. In order to provide a literally identical structure for both client and service code, the shared structures will be placed in a separate library.

Thursday 2 December 2010

Project blog

I will be using this blog to document my ideas and progress whilst designing and developing an anti-virus software program. This project is named Aether, after Zeus' defensive wall in Greek mythology.