Ответ 1
Возможные? Не так, как вам кажется. Задача состоит из двух частей:
1) Как получить двоичный код в памяти
Когда мы укажем /dev/stdout
в качестве выходного файла в Linux, мы можем подключиться к нашей программе x0
, которая читает
исполняемый файл из stdin и выполняет его:
gcc -pipe YourFiles1.cpp YourFile2.cpp -o/dev/stdout -Wall | ./x0
В x0
мы можем просто прочитать от stdin до достижения конца файла:
int main(int argc, const char ** argv)
{
const int stdin = 0;
size_t ntotal = 0;
char * buf = 0;
while(true)
{
/* increasing buffer size dynamically since we do not know how many bytes to read */
buf = (char*)realloc(buf, ntotal+4096*sizeof(char));
int nread = read(stdin, buf+ntotal, 4096);
if (nread<0) break;
ntotal += nread;
}
memexec(buf, ntotal, argv);
}
Также было бы возможно, чтобы x0
выполнял компилятор напрямую и считывал вывод. На этот вопрос был дан ответ: Перенаправление вывода exec в буфер или файл
Предостережение: я просто понял, что по какой-то странной причине это не работает, когда я использую pipe |
, но работает, когда я использую x0 < foo
.
Примечание. Если вы хотите изменить свой компилятор, или вы делаете JIT, как LLVM, clang и другие фреймворки, вы можете напрямую генерировать исполняемый код. Однако для остальной части этого обсуждения я предполагаю, что вы хотите использовать существующий компилятор.
Примечание: выполнение через временный файл
Другие программы, такие как UPX, выполняют аналогичное поведение, выполняя временный файл, это проще и переносимо, чем описанный ниже подход. В системах, где /tmp
отображается на RAM-диск, например, на типичные серверы, временный файл будет в любом случае основан на памяти.
#include<cstring> // size_t
#include <fcntl.h>
#include <stdio.h> // perror
#include <stdlib.h> // mkostemp
#include <sys/stat.h> // O_WRONLY
#include <unistd.h> // read
int memexec(void * exe, size_t exe_size, const char * argv)
{
/* random temporary file name in /tmp */
char name[15] = "/tmp/fooXXXXXX";
/* creates temporary file, returns writeable file descriptor */
int fd_wr = mkostemp(name, O_WRONLY);
/* makes file executable and readonly */
chmod(name, S_IRUSR | S_IXUSR);
/* creates read-only file descriptor before deleting the file */
int fd_ro = open(name, O_RDONLY);
/* removes file from file system, kernel buffers content in memory until all fd closed */
unlink(name);
/* writes executable to file */
write(fd_wr, exe, exe_size);
/* fexecve will not work as long as there in a open writeable file descriptor */
close(fd_wr);
char *const newenviron[] = { NULL };
/* -fpermissive */
fexecve(fd_ro, argv, newenviron);
perror("failed");
}
Предостережение: обработка ошибок не учитывается. Включает для краткости.
Примечание: объединив шаги main()
и memexec()
в одну функцию и используя splice(2)
для копирования непосредственно между stdin
и fd_wr
, программа может быть значительно оптимизирована.
2) Выполнение непосредственно из памяти
Один из них не просто загружает и выполняет двоичный файл ELF из памяти. Некоторая подготовка, в основном связанная с динамической связью, должна произойти. Существует много материала, объясняющего различные этапы процесса связывания ELF и изучения его, заставляя меня считать, что это теоретически возможно. См. Например, этот близкий вопрос на SO, однако, похоже, не существует рабочего решения.
Обновить UserModeExec, похоже, очень близко.
Написание рабочей реализации потребует очень много времени и, несомненно, вызовет некоторые интересные вопросы. Мне нравится думать, что это по дизайну: для большинства приложений крайне нежелательно (случайно) выполнять свои входные данные, потому что позволяет впрыск кода.
Что происходит, когда выполняется ELF? Обычно ядро получает имя файла, а затем создает процесс, загружает и сопоставляет различные разделы исполняемого файла в памяти, выполняет множество проверок здравомыслия и маркирует его как исполняемый файл перед передачей управления, а имя файла возвращается к компоновщику времени выполнения ld-linux.so
(часть libc). Он заботится о перемещении функций, обработке дополнительных библиотек, настройке глобальных объектов и переходе на точку входа исполняемых файлов. AIU этот тяжелый подъем выполняется с помощью dl_main()
(реализуется в libc/elf/rtld.c).
Даже fexecve
реализуется с использованием файла в /proc
, и именно эта потребность в имени файла приводит нас к переопределению частей этого процесса связывания.
Библиотека
- UserModeExec
- libelf - чтение, изменение, создание файлов ELF
- eresi - играть с эльфами
- OSKit (похоже, как мертвый проект)
Чтение
- http://www.linuxjournal.com/article/1060?page=0,0 - введение
- http://wiki.osdev.org/ELF - хороший обзор
- http://s.eresi-project.org/inc/articles/elf-rtld.txt - более подробное объяснение для Linux
- http://www.codeproject.com/Articles/33340/Code-Injection-into-Running-Linux-Application - как попасть в мир приветствия
- http://www.acsu.buffalo.edu/~charngda/elf.html - хорошая ссылка структуры ELF
- Погрузчики и компоновщики от John Levine - deeoer объяснение ссылки
Вопросы, относящиеся к SO
- Linux-пользовательский загрузчик ELF
- Подтверждение поиска символа динамического загрузчика ELF
- перемещение ELF нагрузка-время
- Как инициализировать глобальные переменные загрузчиком эльфа
Итак, кажется возможным, вы сами решаете, насколько это практично.