Старая стотья про взлом институтской программы тестирования
Конец семестра приблизился как всегда незаметно. И, как водится, начались напряги со сдачей лабораторных работ по информатике. В семестре их всего 3 (надо сдать за 4 занятия), так что я с друзьями не особо сильно беспокоился о них и, в итоге, проспав первые 2 занятия и сдав одну на третьем, оказался в нехорошем положении, узнав о том, что преп свалил на конференцию, а 4ое занятие отменили. Оставалось ещё 1+1доп занятие с другой группой, на которых мне предстояло сдать 2 лабы. С нашим препом задача не из лёгких. Ну со второй я кое-как разобрался, а вот последняя представляла из себя программу, эмулирующую работу простейшей трёхадресной ЭВМ, поддерживающей команды ввода/вывода данных, арифметические операции и даже (!) условные переходы. По ходу выполнения работы студент должен пошагово выполнить несколько команд, указывая действия типа Чтения Счетчика Команд, Запись Адреса Команды на Шину Адреса и т.п. муть. Каждый тип команды (их 8) состоял из последовательности 8-15 операций, которые надо было по порядку запомнить. Шпора по этой лабе занимала лист тетрадного формата и легко палилась препом (из некоторых студенток он вытаскивал по 3 шпоры)) ), так что учить всё это желания не было, и я решил пойти другим путём. Результаты выполнения работы отображались на экране, надпись гласила о том, на каком месте программа завершила работу и о количестве допущенных студентом ошибок плюс общее кол-во ответов. Похожая табличка выводилась в лог проги. Зная, что прогу на компе в аудитории можно без проблем подменить, я составил адский план сдачи работы)).
В общем, мне нужно было поменять кол-во ошибок, но так, чтобы программа не вызвала подозрений. Продумав варианты, я решил поступить просто (может, это отдаёт ламерщиной, но мне некогда было играть в эстетов)) ): сделать так, чтобы в том случае, когда фамилия студента содержит точку, программа вне зависимости от истинного количества допущенных ошибок всегда выдавала приемлемое число (я решил поставить от 5 до 15), но в лог писала фамилию с отрезанной после точки части. Т.е. если фамилия “Kozlov.1988”, в лог программы будет записано лишь “Kozlov”. Для простых смертных (которые не знали о хитрости программы) она будет работать корректно и выдавать честные результаты.
Начнём, пожалуй)) Потрирая ручки, запускаем HIEW (юзался хиев версии 7.10), открываем файл lb9001.exe и видим, что программа написана под MS-DOS на Турбо Си, ничем не упакована и текстовые строки “Количество ошибок %4d”, присутствуют в первозданном виде. Теперь необходимо найти код, который это выводит. Загружаем исследуемую прогу в дизассемблер (IDA Pro 4.3.0), игнорируем сообщение о возможном оверлее на конце, и ждем завершения работы дизассемблера. К счастью, программа небольшая, и долго ждать не приходится, и мы попадаем в точку входа. Нас интересуют перекрёстные ссылки на строки с сообщением о кол-ве ошибок, поэтому жмём Alt-T и вводим нужную строку. Но.. видимо, IDA не смогла автоматически распознать ссылки на эти строки, поэтому нам необходимо будет найти их вручную. Для начала надо обнаружить сами строки. Они находятся в сегменте данных программы. Глядя в HIEW, смотрим адреса, по которым расположены строки. Учитывая, что базовый адрес загрузки равен 0x1000 и размер заголовка также 0x1000, а сама строка лежит по смещению 0x129CA от начала файла, нажимаеи G в IDA и вводим туда 219C:A (в формате сегмент:смещение) и видим искомую строку. Тут необходимо кое-что прояснить. Почему мы проводим такие странные вычисления? Дело в том, что досовские .exe-файлы устроены таким образом (в отличие от .com-программ), что они могут занимать больше одного сегмента памяти, и их местоположение до загрузки неизвестно. Загрузчик работает приблизительно так. Сначала он считывает заголовок программы, анализирует его, и, получив необходимую информацию (размер загружаемого модуля, размер заголовка), проецирует в память модуль. Причём заголовок в память не проецируется. Поэтому относительная адресация начинается с конца заголовка. То есть начало программы (её загружаемая часть) лежит обычно сразу после заголовка и загружается по неизвестному наперёд адресу, называемому базовым адресом загрузки. Как же происходит адресация переменных и кода, если адреса, по которым их загрузят, неизвестна на этапе генерации кода? Для решения этой проблемы в .exe-файлах была создана так называемая таблица перемещаемых элементов, которая представляет собой просто массив адресов, по которым загрузчик во время загрузки программы добавляет значение базового адреса загрузки. Это происходит до выполнения программы. Таким образом, адреса из относительных становятся абсолютными, и, когда настройка адресов завершается, загрузчик передаёт программе управление. Таким образом, чтобы определить смещение байта (строки) относительно базового адреса загрузки, необходимо из физического смещения элемента в файле (смещения от начала файла на диске) вычесть размер заголовка (незагружаемой части программы). Размер заголовка содержится в нём самом, см. поле Paragraphs in header (указано в параграфах, т.е. блоках по 16 байт (или 0x10) ). Поэтому, вычитая из 0x129CA размер заголовка (0x100 * 0x10), мы получаем адрес элемента относительно начала загружаемой части программы – 0x119CA. Если в IDA Pro при дизассемблировании установить значения Loading segment и Loading offset в 0, то адреса, вычисленные таким образом, совпадут. Если же оставить по умолчанию 0x1000, то IDA “загрузит” программу по этому базовому адресу, и тогда, чтобы получить адрес в IDA, нужно будет к полученному прибавить базовый адрес Loading segment : Loading offset. Аналогичная ситуация и с перемещаемыми элементами. IDA выступает в роли загрузчика и добавляет по адресам из таблицы relocations table свой базовый адрес загрузки, как это сделал бы и настоящий загрузчик операционной системы.
Итак, мы нашли строки в IDA, они выглядят приблизительно так:
a9 db '**** 9',0Ah,0 aPrerivanieObuc db ' Прерывание обучения.',0Ah,0 aS db ' %s',0Ah,0 aNomerPosledn_0 db ' Номер последнего задания %4d ',0Ah db ' Номер последнего такта %4d ',0Ah,0 aKolicestvoOs_0 db ' Количество ошибок %4d ',0Ah db ' Количество ответов %4d ',0Ah,0 aPrerivanieOb_0 db 'Прерывание обучения.',0Ah,0 aNomerPosledneg db 'Номер последнего задания %4d ',0Ah db ' Номер последнего такта %4d ',0Ah,0 aKolicestvoOsib db 'Количество ошибок %4d ',0Ah db ' Количество ответов %4d ',0Ah,0 aR db 'R',0 aS_0 db 'S',0 a3d db '%3d',0 |
теперь надо найти ссылки на них из кода. Для этого вбиваем в окно поиска (Alt-T) смещение строки aKolicestvoOs_0, равное 51E, и ищем места похожие на вывод. Вскоре натыкаемся на такое место:
push word ptr [bp+stream] ; stream call _fprintf add sp, 0Ch mov ax, [bp+var_48C] add ax, [bp+var_48E] push ax ; <-- количество ответов push [bp+var_48E] ; <-- количество ошибок push ds mov ax, 051Eh ; <-- " Количество ошибок %4d \ Количество ответов %4d" push ax ; заталкиваем указатель в стек push word ptr [bp+stream+2] push word ptr [bp+stream] ; File *hf call _fprintf add sp, 0Ch push word ptr [bp+stream+2] push word ptr [bp+stream] call _fclose |
Да это ведь не что иное как вывод строки в файл-лог! Смотрим листинг в окрестностях, и чуть ниже находим аналогичный код, но вместо fprintf там printf! Это, как уже вы наверное догадались, код, выводящий сообщение на экран. Всё! Нужный кусок кода мы локализовали, можно нажать Alt-M, добавить в IDA закладку, и приступить к анализу этого кода. В принципе, всё ясно даже при беглом осмотре. Функциям printf/fprintf передаётся форматная строка и значение количества ошибок, которое мы должны подменить своими, хе-хе!)) В общем, схема такая: мы дописываем кусочки кода, которые вызываюся вместо printf/fprintf и в зависимости от имени студента (с точкой или без), меняют в стеке перед вызовом функции printf количество ошибок. В результате printf выводит нужное нам значение, а программа продолжает работу. Аналогично поступаем и с fprintf. НО! Мы забыли что хотим сделать так, чтобы точка не фигурировала в логе, дабы, как говорит один мой знакомый, не палить контору. Для этого надо сделать перехват раньше чем имя будет выведено в лог. Смотрим листинг чуть выше.. А вот и вывод имени!
loc_15B1F: ; CODE XREF: _main+1516j push ss lea ax, [bp+var_40] push ax push ds mov ax, offset aS ; " %s\n" - эта строка и есть имя push ax ; format push word ptr [bp+stream+2] push word ptr [bp+stream] ; stream call _fprintf ; <-- сюда ставим jmp на свой код add sp, 0Ch push [bp+var_480] push [bp+var_482] push ds mov ax, offset aNomerPosledn_0 push ax ; format push word ptr [bp+stream+2] push word ptr [bp+stream] ; stream call _fprintf add sp, 0Ch mov ax, [bp+var_48C] add ax, [bp+var_48E] push ax push [bp+var_48E] push ds mov ax, offset aKolicestvoOs_0 push ax ; format push word ptr [bp+stream+2] push word ptr [bp+stream] ; stream |
Я сделал так, что код, который вызывался вместо fprintf(” %s\n”, user_name), выполнял проверку на наличие точки, если она присутствует, то записывал вместо него нулевой байт (строка таким образом срезалась, оставляя лишь часть, которая была ДО точки) и проставлял переменную-флаг в 1 и заодно вычислял случайное значение количества ошибок (от 5 до 15), тоже записвая в глобальную переменную прямо в коде. Тот код, который перехватывал бы вывод в файл и на экран, работал бы в зависимости от состояния флага. Если он равен 0, то количество ошибок не менялось, а если 1, то код менял бы параметр функции printf и выводилось бы нужное значение. Хотя можно было поступить проще. Так как адрес переменной, содержащей кол-во ошибок, фиксирован, то можно было не делать трёх перехватов, а в первом же просто поменять значение [bp+var_48E])). Но.. мне показалось так эстетичнее). Сейчас я покажу первый способ, ибо я реализовал именно его.
Таким образом, нам надо написать код процедуры-перехватчика и поставить на него jmp вместо вызова fprintf или до него, чтобы потом из перехватчика вернуть управление на следующую команду (тоже jmp). Для начала необходимо найти свободное место, куда записать наш зловредный код. К счастью, в программе оказалось довольно много неиспользуемых RTL-функций, типа unixtodos(). На неё есть только 1 перекрёстная ссылка, да и то из подпрограммы stime(), на которую уже ссылок нет. Значит, с большой вероятностью можно забить тело функций своим кодом, не нанеся программе вреда. Конечно, эстетичнее было бы дописать кусок кода в конец файла, но в данном случае это не критично. Итак, забиваем всю функцию командами nop (опкод – 0x90). Запускаем,.. всё работает на первый взгляд нормально. Ладно, продолжим. Я не буду описывать детально, как я ассемблировал нужный код, просто покажу результат с некоторыми комментариями и пояснениями.
В общем, по смещению 0x10E43 (физическое смещение 0x11E43) мы видим следующую картину:
Обратите внимание на выделенные другим цветом слова. Это – как раз те самые перемещаемые элементы. Эти байты обладают интересной особенностью: на них нельзя писать выполнимый код, так как (см. выше процесс загрузки) при загрузке их значение изменяется загрузчиком, и код, написанный поверх них, становится неработоспособным. Надо либо удалить ссылку из relocations table на это слово, либо обойти код, сделав jmp short или какой-нить другой хентайсткий изврат)).
Листинг написанного кода (всё делалось наспех, так что не судите слишком строго):
; обработчик printf (начинается с адреса 00010E46) 00010E43: 0000 add [bx][si],al 00010E45: 90 nop 00010E46: EAC55B0000 jmp 00000:05BC5 ; возврат к исходному printf 00010E4B: 60 pusha ; сюда происходит переход из программы (jmp вместо printf) 00010E4C: 9C pushf 00010E4D: 90 nop 00010E4E: 90 nop 00010E4F: 90 nop 00010E50: 9D popf 00010E51: 61 popa 00010E52: 2EA19400 mov ax,cs:[0094] ; проверяем флаг того, что студент-читер)) 00010E56: 85C0 test ax,ax 00010E58: 7433 je 000010E8D 00010E5A: 2EA19600 mov ax,cs:[0096] ; читаем сколько ошибок поставить читеру 00010E5E: 55 push bp 00010E5F: 8BEC mov bp,sp 00010E61: 894606 mov [bp][06],ax ; изменяем параметр кол-ва ошибок 00010E64: 5D pop bp 00010E65: EB26 jmps 000010E8D ; переход к вызову оригинального printf'a ; а это уже интерсептор самого первого fprintf'a (который выводит имя) ; здесь вызывается процедура, которая делает проверку фамилии на наличие точки ; и заполняющей переменные 00010E67: EA365B0000 jmp 00000:05B36 ; переход к ориг. коду 00010E6C: 60 pusha 00010E6D: 9C pushf 00010E6E: E88800 call 000010EF9 ; вызов основной процедуры 00010E71: 9D popf 00010E72: 61 popa 00010E73: 90 nop 00010E74: 9A0C00B00F call 00FB0:0000C ; вызов оригинального fprintf'a 00010E79: EBEC jmps 000010E67 ; кусок обработчика printf (который сверху) 00010E8D: 9A0BEF0000 call 00000:0EF0B ; вызов ориг. printf 00010E92: EBB2 jmps 000010E46 ; возврат в программу ; переменные)) 00010E94: 0000 add [bx][si],al ; ! это переменная-флаг 00010E96: 0000 add [bx][si],al ; ! а здесь лежит кол-во ошибок ; перехватчик второго fprintf'a ; проверяет значение флага cs:[0094], и, если равно 1, заменяет параметр fprintf'a 00010EA5: EA755B0000 jmp 00000:05B75 00010EAA: 2EA19400 mov ax,cs:[0094] 00010EAE: 85C0 test ax,ax 00010EB0: 740B je 000010EBD 00010EB2: 2EA19600 mov ax,cs:[0096] 00010EB6: 55 push bp 00010EB7: 8BEC mov bp,sp 00010EB9: 89460A mov [bp][0A],ax ; изменяем параметр кол-ва ошибок 00010EBC: 5D pop bp 00010EBD: 9A0C00B00F call 00FB0:0000C ; вызов fprintf 00010EC2: EBE1 jmps 000010EA5 ; возврат к оригинальному коду ; эта подпрограмма вызывается в самом первом перехватчике (см 00010E6E) ; именно здесь устанавливается переменная по адресу cs:[0094], которая равна 1 если имя содержит точку ; здесь же генерируется случайное число от 5 до 15 00010EF9: 8D76C0 lea si,[bp][-40] ; загружаем в ds:si адрес имени (фамилии) 00010EFC: 1E push ds 00010EFD: 33C0 xor ax,ax 00010EFF: 2EA39400 mov cs:[0094],ax ; обнуляем переменные 00010F03: 2EA39600 mov cs:[0096],ax 00010F07: 16 push ss 00010F08: 1F pop ds 00010F09: F9 stc 00010F0A: AC lodsb 00010F0B: 84C0 test al,al 00010F0D: 7408 je 000010F17 00010F0F: 3C2E cmp al,02E ;\".\" 00010F11: 75F7 jne 000010F0A 00010F13: 4E dec si 00010F14: C60400 mov b,[si],000 00010F17: 90 nop 00010F18: 2EA39400 mov cs:[0094],ax 00010F1C: 1F pop ds 00010F1D: 0F31 rdtsc 00010F1F: C1C808 ror ax,008 ;\" \" 00010F22: 250F00 and ax,0000F ;\" \" 00010F25: EB09 jmps 000010F30 ; а это похоже мусор, оставшийся от кодирования 00010F27: 90 nop 00010F28: 90 nop 00010F29: 90 nop 00010F2A: 9A18007E10 call 0107E:00018 00010F2F: C3 retn ; конец процедуры 00010F30: 3D0400 cmp ax,00004 ; если <= 4, добавим 4 (а то слишком мало) 00010F33: 7703 ja 000010F38 00010F35: 050400 add ax,00004 00010F38: 2EA39600 mov cs:[0096],ax 00010F3C: C3 retn |
Здесь важно только то, что все межсегментные переходы должны иметь перемещаемые сегментные составляющие (последние 2 байта команд call far или jmp far должны быть перемещаемыми, чтобы загрузчик собрал файл корректно). Чтобы не добавлять новые входы в relocations table, я иногда подгонял адреса переходов. Но в одном месте мне все-таки пришлось добавить чтобы не так сильно уродовать код (он и так элегантностью не отличается)) ) это было в месте перехода 10E67. Как добавить новый элемент в таблицу перемещений? Очень просто. Берём адрем элемента (в данном случае 10E6A, последние 2 байта команды), переписываем адрес в формате сегмент:смещение в обратном порядке следования байт (получается 10E0:006A -> 6A 00 E0 10) и прописываем эти байтики в конец relocations table (конец таблицы легко находится из заголовка, там есть поля, показывающие начало таблицы, её размер). И нельзя забывать о том, чтобы сделать ПЛЮСАДИН в размер таблицы перемещения)). Всё! если сделали всё правильно, нажимаем Alt-F4 (HIEW->Reload) и HIEW послушно отобразит новый перемещаемый элемент, выделив его другим цветом (как на скриншоте).
При пректировании такого кода необходимо очень тщательно следить за изменением регистров, особенно сегментных, а также ни в коем случае не допускать разбалансировки стека. Оба этих побочных эффекта могут убить программу на месте. Ну и конечно, с отладкой тоже возникают проблемы. К примеру, этот код я писал без отладчика, ибо Turbo Debugger слетал с этой программы, а SoftICE на виртуальную машину мне было ставить лень.
В общем, программа работала теперь так как надо и я пошёл сдавать. Сел за монитор, достал флешу, незаметно заменил программу и, зажав ENTER на ответах, за минуту сдал лабу. Наш премудрый ассистент был слишком занят вытаскиванием шпаргалок из-под юбок студенток, поэтому я не вызвал подозрений.
Файло прилагается:
Скачать архив с исходной программой
Скачать архив с пропатченной лабой
by Elw00d, © 2007
4