Решаем тесты безупречно в MyTestX
Мы сидели с друзьями на зимней сессии и готовились к экзамену по основам алгоритмизации и программирования. Экзамен состоял из двух частей: тестовая часть и практика. Тестирование у нас обычно проводиться с использованием MyTestX. Это примитивное ПО для проведения компьютерного тестирования. Тем не менее почти на любом экзамене или зачете у нас используется именно эта программа.
Интерфейс клиента MyTextX
И тут один из моих друзей говорит: «А давай взломаем MyTest». Мысли о взломе различного ПО у нас возникали часто, но обычно в виде шутки. Но в данном случае я подумал, а почему бы и нет? Вряд ли разработчик при создании своего творения озаботился о безопасности или обфусцировал свой код. Под взломом я подразумеваю изменение некоторых байтиков в программе, чтобы проходить тесты на высокие баллы. Как окажется в дальнейшем, я был прав, и мне удастся довольно просто сделать так, чтобы результаты моих тестов были безупречными.
Обзор MyTestX
MyTestX — довольная старая программа для компьютерного тестирования.
В обычный дистрибутив входят 3 исполняемых файла:
MyTestStudent — клиентское приложение,
MyTestServer — серверное приложение,
MyTestEditor — утилита для создания и редактирования тестов.
Все тесты сохраняются в формате .mtx
. Их можно как отдельно открыть в клиенте, так и раздать с сервера. Меня интересует вариант с сервером, так как у нас все тесты проходят именно в таком режиме.
Для коммуникации между клиентом и сервером существует собственный протокол. Один умелец уже проделал работу по анализу протокола и написал программу для дампа тестов в формате .mtx
с сервера. Открыв их в редакторе, можно было увидеть все верные ответы. Также тесты могут быть запаролены, для чего этот же человек написал распароливатель.
Меня не устроил такой вариант, так как я хотел прямо из клиента пройти тест на высокий балл, а не заучивать верные варианты из тестового файла. Поэтому я приступил к своему анализу.
Цели которые я для себя поставил:
Первый вариант в вопросах с одиночным выбором всегда правильный,
Во всех остальных вопросах любой выбор правильный,
Вырезать монопольный режим (программа открывается во весь экран без возможности свернуть её).
Взлом клиента
Первым делом я решил найти функцию проверки верного ответа. Для этого мне надо было найти главный цикл программы.
У MyTestX есть одна особенность. При определенных настройках в файле теста, после ответа на вопрос будут выведены правильные варианты, а так же появиться окно с результатом. Выглядит это вот так:
Новое окно в MyTestX
Нас интересует именно окно, так как оно создаётся при помощи функции MessageBoxW
. Поставив бряк на эту функцию мы можем попробовать найти главный цикл. После установки бряка, я вышел не несколько функций вверх и попал в очень большую функцию проверки корректности ответа.
Коротко, в ней есть огромный switch
, который в зависимости от типа вопрос (с одним выбором, с несколькими выборами, сопоставление и т.д), проверяет его корректность. А также в этой функции есть булевая переменная, отвечающая за финальный вердикт. Эта переменная и станет нашей целью.
jump-таблица для switch
На этом этапе я начал делать первые правки. По умолчанию, переменная, с вердиктом по вопросу (var_correct_answer
) ставиться в единицу, и только потом, в кейсах, меняется на ноль.
Кейс, с проверкой вопросов с одним вариантом ответа. var_correct_answer
ставиться в 0
Поэтому я решил в тупую убрать все обнуления переменной. Для этого мне надо было просто поменять один байт во всех присвоениях. А так как смещение относительно ebp
у данной переменной одинаковое в рамках данной функции я просто поменял все байты c6 45 b7 00
на c6 45 b7 01
во всём файле.
Присвоение нуля. mov [ebp+var_correct_answer], 0
соответствует байтам c6 45 b7 00
После изменения этих байтов любой ответ считался верным, и все тесты проходились на 100%, какой бы вариант ответа я бы не выбирал. Это также работало в связке с сервером, так как клиент посылает только конечный результат на сервер.
Я бы мог тут остановиться, но проходить все тесты на 100% это как-то подозрительно, поэтому я решил сделать так, чтобы только первый вариант ответа в вопросах с одиночным выбором всегда был верный.
Делаем верным только первый вариант
Чтобы сделать только первый вариант верным, мне нужно было модифицировать алгоритм проверки верности. Алгоритм работает следующем образом:
В регистре
ebx
храниться номер текущего варианта ответа (в более новых версиях номер храниться в стеке),В регистре
eax
храниться 0 или 1, в зависимости от того, верный ли вариант под номеромebx
,В регистре
edx
храниться 0 или 1, в зависимости от того, выбрал ли пользователь вариантebx
,В цикле увеличивается значение
ebx
и перебираются все варианты ответа.
При нормальной работе программы, в цикле сравниваются значения регистров dl
и al
и если они различаются (пользователь выбрал неверный вариант ответа), var_correct_answer
ставиться в 0.
Участок кода, который нам интересен
Чтобы сделать только первый вариант верный, я заменил условный переход jz
на jne
и сравнение cmp dl, al
на cmp dl, bl
(в более новых версиях пришлось убрать вызов функции func_check_if_correct
, и заменить его на перемещение счетчика из стека в регистр). Изменив эти инструкции, мне удалось добиться желаемого результата — верным считается только первый вариант ответа. Это привело к довольно забавным последствиям в некоторых случаях.
Последствия патча
Избавляемся от монопольного режима
Далее стояла задача избавиться от монопольного режима. Уж больно неудобно было, когда ты не мог свернуть программу тестирования. Хотя теперь это уже было не нужно (так как первый вариант всегда был верным), я все равно решил вырезать его.
Тут методика очень похожа на вариант со взломом верных ответов. Дело в том, что когда программа открывается во весь экран, происходят множественные вызовы функции SetWindowLong
. Сначала я установил на неё бряк и подождал, пока у программы пропадёт рамка. Затем выйдя на пару функций вверх, мне удалось найти функцию, в которой программа переходила в полноэкранный режим (func_go_fullscreen
). Она вызывалась всего в одном месте.
Вызов функции func_go_fullscreen
Здесь я решил изменить условный переход jz
, на jnz
и 0 на FF в cmp
. Таким образом, условие никогда не будет верным, и функция никогда не будет вызвана.
Листинг ассемблера того же куска кода
Как и ожидалось, теперь программа не открывалась во весь экран и можно было спокойно свернуть её, даже если в настройках файла теста стоял монопольный режим.
Заключение
Довольно интересно было покопаться в реальном ПО и решить реальную задачу. Я написал патчер, для автоматического применения правок, описанных в статье и залил его на GitHub. Ссылка для всех заинтересованных: https://github.com/Reedus0/CrackTest