Избавляемся от «исторических причин» в cmd.exe

    image

    Сфера разработки программного обеспечения является одной из тех областей человеческой деятельности, где термин «исторические причины» используется наиболее часто. Оно и понятно — многие «долгоиграющие» проекты наподобие ядер различных операционных систем, браузеров и прочего обросли за время своего существования нехилым арсеналом вещей, менять поведение которых станет далеко не каждый, даже если перфекционист внутри разработчика говорит обратное. Вероятнее всего, большая часть кода была написана программистами, которые уже давно не работают в компании, а даже те, кто ещё связывают свои жизни с данной корпорацией, сомневаются, что остальные компоненты программного комплекса нормально отрерагируют на те или иные изменения. «Нет уж, лучше оставлю, как оно есть».

    В качестве примера одной из таких вещей можно назвать cmd.exe. Да-да, это тот самый интерпретатор командной строки, входящий в поставку всех современных (и не очень) операционных систем семейства Windows. Исторических причин у него накопилось изрядное количество — достаточно вспомнить хотя бы то, как необходимо производить вставку и копирование в данный интерпретатор (ради справедливости стоит сказать, что в Windows 10 эту ситуацию наконец исправили, да и приложения наподобие ConEmu здорово в этом помогают). Но речь сегодня пойдёт о другом поведении, которое заствляет задуматься впервые столкнувшегося с cmd.exe человека, казалось бы, там, где этого совсем не требуется.

    Как вы знаете, одной из команд, которые воспринимает cmd.exe, является «CD». Официальный хелп по этой команде сообщает следующее:

    C:\Users\Nikita.Trophimov>CD /?
    Displays the name of or changes the current directory.
    [...]

    Казалось бы, всё просто. Вызываешь CD без аргумента — в stdout выводится путь до текущей директории, передаёшь другую директорию в качестве аргумента — он сменяет текущую директорию на указанную. Подводные камни тут начинаются в том случае, если пользователь решил сменить директорию одновременно вместе с диском. Например, если вы находитесь в директории «C:\Windows\system32», то команда «CD D:\books» не сделает ровным счётом ничего. На мой взгляд, очевидного для новых пользователей в этом совершенно ничего нет, так что их спасает гугл или официальная документация, которая, кстати, сообщает:

    Use the /D switch to change current drive in addition to changing current
    directory for a drive.

    Разумеется, этот вопрос, равно как и причины возникновения подобного поведения, уже не раз обсуждался в интернете (например, тут), так что останавливаться на подобных вещах мы не будем. Вместо этого мы попробуем отладить cmd.exe, чтобы убрать необходимость явного указания ключа "/D".

    Как протекал процесс, и что из этого вышло, читайте под катом.

    Всё большая часть ОС семейства Windows является 64-битными, что не является исключением и в моём случае. Все стандартные утилиты (calc.exe, taskmgr.exe, наш с вами cmd.exe и т.д.) также обзавелись 64-битными аналогами, которые и поставляются по дефолту вместе с операционной системой. Для реверса это означает то, что мы в данном случае не можем, к сожалению, воспользоваться уже привычным нам по предыдущим статьям (которые можно найти, например, тут) OllyDbg (кстати, работа над поддержкой x64 до сих пор ведётся).

    Какие у нас есть варианты? С x64 умеют работать как минимум IDA Pro и относительно новый x64_dbg. К сожалению, поддержкой x64 обладают лишь платные версии IDA Pro, так что предлагаю остановиться на втором варианте.

    Делаем копию cmd.exe, скачиваем снэпшот последней версии x64_dbg, запускаем его и загружаем в него исследуемый нами исполняемый файл:

    image

    Нажимаем F9 до тех пор, пока программа не перестанет останавливаться на брейкпоинтах (приятно, что очень многие хоткеи из OllyDbg работают и тут), делаем right-click по содержимому окна CPU -> Search for -> String references и ищем строку "/D":

    image

    Ставим на каждую из них по бряку при помощи F2, вводим в окно запущенного процесса cmd.exe команду «CD /D D:\books» (предполагая, что мы, разумеется, находимся на другом диске) и останавливаемся на бряке по адресу 0x7F6D01F972A:

    image

    Рядом с бряком находится вызов функции _wcsnicmp, используемой для сравнения указанного кол-ва байт в переданных ей строках:

    image

    Важно понимать, что, в отличие от x86, в x64 используется совершенно другой calling convention:

    The Microsoft x64 calling convention is followed on Microsoft Windows and pre-boot UEFI (for long mode on x86-64). It uses registers RCX, RDX, R8, R9 for the first four integer or pointer arguments (in that order), and XMM0, XMM1, XMM2, XMM3 are used for floating point arguments. Additional arguments are pushed onto the stack (right to left). Integer return values (similar to x86) are returned in RAX if 64 bits or less. Floating point return values are returned in XMM0. Parameters less than 64 bits long are not zero extended; the high bits are not zeroed

    В данном случае в качестве строковых аргументов функции _wcsnicmp передаются "/D" и "/D D:\books", а в регистре R8 хранится информация о том, сколько байт необходимо сравнивать (в данном случае 2). Разумеется, в этом случае в результате вызова функции _wcsnicmp в регистре EAX окажется ноль, что заставит программу перейти по адресу 0x7F6D01F97F2.

    Первое, что приходит на ум — это сделать данный переход безусловным (поменять инструкцию JE на JMP), заставив таким образом программу думать, что ей всегда был передан аргумент "/D". Давайте так и поступим. Нажимаем F9, перемещаемся в предыдущую директорию для единообразия исходных данных, вводим команду «CD D:\books» (обратите внимание на отсутствие ключа "/D"), выделяем строку с инструкцией je cmd.7F6D01F97F2, находящейся по адресу 0x7F6D01F9747, нажимаем пробел и меняем JE на JMP, не забывая поставить галочку рядом с надписью «Fill with NOP's»:

    image

    Снова нажимаем F9 и видим, что команда всё равно некорректно завершила свою работу, но уже, по крайней мере, не промолчала, как это было в прошлый раз:

    image

    Ставим бряк на JMP'е и занимаемся трассировкой. Сразу же после прыжка в регистр RCX попадает «урезанная» версия строки, которая хранится по адресу, указанному в регистре RBX. Если быть более точным, из неё «удаляются» первые два символа (два, потому что строки юникодовые, что можно было бы понять по сигнатуре функции _wcsnicmp и символу «L» перед строковыми литералами, в связи с чем на каждый из них требуется по два байта, а команда «обрезает» строку при помощи RBX+4):

    image

    Несложно догадаться, что делается это как раз для того, чтобы убрать из строки, содержащей интересующий программу путь до директории, ключ "/D", который и состоит из двух символов. Разумеется, нам этого делать уже не надо, т.к. теперь подобные действия будут «обрезать» часть пути до указанной пользователем директории. Что ж, заменим данную инструкцию на lea rcx, qword ptr ds:[rbx] (занопить её нельзя, т.к. в регистр RCX всё же должно попасть значение):

    image

    Снова вводим команду без указания ключа "/D", и… Видим, что переход в нужную директорию действительно осуществляется.

    Для того, чтобы сохранить проделанные нами изменения, открываем меню «Patches» при помощи Ctrl-P, проверяем, что выделены все необходимые изменения, нажимаем на кнопку «Patch File» и выбираем имя для пропатченной версии cmd.exe.

    К сожалению, даже если у нас получится заменить оригинальный cmd.exe из директории "%WINDIR%\system32" на пропатченный, Windows всё равно восстановит прежнюю исполняемого версию файла из кеша, так что сделайте отдельный ярлык для пропатченного бинарника и пользуйтесь им.

    Послесловие


    Порой даже мелочи могут сделать нашу жизнь проще и приятнее или, наоборот, лишь усугубить положение дел. Если Вы уже несколько раз споткнулись о подводный камень в виде недостающего флага "/D", то почему бы не взять в руки отладчик и не исправить эту ситуацию? Не забывайте, что баги и «исторические причины» встречаются сплошь и рядом, а править их разработчики намереваются далеко не всегда.

    Справедливости ради стоит отметить, что в PowerShell необходимость в указании ключа "/D" для команды CD всё же убрали.

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

    Подробнее
    Реклама
    Комментарии 38
    • +7
      Например, если вы находитесь в директории «C:\Windows\system32», то команда «CD D:\books» не сделает ровным счётом ничего.


      ну вообще-то это команда сменит путь, но на другом диске, просто после этого нужно ввести команду
      D:
      

      • +1
        Да, Вы правы, только всё равно таким решением пользоваться не очень удобно (можно ещё сначала переключиться на другой диск при помощи «D:», а потом ввести необходимую команду без ключа "/D").
        • 0
          Но в статье стоит поправить этот момент, ибо это не «ничего».
          d:
          cd books

          равноправно

          cd d:\books
          d:
      • +5
        А можно было просто написать свою команду, которая бы работала как нужно.
        • 0
          Что Вы имеете ввиду? Отдельную команду в бинарник для этого добавить? Но зачем?
          • +6
            Нет, например, поместить батник с таким содержимым:
            cd /D %1
            
            в PATH.

            Назвать его, например, zd.bat, тогда можно использовать команду «zd D:\books», которая сможет сменить диск. Название, конечно, другое, но так проще сделать.
            • +5
              Собирался погундеть о том, что дочерний процесс не может изменить cwd родителя, но потом осознал, что cmd не запускает батники в дочерних процессах.
              • 0
                Только тогда в .bat файлах придется вызывать команду zd через call zd…
                Иначе управление обратно не вернется.

                Все же выходит неполный аналог cd, если приходится запоминать особенности его вызова.
                • 0
                  > Иначе управление обратно не вернется
                  Вообще-то всё вернется, если в конец zd.cmd добавить ещё и Exit /B
                  Ну, и чтобы совсем правильно — %1 заменить на %*, мало ли — вдруг путь с пробелами будет, чтобы не брать в кавычки.
                  • 0
                    >Вообще-то всё вернется, если в конец zd.cmd добавить ещё и Exit /B
                    Неработает.

                    Например, запуск
                    1.bat
                    @echo off
                    echo 1.1
                    2.bat
                    echo 1.2


                    2.bat
                    @echo off
                    echo 2
                    Exit /B


                    имеет такой результат
                    C:\1\test>1.bat
                    1.1
                    2


                    >Ну, и чтобы совсем правильно — %1 заменить на %*, мало ли — вдруг путь с пробелами будет, чтобы не брать в кавычки.
                    Спасибо за полезную инфу.
          • НЛО прилетело и опубликовало эту надпись здесь
          • +46
            Задайте алиас. Делов-то.
            C:\> doskey cd=cd /D $*
            C:\> cd Y:\
            Y:\>
            
            • 0
              Тогда сломается «cd» без параметров. Которое, впрочем, эквивалентно команде «echo %cd%»
              • 0
                Хоть тема и старая, но оставлю как себе напоминалочку. Привыкшим к Линуксовой консоли лучше сделать вот так:
                doskey pwd=cd & doskey cd=cd /D $* & doskey ls=dir $*
                А ещё это можно «засейвить», чтобы оно всегда эпплаилось при запуске каждого консольного окна:
                [HKEY_CURRENT_USER\SOFTWARE\Microsoft\Command Processor]
                "AutoRun"="doskey pwd=cd & doskey cd=cd /D $* & doskey ls=dir $*"


                Соответственно тогда cd здесь работает именно так, как описано в статье, для cd без параметров есть pwd (аналогичная команда есть под Линуксом), ну а ls — это просто из Линукса привычка :)
            • –2
              Мне почему-то кажется, что количество калорий, потраченное на дебаг и пропатчивание cmd.exe, эквивалентно количеству калорий, которое будет потрачено, если руками вводить букву диска в течении двухсот лет. Я не ретроград, но всё-таки эта фича из тех, к которым проще привыкнуть, чем побороть. Через пару дней работы с cmd уже на автомате сначала вводишь букву диска в командной строке, а потом уже делаешь CD в нужный каталог.
              • +6
                Статья ценна тем что в ней показано как можно подправить программу, чтобы она делала то что требуется так как автору хочется. Конечно есть множество howto по работе с отладчиками, но многие узнают о том что так можно именно благодаря этой странице.
                • +1
                  Совершенно согласен. Работа с отладчиком на конкретном примере с конкретной целью — самое главное в данной статье
              • 0
                А чего вы его не выложили куда-нить?
                • +7
                  А Вы бы стали запускать такой бинарник? А если Вы бы полезли смотреть бинарный дифф, то одним бы им дело явно не обошлось — сырые байты без дизассемблирования вряд ли скажут Вам о чём-то конкретном. Ну, а с таким раскладом можно уже и проделать все те же действия, что были указаны в статье
                  • 0
                    сырые байты без дизассемблирования вряд ли скажут Вам о чём-то конкретном
                    Если это 90 90 90 90, то очень даже скажут. И ещё скажет объём такого диффа.
                    • +1
                      Да? Всё ведь зависит от контекста, в котором они написаны. Допустим, мы могли бы занопить такое место в бинарнике, которое бы позволило программе перейти на ветку кода, где выполняется отправка каких-то пользовательских данных на сервер или выключается компьютер. Последнее вполне реально в случае cmd.exe, в котором как раз имеется команда «shutdown»
                      • +1
                        Это несколько сложнее, нежели непосредственно внедрить вредоносный код, скажем, биткоин-майнера. Я всегда проверяю внешние бинари, особенно если приходится их патчить, но это не значит, что каждый раз я просматриваю весь листинг после дизассемблирования. Кстати, было бы интересно посмотреть на модификацию cmd.exe одними лишь нопами, которая бы отправляла данные о пользователе на заранее заданный сервер. :)
                        • +1
                          Это несколько сложнее, нежели непосредственно внедрить вредоносный код, скажем, биткоин-майнера

                          В случае выполнения команды «shutdown» вовсе необязательно. Вполне возможно, что в cmd.exe есть switch-case с определением очередной введённой пользователем команды, и, если занопить JMP'ы в нужных местах, можно как раз перейти не к обработке оригинальной команды, а к выполнению кода, находящегося в ветке команды «shutdown»

                          Кстати, было бы интересно посмотреть на модификацию cmd.exe одними лишь нопами, которая бы отправляла данные о пользователе на заранее заданный сервер

                          Конкретно к cmd.exe относился лишь второй пример:
                          Последнее вполне реально в случае cmd.exe, в котором как раз имеется команда «shutdown»
                          • +1
                            В случае выполнения команды «shutdown» вовсе необязательно.
                            В случае намеренного выполнения команды «shutdown» первый же комментарий здесь не позволил бы остальным запускать прогу, а Ваша карма была бы в значительной мере подпорчена.
                            • –1
                              Shutdown можно выполнять отложенный, чтобы не было сразу понятно, из-за чего он произошёл. Да и занопить можно попытаться так, чтобы реальная команда (например, CD в данном случае) тоже выполнялась
                              • +1
                                Ну, или придумать ещё десяток отговорок можно, конечно. Вариантов масса. Можно, например, nop-ами и нулями кодировать двоичный код, а затем дешифровать в памяти в рантайме.
                • 0
                  Первое, о чем подумал, начав читать: а нет ли переменной %CDCMD%, аналогичной %DIRCMD%?
                  • +4
                    Четверть века даже не задумывался, что описанное автором является «проблемой». Но за раскопки кода плюсую.
                    • +1
                      Скрытый текст
                      А я вообще давно отказался от дисков в пользу NTFS-папок… =)
                      • 0
                        Через mountvol? Интересно как у вас это реализовано, я когда-то делал себе батник, который монтировал диски в папки и скрывал диски, но как-то не прижилось.
                        • 0
                          Может это, конечно, не кошерно, но я просто пользуюсь оснасткой «Управление дисками» (diskmgmt.msc).
                        • 0
                          Стандартное управление дисками позволяет убрать букву диска и смонтировать раздел в папку.
                        • 0
                          А смысл в чем?
                        • 0
                          А еще можно пользоваться pushd вместо cd. Заодно решается проблема с UNC.
                          • +4
                            Все же статья не про избавление от исторических причин. Я не знаю, кто бы пользовался cmd.exe так активно, чтобы именно cd мешал жить. Больше пользуются или менеджерами типа FAR или powershell, где подобные вещи исправлены.

                            А если писать скрипты, то явно не стоит привязываться к тому, что они будут всегда выполняться в подпатченном cmd.exe
                            В общем, не для продакшена.
                            • 0
                              Исторических причин у него накопилось изрядное количество — достаточно вспомнить хотя бы то, как необходимо производить вставку и копирование в данный интерпретатор (ради справедливости стоит сказать, что в Windows 10 эту ситуацию наконец исправили, да и приложения наподобие ConEmu здорово в этом помогают).

                              А с каких пор в настройках cmd.exe можно установить галочки «Выделение мышью» и «Быстрая вставка»? Они решают проблему даже без необходимости запускать олю.

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.