I want to present a concept for a “hypervisor” that can exclusively run userland applications. It can also be considered a mandatory access control (MAC) suite that can run as a userland application (as opposed to being a kernel module).
The idea is simple; the program (from hereon referred to as ULVM – userland virtual machine) will open the binary to be executed and load the data embedded in its individual segments into memory allocated by ULVM. Then the following cycle is initiated:
- Locate the next instruction that is either a control flow instruction or the INT instruction (this is ‘a’).
- Alter all memory references up until (a) (read below for an explanation on this).
- Put all code up until (a) in buffer (b).
- Call a special stub written in assembly language from C with two arguments: 1) the memory location of buffer (b) and 2) the memory location of an array of unsigned 32 bit integers where intermediate register values will be stored.
- Emulate the subsequent control-flow or interrupt 0x80 instruction.
- Go to 1.
What we will need to make this work is:
- A function that, given a chunk of machine code, can assess where an instruction starts and where it ends (a instruction length disassembler will suffice) and whether an instruction is a control flow instruction or an INT.
- A buffer of memory that can be executed.
- A region of memory into which we can store intermediate register values.
- A chunk of machine code (the stub) that will 1) load the register values stored in the aforementioned region of memory to the real processor registers 2) jump to the buffer of memory with the executable code. 3) Store the register values now stored in the processor back into the intermediate register storage array.
- A function that can emulate control flow instructions.
- A routine that can detect and alter memory operands (see below under the heading “Memory management”).
Consider the following assembly language program:
XOR EAX, EAX MOV EBX, EAX JMP L NOP L: INC EAX INT 0x80
And consider the following (simplified/pseudo-coded) source code of ULVM.
extern “C” void run_code(void* code, uint32_t* registers);
memset(registers, 0, sizeof(registers));
unsigned int i = 0;
while ( .. )
unsigned int j = next_jump_or_int();
memcpy(machinecode_buffer, &(the_binary[i]), j – i);
the_binary[j] = 0xC3; // Terminate with a RET
If one were to run the above program in ULVM, the following scenario would happen (according to the rules enumerated above):
- Locate the next instruction that is either a control flow instruction or an software interrupt. For the example above this entails that the instructions on lines 1 and 2 are selected.
- Instructions 1 and 2 are put in ‘machinecode_buffer’ and appended with a 0xC3 RET instruction.
- run_code is invoked, which will retrieve the register values from the ‘registers’ array and put them in the processor registers, then JMP to machinecode_buffer, and then put the register values in in the processor at that moment back to the ‘registers’ array.
- Control-flow is given back to the main C program which will then emulate the JMP instruction by altering the value of EIP in the ‘registers’ array.
- Loop. Put ‘INC EAX’ in ‘machinecode_buffer’, run_code is invoked, and INT 0x80 is intercepted.
An additional detail that has to be taken into consideration is how to handle memory access properly. Once a binary has been loaded and the loop of routines outlined above is initiated, a program will typically at some point access memory. Considering the fact that an ELF files specifies various regions of memory (sections) by size and vaddr (virtual address), the additional task of the ULVM is to allocate memory for each section prior to entering the execution loop.
The following is an excerpt of objdump -x /bin/bash:
Program Header: PHDR off 0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2 filesz 0x00000120 memsz 0x00000120 flags r-x INTERP off 0x00000154 vaddr 0x08048154 paddr 0x08048154 align 2**0 filesz 0x00000013 memsz 0x00000013 flags r-- LOAD off 0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12 filesz 0x0000a024 memsz 0x0000a024 flags r-x LOAD off 0x0000af10 vaddr 0x08053f10 paddr 0x08053f10 align 2**12 filesz 0x00000240 memsz 0x00000834 flags rw- DYNAMIC off 0x0000af24 vaddr 0x08053f24 paddr 0x08053f24 align 2**2 filesz 0x000000c8 memsz 0x000000c8 flags rw- NOTE off 0x00000168 vaddr 0x08048168 paddr 0x08048168 align 2**2 filesz 0x00000044 memsz 0x00000044 flags r-- EH_FRAME off 0x00009068 vaddr 0x08051068 paddr 0x08051068 align 2**2 filesz 0x000002cc memsz 0x000002cc flags r-- STACK off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2 filesz 0x00000000 memsz 0x00000000 flags rw- RELRO off 0x0000af10 vaddr 0x08053f10 paddr 0x08053f10 align 2**0 filesz 0x000000f0 memsz 0x000000f0 flags r--
Once ULVM’s disassembler detects an attempted memory access in code that has yet to be executed, it has to convert the vaddr to memory allocated by ULVM and rewrite the instruction.
MOV ECX, 0x08053f10
MOV EAX, [ECX]
would be converted dynamically to refer to some array of bytes that was allocated by ULVM when it was loading the sections.
There is a catch, though: if we are to rewrite instructions on the fly, the relative addressing of control flow instructions may imply jumping to the wrong part in the code, since the rewritten instruction may differ in size from the original instruction. It may prove hard to alter all relatively addressed control flow instructions affected by this change on the fly; luckily there are other options.
It would be possible to execute all instruction up until the memory accessing instruction, then emulate the memory accessing instruction and alter the intermediate register storage in the process, and proceed with the next instruction. However, this approach presupposes the existence of extensive emulator capabilities in ULVM, which, considering the fact that we are aiming for speed and being as close to “virtualization” as possible, may be an unwanted (though necessary) feature.
An attempt at a real-world implementation of the concepts presented in this document may unearth more effective solutions to this problem.
In Linux, kernel functions such as opening files are invoked through interrupt 0x80. By differentiating between INT 0x80 instructions and other instructions, we can effectively put a hook on all access to kernel functions. A conceivable scenario could be that two things are supplied to ULVM upon invoking it:
- the path of the to-be-executed binary and its command line parameters
- a rule set specifying which kernel functions the binary is allowed to invoke (and under which circumstances).
It is also conceivable to replace option 2 with a callback function; this could be a function written in C or a Python script that is invoked when the executed binary triggers interrupt 0x80. Note that such a strategy not only allows the user of this system to restrict access to certain kernel functions, it also allows for logging of calls to these functions, or any arbitrary response for that matter. It would also be possible to create a virtual environment with a virtual file system while giving the executed binary the impression that its operating on real files and data. This could be useful in doing a security assessment of a given binary.
The above outlines the main idea behind this virtual machine for userland binaries. However, some things have to be taken into consideration and handled properly, such as checks that assure the binary isn’t writing to non-writable segments, for example.