Telnet и echo — кто прав, кто виноват
В комментариях к предыдущей статье у нас разразился небольшой спор на тему того, помогает ли команда «set localecho» в решении проблемы с отсутствием echo при взаимодействии с сервером bat.org. Я стоял на том, что команда эта не сделает ровным счётом ничего для исправления рассмотренной ситуации, и говорил это вовсе неспроста — специально после одного из комментариев я решил ещё раз проверить мою правоту в данном вопросе. Проделав все необходимые действия (запуск telnet.exe, нажатие Ctrl-], ввод команды «set localecho» и двойное нажатие клавиши Enter), я в очередной раз убедился, что был прав. О чём же тогда так уверенно твердят остальные?
Я попросил выслать мне бинарник «работающего» telnet-клиента и версию ОС, на котором он запускался, в личку. Убедившись, что версии ОС совпадают (использовалась Windows 7 SP1×64), я обратил своё внимание на сам telnet-клиент. Хеши совпали. Запустив «работающий», по заверению пользователя k0ldbl00d, бинарник, я с удивлением обнаружил, что на моём компьютере не работает и он.
Может быть, дело в окружении, в котором выполняется telnet.exe? Оригинальный исполняемый файл был взят из директории »%WINDIR%\System32», так что я запустил свой telnet-клиент оттуда, и… Обнаружил, что команда «set localecho» корректно отрабатывает при таком раскладе. А если скопировать тот же самый исполняемый файл в любую другую директорию и воспользоваться уже им, то, несмотря на то, что основной функционал telnet.exe будет продолжать работать, команды перестанут выполнять то, что от них требуется.
В чём же дело? Давайте раберёмся.
Как протекал процесс, и что из этого вышло, читайте под катом. Перед прочтением данной статьи также настоятельно рекомендую ознакомиться с предыдущими, т.к. в них уже объяснены многие из опущенных здесь моментов.
Во-первых, мой взгляд упал на отсутствие «приветствующих» сообщений в случае выполнения telnet.exe в директории, отличной от »%WINDIR%\System32»:
В случае корректной работы (запуск из »%WINDIR%\System32»)
В случае некорректной работы (запуск из «C:\»)
Что ж, давайте поставим бряки на инструкциях, которые обращаются к данным строкам. Но перед тем, как это делать, давайте отключим ASLR. Сделать это при помощи способа, использованного в предыдущей статье (редактирование поля «DLL flags» в бинарнике), не получится, ведь нормальная замена исполняемых файлов в директории »%WINDIR%\System32» практически невозможна. Следовательно, предлагаю отключить ASLR для всей системы в целом. Нажимаем Win-R → regedit → лезем в «HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management» → создаём или редактируем уже существующую опцию под названием «MoveImages», чтобы сделать её равной нулю. После этого перезагружаем систему и наслаждаемся отключённым ASLR.
Запускаем telnet.exe, находящийся в директории »%WINDIR%\System32», в OllyDbg, останавливаемся на Entry Point и ищем «Welcome to» в списке найденных OllyDbg строк, на которые ссылаются инструкции в модуле «telnet». Видим сообщение «Item not found» и начинаем думать в сторону динамической загрузки строк из сторонних ресурсов.
Нажимаем F9, дожидаемся старта telnet-клиента и снова ищем ту же самую строку:
Нажимаем Enter, предварительно выделив найденную только что строку, и видим инструкции, которые обращаются к строкам из «приветствующих» сообщений:
Давайте поставим хардварный бряк на запись по адресу 0×01029C40, перезапустим приложение и посмотрим, откуда всё же берутся эти строки. Точка останова срабатывает тут:
Как видите, мы находимся где-то в недрах WinAPI-функции LoadString (да, явно этого, к сожалению, не заметно, однако, вероятнее всего, LoadStringBaseEx вызывается как раз из неё). Прыгаем на ближайший пользовательский код, который в данном случае находится по адресу 0×0100C788, и видим, что рядом находятся такие же вызовы для получения других строк:
Посмотрим чуть Выше и убедимся, что в данных местах действительно вызывалась LoadString:
Давайте узнаём, из какого модуля telnet.exe берёт данные строки. Ставим бряк по адресу 0×0100C62A, перезапускаем отладку и видим, что аргумент, отвечающий за имя модуля в WinAPI-функции GetModuleHandle, равен нулю:
Согласно документации, в таком случае GetModuleHandle возвращает, по сути, хэндл файла текущего процесса:
lpModuleName [in, optional]
[…]
If this parameter is NULL, GetModuleHandle returns a handle to the file used to create the calling process (.exe file)
Если нажать два раза F8, то мы увидим, что это действительно так:
Далее этот хэндл используется во всех встреченных нами вызовах LoadString. Например,
Но в чём же тогда проблема? Если LoadString берёт строки из того же самого файла, то как на успешность их извлечения может повлиять смена рабочей директории?
Для начала давайте возьмём в руки Resource Hacker и посмотрим, найдёт ли он STRINGTABLE в telnet.exe:
Как видите, её нет. Да что ж такое? Откуда тогда эти строки вообще берутся? Подгружаются в тот же модуль в run-time? Давайте перезапустим отладку и посмотрим, нет ли их в памяти процесса уже на старте приложения. Нажимаем Ctrl-F2 в OllyDbg, жмём Alt-M, чтобы открыть окно «Memory», выделяем левой кнопкой мыши первую строчку, нажимаем Ctrl-B и ищем юникодовую строку «Welcome». Эта и остальные строки действительно находятся в памяти процесса уже на данном этапе:
Посмотрим, откуда был замаппеен данный участок памяти. Нажимаем Alt-M и видим:
Теперь понятно — это MUI. Если создать директорию под названием «en-US», например, в корне диска C, положить туда файл telnet.exe.mui и запустить ранее некорректно работавший telnet-клиент, мы увидим, что теперь команда «set localecho» ведёт себя абсолютно правильно.
Но постойте. Даже если telnet.exe не мог вывести на экран какие-то строки, как это могло повлиять на сам результат выполнения того же «set localecho»? Ведь одно дело что-то не вывести, и совсем другое дело не отрерагировать на введённую пользователем команду должным образом.
Давайте поставим бряк на обращение к строке «Microsoft Telnet> », которая является «приглашением» для ввода следующей команды. Такая инструкция находится по адресу 0×0100BEAA:
Нажимаем Ctrl-] в окне telnet’а, бежим по отлаживаемому приложению при помощи F8 и останавливаемся на ближайшем вызове WinAPI-функции ReadConsole:
Вводим команду «set localecho» и изучаем, что происходит после считывания строки из стандартного потока ввода.
Сначала в программе проверяется корректность выполнения функции ReadConsole (возвращаемое значение не ноль, кол-во прочитанных байт больше нуля etc):
Через некоторое время после этого мы попадаем в цикл, в котором проверяется первый символ введённой пользователем команды (в нашем случае это 's') на равенство с первыми символами таких команд, как, например, «quit» или «set»:
Если запустить отладку из другого места, в котором не лежит директории «en-US» с необходимым .mui-файлом, то мы увидим, что строки с названиями команд будут пустыми! Это наталкивает на мысль, что даже они хранятся в файле telnet.exe.mui. А убедиться в этом можно, поискав соответствующие строки в памяти процесса:
Следовательно, в случае отсутствия .mui-файла telnet-клиент не мог даже понять, что за команда была введена пользователем, т.к. строки для сравнения не были загружены.
Зачем было выносить даже эти строки в .mui-файл — лично для меня загадка. Возможно, Microsoft не знали, насколько далеко зайдёт интернационализация их продуктов (например, «включить локальное эхо»), или, может быть, просто хотели иметь единое место, где хранились бы абсолютно все используемые в программе строки.
Послесловие
Порой даже малейшее изменение условий, в которых выполняется исследуемое приложение, может повлиять на самые неожиданные линии поведения. Не ленитесь перепроверять все возможные варианты и внимательно относитесь к любым изменениям в логике работы приложения.
Спасибо за внимание, и снова надеюсь, что статья оказалась кому-нибудь полезной.