В ассемблере можно передавать значения через менее изменчивые регистры или изменчивые регистры. Например, я могу передать аргументы printf
, используя edi
и esi
. Вместо этого я могу использовать ebx
и ecx
. Этот пример — очень простой надуманный. Мне больше любопытно, как это работает с гораздо более сложными программами, вызывающими несколько функций из libc
.
Например, в атаках с возвратно-ориентированным программированием злоумышленник может использовать гаджеты для использования тех же регистров, которые использовались для предыдущей функции, чтобы вставлять в них новые значения из стека, а затем возвращаться к другой функции libc
, которая использует те же регистры, например с write
и read
можно использовать pop rsi
в атаках ROP
, чтобы использовать любую функцию, если они просочились в глобальную таблицу смещений. Мой общий вопрос можно задать так:
Если злоумышленник наследует регистры от предыдущего вызова read
следующим образом:
0x00005555555552d0 <+107>: lea rcx,[rbp-0xd0] <- Memory address of buffer "msg"
0x00005555555552d7 <+114>: mov eax,DWORD PTR [rbp-0xe4] <- contains client fd 0x4
0x00005555555552dd <+120>: mov edx,0x400 <- 1024 (size of bytes to write to memory location/buffer)
0x00005555555552e2 <+125>: mov rsi,rcx
0x00005555555552e5 <+128>: mov edi,eax
0x00005555555552e7 <+130>: call 0x5555555550d0 <read@plt>
Как процессор узнает, какие аргументы передать для записи, если регистры, переданные для записи, различны:
0x00005555555552b1 <+76>: call 0x555555555080 <strlen@plt>
0x00005555555552b6 <+81>: mov rdx,rax <- store return value from strlen into rdx
0x00005555555552b9 <+84>: lea rcx,[rbp-0xe0] <- message to write
0x00005555555552c0 <+91>: mov eax,DWORD PTR [rbp-0xe4] <- client file descriptor
0x00005555555552c6 <+97>: mov rsi,rcx
0x00005555555552c9 <+100>: mov edi,eax
0x00005555555552cb <+102>: call 0x555555555060 <write@plt>
Понятно, что read
не использует rdx
, а write
не использует edx
, так как же процессор узнает, что выбрать, например, если злоумышленник использовал только гаджет, который вставляет значение в rsi?
Я не могу понять, как процессор знает, из каких регистров выбирать (rdx
или edx
). Как процессоры выбирают значения для передачи libc
функциям или функциям/процедурам, если уж на то пошло?
В частности, для вашего примера write
mov eax,DWORD PTR [rbp-0xe4]
не может быть подсчетом, так как он переносится на edi
. Это явно дескриптор файла. Счетчик уже введен в rdx
более ранним кодом, который вы не показали.
Я вижу, что изменил второй пример.
Поэтому, поскольку я упустил из виду, что eax
был перемещен в edi
, я не увидел, что rcx
также является аргументом. Это все еще не объясняет разницу между rcx
и edx
. Это разные регистры, так как же процессор узнает, какой из них использовать?
rcx
тоже не аргумент (в данном случае). Он перемещен в rsi
. Ни rax
, ни rcx
не используются для передачи аргументов. В показанном коде они просто временные, они перемещаются в правильные регистры аргументов перед вызовом функции.
Хорошо, я вижу, если вы добавите это как ответ, я приму это.
Да, порядок имеет значение. Порядок, в котором они сопоставляются с аргументами, а не порядок, в котором вы загружаете значения. См. документацию по соглашению о вызовах
@Jester, так как процессор узнает, что нужно выбирать между rdx
и edx
?
Процессор очень тупой, он ничего не знает. Буквально он делает только то, что говорят инструкции, а инструкции в конечном итоге пишутся программистом прямо или косвенно (через компиляцию). Компилятор знает об этом благодаря соглашению о вызовах, принятому авторами компилятора, и они могут свободно выбирать соглашение, которое они хотят для этой цели, им не нужно соответствовать конкретному ранее определенному соглашению. Если они это сделают, это их свободный выбор. В конце концов, авторы компилятора знают и строят компилятор вокруг этого... Процессор делает только то, что ему говорят, что он не может думать сам.
Процессор ничего не знает; регистры не индексируются, и единственный порядок, который они имеют в отношении ЦП, - это номера регистров, используемые в машинном коде. (И для таких вещей, как инструкции сохранения нескольких регистров, такие как устаревший 32-битный режим pusha
/ popa
или xsave
для сохранения состояния FPU / SIMD.)
То, что ищет аргументы в определенных местах вызываемой функции, это... дополнительный код (программное обеспечение), сгенерированный компилятором, который скомпилировал функцию с ее аргументами, объявленными определенным образом. Помните, что printf
— это еще одно программное обеспечение, не встроенное в процессор.
Компилятор знает стандартное соглашение о вызовах для целевой платформы (в данном случае оно определено в X86-64 System V ABI), поэтому, если и вызывающий, и вызываемый объект соглашаются с соглашением о вызовах, код вызова помещает аргументы в места, которые вызываемые смотреть на них.
Стандартизация этого соглашения о вызовах — это то, как мы можем связать код из разных компиляторов в одну программу и выполнять вызовы в библиотеки.
Кстати, то же самое касается системных вызовов; вы помещаете номер вызова в определенный регистр и запускаете инструкцию, которая переключается в режим ядра (например, syscall
). Теперь ядро работает и может просматривать значения, оставшиеся в регистрах. Он использует номер вызова для индексации таблицы указателей функций, вызывая ее с другими аргументами в стандартных регистрах передачи аргументов. (Или туда, куда им нужно перейти в соответствии с соглашением о вызовах C, которое обычно отличается от соглашения о вызовах системных вызовов.)
Я просто не понимаю, как процессор знает, что выбрать rdx
или edx
. Если вы можете ответить, что я приму ваш ответ.
@asd40732: О каком выборе вы говорите? При декодировании машинного кода разница между add eax, edx
и add rax, rdx
составляет 1 бит в префиксе REX, который выбирает 64-битный размер операнда (REX.W=1). dx/edx/rdx имеют одинаковый номер регистра в машинном коде, размер задается атрибутом размера операнда инструкции (префиксы и код операции). wiki.osdev.org/X86-64_Instruction_Encoding#REX_prefix.
@ asd40732: Если вы имеете в виду, как компилятор решает, какой размер операнда использовать, обычно он соответствует ширине типа переменной C. (Запись 32-битного регистра неявно расширяет нулем до 64-битного, поэтому mov edx, 1234
компилятор будет передавать size_t
или uint64_t
аргумент, например, 3-й аргумент write
; Зачем выполнять инструкции x86-64 для 32-битных регистров? обнулить верхнюю часть полного 64-битного регистра?). Но решение принимает компилятор, а не процессор, как объяснили я и old_timer в обоих наших ответах.
также по теме: Преимущества использования 32-битных регистров/инструкций в x86-64
Это не выбор процессора. Он определяется соглашением о вызовах. Обратите внимание, что поскольку
read
иwrite
принимают одинаковое количество и тип аргументов, они используют одни и те же регистры. В Linux x86-64 этоrdi
,rsi
иrdx
для аргументовfd
,buf
иcount
соответственно. Непонятно, почему вы думаете, что они разные.