Pull to refresh

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

Reading time 5 min
Views 47K
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 всё же убрали.

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

Articles