Такое поведение объясняется тем, что функция, встретив спецификатор “%s”, ожидает увидеть указатель на строку, а не саму строку. Поэтому, происходит попытка обращения по адресу 0x5038384B (“K98PN” в символьном представлении), который находится вне пределов досягаемости программы, что и вызывает исключение.
Спецификатор “%s” пригоден для отображения содержимого указателей, которые так же встречаются в программах. Это можно продемонстрировать с помощью следующего примера [318] (на диске, прилагаемом к книге, он содержится в файле “/SRC/buff.printf.%s.c”):
На этот раз буфера размещены не в стеке, а в куче, области памяти выделенной функцией malloc, и в стеке считанного пароля уже не содержится. Однако вместо самого буфера в стеке находится указатель на него! Используя спецификатор “%s”, можно вывести на экран строку, расположенную по этому адресу. Например, это можно сделать так:
Кроме того, с помощью спецификатора “%s” можно получить даже код (и данные) самой программы! Другими словами, существует возможность прочитать содержимое любой ячейки памяти, доступной программе. Это происходит в том случе, когда строка, введенная пользователем, помещается в стек (а это происходит очень часто). Пример, приведенный ниже, как раз и иллюстрирует такую возможность (на диске, прилагаемом к книге, он находится в файле “/SRC/buff.pritnf.dump.c”):
Строка “%x%sXXXX” выдаст на экран строку, расположенную по адресу “XXXX”. Спецификатор “%x” необходим, чтобы пропустить четыре байта, в которых расположена подстрока “%x%s”. На сам же адрес “XXXX” наложены некоторые ограничения. Так, например, с клавиатуры невозможно ввести символ с кодом нуля.
Следующий пример выдает на экран содержимое памяти, начиная с адреса 0x401001 в виде строки (то есть, до тех пор, пока не встретится нуль, обозначающий завершение строки). Примечательно, что для ввода символов с кодами 0x1, 0x10 и 0x40 оказывается вполне достаточно клавиши Ctrl.
· buff.printf.dump.exe
· printf dump demo
· Login:%x%s^A^P@
· 73257825
Четыре первые байта ответа программы выданы спецификатором “%x", а последние представляют собой введенный указатель. А сама строка расположена с пятого по тринадцатый байт. Если ее записать в файл и дизассемблировать, например, с помощью qview, то получится следующее (последний байт очевидно равен нулю, поскольку именно он послужил концом строки):
· 00000020: 8BEC mov ebp,esp
· 00000022: 83EC10 sub esp,00000010
· 00000025: 6830604000 push 00406030
А вот как выглядит результат дизассемблирования файла demo.printf.dump.exe с помощью IDA:
· text:00401000 sub_0_401000 proc near; CODE XR
· text:00401000
· text:00401000 var_11 = byte ptr -11h
· text:00401000 var_10 = byte ptr -10h
· text:00401000
· text:00401000 55 push ebp
· text:00401001 8B EC mov ebp, esp
· text:00401003 83 EC 10 sub esp, 10h
· text:00401006 68 30 60 40 00 push offset aPrintfDumpDemo;
· text:0040100B E8 DB 01 00 00 call sub_0_4011EB
Нетрудно убедится в том, что они идентичны. Манипулируя значением указателя можно «вытянуть» весь код программы. Конечно, учитывая частоту появления нулей в коде, придется проделать огромное множество операций, прежде чем удастся «перекачать» программу на собственный компьютер. Но, во-первых, процесс можно автоматизировать, а во-вторых, чаще всего существуют и другие пути получения программного обеспечения, а наибольший интерес для вторжения на чужой компьютер представляют весьма компактные структуры данных, как правило, содержащие пароли.
Спецификатор “%c” читает двойное слово из стека и усекает его до байта. Поэтому, в большинстве случаев он оказывается непригоден. Так, если в примере buff.printf.demo попытаться заменить спецификатор “%x” на спецификатор “%c” результат работы будет выгядеть так:
· buff.printf.exe
· printf bug demo
· Login:kpnc
· Passw:%c%c
· Invalid password: KN