Ошибка сегментации при вызове функции, находящейся в куче

Я пытаюсь немного изменить правила и malloc буфер, затем скопируйте функцию в буфер.

Вызов буферизованной функции работает, но функция вызывает ошибку сегментации, когда я пытаюсь вызвать другую функцию внутри.

Любые мысли, почему?

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

int foo(int x)
{
    printf("%d\n", x);
}

int bar(int x)
{
}

int main()
{
    int foo_size = bar - foo;

    void* buf_ptr;

    buf_ptr = malloc(1024);

    memcpy(buf_ptr, foo, foo_size);

    mprotect((void*)(((int)buf_ptr) & ~(sysconf(_SC_PAGE_SIZE) - 1)),
             sysconf(_SC_PAGE_SIZE),
             PROT_READ|PROT_WRITE|PROT_EXEC);

    int (*ptr)(int) = buf_ptr;

    printf("%d\n", ptr(3));

    return 0;
}

Этот код выдает segfault, если я не изменю функцию foo, чтобы:

int foo(int x)
{
    //Anything but calling another function.
    x = 4;
    return x;
}

Примечание:

Код успешно копирует foo в буфер, я знаю, что сделал некоторые предположения, но на моей платформе они в порядке.

Ответ 1

Ваш код не является независимым от положения, и даже если бы он был, у вас нет правильных перемещений, чтобы переместить его в произвольное положение. Ваш вызов printf (или любой другой функции) будет выполняться с помощью относительной адресации pc (через PLT, но помимо этой точки). Это означает, что команда, сгенерированная для вызова printf, не является вызовом статического адреса, а скорее "вызывает функцию X байтов из текущего указателя инструкции". Поскольку вы переместили код, вызов выполняется с плохим адресом. (Я предполагаю i386 или amd64 здесь, но, как правило, это безопасное предположение, люди, которые находятся на странных платформах, обычно упоминают это).

Более конкретно, x86 имеет две разные инструкции для вызовов функций. Один из них - это вызов относительно указателя инструкции, который определяет назначение вызова функции, добавляя значение к текущему указателю инструкции. Это наиболее часто используемый вызов функции. Вторая команда - это вызов указателя внутри регистра или ячейки памяти. Это гораздо менее часто используется компиляторами, потому что для этого требуется больше ограничений памяти и останавливается конвейер. Как реализованы разделяемые библиотеки (ваш вызов printf на самом деле переходит к общей библиотеке) заключается в том, что для каждого вызова функции, который вы делаете вне своего собственного кода, компилятор будет вставлять поддельные функции рядом с вашим кодом (это PLT, который я упоминал выше). Ваш код выполняет обычный pc-относительный вызов этой поддельной функции, а функция fake найдет реальный адрес printf и вызовет это. На самом деле это не имеет значения. Почти любой вызов нормальной функции, который вы делаете, будет относиться к компьютеру и будет терпеть неудачу. Ваша единственная надежда в коде вроде этого - указатели на функции.

Возможно, вы также столкнетесь с некоторыми ограничениями на исполняемый mprotect. Проверьте возвращаемое значение mprotect, в моей системе ваш код не работает по еще одной причине: mprotect не позволяет мне это делать. Вероятно, потому что в блочном блоке памяти malloc есть дополнительные ограничения, которые предотвращают исполняемые защиты его памяти. Это приводит меня к следующему пункту:

Вы сломаете вещи, вызвав mprotect в памяти, которая не управляется вами. Это включает память, полученную от malloc. Вы должны только mprotect вещи, которые вы получили от ядра, через mmap.

Здесь приведена версия, демонстрирующая, как сделать эту работу (в моей системе):

#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#include <err.h>

int
foo(int x, int (*fn)(const char *, ...))
{
        fn("%d\n", x);
        return 42;
}

int
bar(int x)
{
        return 0;
}

int
main(int argc, char **argv)
{
        size_t foo_size = (char *)bar - (char *)foo;
        int ps = getpagesize();

        void *buf_ptr = mmap(NULL, ps, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_PRIVATE, -1, 0);

        if (buf_ptr == MAP_FAILED)
                err(1, "mmap");

        memcpy(buf_ptr, foo, foo_size);

        int (*ptr)(int, int (*)(const char *, ...)) = buf_ptr;

        printf("%d\n", ptr(3, printf));

        return 0;
}

Здесь я злоупотребляю знаниями о том, как компилятор будет генерировать код для вызова функции. С помощью указателя функции я принудительно его генерирую инструкцию вызова, которая не является pc-relative. Кроме того, я сам управляю распределением памяти, чтобы получить правильные разрешения от запуска и не запускать никаких ограничений, которые могут иметь brk. В качестве бонуса мы выполняем обработку ошибок, которые на самом деле помогли мне найти ошибку в первой версии этого эксперимента, и я также исправил другие незначительные ошибки (например, отсутствовал), что позволило мне включить предупреждения в компилятор и уловить еще одну потенциальную проблему.

