Учим telnet.exe правильной игре в MUD'ы

    image

    Как-то раз, решив сыграть во что-нибудь необычное, я обратил свой взгляд на MUD'ы — текстовые компьютерные многопользовательские игры с чатом. Играть в них можно как при помощи специализированных клиентов, написанных под конкретные сервера, так и по telnet'у.

    Выбрав один из существующих на данный момент серверов (https://www.bat.org/), я вооружился дефолтным telnet-клиентом под Windows и… Почувствовал разочарование. Нет, дело вовсе не в игре, а в том, как с этой игрой взаимодействует telnet.exe. Печально осознавать, но ни один из вводимых мной символов (имя персонажа, разнообразные действия etc) не отображался на экране консоли. Да, команды отправлялись по нажатию клавиши Enter, но отсутствие даже минимального интерактива делало такую игру практически невозможной (особенно неудобно было удалять введённые ранее символы, ведь приходилось подсчитывать в уме, сколько символов ты уже удалил и на каком находишься в текущий момент).

    Недолго думая, я решил попробовать приконнектиться к тому же самому серверу при помощи putty и… Вот это да! Я вижу вводимые мной символы!

    Почему же echo не работает в telnet.exe? Можно ли это каким-то образом исправить? Давайте разберёмся.

    Как протекал процесс, и что из этого вышло, читайте под катом. Перед прочтением данной статьи также настоятельно рекомендую ознакомиться с предыдущими, т.к. в них уже объяснены многие из опущенных здесь моментов.

    Первый шаг — заполучить подопытного. Устанавливаем telnet-клиент (Win-R -> appwiz.cpl -> Turn Windows features on or off -> ставим галочку рядом с надписью «Telnet Client» и нажимаем на кнопку «OK») и копируем исполняемый файл telnet.exe из "%WINDIR%\System32" в любую другую директорию.

    Следующий шаг — вооружиться необходимым инструментарием. Скачиваем PE Tools и OllyDbg, о которых я уже не раз упоминал в предыдущих статьях, и распаковываем их в любую удобную для нас директорию.

    Далее необходимо понять, включена ли технология ASLR для бинарника, который мы собираемся исследовать. Запускаем PE Tools, жмём Alt-1, выбираем telnet.exe и нажимаем на кнопку «Optional Header»:

    image

    Да, ASLR включена. Давайте отключим её — заменяем 0x8140 на 0x8100 (почему именно так уже объяснялось ранее — смотрите, например, тут) и нажимаем на кнопку «Ok».

    Итак, какие мысли? Первое, что пришло мне на ум — то, что приложение может явно «отключать» echo при помощи WinAPI-функции SetConsoleMode. Запускаем наш бинарник в OllyDbg, открываем окно со списком межмодульных вызовов и видим, что вызовы этой функции действительно присутствуют в приложении:

    image

    Ставим на них бряки, жмём F9 и останавливаемся на одной из точек останова:

    image

    Давайте посмотрим на аргументы в окне стека:

    image

    Читаем документацию:

    ENABLE_ECHO_INPUT
    0x0004

    Characters read by the ReadFile or ReadConsole function are written to the active screen buffer as they are read. This mode can be used only if the ENABLE_LINE_INPUT mode is also enabled

    То, что нужно! Однако есть способ даже проще — просто не вызывать эту функцию:

    When a console is created, all input modes except ENABLE_WINDOW_INPUT are enabled by default

    Давайте перезапустим отладку, занопим её вызов

    image

    и проверим, работает ли echo теперь. Нет, результат тот же, что и раньше — вводимые символы не отображаются на экране консоли.

    Ладно, давайте дождёмся момента, когда игра попросит ввести имя

    image

    , и нажмём F12 (Pause) в OllyDbg.

    Предлагаю осмотреться, чтобы понять, где мы находимся в текущий момент. Для начала открываем Call Stack по нажатию Alt-K:

    image

    Итак, мы висим где-то в недрах user32.dll. Прыгаем на ближайший «пользовательский» код (т.е. код, принадлежащий модулю telnet), находящийся по адресу 0x0100D0D0, откуда мы и попали в user32.dll:

    image

    Опытному разработчику под Windows уже должно быть понятно, адрес какой функции лежит, вероятнее всего, в регистре EDI на момент выполнения выделенной инструкции — GetMessage. Но давайте убедимся в этом лично. Ставим бряк по данному адресу, перезапускаем отладку и жмём F9 до попадания на требуемое место:

    image

    Как видите, это действительно GetMessage. Проблема в нашем случае заключается в том, что эта функция не вернёт управление вызвавшему её коду до нажатия клавиши Enter, что означает то, что она не имеет к echo ровным счётом никакого отношения.

    Тогда давайте взглянем, чем в этот момент занимаются остальные потоки (если они, конечно, вообще есть). Снова запускаем программу на дальнейшее выполнение при помощи F9, нажимаем F12 и открываем окно «Threads» (View -> Threads):

    image

    Открываем каждый из них в окне CPU (right-click по соответствующей строчке в окне Threads -> Open in CPU), за исключением выделенного красным цветом (это текущий поток, который мы только что рассмотрели), и смотрим на их Call Stack'и. Ваше внимание должен был привлечь поток со следующим стеком вызовов:

    image

    ReadConsoleInput — это уже более интересная функция в нашем случае. Ставим бряк на её вызов, перезапускаем отладку и… Останавливаемся на нём каждый раз, как передаём фокус окну telnet'а:

    image

    Обратите внимание, что рядом находится switch, в котором, вероятнее всего, выполняется прыжок на обработчик соответствующего события. Побегав в отладчике, можно узнать, что в случае смены фокуса управление передаётся default case'у:

    image

    Судя по анализу кода, проведённому OllyDbg'ом, вариантов тут не так уж и много — помимо default case'а, у нас также имеются case'ы 10 и 1, первый из которых после выполнения нескольких инструкций осуществляет прыжок на только что рассмотренный default case. Давайте попробуем убрать бряк с вызова функции ReadConsoleInput и поставить бряк на case 1:

    image

    Перезапускаем отладку, дожидаемся сообщения с просьбой ввести имя, нажимаем '1' и останавливемся на этом самом case-блоке:

    image

    Что мы можем сделать теперь? А теперь можно сверить работу telnet.exe в случае коннекта к bat.org и, допустим, smtp.gmail.com, где, как я помню, echo работало корректно. Открываем окно «Run trace» (View -> Run trace), жмём на него правой кнопкой мыши, выбираем пункт меню под названием «Log to file», выбираем любое имя файла и нажимаем Ctrl-F11 (Trace into). После выполнения трассировки закрываем файл (right-click по окну «Run trace» -> Close log file) и проделываем то же самое в случае smtp.gmail.com:25 (в случае явного указания порта telnet'у IP-адрес необходимо отделять от него символом пробела, т.е. команда должна выглядеть следующим образом — «telnet.exe smtp.gmail.com 25»).

    Заметная разница в поведении начинается с адреса 0x0100A2F9:

    В случае bat.org
    Address  Thread   Command                                   ; Registers and comments
    0100AB9F 00002EA0 JNZ telnet.0100AED2
    0100ABA5 00002EA0 TEST BYTE PTR SS:[EBP-24],3
    0100ABA9 00002EA0 JE telnet.0100AED2
    [...]
    0100A2F7 00002EA0 TEST EAX,EAX
    0100A2F9 00002EA0 JNZ SHORT telnet.0100A304
    0100A2FB 00002EA0 TEST BYTE PTR DS:[1010740],10
    [...]
    

    В случае smtp.gmail.com
    Address  Thread   Command                                   ; Registers and comments
    0100AB9F 00002EA0 JNZ telnet.0100AED2
    0100ABA5 00002EA0 TEST BYTE PTR SS:[EBP-24],3
    0100ABA9 00002EA0 JE telnet.0100AED2
    [...]
    0100A2F7 000031D4 TEST EAX,EAX
    0100A2F9 000031D4 JNZ SHORT telnet.0100A304
    0100A304 000031D4 PUSH EDI                                  ; Arg4 = 01024CA0
    [...]
    


    В том случае, когда telnet.exe общается с bat.org, прыжок по адресу 0x0100A304 не осуществляется. Давайте сделаем из инструкции по адресу 0x0100A2F9 безусловный переход. Перезапускаем отладку, переходим в модуль «telnet», нажимаем Ctrl-G, вводим в появившееся окно адрес 0x0100A2F9 и нажимаем Enter. Теперь нажимаем пробел и заменяем инструкцию JNZ на JMP:

    image

    Нажимаем F9, вводим '1' в окно telnet'а на просьбу выбрать один из предложенных вариантов или ввести имя и… Видим введённый нами символ:

    image

    Если побегать в отладчике, можно увидеть, что теперь мы попадаем в ветку кода, где выполняются вызовы таких WinAPI-функций, как SetConsoleCursorPosition и WriteConsoleOutputCharacter:

    image

    Почему же тогда мы не попадали сюда раньше? Давайте посмотрим, от чего зависило принятие решения об осуществлении прыжка:

    image

    Зависило оно от результата выполнения операции TEST EAX,EAX, а в регистр EAX значение попадало из адреса 0x01010754, как видно на предыдущем скриншоте. Что ж, давайте попытаемся понять, почему оно было равно нулю в случае bat.org.

    Для того, чтобы выяснить это, предлагаю поставить хардварный бряк на запись по адресу 0x01010754. Чтобы прыгнуть на него, нажимаем правой кнопкой мыши на инструкцию, находящуюся по адресу 0x0100A2BD -> Follow in Dump -> Memory address:

    image

    Жмём правой кнопкой мыши на первый байт по указанному адресу -> Breakpoint -> Hardware, on write -> Dword. Перезапускаем отладку и находим последнее обращение к адресу 0x01010754, когда в него попадает ноль. Такое обращение находится тут:

    image

    Если мы посмотрим на Call Stack и прыгнем на процедуру, откуда нас сюда позвали, то мы увидим вызов функции recv с последующим разбором пришедших данных:

    image

    Обратите внимание на константу 0xFF. Согласно спецификации telnet'а, после этого байта следуют команды, использующиеся в данном протоколе:

       The following are the defined TELNET commands.  Note that these codes
       and code sequences have the indicated meaning only when immediately
       preceded by an IAC.
    
          NAME               CODE              MEANING
    
          SE                  240    End of subnegotiation parameters.
          NOP                 241    No operation.
          Data Mark           242    The data stream portion of a Synch.
                                     This should always be accompanied
                                     by a TCP Urgent notification.
          Break               243    NVT character BRK.
          Interrupt Process   244    The function IP.
          Abort output        245    The function AO.
          Are You There       246    The function AYT.
          Erase character     247    The function EC.
          Erase Line          248    The function EL.
          Go ahead            249    The GA signal.
          SB                  250    Indicates that what follows is
                                     subnegotiation of the indicated
                                     option.
          WILL (option code)  251    Indicates the desire to begin
                                     performing, or confirmation that
                                     you are now performing, the
                                     indicated option.
          WON'T (option code) 252    Indicates the refusal to perform,
                                     or continue performing, the
                                     indicated option.
          DO (option code)    253    Indicates the request that the
                                     other party perform, or
                                     confirmation that you are expecting
                                     the other party to perform, the
                                     indicated option.
          DON'T (option code) 254    Indicates the demand that the
                                     other party stop performing,
                                     or confirmation that you are no
                                     longer expecting the other party
                                     to perform, the indicated option.
          IAC                 255    Data Byte 255.
    


    Посмотрев на стек, можно увидеть, что мы солкнулись с последовательностью байт 0xFF 0xF9, которая обозначает команду под названием «Go ahead». По поводу неё на сайте Microsoft сообщается следующее:

    The original Telnet implementation defaulted to half duplex operation. This means that data traffic could only go in one direction at a time and specific action is required to indicate the end of traffic in one direction and that traffic may now start in the other direction. [This similar to the use of «roger» and «over» by amateur and CB radio operators.] The specific action is the inclusion of a GA character in the data stream

    По какой-то причине в реализации telnet-клиента от Microsoft эта команда влияет на echo, так и не возвращая содержимое по адресу 0x01010754 в значение, отличное от нуля.

    В этом можно убедиться, написав небольшой сервер на Python:

    import socket, threading
     
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('', 1900))
    s.listen(1)
     
    class daemon(threading.Thread):
    
        def __init__(self, (socket, address)):
            threading.Thread.__init__(self)
            self.socket = socket
            self.address = address
     
        def run(self):
            self.socket.send('Greetings!')
     
            while True:
     
                data = self.socket.recv(1024)
     
                if data[0] == '1':
                    data = 'Response'
                elif data[0] == '2':
                    data = bytearray()
                    data.append(0xFF)
                    data.append(0xF9)
     
                self.socket.send(data);
     
            self.socket.close()
     
    while True:
        daemon(s.accept()).start()
    

    Если запустить данный сервер и подключиться к нему при помощи команды «telnet.exe 127.0.0.1 1900», то мы сможем наблюдать, что echo будет работать ровно до тех пор, пока мы не получим ответ на нашу команду '2':

    Greetings!1Response1Response1Response1Response2ResponseResponseResponseResponseResponseResponse

    Но это ещё не всё! На самом деле, другие команды также обладают подобным поведением. Например, последовательность байт 0xFF 0xF1, обозначающая «No operation», абсолютно так же «отключает» echo в telnet-клиенте.

    Баг? Фича? Кто его знает. Главное, что теперь мы научили наш telnet.exe правильной игре в MUD'ы!

    Послесловие


    Конечно, решение ещё не идеально. Например, при нажатии на клавишу Backspace символ, находящийся перед курсором, не удаляется (однако изменяется «внутреннее» представление введённой пользователем команды, как это и ожидается). Да, это всего лишь косметический момент и с ним можно смириться, но ведь именно с косметических неудобств мы и начали эту статью, верно?

    Спасибо за внимание, и снова надеюсь, что статья оказалась кому-нибудь полезной.

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 24

      0
      Ого, в MUD'ы все еще играют?
        +4
        Да, равно как и в Ultima Online, Tibia Online etc
          +5
          Еще как! Например в былины. Сам периодически бегаю там.
            0
            Вот прям сам или конфиг? А то человек, способный хоть немного программировать, может в мадах напрограммировать хоть бота. А уж аудитории Хабра сам бог велел!.. Как удержаться от соблазна?
              0
              Прямо сам. Разве что 4-5 хоткеев на рекол, уклон прочее
            +1
            Совсем недавно на хабре была статья про слепого программиста, который упоминал, что из всех компьютерных игрушек ему доступны немногие.
            Но вот MUD-ы как раз доступны, причем в некоторых MUD-ах даже специально для незрячих добавляются теги, которые помогают звуковому движку ориентироваться. И мне сложно представить более сложную игру чем MUD-ы для людей без зрения.
              0
              Ну так это просто здорово! Я сам когда то играл в Адамант Адан будучи студентом, и рад, что это дело живо и пользуется спросом.
            +1
            Насколько я помню (проверить сейчас не на чем), в стандартном клиенте telnet от Windows можно было через опции включить echo на экран.
            Что-то типа set echo
              0
              Если Вы про команду «set localecho», то это не помогает в случае bat.org
              +2
              Главное, что теперь мы научили наш telnet.exe правильной игре в MUD'ы!


              Для правильной игре в мады, нужно пользоваться jaba mud client. Там вам и эхо, и конфиги с биндами, да алиасами, и все что душе угодно.
                0
                Я всё время с mmc играл. Скрипты на перле — наше всё.
                0
                Не-не, без mmc в MUD играть нельзя :-)
                  +12
                  Статья достойна уважения, но:
                  1. Ctrl+]
                  2. set localecho
                  3. PROFIT
                    +1
                    Пожалуйста, проверяйте информацию перед тем, как писать её сюда. Команда «set localecho» не сделает ровным счётом ничего в случае bat.org. Это было первым, что я проверил перед тем, как начать реверсить telnet.exe
                      0
                      Что именно не сделает? Насколько я понял из вашей статьи, основная проблема — отсутствие вывода печатаемых символов на экран. Именно эта проблема решается последовательностью которую я привёл.
                        +1
                        Может, я что-то не так делаю? Выполняю команду «telnet.exe bat.org», нажимаю Ctrl-], ввожу «set localecho», нажимаю два раза клавишу Enter и пытаюсь ввести какие-нибудь символы. Верно? Если да, то ничего не происходит (по крайней мере, в моём случае)
                          0
                          Я делаю всё именно в том же порядке, но у меня работает:
                          youtu.be/pSUAYqpws3E
                          Есть вероятность того, что в Windows 8.x что-то поломалось в клиенте telnet.
                            +2
                            Скиньте, пожалуйста, бинарник Вашего telnet-клиента и версию ОС мне в личку, если не сложно
                              0
                              Для интересующихся: бинарник скинут, найдены интересные «особенности».
                    0
                    Эх, надо бы понастольгировать :)
                      0
                      Айда в Былины!
                        0
                        Спасибо, интересно! От себя добавлю, что вместо cmd.exe/putty удобней использовать ConEmu, где есть и табы, и настроить нормально можно внешний вид, и поддержка и cmd.exe, и mintty (cygwin/mingw).
                          +1
                          Круто что топ 1 в рейтинге нормальный юзер с клевыми постами, а не очередной ализар :)

                          Only users with full accounts can post comments. Log in, please.