Можно ли заменить каждый экземпляр определенной функции на пустышку в скомпилированном двоичном файле?
Можно ли изменить способ, которым существующие двоичные x86-64 ссылки и / или вызывает одну конкретную функцию. В частности, возможно ли изменить двоичный файл так, чтобы ничего не происходило (аналогично nop
) в те времена, когда эта функция обычно выполнялась?
Я понимаю, что существуют мощные специализированные инструменты (например, декомпиляторы / дизассемблеры) для решения именно такой задачи, но мне действительно интересно, достаточно ли для того, чтобы исполняемые форматы были "читаемыми" человеком, чтобы иметь возможность выполнять подобные операции. вещь (по крайней мере, в небольших программах) только с vim и hex-редактором.
Являются ли определенные форматы исполняемых файлов (например, mach-o, elf, независимо от того, какие окна используются и т. Д.) Более читабельными, чем другие? Они все просто совершенно непонятные тарабарщины? Любые экспертные мнения и / или хорошие прыжки от точек / ссылок будут с благодарностью.
отказ
Кто-то пришел и быстро проголосовал за первоначальную версию этого вопроса, поэтому я хочу прояснить это: я не заинтересован в отключении каких-либо серийных проверок или проверок безопасности или чего-либо подобного. Первоначально я хотел, чтобы программа перестала издавать действительно раздражающий шум, но теперь мне просто интересно, как работают компиляторы и исполняемые файлы.
Я в этом для образовательной ценности, и я думаю, что другие люди на SE будут заинтересованы в ответе. Тем не менее, я ценю, что другим может быть не так комфортно с этой темой. Если у вас есть сомнения по поводу того, что я сказал, пожалуйста, оставьте комментарий, и я обещаю, что я изменю свой пост.
2 ответа
Это тривиально, когда рассматриваемая функция находится в самом двоичном файле и использует стандартные соглашения о вызовах. Пример:
void make_noise() { printf("Quack!\n"); }
int fn1() { puts("fn1"); make_noise(); return 1; }
int fn2() { puts("fn2"); make_noise(); return 2; }
int main() { puts("main"); return fn1() + fn2() - 3; }
gcc -w t.c -o a.out && ./a.out
Это выводы (ожидаемые):
main
fn1
Quack!
fn2
Quack!
Теперь давайте избавимся от шума:
gdb -q --write ./a.out
(gdb) disas/r make_noise
Dump of assembler code for function make_noise:
0x000000000040052d <+0>: 55 push %rbp
0x000000000040052e <+1>: 48 89 e5 mov %rsp,%rbp
0x0000000000400531 <+4>: bf 34 06 40 00 mov $0x400634,%edi
0x0000000000400536 <+9>: e8 d5 fe ff ff callq 0x400410 <puts@plt>
0x000000000040053b <+14>: 5d pop %rbp
0x000000000040053c <+15>: c3 retq
End of assembler dump.
Это говорит нам о нескольких вещах:
- Функция, от которой мы хотим избавиться, запускается по адресу
0x40052d
- ОП-код
retq
инструкция0xC3
,
Давай исправим retq
как первая инструкция make_noise
и посмотрим, что произойдет:
(gdb) set *(char*)0x40052d = 0xc3
(gdb) disas make_noise
Dump of assembler code for function make_noise:
0x000000000040052d <+0>: retq
0x000000000040052e <+1>: mov %rsp,%rbp
0x0000000000400531 <+4>: mov $0x400634,%edi
0x0000000000400536 <+9>: callq 0x400410 <puts@plt>
0x000000000040053b <+14>: pop %rbp
0x000000000040053c <+15>: retq
End of assembler dump.
Это сработало!
(gdb) q
Segmentation fault (core dumped) ## This is a long-standing GDB bug
А теперь давайте запустим исправленный бинарный файл:
$ ./a.out
main
fn1
fn2
Вуаля! Нет шума.
Если функция находится в другом бинарном файле, LD_PRELOAD
методы, упомянутые Флорианом Ваймером, обычно проще, чем бинарное исправление.
ELF динамические связи часто поддерживают реализации LD_PRELOAD
а также LD_AUDIT
модули, которые могут перехватывать вызовы в другой общий объект. LD_AUDIT
предлагает больший контроль и существует в GNU/Linux (но документация Solaris является канонической ссылкой).
Для вызовов внутри одного и того же общего объекта это может оказаться невозможным, если целевая функция не экспортируется (или вызов выполняется через скрытый псевдоним; glibc делает это много). Если у вас есть отладочная информация, вы можете использовать systemtap для перехвата вызова. Если функция встроенная, перехват вызова может быть невозможен даже при использовании системной записи, поскольку в потоке инструкций нет точного места, где происходит вызов.