Бессмысленная попытка сравнить rsp по вызову без использования регистров

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

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

Вот один из способов сделать это:

foo_thunk:
push rsp              ; save the value of rsp
call foo              ; call the underlying function
add rsp, 8            ; adjust rsp by 8, popping the saved value
cmp rsp, [rsp - 8]    
jne bad_rsp           ; bad foo!
ret

Проблема в том, что при этом осуществляется доступ к значению [rsp - 8], которое выше rsp, то есть не в стеке, а в туманной области над стеком. Это должно быть там, где у вас есть красная зона, но не тогда, когда ее нет.

Какие есть альтернативы? Производительность и размер кода важны.


person BeeOnRope    schedule 24.10.2017    source источник
comment
Должен ли он быть повторно введен?   -  person harold    schedule 25.10.2017
comment
Кроме того, это нарушает требование выравнивания. Конечно, вам не нужно настраивать rsp перед сравнением, поэтому вам не нужно обращаться к указателю стека. Хуже того, в стеке могут быть аргументы, которые вам нужно обработать.   -  person Jester    schedule 25.10.2017
comment
@гарольд - да, это так.   -  person BeeOnRope    schedule 25.10.2017
comment
PS: если rsp будет уничтожен, скорее всего, foo не вернется к вам, поэтому я не вижу особого смысла проверять. XY проблема?   -  person Jester    schedule 25.10.2017
comment
@Jester - хорошая мысль, исправлено (я думаю).   -  person BeeOnRope    schedule 25.10.2017
comment
@Jester - да, в большинстве случаев вы правы, и эта проверка не сработает. И все же я хотел бы знать, как это написать.   -  person BeeOnRope    schedule 25.10.2017
comment
Jester также прав насчет того, что эти броски функций с достаточным количеством параметров выходят из строя, если они перебрасываются в стек.   -  person Michael Petch    schedule 25.10.2017
comment
Для значений можно выделить отдельный стек.   -  person Jester    schedule 25.10.2017
comment
@Jester Отдельное пространство для значений было бы моим методом.   -  person Michael Petch    schedule 25.10.2017
comment
Другой вариант: sub rsp,8 push verify_rsp_is_valid call foo_with_no_args_in_stack ret verify_rsp_is_valid: add rsp,8 ret. Будет продолжаться правильно только тогда, когда rsp будет действительным, или foo поймет вашу хитрость и прыгнет напрямую или оставит verify_rsp_is_valid адрес в стеке на нужном месте. В случае плохого rsp вы случайно окажетесь в другом месте. Но это то же самое, что и обычный foo в любом случае, поэтому практическое удобство использования выглядит близким к нулю, я бы скорее понял, что какая-то канареечная система проверяет перезапись стека, чем проверяет сам rsp.   -  person Ped7g    schedule 25.10.2017
comment
Правильно @ Ped7g - но это не перезапись стека: это проверка правильности обработки rsp (но это не сработает). Канарейка проверяет перезапись (и, как и эта проверка, лучше всего подходит для callee, так как она не будет работать в преобразователе).   -  person BeeOnRope    schedule 25.10.2017
comment
@ Ped7g: Это все равно испортит любой вызов функции, где есть достаточно параметров, из которых они попадают в стек.   -  person Michael Petch    schedule 25.10.2017
comment
@MichaelPetch, конечно ... поэтому я назвал это foo_with_no_args_in_stack ... но в целом это просто очень глупая альтернатива, если подумать. Кроме того, даже если это прозрачно для обычного кода, это приветствуется как вектор атаки для любого вредоносного кода, поэтому я бы определенно не советовал использовать что-то подобное. Насколько я понимаю, если call foo позже дойдет до следующей инструкции, это означает, что rsp верна на 99,99%, или вы подверглись сильной атаке. Другой альтернативой может быть самостоятельная модификация для сохранения rsp как непосредственного для cmp, но cmp rsp,imm64 недействительна??   -  person Ped7g    schedule 25.10.2017
comment
И самомодификация в такой непосредственной близости может повлиять на производительность, поэтому с точки зрения производительности, вероятно, лучше хранить/восстанавливать другие регистры и распространять механизм тестового кода на те, у которых есть отдельный стек для значений проверки rsp. (я больше люблю думать вслух, т.к. считаю, что BeeOnRope достаточно опытен, чтобы ловить только сигнал и игнорировать шумы)..так же ловить прогу. ошибок даже cmp esp,imm32 может быть достаточно!   -  person Ped7g    schedule 25.10.2017
comment
Просто для мотивации: это был подвопрос этого, который предназначен для быстрой проверки интерфейса C-asm (или интерфейса asm-asm) в режиме отладки (т. е. его можно отключить с помощью переменной времени компиляции). Я отлаживал слишком много гейзенбагов, когда какой-то регистр, сохраняемый вызываемым пользователем, стирался (или, что еще хуже: иногда стирался), что нарушало (или нет) вызывающий код замечательным образом, в зависимости от флагов компилятора и т. д. Эта идея Однако проверка rsp в преобразователе кажется DOA.   -  person BeeOnRope    schedule 25.10.2017
comment
@Jester - на самом деле я думаю, что выравнивание исходного кода было правильным. Насколько я понимаю, вызывающий должен иметь выровненный стек на 16 байт в момент вызова, что означает, что он снова смещен на 8 байт в вызываемом объекте, поскольку call подталкивает адрес возврата. Таким образом, sub rsp, 8 снова выравнивает его.   -  person BeeOnRope    schedule 25.10.2017
comment
@BeeOnRope, да, плохо, извини!   -  person Jester    schedule 25.10.2017
comment
@Jester - ну, твоя гениальная идея разделения стеков - это особый соус, который мне нужен все время, поэтому, в конце концов, этот явно бессмысленный вопрос того стоил.   -  person BeeOnRope    schedule 25.10.2017


