When learning about Linux syscalls and I/O operations, one will always encounter the operation that allows jumping to a given location in an open file, or as it is more commonly called, seeking. Depending on the specifics of the functionality required, there are multiple primitive alternatives for achieving this, such as seek and lseek.
Usually, seeks are followed by writes or reads, because the information contained at the destination offset is needed by the program performing the operation. So a naturally arising I/O pattern would be something like
<pattern diagram here>
Files for IPC
It is well known that files have been used for inter-process communication. The most basic example of this is memory-mapped files, which are shared between two or more different processes. The information contained within them is therefore accessible to both of the processes, as they can write to and read from them.
Using a normal file is also feasible, but it has the additional overhead of having to write everything to the disk before it becomes accessible to the other process. In both of these cases, the processes' instructions do not possess a guaranteed execution order, so thread safety mechanisms have to be put in place for our system not to end up in an invalid state. This is somewhat easier with files on the disk because they have associated access rights and can be locked by using native mechanisms.
Unless explicitly specified, valid file descriptors are inherited by child processes. This means that when a process opens a file for reading or writing, and then creates a new process (suppose it calls fork()), the newly created process will be able to perform operations on that open file. However, because they point to the same File Descriptor Table entry, the offset position in the file will be shared by the two processes!
Seek to Win
Knowing that the current offset is implicitly shared and that it can be queried by a zero-move, we can actually use the position in the file itself to relay messages. A simple idea is to take a string, and always seek out the position which corresponds to the code of the next character. We also need to add a synchronization condition, because we cannot be sure that the operations will be performed in the correct order unless we make sure ourselves.
#define SIG_SYNC 0
#define SIG_END 1
int sendText(int fd, char *text) {
for (int i = 0; i < strlen(text); i++) {
// Use the character as the seek position!
lseek(fd, text[i] + SIG_END, SEEK_SET);
// Wait until the child process signals that it has read the character
while (lseek(fd, 0, SEEK_CUR) != SIG_SYNC);
}
// Send the end character
lseek(fd, SIG_END, SEEK_SET);
return 0;
}
int receiveText(int fd, char *text) {
int len = 0;
while (1) {
// Wait until the parent process has finished writing
while (lseek(fd, 0, SEEK_CUR) == SIG_SYNC);
// Read the character sent by the parent process
int chr = lseek(fd, 0, SEEK_CUR);
chr -= SIG_END; // Ignore special values
text[len++] = chr;
if (chr == 0) {
// Null byte received, we're done
break;
}
// Signal the parent process that we have read the character, and it can take over
lseek(fd, SIG_SYNC, SEEK_SET);
}
}
I tested this and made a Gist with a full, working example. If you want to try it for yourself, you can take a look. This is what the output looks like:
[+] Parent process created by 10430 with pid 11649
[i] The text to send is: "Your random number is 92."
[+] Parent process sending text
[+] Child process created by 11649 with pid 11650
[i] Child process receiving text
[i] Parent process sent text
[i] Child process received text:
"Your random number is 92."
[-] Child process exited
[-] Parent process exited
Optimizations
It can be noted that the sample code above transfers one byte at a time. However, the argument type for the seek position is off_t, of which we may assume that it has a size of at least 4 bytes, but it can also be of 8 bytes (according to the POSIX standard). Therefore, in the best-case scenario, we can speed this operation up by a factor of 8.
A better implementation of the same technique can be found .
Stealth Factor
Upon using this technique, an empty file will be created. No writes or reads are necessary, and as such, the file itself which is being created is of no interest to us. Additionally, the fact that the technique breaks the payload into chunks of 8 bytes assures that no signature-based detection can be performed on it while it is being transmitted.
Because this technique makes use of common syscalls, nothing is inherently "suspicious" about it. It cannot be efficiently monitored with common hooks and basic techniques, as monitoring all seek calls is just not feasible when it comes to resource usage. At the very least, it would be difficult for consumer-level antimalware solutions to employ a relevant detection mechanism, and even then, they would only be relying on heuristics to detect irregular seek patterns.
To combat this, one could add a simple pseudo-random condition to actually write some garbage to the file every n (or maybe a variable number of) seeks. Reading can also be used, maybe even on existing files. Adapting this technique to evade detection is very easy, should it ever be deemed necessary.
Windows Equivalent
I haven't tested this technique on Windows, however in theory it should be possible. Instead of file descriptors, handles would be used, and for the seek operation, one could call SetFilePointerEx(), which is not inherently suspicious. Aside from some quirks, the general idea can probably be implemented for many different buffer-based transmission mechanisms.
Conclusion
In conclusion, inter-process communication using offset jumps presents a covert way of transmitting data between processes in a Linux environment. By utilizing the implicit sharing of the current offset between processes, this technique allows for message relaying through file operations. The sample code provided shows how to send and receive text between parent and child processes using offset jumps. This technique can be optimized by transferring more than one byte at a time, and it is not easily detectable by signature-based detection techniques due to the breaking of the payload into chunks of 8 bytes. Since it only uses common syscalls, it cannot be efficiently monitored by common hooks.
This is a test comment to see if it would work