Выполнение машинного кода в памяти

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

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

#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.

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

Проблема в неправильном литье или в чем-то другом?


person anonymous coward    schedule 07.01.2010    source источник
comment
Чарли: Если вы когда-нибудь поймете смысл всех этих ответов, вместо того, чтобы использовать приведенный указатель на функцию, как у вас есть, вам может быть лучше написать какой-нибудь базовый преобразователь, который динамически управляет аргументами стека. При использовании gcc функция, объявленная как function() attribute ((naked)); и см. gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html для получения дополнительных примеров. Таким образом, вы вызываете ту же функцию, которая решает, должен ли динамически загружаемый код быть снабжен N числом аргументов/соглашением о вызовах и т. д. В любом случае вам, вероятно, следует искать FFI и тому подобное.   -  person RandomNickName42    schedule 07.01.2010
comment
Я почти уверен, что ОП просто неправильно понимает основы работы исполняемых файлов. Используйте библиотеку динамической компоновки для выполнения собственного динамического кода и exec для выполнения других приложений.   -  person Jimbo    schedule 07.01.2010
comment
@Джимбо, ты совершенно прав. Я хотел посмотреть, смогу ли я это сделать, поэтому я подумал, где я могу найти машинный код?, и решил просто взять исполняемый файл, не задумываясь об этом :/   -  person anonymous coward    schedule 08.01.2010
comment
Возможно, вам повезет с компиляцией в веб-сборку.   -  person mike    schedule 13.11.2018


Ответы (9)


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

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

person ta.speot.is    schedule 07.01.2010
comment
Я думаю, это потому, что он пытается выполнить приложение в выделенной ему памяти, я не верю, что какая-либо функция создания процесса работает так. Функции создания потока могут, но он загружает файл на диске в память, а затем пытается выполнить эту память. - person RandomNickName42; 07.01.2010
comment
Если память не помечена как исполняемая, он не сможет ее выполнить, но он также загружает файл ELF в память, а затем пытается вызвать заголовок ELF, первые четыре байта которого равны 0x7f 'E' 'L' 'Ф' - person ta.speot.is; 07.01.2010
comment
Забавный факт: 0x7F — это основной код операции для JNLE. Так что, может быть, первое, что пытается сделать код, — это перейти на мусорный адрес? В любом случае: выполнение заголовка ELF не сработает. - person ta.speot.is; 07.01.2010

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

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

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

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, другое уродливое поведение обязательно возникнет в других реализациях.

person Community    schedule 07.01.2010
comment
Я посмотрю на это. У меня было ощущение, что это может быть как-то связано с этим. - person anonymous coward; 07.01.2010
comment
На самом деле это неправильно, вы можете сделать это с помощью malloc, вам просто нужно использовать mprotect. - person RandomNickName42; 07.01.2010
comment
ОК, если вы ПРОЧИТАЕТЕ его КОД, вы увидите, что он ЗАГРУЗИВАЕТ ФАЙЛ, чтобы ВЫПОЛНИТЬ. ФАКТ, что это СОСТАВЛЯЕМЫЙ БИНАРИЙ, означает, что его текстовая область имеет РАЗМЕР СТРАНИЦЫ, ВЫРАВНЕННЫЙ ВСЕГДА. Если он mprotect в КУЧЕ, то ЕДИНСТВЕННАЯ ВОЗМОЖНАЯ ПРОБЛЕМА заключается в том, что файл, который он ЗАГРУЗИЛ для ВЫПОЛНЕНИЯ, будет иметь некоторые из .data, возможно, MARKED EXEC, если он сам это не отрегулировал. Но у них НЕ ПРОБЛЕМА с тем, чтобы заставить HEAP +x, JAVA и MONO делать это все время. - person RandomNickName42; 07.01.2010
comment
Не слишком волнуйтесь, mmap, mprotect и т. д. защищают/снимают защиту только в страницах, а не в байтах. Реализации malloc помещают данные malloc в предварительно выделенные фрагменты, поэтому, если вы измените защиту в своем фрагменте, он, вероятно, будет добавлен или добавлен к другим данным malloc, разделяющим ту же страницу (страницы). Если вы используете mprotect, защита будет либо (r|)w|x, либо r|x, в любом случае вашим данным r|w на странице (страницах) это не понравится, т.е. segfault или вы оставляете эти данные доступными для внедрения исполняемого кода. - person ; 07.01.2010
comment
да, не волнуйтесь, я всех успокоил, даже решил, что ваш пост полезен после вашего примера кода. Однако в любом случае, если вы видите из моего кода, malloc работает просто отлично +rwx, даже если вы добавите free во все 3 памяти, выделенной в куче, которые вызвал пример, который я показываю, это не проблема или проблема со стабильностью. Единственное, что вы можете непреднамеренно разрешить некоторую память в куче как +x, но на самом деле это не имеет большого значения. - person RandomNickName42; 07.01.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. извините за предыдущую битую ссылку :|

