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

Я пытаюсь немного подправить правила, и 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;
}

Этот код будет генерировать ошибку, если я не изменю foo функция для:

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

НОТА:

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

3 ответа

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

В частности, в x86 есть две разные инструкции для вызова функций. Одним из них является вызов относительно указателя инструкций, который определяет место назначения вызова функции путем добавления значения к текущему указателю инструкций. Это наиболее часто используемый вызов функции. Вторая инструкция - это вызов указателя внутри регистра или ячейки памяти. Это гораздо реже используется компиляторами, потому что требует большего количества косвенных обращений к памяти и останавливает конвейер. Способ реализации разделяемых библиотек (ваш призыв к printf фактически пойдет в разделяемую библиотеку), то есть для каждого вызова функции, который вы делаете вне вашего собственного кода, компилятор будет вставлять поддельные функции рядом с вашим кодом (это PLT, который я упоминал выше). Ваш код выполняет обычный вызов ПК для этой фальшивой функции, и фальшивая функция найдет реальный адрес 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;
}

Здесь я злоупотребляю знаниями о том, как компилятор будет генерировать код для вызова функции. Используя указатель на функцию, я заставляю его генерировать инструкцию вызова, не относящуюся к компьютеру. Кроме того, я сам управляю распределением памяти, чтобы мы с самого начала получали правильные разрешения и не сталкивались с какими-либо ограничениями, 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 <printf@plt>
  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". Это вызов функции для ПК. 0xfffffe91 байт перед указателем нашей инструкции. Он обрабатывается как 32-битное значение со знаком, а указатель инструкции, используемый в вычислении, является адресом следующей инструкции. Итак, 0x40069f (следующая инструкция) - 0x16f (0xfffffe91 впереди 0x16f байт сзади со знаком математики) дает нам адрес 0x400530, и, глядя на разобранный код, я нахожу это по адресу:

0000000000400530 <printf@plt>:
  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". Нет компьютерной относительной адресации, и поэтому она работает.

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

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

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

Другие вопросы по тегам