The EvilVM architecture presents several major components. This post describes each, in order to provide a general overview of how the system works.
The agent is an executable object, either as a position independent shellcode or an executable. The agent bootstraps using rudiments drawn from KERNEL32.DLL, and thus has no dependencies other than what should always be present in any process on a standard Windows machine. After bootstrapping the compiler, the agent enters an infinite loop processing incoming messages on its input stream, and returning data on its output stream.
Agents are configurable, and may vary depending on choices made when they are built. For example, agents may use any of several transport layers for moving data on the input and output streams. One agent may use an HTTP transport, while another a TCP socket, and yet another might interact with the console STDIN and STDOUT streams. Above the transport layer, however, all agents run the same compiler and use the same scheme for interacting with the user.
Agents are built using the
build.rb script provided in the project’s root directory.
The user runs a server process, which will receive connections from deployed agents. This server provides functionality for maintaining multiple, concurrent sessions. There is also facility for delivering code to a running agent, as well as interaction with the agent’s outer interpreter (REPL or “shell”).
The server is designed to receive connections only via a TCP socket. Other transports require use of server shims. At initial release, the only such shim is for supporting the HTTP transport – it manages sessions with remote agents, and tunnels their I/O streams through TCP sockets to the server component.
In general, you can conceive of communications with the server as a simple stream of bytes in and out. However, the server does have some implicit handling of special cases to handle the agents more effectively. Most of these behaviors involve use of ASCII control codes, and are mostly in place to simplify the user’s experience.
The agent embeds only a very small dictionary of Forth words, as small size is one of its key objectives. For example, the language as embedded in the compiler is not even aware of key syntax such as
if ... else ... then conditionals, loops, and comments. These, and a variety of other important primitives are intended to be provided to the freshly bootstrapped compiler on initial connection. This core API is implemented in the
The contents of this core API are intended to remain fairly static, and serve as a consistent foundation for building more complex behaviors. It should typically not be modified in regular use.
A number of code modules that implement different capabilities on top of the core API are provided. These use a simple notation in header comments for preloading dependencies, and server as a system of programs providing deployment capabilities for the system. Some modules may alter the language in incompatible ways, and this codebase should be considered fluid and malleable.
Some of these samples are more like libraries (such as the content in
parsers.fth), while others are end-user functionality (such as
procdump.fth). The user is expected to use these either for their functionality, or as reference to implement new modules for desired behavior.
Generation of the agent can include additional customizations on top of simple assembly of a configured agent. Encapsulation modules are wrappers for encoding, compressing, or disguising the payload. At initial release, these are few in number, but will probably grow over time.