person RandomNickName42    schedule 07.01.2010
comment
Обратите внимание на то, что этот IS кросс-платформенный и полностью абстрактный, содержит детали спецификаций формата файлов или любые требования для игры с защитой страниц и тому подобное. - person RandomNickName42; 07.01.2010
comment
Пфффф, я люблю, когда меня голосуют без каких-либо рассуждений, будь реален. UPX - это способ сделать это, использовать что-либо еще наивно. Вы можете легко использовать его для загрузки exe-файлов для вас или API-интерфейсов более низкого уровня, которые испускают заглушки динамической сборки, которые могут загружать/запускать произвольные сжатые блоки памяти или иным образом. - person RandomNickName42; 07.01.2010
comment
Ну, мы не знаем, как он собирается вставить машинный код в память. Что, если он пишет интерпретатор байт-кода, и код будет генерироваться в памяти? Загрузка эха (каким бы неверным ни был код) могла быть доказательством концепции того, что код можно генерировать и выполнять на лету. - person ta.speot.is; 07.01.2010
comment
malloc не обеспечивает выравнивание страницы, ваш код может работать, а может и не работать. вы можете использовать выровненное по странице подмножество блока mallocd, что было бы безопасно, или, возможно, использовать posix_memalign, если он у вас есть - person Hasturkun; 07.01.2010
comment
Надеюсь, вы не возражаете против моего редактирования, ваша ссылка UPX указывала куда-то грязно. - person Hasturkun; 07.01.2010
comment
Спасибо за исправление ссылки, и да, сейчас она довольно небрежно составлена ​​:) Но она ТОЧНО делает то, о чем просил Чарли, чувак, я надеюсь, что получу принятый ответ на этот вопрос, ахха - person RandomNickName42; 07.01.2010
comment
Теперь, как мы можем сделать это для динамически связанных файлов ELF? - person Lothar; 06.12.2012
comment
Я гуглил, как выполняется компиляция JIT, и нашел ваш ответ - JIBL как раз вовремя загружает двоичный файл LOL. +1, так как это дает некоторое представление. - person doc; 30.12.2012
comment
Я не понимаю - как это может быть кроссплатформенным с интеловской сборкой там? Вы можете запускать код C на Raspberry Pi, PS 2, PDP-11 или даже AS400. - person Jerry Jeremiah; 04.11.2015

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

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

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

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

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

person Edmund    schedule 07.01.2010

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

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

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

person Clifford    schedule 07.01.2010
comment
Да, хорошо, независимо от позиции, Чарли может использовать -fPIC при использовании gcc, но, к сожалению, в Windows это не простой способ получить скомпилированные приложения PIC C. - person RandomNickName42; 07.01.2010

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

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

Точная компиляция
Среда выполнения Java HotSpot JIT

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

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

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

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

person Sudhanshu    schedule 07.01.2010

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

person haavee    schedule 07.01.2010
comment
используя такой метод, вы, вероятно, захотите найти __libc_start_main - person RandomNickName42; 07.01.2010

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

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

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

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

int main(int argc, char* argv[])
{
    return execv(argv[1],argv+2);
}
person Will    schedule 07.01.2010
comment
exec этого не делает, он пытается загрузить приложение в память вручную. exec ожидает аргумент пути к файлу, а не адрес памяти. - person RandomNickName42; 07.01.2010
comment
Он открывает двоичный файл с помощью fopen, а затем пытается запрыгнуть в него. Если бы он вместо этого просто передал этот путь к exec... Спасибо за даунмод. - person Will; 07.01.2010
comment
Если вы объясните мне, как, по вашему мнению, exec на самом деле делает то, что он просил, то есть выполняет машинный код в памяти, я мгновенно уберу с вас любой отрицательный голос, однако это совершенно не то, что он просил, из того, что я могу сказать. Спасибо за соответствующий голос против. - person RandomNickName42; 07.01.2010
comment
Я не минусовал UPX. Я добавил вырезание-вставку-изменение кода в исходный вопрос. - person Will; 07.01.2010
comment
Как однажды сказал Брюс Ли Мой стиль? Это как искусство сражаться без боя. хороший. - person RandomNickName42; 07.01.2010

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

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

person Jimbo    schedule 07.01.2010
comment
Не файлы MSDOS .COM - это просто двоичный образ машинного кода - очень плохо, что они были ограничены 64 КБ ... - person Jerry Jeremiah; 04.11.2015