Сфера разработки программного обеспечения является одной из тех областей человеческой деятельности, где термин «исторические причины» используется наиболее часто. Оно и понятно — многие «долгоиграющие» проекты наподобие ядер различных операционных систем, браузеров и прочего обросли за время своего существования нехилым арсеналом вещей, менять поведение которых станет далеко не каждый, даже если перфекционист внутри разработчика говорит обратное. Вероятнее всего, большая часть кода была написана программистами, которые уже давно не работают в компании, а даже те, кто ещё связывают свои жизни с данной корпорацией, сомневаются, что остальные компоненты программного комплекса нормально отрерагируют на те или иные изменения. «Нет уж, лучше оставлю, как оно есть».
В качестве примера одной из таких вещей можно назвать 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, запускаем его и загружаем в него исследуемый нами исполняемый файл:
Нажимаем F9 до тех пор, пока программа не перестанет останавливаться на брейкпоинтах (приятно, что очень многие хоткеи из OllyDbg работают и тут), делаем right-click по содержимому окна CPU -> Search for -> String references и ищем строку "/D":
Ставим на каждую из них по бряку при помощи F2, вводим в окно запущенного процесса cmd.exe команду «CD /D D:\books» (предполагая, что мы, разумеется, находимся на другом диске) и останавливаемся на бряке по адресу 0x7F6D01F972A:
Рядом с бряком находится вызов функции _wcsnicmp, используемой для сравнения указанного кол-ва байт в переданных ей строках:
Важно понимать, что, в отличие от 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»:
Снова нажимаем F9 и видим, что команда всё равно некорректно завершила свою работу, но уже, по крайней мере, не промолчала, как это было в прошлый раз:
Ставим бряк на JMP'е и занимаемся трассировкой. Сразу же после прыжка в регистр RCX попадает «урезанная» версия строки, которая хранится по адресу, указанному в регистре RBX. Если быть более точным, из неё «удаляются» первые два символа (два, потому что строки юникодовые, что можно было бы понять по сигнатуре функции _wcsnicmp и символу «L» перед строковыми литералами, в связи с чем на каждый из них требуется по два байта, а команда «обрезает» строку при помощи RBX+4):
Несложно догадаться, что делается это как раз для того, чтобы убрать из строки, содержащей интересующий программу путь до директории, ключ "/D", который и состоит из двух символов. Разумеется, нам этого делать уже не надо, т.к. теперь подобные действия будут «обрезать» часть пути до указанной пользователем директории. Что ж, заменим данную инструкцию на lea rcx, qword ptr ds:[rbx] (занопить её нельзя, т.к. в регистр RCX всё же должно попасть значение):
Снова вводим команду без указания ключа "/D", и… Видим, что переход в нужную директорию действительно осуществляется.
Для того, чтобы сохранить проделанные нами изменения, открываем меню «Patches» при помощи Ctrl-P, проверяем, что выделены все необходимые изменения, нажимаем на кнопку «Patch File» и выбираем имя для пропатченной версии cmd.exe.
К сожалению, даже если у нас получится заменить оригинальный cmd.exe из директории "%WINDIR%\system32" на пропатченный, Windows всё равно восстановит прежнюю исполняемого версию файла из кеша, так что сделайте отдельный ярлык для пропатченного бинарника и пользуйтесь им.
Послесловие
Порой даже мелочи могут сделать нашу жизнь проще и приятнее или, наоборот, лишь усугубить положение дел. Если Вы уже несколько раз споткнулись о подводный камень в виде недостающего флага "/D", то почему бы не взять в руки отладчик и не исправить эту ситуацию? Не забывайте, что баги и «исторические причины» встречаются сплошь и рядом, а править их разработчики намереваются далеко не всегда.
Справедливости ради стоит отметить, что в PowerShell необходимость в указании ключа "/D" для команды CD всё же убрали.
Спасибо за внимание, и снова надеюсь, что статья оказалась кому-нибудь полезной.
Комментарии (37)
stepik777
23.06.2015 22:08+5А можно было просто написать свою команду, которая бы работала как нужно.
NikitaTrophimov Автор
23.06.2015 22:10Что Вы имеете ввиду? Отдельную команду в бинарник для этого добавить? Но зачем?
stepik777
23.06.2015 22:17+6Нет, например, поместить батник с таким содержимым:
в PATH.cd /D %1
Назвать его, например, zd.bat, тогда можно использовать команду «zd D:\books», которая сможет сменить диск. Название, конечно, другое, но так проще сделать.hardex
23.06.2015 23:38+5Собирался погундеть о том, что дочерний процесс не может изменить cwd родителя, но потом осознал, что cmd не запускает батники в дочерних процессах.
Alexsmt
24.06.2015 03:39Только тогда в .bat файлах придется вызывать команду zd через call zd…
Иначе управление обратно не вернется.
Все же выходит неполный аналог cd, если приходится запоминать особенности его вызова.Olanonymous
24.06.2015 17:22> Иначе управление обратно не вернется
Вообще-то всё вернется, если в конец zd.cmd добавить ещё и Exit /B
Ну, и чтобы совсем правильно — %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 заменить на %*, мало ли — вдруг путь с пробелами будет, чтобы не брать в кавычки.
Спасибо за полезную инфу.
ntfs1984
23.06.2015 22:20+1Ну вообще это жуткое легаси, тянущееся еще с DOS…
NikitaTrophimov Автор
24.06.2015 10:55Да, об этом и говорится в статье, ссылку на которую я привёл в тексте
DrPass
23.06.2015 22:45-2Мне почему-то кажется, что количество калорий, потраченное на дебаг и пропатчивание cmd.exe, эквивалентно количеству калорий, которое будет потрачено, если руками вводить букву диска в течении двухсот лет. Я не ретроград, но всё-таки эта фича из тех, к которым проще привыкнуть, чем побороть. Через пару дней работы с cmd уже на автомате сначала вводишь букву диска в командной строке, а потом уже делаешь CD в нужный каталог.
k0ldbl00d
23.06.2015 23:13+6Статья ценна тем что в ней показано как можно подправить программу, чтобы она делала то что требуется так как автору хочется. Конечно есть множество howto по работе с отладчиками, но многие узнают о том что так можно именно благодаря этой странице.
maisvendoo
23.06.2015 23:20+1Совершенно согласен. Работа с отладчиком на конкретном примере с конкретной целью — самое главное в данной статье
Psychosynthesis
23.06.2015 23:13А чего вы его не выложили куда-нить?
NikitaTrophimov Автор
23.06.2015 23:39+7А Вы бы стали запускать такой бинарник? А если Вы бы полезли смотреть бинарный дифф, то одним бы им дело явно не обошлось — сырые байты без дизассемблирования вряд ли скажут Вам о чём-то конкретном. Ну, а с таким раскладом можно уже и проделать все те же действия, что были указаны в статье
Sadler
24.06.2015 10:13сырые байты без дизассемблирования вряд ли скажут Вам о чём-то конкретном
Если это 90 90 90 90, то очень даже скажут. И ещё скажет объём такого диффа.NikitaTrophimov Автор
24.06.2015 10:52+1Да? Всё ведь зависит от контекста, в котором они написаны. Допустим, мы могли бы занопить такое место в бинарнике, которое бы позволило программе перейти на ветку кода, где выполняется отправка каких-то пользовательских данных на сервер или выключается компьютер. Последнее вполне реально в случае cmd.exe, в котором как раз имеется команда «shutdown»
Sadler
24.06.2015 11:06+1Это несколько сложнее, нежели непосредственно внедрить вредоносный код, скажем, биткоин-майнера. Я всегда проверяю внешние бинари, особенно если приходится их патчить, но это не значит, что каждый раз я просматриваю весь листинг после дизассемблирования. Кстати, было бы интересно посмотреть на модификацию cmd.exe одними лишь нопами, которая бы отправляла данные о пользователе на заранее заданный сервер. :)
NikitaTrophimov Автор
24.06.2015 11:13+1Это несколько сложнее, нежели непосредственно внедрить вредоносный код, скажем, биткоин-майнера
В случае выполнения команды «shutdown» вовсе необязательно. Вполне возможно, что в cmd.exe есть switch-case с определением очередной введённой пользователем команды, и, если занопить JMP'ы в нужных местах, можно как раз перейти не к обработке оригинальной команды, а к выполнению кода, находящегося в ветке команды «shutdown»
Кстати, было бы интересно посмотреть на модификацию cmd.exe одними лишь нопами, которая бы отправляла данные о пользователе на заранее заданный сервер
Конкретно к cmd.exe относился лишь второй пример:
Последнее вполне реально в случае cmd.exe, в котором как раз имеется команда «shutdown»
Sadler
24.06.2015 11:17+1В случае выполнения команды «shutdown» вовсе необязательно.
В случае намеренного выполнения команды «shutdown» первый же комментарий здесь не позволил бы остальным запускать прогу, а Ваша карма была бы в значительной мере подпорчена.NikitaTrophimov Автор
24.06.2015 11:36-1Shutdown можно выполнять отложенный, чтобы не было сразу понятно, из-за чего он произошёл. Да и занопить можно попытаться так, чтобы реальная команда (например, CD в данном случае) тоже выполнялась
Sadler
24.06.2015 11:41+1Ну, или придумать ещё десяток отговорок можно, конечно. Вариантов масса. Можно, например, nop-ами и нулями кодировать двоичный код, а затем дешифровать в памяти в рантайме.
Vapaamies
24.06.2015 10:20Первое, о чем подумал, начав читать: а нет ли переменной %CDCMD%, аналогичной %DIRCMD%?
rw6hrm
24.06.2015 11:07+4Четверть века даже не задумывался, что описанное автором является «проблемой». Но за раскопки кода плюсую.
elanc
24.06.2015 12:28+1Скрытый текстА я вообще давно отказался от дисков в пользу NTFS-папок… =)Ajex
24.06.2015 12:54Через mountvol? Интересно как у вас это реализовано, я когда-то делал себе батник, который монтировал диски в папки и скрывал диски, но как-то не прижилось.
elanc
24.06.2015 13:04Может это, конечно, не кошерно, но я просто пользуюсь оснасткой «Управление дисками» (diskmgmt.msc).
elanc
24.06.2015 13:09Если вдруг кому стало интересно и кто ещё не знает «как» — support.microsoft.com/en-us/kb/323424/ru
goletsa
24.06.2015 13:24Стандартное управление дисками позволяет убрать букву диска и смонтировать раздел в папку.
saboteur_kiev
24.06.2015 17:58+4Все же статья не про избавление от исторических причин. Я не знаю, кто бы пользовался cmd.exe так активно, чтобы именно cd мешал жить. Больше пользуются или менеджерами типа FAR или powershell, где подобные вещи исправлены.
А если писать скрипты, то явно не стоит привязываться к тому, что они будут всегда выполняться в подпатченном cmd.exe
В общем, не для продакшена.
DarkByte
30.06.2015 09:46Исторических причин у него накопилось изрядное количество — достаточно вспомнить хотя бы то, как необходимо производить вставку и копирование в данный интерпретатор (ради справедливости стоит сказать, что в Windows 10 эту ситуацию наконец исправили, да и приложения наподобие ConEmu здорово в этом помогают).
А с каких пор в настройках cmd.exe можно установить галочки «Выделение мышью» и «Быстрая вставка»? Они решают проблему даже без необходимости запускать олю.
Geograph
ну вообще-то это команда сменит путь, но на другом диске, просто после этого нужно ввести команду
NikitaTrophimov Автор
Да, Вы правы, только всё равно таким решением пользоваться не очень удобно (можно ещё сначала переключиться на другой диск при помощи «D:», а потом ввести необходимую команду без ключа "/D").
saboteur_kiev
Но в статье стоит поправить этот момент, ибо это не «ничего».
d:
cd books
равноправно
cd d:\books
d: