Выполнение машинного кода в памяти - PullRequest
26 голосов
/ 07 января 2010

Я пытаюсь выяснить, как выполнить машинный код, хранящийся в памяти.

У меня есть следующий код:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[])
{
    FILE* f = fopen(argv[1], "rb");

    fseek(f, 0, SEEK_END);
    unsigned int len = ftell(f);
    fseek(f, 0, SEEK_SET);

    char* bin = (char*)malloc(len);
    fread(bin, 1, len, f);

    fclose(f);

    return ((int (*)(int, char *)) bin)(argc-1, argv[1]);
}

Приведенный выше код прекрасно компилируется в GCC, но когдаЯ пытаюсь выполнить программу из командной строки следующим образом:

./my_prog /bin/echo hello

Ошибка программы.Я понял, что проблема в последней строке, поскольку ее комментирование останавливает segfault.

Не думаю, что я делаю это совершенно правильно, так как я все еще разбираюсь в функцияхуказатели.

Является ли проблема неисправным составом актеров или чем-то еще?

Ответы [ 9 ]

28 голосов
/ 07 января 2010

Вам нужна страница с разрешениями на запись. Смотрите mmap (2) и mprotect (2), если вы находитесь под Unix. Вы не должны делать это, используя malloc.

Кроме того, прочитайте, что говорили другие, вы можете запускать только машинный код, используя ваш загрузчик. Если вы попытаетесь запустить заголовок ELF, он, вероятно, все равно будет segfault.

Относительно содержания ответов и downmods:

1 - OP сказал, что он пытается запустить машинный код, поэтому я ответил на это, а не выполнил исполняемый файл.

2 - Узнайте, почему вы не смешиваете функции malloc и mman:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/mman.h>

int main()
{
    char *a=malloc(10);
    char *b=malloc(10);
    char *c=malloc(10);
    memset (a,'a',4095);
    memset (b,'b',4095);
    memset (c,'c',4095);
    puts (a);
    memset (c,0xc3,10); /* return */

    /* c is not alligned to page boundary so this is NOOP.
     Many implementations include a header to malloc'ed data so it's always NOOP. */
    mprotect(c,10,PROT_READ|PROT_EXEC);
    b[0]='H'; /* oops it is still writeable. If you provided an alligned
    address it would segfault */
    char *d=mmap(0,4096,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANON,-1,0);
    memset (d,0xc3,4096);
    ((void(*)(void))d)();
    ((void(*)(void))c)(); /* oops it isn't executable */
    return 0;
}

Он отображает именно такое поведение в Linux x86_64, другое уродливое поведение, которое обязательно возникнет в других реализациях.

12 голосов
/ 07 января 2010

Использование malloc работает нормально.

ОК, это мой окончательный ответ, пожалуйста, обратите внимание, что я использовал оригинальный код плаката. Я загружаю с диска скомпилированную версию этого кода в выделенную область кучи «bin», как это делал оригинальный код (имя исправлено без использования argv, а значение 0x674 от;

objdump -F -D foo|grep -i hoho
08048674 <hohoho> (File Offset: 0x674):

Это можно найти во время выполнения с помощью BFD (библиотеки дескрипторов двоичных файлов) или чего-то еще, вы можете вызывать другие двоичные файлы (не только себя), если они статически связаны с одним и тем же набором библиотек.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>

unsigned char *charp;
unsigned char *bin;

void hohoho()
{
   printf("merry mas\n");
   fflush(stdout);
}

int main(int argc, char **argv)
{
   int what;

   charp = malloc(10101);
   memset(charp, 0xc3, 10101);
   mprotect(charp, 10101, PROT_EXEC | PROT_READ | PROT_WRITE);

   __asm__("leal charp, %eax");
   __asm__("call (%eax)" );

   printf("am I alive?\n");

   char *more = strdup("more heap operations");
   printf("%s\n", more);

   FILE* f = fopen("foo", "rb");

   fseek(f, 0, SEEK_END);
   unsigned int len = ftell(f);
   fseek(f, 0, SEEK_SET);

   bin = (char*)malloc(len);
   printf("read in %d\n", fread(bin, 1, len, f));
   printf("%p\n", bin);

   fclose(f);
   mprotect(&bin, 10101, PROT_EXEC | PROT_READ | PROT_WRITE);

   asm volatile ("movl %0, %%eax"::"g"(bin));
   __asm__("addl $0x674, %eax");
   __asm__("call %eax" );
   fflush(stdout);

   return 0;
}

работает ...

co tmp # ./foo
am I alive?
more heap operations
read in 30180
0x804d910
merry mas

Вы можете использовать UPX для управления загрузкой / изменением / исполнением файла.

P.S. извините за предыдущую неработающую ссылку: |

12 голосов
/ 07 января 2010

Мне кажется, вы загружаете изображение ELF, а затем пытаетесь прыгнуть прямо в заголовок ELF? http://en.wikipedia.org/wiki/Executable_and_Linkable_Format

Если вы пытаетесь выполнить другой двоичный файл, почему бы вам не использовать функции создания процессов для какой бы платформы вы не использовали?

3 голосов
/ 07 января 2010

То, что вы пытаетесь сделать, является чем-то похожим на то, что делают переводчики. За исключением того, что интерпретатор читает программу, написанную на интерпретируемом языке, таком как Python, компилирует этот код на лету, помещает исполняемый код в память и затем выполняет его.

Возможно, вы захотите узнать больше и о своевременной компиляции:

Как раз вовремя
Java HotSpot JIT runtime

Доступны библиотеки для генерации кода JIT, такие как GNU lightning и libJIT , если вы заинтересованы. Однако вам придется сделать гораздо больше, чем просто читать из файла и пытаться выполнить код. Пример сценария использования будет:

  1. Читать программу, написанную на языке сценариев (возможно самостоятельно).
  2. Разобрать и скомпилировать источник в промежуточный язык понимается библиотека JIT.
  3. Используйте библиотеку JIT для генерации кода для этого промежуточного представление для процессора вашей целевой платформы.
  4. Выполнить код, сгенерированный JIT.

А для выполнения кода вам придется использовать такие методы, как использование mmap () для отображения исполняемого кода в адресное пространство процесса, маркировки исполняемой страницы и перехода к этому фрагменту памяти. Это сложнее, чем это, но это хорошее начало для понимания того, что происходит под всеми этими интерпретаторами языков сценариев, таких как Python, Ruby и т. Д.

онлайн-версия книги " Компоновщики и загрузчики " предоставит вам больше информации о форматах объектных файлов, о том, что происходит за кулисами при выполнении программы, о ролях из линкеров и загрузчиков и тд. Это очень хорошее чтение.

3 голосов
/ 07 января 2010

Скорее всего, именно к этому коду переходит вызов через функцию-указатель, который вызывает ошибку, а не сам вызов. Из кода, который вы опубликовали, невозможно определить, является ли этот код, загруженный в bin, действительным. Лучше всего использовать отладчик, переключиться в представление на ассемблере, разбить оператор return и перейти на вызов функции, чтобы определить, что код, который вы ожидаете запустить, действительно выполняется и действителен.

Обратите также внимание, что для того, чтобы вообще работать, код должен быть независимым от позиции и полностью разрешенным.

Более того, если ваш процессор / ОС обеспечивает предотвращение выполнения данных, то попытка, вероятно, обречена. В любом случае, в лучшем случае не рекомендуется загружать код для ОС.

3 голосов
/ 07 января 2010

Типичный исполняемый файл имеет:

  • заголовок
  • код входа, который вызывается до main(int, char **)

Первое означает, что вы обычно не можете ожидать, что байт 0 файла будет исполняемым; Между тем, информация в заголовке описывает, как загрузить оставшуюся часть файла в память и с чего начать.

Второе означает, что когда вы нашли точку входа, вы не можете ожидать, что она будет обрабатываться как функция C, принимающая аргументы (int, char **). Возможно, его можно использовать как функцию, не требующую параметров (и, следовательно, не требующую ничего перед тем, как вызывать ее). Но вам нужно заполнить среду, которая, в свою очередь, будет использоваться кодом ввода для создания строк командной строки, передаваемых main.

Выполнение этого вручную под заданной ОС пошло бы на некоторую глубину, которая мне недоступна; но я уверен, что есть гораздо более хороший способ сделать то, что вы пытаетесь сделать. Вы пытаетесь выполнить внешний файл как операцию включения-выключения или загружаете внешний двоичный файл и рассматриваете его функции как часть вашей программы? Обе они обслуживаются библиотеками C в Unix.

2 голосов
/ 07 января 2010

Использовать операционную систему для загрузки и выполнения программ.

В Unix вызовы exec могут сделать это.

Ваш фрагмент в вопросе может быть переписан:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char* argv[])
{
    return execv(argv[1],argv+2);
}
1 голос
/ 07 января 2010

Вы можете dlopen () файл, найти символ "main" и вызвать его с 0, 1, 2 или 3 аргументами (все типа char *) через приведение к указателю на функцию-возвращение-int 0,1,2-по принимать, or3-символ *

0 голосов
/ 07 января 2010

Исполняемые файлы содержат гораздо больше, чем просто код. Заголовок, код, данные, больше данных, эти вещи разделяются и загружаются в разные области памяти ОС и ее библиотеками. Вы не можете загрузить программный файл в один фрагмент памяти и ожидать перехода к его первому байту.

Если вы пытаетесь выполнить собственный произвольный код, вам нужно изучить динамические библиотеки, потому что это именно то, для чего они нужны.

...