Если вы хотите углубиться в это, вы можете сделать что-то подобное. Я добавил две версии функции:

int
oldfoo(int x)
{
        printf("%d\n", x);
        return 42;
}

int
foo(int x, int (*fn)(const char *, ...))
{
        fn("%d\n", x);
        return 42;
}

Скомпилируйте все это и разоберите его:

$ cc -Wall -o foo foo.c
$ objdump -S foo | less

Теперь мы можем посмотреть на две сгенерированные функции:

0000000000400680 <oldfoo>:
  400680:       55                      push   %rbp
  400681:       48 89 e5                mov    %rsp,%rbp
  400684:       48 83 ec 10             sub    $0x10,%rsp
  400688:       89 7d fc                mov    %edi,-0x4(%rbp)
  40068b:       8b 45 fc                mov    -0x4(%rbp),%eax
  40068e:       89 c6                   mov    %eax,%esi
  400690:       bf 30 08 40 00          mov    $0x400830,%edi
  400695:       b8 00 00 00 00          mov    $0x0,%eax
  40069a:       e8 91 fe ff ff          callq  400530 <[email protected]>
  40069f:       b8 2a 00 00 00          mov    $0x2a,%eax
  4006a4:       c9                      leaveq
  4006a5:       c3                      retq

00000000004006a6 <foo>:
  4006a6:       55                      push   %rbp
  4006a7:       48 89 e5                mov    %rsp,%rbp
  4006aa:       48 83 ec 10             sub    $0x10,%rsp
  4006ae:       89 7d fc                mov    %edi,-0x4(%rbp)
  4006b1:       48 89 75 f0             mov    %rsi,-0x10(%rbp)
  4006b5:       8b 45 fc                mov    -0x4(%rbp),%eax
  4006b8:       48 8b 55 f0             mov    -0x10(%rbp),%rdx
  4006bc:       89 c6                   mov    %eax,%esi
  4006be:       bf 30 08 40 00          mov    $0x400830,%edi
  4006c3:       b8 00 00 00 00          mov    $0x0,%eax
  4006c8:       ff d2                   callq  *%rdx
  4006ca:       b8 2a 00 00 00          mov    $0x2a,%eax
  4006cf:       c9                      leaveq
  4006d0:       c3                      retq

Инструкция для вызова функции в случае printf - "e8 91 fe ff ff". Это вызов функции pc-relative. 0xfffffe91 байт перед указателем нашей инструкции. Он обрабатывается как знаковое 32-битное значение, а указатель инструкции, используемый в вычислении, является адресом следующей команды. Итак, 0x40069f (следующая инструкция) - 0x16f (0xfffffe91 впереди - 0x16f байтов с подписанной математикой) дает нам адрес 0x400530, и, глядя на разобранный код, я нахожу это по адресу:

0000000000400530 <[email protected]>:
  400530:       ff 25 ea 0a 20 00       jmpq   *0x200aea(%rip)        # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
  400536:       68 01 00 00 00          pushq  $0x1
  40053b:       e9 d0 ff ff ff          jmpq   400510 <_init+0x28>

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

Вторая функция генерирует команду вызова функции "ff d2". Это означает "вызов функции по адресу, хранящемуся в регистре rdx". Нет адресной адресации и почему она работает.

Ответ 2

Компилятор может свободно генерировать код так, как он хочет, если наблюдаемые результаты верны (как правило). Итак, вы делаете всего лишь вызов поведения undefined.

Visual Studio иногда использует реле. Это означает, что адрес функции просто указывает на относительный скачок. Это вполне допустимо для стандартного из-за того, что, как правило, но это определенно нарушит такую ​​конструкцию. Другая возможность состоит в том, чтобы локальные внутренние функции вызывались с относительными скачками, но вне самой функции. В этом случае ваш код не будет копировать их, а относительные вызовы будут указывать на случайную память. Это означает, что с разными компиляторами (или даже с разными вариантами компиляции на одном компиляторе) это может дать ожидаемый результат, сбой или прямое завершение программы без ошибок, которая является точно UB.

Ответ 3

Я думаю, что могу немного объяснить. Прежде всего, если обе ваши функции не имеют оператора возврата внутри, поведение undefined вызывается в соответствии со стандартом §6.9.1/12. Во-вторых, что наиболее часто встречается на многих платформах, а ваше, по-видимому, также следующее: относительные адреса функций жестко закодированы в двоичный код функций. Это означает, что если у вас есть вызов "printf" внутри "foo", а затем вы перемещаетесь (например, выполняете) из другого места, этот адрес, из которого следует вызывать "printf", становится плохим.