Для HW в классе безопасности я хочу продемонстрировать атаку, основанную на разнице между bar()
(сообщает компилятору, что количество и тип аргументов неизвестны) и bar(void)
(сообщает компилятору, что функция не принимает аргументы).
Я видел эту ветку, в которой подробно описывается их различие, но не смог создать на его основе успешную атаку.
Мой код:
#include <stdio.h>
void bar(){
int a = 4;
printf("%d", a);
}
int main() {
bar("hhhhhhhhhhhhhhhhhhhhhhhh");
return 0;
}
Я ожидаю, что переданная строка переполнит переменную «а», но этого не происходит. Как ни странно, при проверке стека с помощью GDB (с использованием «информационного фрейма») он не говорит, что строка даже была передана.
В вашем посте нет вопросов. Stack Overflow предназначен для того, чтобы задавать конкретные вопросы.
Я не уверен, как программа C, которая «атакует» другую функцию, работающую в том же пространстве памяти, делает что-то ценное, учитывая, что (как программа C) вы имеете полный доступ к той же памяти, что и любая вызываемая вами функция ( а функции OS/system-lib — это просто оболочки над системными вызовами, которые невосприимчивы к такого рода «угрозам»).
Хе, я всегда думал, что декларация определения — это всегда прототип, но, видимо, нет.
Возможно, у какого-нибудь K&R C старой школы были такие проблемы... но в наши дни будет очень сложно найти компилятор, который не ставит барьеров в отношении этих древних проблем. Вам больше повезет при использовании функции с проблемой переполнения буфера или функции с переменным аргументом, в которой отсутствует ожидаемый переменный параметр. например printf("%s%s", p);
По крайней мере, начиная с C89, определение функции с пустым списком аргументов эквивалентно списку аргументов void
; в противном случае количество и типы аргументов не указаны (6.7.6.3/14).
В современных архитектурах обычно аргументы передаются просто вызывающей процедурой, помещающей значения аргументов в определенные места (в регистрах процессора или в аппаратном стеке). Тогда вызываемая подпрограмма с параметром, соответствующим некоторому аргументу, будет использовать этот аргумент, получая его значение из назначенного места.
Если вызываемая подпрограмма не имеет такого параметра, она не получит значение из назначенного места. Тогда ничего больше не произойдет в отношении спора. Вызывающая подпрограмма может поместить значение в указанное место, но вызываемая подпрограмма не будет его использовать. Ничего не будет перегружено. Аргумент просто игнорируется и не имеет никакого эффекта.
В этой ситуации атака невозможна.
Существуют и другие методы передачи аргументов, которые могут потребовать большего взаимодействия и сотрудничества между вызывающими и вызываемыми подпрограммами, но, скорее всего, вы не используете вычислительную платформу с такими методами в своем классе безопасности.
Даже если параметр будет передан по значению, ничего не произойдет, поскольку основной и бар будут иметь отдельные кадры стека, поэтому ничего не будет перекрываться (как показано в моем ответе).
Аргументы передаются куда-то (регистр/стек/куча) в зависимости от ABI, но если вызываемый объект для примеров никогда не извлекает стек, то к аргументам не будет доступа. Переменная a
выделяется при входе в функцию, поэтому после того, как аргументы были записаны в вызывающей стороне, перед вызовом, ее нельзя перезаписать.
Я не эксперт, но, возможно, вы сможете перезаписать обратный адрес, который используется вызываемым абонентом, с помощью вашего подхода. Но я бы не стал считать это проблемой безопасности, потому что как вызывающий абонент вы несете ответственность за написание обратного адреса, поэтому вы все равно можете туда что-то написать.
Нет, вы ничего не перезапишете.
@0___________ Но разве адрес возврата не записывается перед аргументами, так что вызываемый должен знать аргументы, чтобы правильно получить доступ к адресу возврата?
@0___________ Разве тогда нельзя было бы позволить вызывающему абоненту указать другой (потенциально неправильный) обратный адрес с помощью подхода OP?
нет, это невозможно
@0___________ Извините, но почему бы и нет?
Как ни странно, при проверке стека с помощью GDB (используя 'info Frame'), это не говорит о том, что строка была передана.
В языке C строки не передаются по значению, а только по указателю. Таким образом, вы передаете ссылку на строковый литерал. Строковый литерал будет храниться там, где реализация хранит константные данные (не в стеке).
Если вы хотите передать большой массив по значению, вам нужно использовать структуры, которые передаются по значению.
bar((struct {char a[30];}){"hhhhhhhhhhhhhhhhhhhhhhhh"});
Он будет помещен в стек (с отключенной оптимизацией).
main:
push rbp
mov rbp, rsp
sub rsp, 32
movabs rax, 7523377975159973992
mov rdx, rax
mov QWORD PTR [rbp-32], rax
mov QWORD PTR [rbp-24], rdx
movabs rax, 7523377975159973992
mov edx, 26728
mov QWORD PTR [rbp-18], rax
mov QWORD PTR [rbp-10], rdx
sub rsp, 32
mov rcx, rsp
mov rax, QWORD PTR [rbp-32]
mov rdx, QWORD PTR [rbp-24]
mov QWORD PTR [rcx], rax
mov QWORD PTR [rcx+8], rdx
mov rax, QWORD PTR [rbp-18]
mov rdx, QWORD PTR [rbp-10]
mov QWORD PTR [rcx+14], rax
mov QWORD PTR [rcx+22], rdx
mov eax, 0
call bar
add rsp, 32
mov eax, 0
leave
ret
Но вызываемая функция создаст свой собственный фрейм стека - отдельный от "hhhhhhhhhhhhhhhhhhhhhhhh"
функции.
bar:
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], 4
mov eax, DWORD PTR [rbp-4]
mov esi, eax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
nop
leave
ret
Поэтому ничего опасного не произойдет, поскольку оба кадра стека не перекрываются.
Давайте рассуждать об этом логически. Если у вас есть функция foo()
, которая принимает любое количество параметров, есть два случая:
Таким образом, если вы напишете код, который злонамеренно вызывает функцию, то на основе вашего вызова функция все равно будет определена параметризованным образом, но переданные параметры будут либо ожидаемыми и приветствуемыми функцией, либо неожиданными и проигнорированными.
Таким образом, внутри функции нет реальной опасности.
За пределами функции существует небольшая «опасность», поскольку, таким образом, если вы скомпилируете два разных кода с определением foo()
в обоих, а затем проведете их реверс-инжиниринг и соедините их вместе, вы можете получить противоречивые определения. Но если это произойдет, такую проблему можно исправить, и хакерам не стоит утруждать себя созданием ошибки, которую можно легко исправить. Никакой кражи данных или даже повреждения данных не происходит.
Тем не менее, реальная опасность такой функции, как foo()
, заключается в том, что она устарела, и некоторые компиляторы с какого-то момента могут отказаться ее разрешать.
РЕДАКТИРОВАТЬ
@Caulder сделал полезное замечание в разделе комментариев, заявив:
Я думаю, что реальная опасность - это вариативная функция, ожидающая аргументов, которые не заданы, тем самым разрушая остальную память.
Я думаю, что реальная опасность - это вариативная функция, ожидающая аргументов, которые не заданы, тем самым разрушая остальную память.
Большинство реализаций просто игнорируют дополнительные аргументы по историческим причинам.