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 всё же убрали.

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

Комментарии (37)


  1. Geograph
    23.06.2015 21:11
    +7

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


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


    1. NikitaTrophimov Автор
      23.06.2015 21:20
      +1

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


      1. saboteur_kiev
        24.06.2015 17:44

        Но в статье стоит поправить этот момент, ибо это не «ничего».
        d:
        cd books

        равноправно

        cd d:\books
        d:


  1. stepik777
    23.06.2015 22:08
    +5

    А можно было просто написать свою команду, которая бы работала как нужно.


    1. NikitaTrophimov Автор
      23.06.2015 22:10

      Что Вы имеете ввиду? Отдельную команду в бинарник для этого добавить? Но зачем?


      1. stepik777
        23.06.2015 22:17
        +6

        Нет, например, поместить батник с таким содержимым:

        cd /D %1
        
        в PATH.

        Назвать его, например, zd.bat, тогда можно использовать команду «zd D:\books», которая сможет сменить диск. Название, конечно, другое, но так проще сделать.


        1. hardex
          23.06.2015 23:38
          +5

          Собирался погундеть о том, что дочерний процесс не может изменить cwd родителя, но потом осознал, что cmd не запускает батники в дочерних процессах.


        1. Alexsmt
          24.06.2015 03:39

          Только тогда в .bat файлах придется вызывать команду zd через call zd…
          Иначе управление обратно не вернется.

          Все же выходит неполный аналог cd, если приходится запоминать особенности его вызова.


          1. Olanonymous
            24.06.2015 17:22

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


            1. Alexsmt
              24.06.2015 19:56

              >Вообще-то всё вернется, если в конец 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 заменить на %*, мало ли — вдруг путь с пробелами будет, чтобы не брать в кавычки.
              Спасибо за полезную инфу.


  1. ntfs1984
    23.06.2015 22:20
    +1

    Ну вообще это жуткое легаси, тянущееся еще с DOS…


    1. NikitaTrophimov Автор
      24.06.2015 10:55

      Да, об этом и говорится в статье, ссылку на которую я привёл в тексте


  1. bolk
    23.06.2015 22:21
    +46

    Задайте алиас. Делов-то.

    C:\> doskey cd=cd /D $*
    C:\> cd Y:Y:\>
    


    1. nickolaym
      24.06.2015 15:04

      Тогда сломается «cd» без параметров. Которое, впрочем, эквивалентно команде «echo %cd%»


  1. DrPass
    23.06.2015 22:45
    -2

    Мне почему-то кажется, что количество калорий, потраченное на дебаг и пропатчивание cmd.exe, эквивалентно количеству калорий, которое будет потрачено, если руками вводить букву диска в течении двухсот лет. Я не ретроград, но всё-таки эта фича из тех, к которым проще привыкнуть, чем побороть. Через пару дней работы с cmd уже на автомате сначала вводишь букву диска в командной строке, а потом уже делаешь CD в нужный каталог.


    1. k0ldbl00d
      23.06.2015 23:13
      +6

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


      1. maisvendoo
        23.06.2015 23:20
        +1

        Совершенно согласен. Работа с отладчиком на конкретном примере с конкретной целью — самое главное в данной статье


  1. Psychosynthesis
    23.06.2015 23:13

    А чего вы его не выложили куда-нить?


    1. NikitaTrophimov Автор
      23.06.2015 23:39
      +7

      А Вы бы стали запускать такой бинарник? А если Вы бы полезли смотреть бинарный дифф, то одним бы им дело явно не обошлось — сырые байты без дизассемблирования вряд ли скажут Вам о чём-то конкретном. Ну, а с таким раскладом можно уже и проделать все те же действия, что были указаны в статье


      1. Sadler
        24.06.2015 10:13

        сырые байты без дизассемблирования вряд ли скажут Вам о чём-то конкретном
        Если это 90 90 90 90, то очень даже скажут. И ещё скажет объём такого диффа.


        1. NikitaTrophimov Автор
          24.06.2015 10:52
          +1

          Да? Всё ведь зависит от контекста, в котором они написаны. Допустим, мы могли бы занопить такое место в бинарнике, которое бы позволило программе перейти на ветку кода, где выполняется отправка каких-то пользовательских данных на сервер или выключается компьютер. Последнее вполне реально в случае cmd.exe, в котором как раз имеется команда «shutdown»


          1. Sadler
            24.06.2015 11:06
            +1

            Это несколько сложнее, нежели непосредственно внедрить вредоносный код, скажем, биткоин-майнера. Я всегда проверяю внешние бинари, особенно если приходится их патчить, но это не значит, что каждый раз я просматриваю весь листинг после дизассемблирования. Кстати, было бы интересно посмотреть на модификацию cmd.exe одними лишь нопами, которая бы отправляла данные о пользователе на заранее заданный сервер. :)


            1. NikitaTrophimov Автор
              24.06.2015 11:13
              +1

              Это несколько сложнее, нежели непосредственно внедрить вредоносный код, скажем, биткоин-майнера

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

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

              Конкретно к cmd.exe относился лишь второй пример:
              Последнее вполне реально в случае cmd.exe, в котором как раз имеется команда «shutdown»


              1. Sadler
                24.06.2015 11:17
                +1

                В случае выполнения команды «shutdown» вовсе необязательно.
                В случае намеренного выполнения команды «shutdown» первый же комментарий здесь не позволил бы остальным запускать прогу, а Ваша карма была бы в значительной мере подпорчена.


                1. NikitaTrophimov Автор
                  24.06.2015 11:36
                  -1

                  Shutdown можно выполнять отложенный, чтобы не было сразу понятно, из-за чего он произошёл. Да и занопить можно попытаться так, чтобы реальная команда (например, CD в данном случае) тоже выполнялась


                  1. Sadler
                    24.06.2015 11:41
                    +1

                    Ну, или придумать ещё десяток отговорок можно, конечно. Вариантов масса. Можно, например, nop-ами и нулями кодировать двоичный код, а затем дешифровать в памяти в рантайме.


  1. Vapaamies
    24.06.2015 10:20

    Первое, о чем подумал, начав читать: а нет ли переменной %CDCMD%, аналогичной %DIRCMD%?


  1. rw6hrm
    24.06.2015 11:07
    +4

    Четверть века даже не задумывался, что описанное автором является «проблемой». Но за раскопки кода плюсую.


  1. elanc
    24.06.2015 12:28
    +1

    Скрытый текст
    А я вообще давно отказался от дисков в пользу NTFS-папок… =)


    1. Ajex
      24.06.2015 12:54

      Через mountvol? Интересно как у вас это реализовано, я когда-то делал себе батник, который монтировал диски в папки и скрывал диски, но как-то не прижилось.


      1. elanc
        24.06.2015 13:04

        Может это, конечно, не кошерно, но я просто пользуюсь оснасткой «Управление дисками» (diskmgmt.msc).


        1. elanc
          24.06.2015 13:09

          Если вдруг кому стало интересно и кто ещё не знает «как» — support.microsoft.com/en-us/kb/323424/ru


      1. goletsa
        24.06.2015 13:24

        Стандартное управление дисками позволяет убрать букву диска и смонтировать раздел в папку.


    1. Ununtrium
      25.06.2015 08:20

      А смысл в чем?


  1. PoliTeX
    24.06.2015 15:53

    А еще можно пользоваться pushd вместо cd. Заодно решается проблема с UNC.


  1. saboteur_kiev
    24.06.2015 17:58
    +4

    Все же статья не про избавление от исторических причин. Я не знаю, кто бы пользовался cmd.exe так активно, чтобы именно cd мешал жить. Больше пользуются или менеджерами типа FAR или powershell, где подобные вещи исправлены.

    А если писать скрипты, то явно не стоит привязываться к тому, что они будут всегда выполняться в подпатченном cmd.exe
    В общем, не для продакшена.


  1. DarkByte
    30.06.2015 09:46

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

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