Можно ли заменить каждый экземпляр определенной функции на пустышку в скомпилированном двоичном файле?

Можно ли изменить способ, которым существующие двоичные 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. 

Это говорит нам о нескольких вещах:

  1. Функция, от которой мы хотим избавиться, запускается по адресу 0x40052d
  2. ОП-код 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 для перехвата вызова. Если функция встроенная, перехват вызова может быть невозможен даже при использовании системной записи, поскольку в потоке инструкций нет точного места, где происходит вызов.

Другие вопросы по тегам