Ответы (3)


Из исходного вопроса я предположил, что вы хотели настроить rsp на 8 до перехода к bad_rsp при возникновении ошибки, которую вы можете сделать с помощью

foo_thunk:
    sub rsp, 8
    mov [rsp], rsp
    call foo
    cmp rsp, [rsp]
    lea rsp, [rsp+8]
    jne bad_rsp
    ret

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

person prl    schedule 24.10.2017
comment
Также возможно: pop rsp вместо lea, за исключением того, что если RSP больше не указывает на ваш сохраненный RSP, вы облажались по-другому. (И pop rsp на IvyBridge и более поздних версиях составляет 3 мопса, медленнее, чем lea). - person Peter Cordes; 25.10.2017

Я думаю, что простое решение — просто использовать sub и mov для настройки стека, а не push:

foo_thunk:
sub rsp, 8            ; 16 to maintain alignment
mov [rsp], rsp        ; save the value of rsp
call foo              ; call the underlying function
cmp rsp, [rsp]    
jne bad_rsp           ; bad foo!
add rsp, 8            ; pop the stack
ret 

Конечно, если вызываемый foo перепутал указатель стека, это, вероятно, вообще не вернется к вызывающему преобразователю, поэтому полезность этой проверки ограничена. Лучше было бы в callee в качестве проверки перед ret.

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

person BeeOnRope    schedule 24.10.2017
comment
Он может изменить rsp, по-прежнему возвращаться, а также пройти этот тест, хотя - person harold; 25.10.2017
comment
@harold - вы имеете в виду перезапись сохраненного значения rsp в стеке? Конечно. Чтобы было ясно, моя мотивация здесь состоит в том, чтобы быстро обнаруживать ошибки программирования, а не в качестве механизма защиты от вредоносного кода. Конечно, при таком подходе он по-прежнему терпит неудачу, потому что функция, которая не может правильно поддерживать rsp, почти никогда не вернется к преобразователю, как указано в комментариях. - person BeeOnRope; 25.10.2017
comment
Вы захотите изменить подпрограмму и добавить 8 вместо 16, чтобы соответствовать выводу, сделанному в комментариях к вопросу. - person prl; 25.10.2017
comment
@BeeOnRope: foo может извлечь адрес возврата, создать кадр стека где-то еще и ret там. Но прохождение этого теста не включает в себя перезапись чего-либо, оно включает в себя создание значения в новом местоположении, указывающего на себя. Ваш чек использует новый rsp. Такая ошибка вряд ли может произойти случайно; Я бы не беспокоился об этом. (Забавный факт: gcc иногда копирует фрейм стека, включая адрес возврата, хотя он не использует его для возврата. Я думаю, просто чтобы иметь фрейм стека для обратных трассировок или чего-то еще. Я забыл почему, может быть, вложенные функции GNU C.) - person Peter Cordes; 25.10.2017

Альтернативный вариант самостоятельного мода, проверяющий только младшие 32b части rsp:

foo_thunk:
mov [cs:check_esp_part+2],esp
call foo              ; call the underlying function
check_esp_part:
cmp esp,0x12345678
jne bad_rsp           ; bad foo!
ret 
person Ped7g    schedule 24.10.2017
comment
В 64-битном режиме вы можете просто записать значение в соседнюю память и использовать cmp rsp, [rip+x]. Логически это просто глобальная переменная (и из-за недоступной для записи кодовой страницы, вероятно, нерабочая;)) и определенно не реентерабельная. - person Jester; 25.10.2017
comment
@Jester забыл об ограничении повторного входа. Адресация + доступ больше похожи на детали реализации;), но реентерабельность делает это совершенно неправильным и бесполезным. - person Ped7g; 25.10.2017
comment
Если вы делаете это для повышения безопасности, для этого требуется страница памяти с разрешениями на запись + выполнение (для самомодифицирующегося кода). Обычно вам это не нужно. - person Peter Cordes; 25.10.2017