Sunday, 12 June 2016

The Crazy Ideas Section - Remote Syscalls

Hi to you all. Sorry for not posting in a very long time, quite busy at work :(
I think I will start a new section in the blog, a section about crazy ideas. A crazy idea is usually a concept that can radically change how stuff works basically. I didnt do it yet because I want to bring the ideas to some level of POC, but most of the times it's too difficult to achieve this goal in my spare time/budget or the POC looks horrible. The concept I will talk about today is the latter case :(

Remote Syscall - actually I think it's quite self explanatory.  You have "Computer A" and "Computer B". You have a software on "Computer A" and you want to run it on "Computer B" by capturing the syscalls on "Computer A", encapsulating them, sending them over (tcp?) to "Computer B", Running the syscall and returning the answer.

This procedure letting me run apps on a remote computer, doing all the user mode operation in my "Computer A" and all the kernel stuff (storage/network) to the "Computer B" - all of this without the application knowing anything.

Why doing it? There are plenty of reasons.
Scenario 1 - You are an IT System Administrator. You got a lot of applications and platforms under your control, from routers/switches up to many servers running many VMs with different usages. There are some solution in order to manage such network, but they are limited to the agent capabilities. In order to get more "logic" to the node management, you need bigger agent on the target. For example, with remote syscall, I can take a super thin linux server (a router for example?), without anything installed on it, and over it run metasploit framework in order to check my servers security, all of this without installing the few hundred megabytes and all of the dependencies of metasploit to my poor thin linux server. Sometimes installing other apps could conflict with the current server installation. For example, if the application on the server requires ruby in a certain version but my metasploit diagnostic tool requires ruby in different version, remote syscall solves it as it doesn't install anything to the server. From an IT point of view its only making sense. In order to manage a server you usually connect to the remote server by SSH, then you are limited by the platform, system commands changes, tools you don't have on the server and can't install them. It's just plain idiotic to work that way.

Scenario 2 - Protecting your code. If you want to separate between user mode and kernel mode. Let's say you are developing an application that analyze the computer, do some calculations and return an answer. If you don't want the remote side to know what calculation you did, you need to separate the computer analysis and the calculation part, easy thing to do with remote syscall as its separate between user mode and kernel mode.

There are a lot of use cases to this technology, depends on your line of work. I am sure every IT Administrator can use this kind of tool heavily and I am sure there are a lot of usages even without IT.

The idea in the beginning didnt strike me as a difficult thing to implement. Getting deeper and deeper into development prove me wrong. As I developed the POC, I realized how all this tool does is map between memory of "Computer A" and "Computer B"

In order to make things simple, I only started with implementing the socket based syscalls
The implementation of the code is quite horrible. I did it on my free time (few days off from work), and the POC is quite complex. The code should be rewritten much better, sorry for that.

Lets start with something simple:
man 2 socket

int socket(int domain, int type, int protocol);

Really simple!
we encapsulate the syscall number, all three ints.
The problem is with file descriptors.

First Problem - File Descriptors

I want to manage file descriptors of the local application and the remote application. I need to keep stdout, stderr and stdin locally, but all the rest remotely. I need to keep track which file descriptors are remote and which are local.

In terms of code, its meaning that we should save the integer that socket returns, and for now just minds it's there.

man 2 write/read

ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);

How do we encapsulate buf?

Second Problem - Pointers/Memory Mapping

This is where the most logic of this tool relies. This syscall is actually quite easy in terms of pointers and memory mapping, but it gets much more complex.
How can I map memory between syscalls on two separate machine?
The way I managed it is by writing a pseudo language that describe syscalls and structs.
For example
S ssize_t write( FD int fd, >+1 void *buf, S int count);

S meaning its stack variable.
FD - the variable is file descriptor
>+1 - the size to the point is +1 (next to current argument)

S ssize_t read( FD int fd, <+1 void *buf, S size_t count);

< - the direction of the pointer. For read is <, for write suppose to be >. can be both if needed.

more complex samples:

S ssize_t recvfrom( FD int sockfd, <+1 *buf, S size_t len, S int flags, <+1R sockaddr_recv *src_addr, < socklen_t *addrlen)

<+1R - need to read the count from pointer

where sockaddr_recv looks something like that

class sockaddr_recv(ctypes.Structure):
    _fields_ = [("sin_family", ctypes.c_ubyte),                
                ("sin_port", ctypes.c_ushort)]

thats how the python can know the struct size.
But sometimes struct can contain pointers too.
Structs should also contains pseudo language!

class msghdr(PointerStruct):
    _fields_ = [('C+1 msg_name', ctypes.c_void_p),                
                ('msg_namelen', ctypes.c_size_t),               
                ('R+1 iovec', ctypes.c_void_p),               
                ('msg_iovlen', ctypes.c_size_t),          
                ('C+1 msg_control', ctypes.c_void_p),          
                ('msg_controllen', ctypes.c_size_t),           
                ('msg_flags', ctypes.c_int)]

Where ioven for example, its struct of its own!

class iovec(PointerStruct):
    _fields_ = [('C+1 iov_base', ctypes.c_void_p),     
                ('iov_len', ctypes.c_size_t)]

The code should recursively deep copy all the structs using those definitions.
Thats a really small taste from the issues I had.

Another problem is poll/select syscalls. Those syscall can manage many numbers of FDs, some of the FDs are configured by me as local, some remote. For example, netcat run select on stdin and socket fd, in order to capture/print to screen the first operation. The remote syscall should split the syscall, run the stdin on local and socket fd on the remote, and understand which of the splitted syscalls returns first.

Thats really really the basic problems you gonna have.

I attached the code to my github.
I hope someone can make this concept great.

(As I said, it looks horrible.)

I would have rewritten it, instead of textual describing a syscall I would probably use python dictionary with each pointer/arg type represented by class.
A better way to override specific syscall behaviour.
Maybe an ugly way, of which each syscall is implemented separately?
More optimization. The recursive deep copy is really slow with zero optimization.

No comments:

Post a Comment