Nano Hash - криптовалюты, майнинг, программирование

Понимание указателей функций extern и void

Мой основной выглядит так:

int main(int argv,char **argc) 
{
     extern system,puts; 
     void (*fn)(char*)=(void(*)(char*))&system;
     char buf[256];

     fn=(void(*)(char*))&puts;
     strcpy(buf,argc[1]);
     fn(argc[2]);
     exit(1);
}

У меня есть общее представление о том, что означает ключевое слово extern и как оно работает в этом коде. Меня смущает объявление функции fn. Здесь объявлен указатель на функцию или функция? Кроме того, почему &system и &puts в конце определения fn.

14.05.2014

  • Еще один вопрос, &system и &puts - это адреса этих встроенных функций, когда они скомпилированы в программу, верно? Кроме того, в чем преимущество объявления известной функции extern и создания указателя на функцию? 14.05.2014
  • Нет никакого преимущества в написании такого кода. Вы должны включить правильные заголовки, чтобы получить правильные прототипы. Этот код выглядит так, как будто он был написан для демонстрации чего-то конкретного. где ты это нашел? 14.05.2014
  • Кстати, имена аргументов main меняются местами: int должно быть argc (количество аргументов), а char** должно быть argv (вектор аргументов). 14.05.2014
  • Я получил это от Уязвимого кода. bright-shadows.net/vuln_code/overflows/abo3.c 14.05.2014
  • Этот код был специально создан для демонстрации уязвимости, связанной с переполнением буфера, и является примером плохо написанного кода. Согласно инструкциям на его домашней странице, вы, вероятно, сможете найти людей, которые работали с этот код на форумах по уязвимому коду. 14.05.2014

Ответы:


1

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

Следующая строка объявляет указатель на функцию и инициализирует его так, чтобы он указывал на функцию system.

 void (*fn)(char*)=(void(*)(char*))&system;

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

 fn=(void(*)(char*))&puts;

Уязвимость в программе находится в этой строке

strcpy(buf,argc[1]);

Если strlen из argc[1] больше размера буфера, то при переполнении буфера возможно изменить значение fn так, чтобы оно указывало на какую-то произвольную функцию, которая затем будет вызываться этой строкой.

fn(argc[2]);

Примечание: как кто-то указал в комментариях, имена argc и argv следует поменять местами.

14.05.2014

2

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

Тем не менее, давайте посмотрим, как это работает:

extern system, puts;

Это говорит о том, что system и puts определены где-то еще, и компоновщик позаботится о том, чтобы предоставить вам их адрес. Компилятор не знает, какой у них тип, поэтому gcc, по крайней мере, предполагает, что это int, и выдает предупреждение. Я не могу найти в стандарте пункт, в котором говорится, является ли это поведение четко определенным, определяемым реализацией или неопределенным, но, безусловно, это плохо. Это особенно плохо, потому что эти символы не int. Однако я нашел это (C11, Приложение J.2 - Неопределенное поведение):

Два объявления одного и того же объекта или функции указывают несовместимые типы (6.2.7).

Также существует вероятность того, что int слишком мал для хранения указателя. Нет конца тому, насколько это плохо.

Итак, посмотрим в памяти, как это выглядит:

system (as 4-byte int)           system (as function)
  BYTE1                             INSTRUCTION1
  BYTE2                             INSTRUCTION2
  BYTE3                             INSTRUCTION3
  BYTE4                             INSTRUCTION4
                                    INSTRUCTION5
                                    INSTRUCTION6
                                    INSTRUCTION7
                                    INSTRUCTION8
                                    ...

Итак, компоновщик делает адрес system одинаковым для каждого скомпилированного объекта (эффективно связывая их вместе). Содержимое system, с другой стороны, зависит от его типа, который компоновщик не знает или не заботится о нем.

Итак, в вашем файле, если сразу после:

extern system, puts;

ты пишешь:

printf("%d\n", system);

вы получите первую пару инструкций функции system, как если бы это была int, и распечатаете ее. Что плохо, и не делай этого.

С другой стороны, как я уже сказал, адрес system, определенный как extern в вашей программе, совпадает с реальной функцией system. Итак, если взять адрес:

&system

и привести его к правильному типу:

(void(*)(char*))&system

