Старая стотья про взлом институтской программы тестирования

Written by elwood

Конец семестра приблизился как всегда незаметно. И, как водится, начались напряги со сдачей лабораторных работ по информатике. В семестре их всего 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) мы видим следующую картину:

hiew-screen

Обратите внимание на выделенные другим цветом слова. Это – как раз те самые перемещаемые элементы. Эти байты обладают интересной особенностью: на них нельзя писать выполнимый код, так как (см. выше процесс загрузки) при загрузке их значение изменяется загрузчиком, и код, написанный поверх них, становится неработоспособным. Надо либо удалить ссылку из 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