вы получаете указатель на функцию, указывающий на реальную функцию system. Обратите внимание, что system только что было int, и мы собираемся вызвать его как функцию, это само по себе неопределенное поведение:

C11 Приложение J-2 (неопределенное поведение):

Доступ к хранимому значению объекта осуществляется иначе, чем через lvalue допустимого типа (6.5).

C11 6,5 (выделено мной):

6. Эффективным типом объекта для доступа к его хранимому значению является объявленный тип объекта, если он есть. ...

7. Доступ к хранимому значению объекта должен осуществляться только выражением lvalue, которое имеет один из следующих типов: 88)

  • тип, совместимый с эффективным типом объекта,
  • квалифицированная версия типа, совместимая с действующим типом объекта,
  • тип, который является подписанным или беззнаковым типом, соответствующим эффективному типу объекта,
  • тип, который является подписанным или беззнаковым типом, соответствующим уточненной версии эффективного типа объекта,
  • тип агрегата или объединения, который включает один из вышеупомянутых типов среди своих членов (включая, рекурсивно, член подагрегата или содержащего объединения), или
  • тип персонажа.

Пройдя все это, вы теперь имеете указатель на функцию, содержащий реальный адрес system. Затем вы называете это в свое удовольствие. То же самое делается с puts.


Это было плохо. Вот как это должно быть сделано на самом деле:

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

int main(int argv,char **argc) 
{
     char buf[256];

     /* system was unused */

     strcpy(buf,argc[1]);
     puts(argc[2]);

     return 0; /* 0 means no error */
}

Помимо пользы от использования файлов заголовков, предоставленных реализацией, давайте посмотрим, как на самом деле работает этот.

Внутри файлов заголовков у вас есть много строк со многими объявлениями. Один для puts может выглядеть так:

int puts(const char *s);

Во-первых, обратите внимание, как указатель на функцию в вашем коде на самом деле имеет неправильную сигнатуру функции? Это плохо. Это плохо, потому что при вызове функции void вызывающей стороне не нужно предоставлять для нее пространство возвращаемого значения, в то время как puts ожидает, что оно там будет, поэтому puts запишет возвращаемое значение там, где он ожидает его найти, но на самом деле это пространство принадлежит чему-то другому. Это называется повреждением стека.

В любом случае, когда компилятор увидит такую ​​строку:

int puts(const char *s);
  1. #P25# <блочная цитата> #P26# #P27#
  2. он будет знать, что это функция и какова ее подпись.

Таким образом, просто вызывая fputs(...), компилятор делает все правильно.

14.05.2014

3

Объявление для fn не объявляет функцию, а объявляет указатель на функцию и инициализирует его. Эта строка:

void (*fn)(char*)=(void(*)(char*))&system;

Эквивалентно:

void (*fn)(char*);
fn = (void(*)(char*)) &system;

Что объявляет fn указателем на функцию, получающую указатель на char (и ничего не возвращающую). Затем этому указателю присваивается адрес system(), приведенный к соответствующему типу, чтобы соответствовать типу fn.

14.05.2014

4
extern system, puts;
// equivalent to
extern int system, puts;

Приведенный выше оператор объявляет две переменные system и puts типа int, который является типом по умолчанию, если он не указан (gcc выдает предупреждение в этом отношении). Декларация означает, что она вводит имя и тип переменной, но не выделяет для нее место.

Здесь объявлен указатель на функцию или функция?

void (*fn)(char*); 

определяет fn как указатель на функцию, которая принимает char * и не возвращает никакого значения. Выражение

(void(*)(char*))&system

берет адрес переменной system и приводит его к указателю типа void(*)(char *), то есть к указателю на функцию, которая принимает char * и не возвращает никакого значения. Таким образом, заявление

void (*fn)(char*)=(void(*)(char*))&system;

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

почему &system и &puts стоят в конце определения fn

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

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

14.05.2014
Новые материалы

Кластеризация: более глубокий взгляд
Кластеризация — это метод обучения без учителя, в котором мы пытаемся найти группы в наборе данных на основе некоторых известных или неизвестных свойств, которые могут существовать. Независимо от..

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

Частный метод Python: улучшение инкапсуляции и безопасности
Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

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

Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

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

Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..