Хабрахабр

Исследование защиты программы VoiceAttack

Которая в итоге подкинула несколько приятных неожиданностей. Осторожно, под катом много скринов в хайрезе и не хайрезе, которые не убраны под спойлер ввиду их невероятной важности. А, ну и ещё там есть шутка про половые органы кентавров, но она тоже включена исключительно ради контекста.
Программа VoiceAttack известна в кругах любителей игр наподобие Star Citizen и Elite: Dangerous, и представляет собой довольно простой инструмент для связывания нажатий клавиш с голосовыми командами. При большом желании её можно использовать и для управления ПК, но полностью она раскрывается после напяливания на голову VR-очков

перерыв на шутку про VR

… и запуска любой из вышеперечисленных игр.

Моё первое знакомство с VoiceAttack состоялось при просмотре замечательно поставленного видео от г-на Rimas, в процессе которого я невольно зауважал распознаватель речи Windows, как-то понимающий такой ужасный акцент.

Так как VR-шлема у меня не было (как, впрочем, и денег на него), я решил поиграться с недавно расшаренным мне через Стим Elite: Dangerous, поуправляв стандартным корабликом с помощью голоса. Сбегав на сайт VoiceAttack, я подивился аппетитам разработчиков (10 баксов за обёртку встроенного распознавателя речи), и сразу решил для себя, что использовать буду максимум демо-версию программы. Однако, при попытке загрузить скачанный профиль, я обнаружил очередной подарок от разрабов — в демо-версии программы не удаётся сохранить больше 20 команд одновременно, что, разумеется, несколько усложняет общение с кораблём — количество контролей в E:D явно больше двадцати.

Поиски таблетки от жадности

Будучи гордым пиратом, не желающим платить 500 рублей за лицензию, я полез в Интернет за кряком. Меня ждало разочарование в виде кучи битых ссылок, ransomware, и прочей мути, предлагаемой Сетью в случае отсутствия необходимых данных. Единственный кусочек информации был найден на форуме exetools, но описывал старую версию программы. Надо было что-то делать, либо плюнуть и не делать уже ничего.

Исследование работы

Итак, для начала просто запустим VoiceAttack. После справки, как и ожидалось, будет предложено зарегистрировать ключ:

Обратившись к встроенной справке, узнаём что ключ шестнадцатизначный, и состоит как из цифр, так и из букв:

После попытки заполнить поля мусором, стало понятно что присутствуют проверки на валидность ключа:

Вводим всеми любимый test@example.com, 16 цифр, и любуемся на проверку ключа на сервере, и сообщение об ошибке:

Собственно, разведку на этом можно и закончить. Принцип регистрации довольно прост — программа явно связывается с удалённым сервером, передаёт наши регистрационные данные, и анализирует ответ. Такой метод правится либо патчем программы, чтобы не лезла в сеть и сразу считала ключ верным, либо, ради удовлетворения интересов месье — пишется эмулятор сервера регистрации. Этим и займёмся.

Анализ и деобфускация

Для анализа экзешников я люблю использовать exeinfope — дёшево и сердито (и есть строчка информации для ламеров!)

Откроем VoiceAttack.exe в exeinfope:

Отлично, нам повезло — .NET + SmartAssembly это сродни джекпоту — будь программа написана на C++ (и не дай Бог только под x64), или обфусцирована чем-нибудь поинтереснее, пришлось бы ковыряться в спагетти криво дизассемблированного вывода. А так мы легко обойдёмся декомпилятором dnSpy и деобфускатором de4dot.

Копируем файлы программы в отдельную папку и деобфусцируем:

Мы готовы к дебагу и анализу кода.

Анализ сетевой активности регистрации

Для начала запустим VoiceAttack, и введём наши невалидные регистрационные данные. Нажмём OK, и быстро остановим программу в дебаггере. Видим следующее:

Как мы и думали, при регистрации программа стучится на сервер voiceattack.com и отправляет туда данные регистрации. Вынимаем строку реквеста:

http://voiceattack.com/Validate.aspx?pv=JcArVyeUmUOZfJVj6utdKw==&em=iEm1cpNSBqMsA06LJExtLntuDo0yvQwPzKuIJhhbLt8=&vk=A1Zkz8zPx2VUdM1oi+mKHHtuDo0yvQwPzKuIJhhbLt8=&sg=OBaQB9KBl8iHF1miBzpp/Q==&nn=gXWP+H1uDbYW+crJfgFNs8gexDihTyvNmjpBzp/I0//f3IvaGDFmFz+ll1WxqPdu6iC0SAGY1eJBMRvl2GIr2A==&pr=0+jAkAfwc3I1ZMHk2zdDz3tuDo0yvQwPzKuIJhhbLt8=

Сразу видна новая проблема — на сервер передаётся явно больше параметров чем задали мы, да ещё и в base64. Пробуем декодировать base64:

Неплохо, данные ещё и зашифрованы. Нужно разбираться дальше.

Анализ и взлом шифрования

Найдём в dnSpy форму регистрации. После недолгих поисков, находим подходящую форму frmRegister и лезем в код события btnOk_Click:

Кажется, то что надо. Видны тексты уже знакомых нам ошибок. Отмотаем код на нужный нам момент соединения с сервером:

HTTP-ответ пишется в переменную response. Найдём, куда записывается зашифрованное тело ответа:

Крутим код дальше, ища упоминания переменной text8 и натыкаемся на её перезапись с помощью некой функции:

Переходим в функцию и видим что попали в точку:

Очевидно, что функция реализует AES-дешифрование входной строки, и возвращает дешифрованный результат. Теперь всё что там остаётся — отловить ключ, содержащийся в переменной this.byte_0. Установим брейкпоинт и запустим программу. На моменте достижения брейкпоинта лезем в значения переменных:

Теперь у нас на руках все явки и пароли. Можно начинать работу над эмуляцией сервера регистрации.

Сервер регистрации

Для начала напишем инструмент шифрования и дешифрования по найденному ключу:

using System;
using System.Security.Cryptography;
using System.Text; public class Program
{ public static void enc(string text) { string result; Rijndael rijndael = Rijndael.Create(); rijndael.Mode = CipherMode.ECB; byte[] bytes = Encoding.ASCII.GetBytes(text); byte[] key = {0x74,0x72,0x75,0x65,0x47,0x52,0x49,0x54}; // Ключ result = Convert.ToBase64String(rijndael.CreateEncryptor(key, null).TransformFinalBlock(bytes, 0, bytes.Length)); Console.WriteLine(result); } public static void dec(string text) { string result; Rijndael rijndael = Rijndael.Create(); rijndael.Mode = CipherMode.ECB; byte[] key = {0x74,0x72,0x75,0x65,0x47,0x52,0x49,0x54}; byte[] bytes = Convert.FromBase64String(text); result = Encoding.ASCII.GetString(rijndael.CreateDecryptor(key, null).TransformFinalBlock(bytes, 0, bytes.Length)); Console.WriteLine(result); } public static void Main(string[] args) { if(args[0]=="enc") { enc(args[1]); } else if (args[0]=="dec") { dec(args[1]); } }
}

Попробуем дешифровать передаваемые на сервер параметры:

Отлично. Теперь мы можем видеть что же всё-таки отправляет программа на сервер. Запросим результат сами по уже знакомому нам url и дешифруем его:

Похоже что сервер передаёт ответ в виде %Результат валидации%_%Информация%. Снова обратимся к коду и узнаем какой ответ означает успех:

Быстро бежим вносить www.voiceattack.com в hosts, шифруем строку «Validation Success_some useless info» нашей программой, хостим страничку, регистрируем программу…

И получаем в ответ целый кукиш в маслом — при открытии программа снова предлагает зарегистрироваться. Означает такое поведение только одно — при запуске внедрена проверка лицензии.

Дорабатываем сервер

Снова лезем в декомпилированный код. Находим метод загрузки главной формы frmMain_Load и после недолгих поисков видим код вызова формы о необходимости регистрации:

Обращаем внимание на условие перехода в ветку загрузки формы — проверяется некая переменная this.bool_89. Отмотаем код чуть выше и посмотрим как формируется её значение:

Налицо проверка соответствия одной строки другой. Справа некий ValidationKey, слева применение метода на списке строк. Как вы думаете, какой метод поочерёдно применяется ко всем этим строкам?

Ставим брейкпоинт на строку сравнения, чтобы узнать значения строк string_ и text. Получаем:

Какие значения подставляются в строки registrationEmail и registrationKey я думаю и так понятно.

Дорабатываем сервер на парсинг GET-запроса и формирование подходящего ответа:

from SimpleHTTPServer import BaseHTTPServer
import SocketServer
import subprocess
from urlparse import urlparse
from urllib import unquote
import ctypes, sys, os
PORT = 80 def aes(action, text): return subprocess.Popen(["VoiceAttackCipher.exe", action, text], stdout=subprocess.PIPE).communicate()[0][:-2] def patch_hosts(): with open("C:\WINDOWS\System32\drivers\etc\hosts", "a") as hosts_file: hosts_file.write("\n127.0.0.1 www.voiceattack.com") def unpatch_hosts(): with open("C:\WINDOWS\System32\drivers\etc\hosts", "r") as hosts_file: hosts_lines = [] for line in hosts_file: if line != "127.0.0.1 www.voiceattack.com": hosts_lines.append(line) hosts_lines[-1] = hosts_lines[-1].strip("\n") with open("C:\WINDOWS\System32\drivers\etc\hosts", "w") as hosts_file: for line in hosts_lines: hosts_file.write(line) class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler): def do_HEAD(s): s.send_response(200) s.send_header("Content-type", "text/html") s.end_headers() def do_GET(s): """Respond to a GET request.""" query = urlparse(s.path).query query_components = dict(qc.split("=") for qc in query.split("&")) print "Got registration request! Processing..." key = aes("dec", unquote(query_components["vk"])) mail = aes("dec", unquote(query_components["em"])) magic_1 = aes("dec", unquote(query_components["pv"])) magic_2 = aes("dec", unquote(query_components["sg"])) print "Key:", key print "E-Mail:", mail print "Magic number #1:", magic_1 print "Magic number #2:", magic_2 print "Creating validation key..." validation_key = aes("enc", "{}{}{}{}".format(magic_1, mail, key, magic_2)) print "Validation key:", validation_key print "Creating response..." response = aes("enc", "Validation Success_{}".format(validation_key)) print "Response:", response s.send_response(200) s.send_header("Content-type", "text/html") s.end_headers() # Отдаём ответ "Validation Success_{}".format(enc(dec(pv)+dec(em)+dec(vk)+dec(sg))) s.wfile.write(response) print "Done! Your program should be registered now!" if __name__ == "__main__": print "Starting VoiceAttack Activation Server..." print "Checking privileges..." is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0 if not is_admin: print "Error: please run this program with admin rights." os.system("pause") sys.exit() print "All good." print "Patching hosts..." patch_hosts() print "VoiceAttack Server Started!" print "You can try to register VoiceAttack." print "Press [Ctrl+C] after successful registration." try: httpd = SocketServer.TCPServer(("", PORT), MyHandler, bind_and_activate=False) httpd.allow_reuse_address = True httpd.server_bind() httpd.server_activate() httpd.serve_forever() except KeyboardInterrupt: httpd.server_close() except Exception as e: print e finally: print "Voice Attack Server is shutting down..." #print "Un-patching hosts..." #unpatch_hosts() # Хостс придётся оставить пропатченным, так как проверка происходит при каждом запуске print "Bye-bye." os.system("pause")

Вместо заключения

Всё вышеописанное производилось исключительно из спортивного интереса, да и больше от счастья что при очередной попытке расковырять программу мне не придётся плеваться в листинги Иды. Разродиться на свой первый пост я хотел давно, а тут и НЛО ещё пригласило — так что призываю писать в комментарии если что-то не так или как-то не очень.

Теги
Показать больше

Похожие статьи

Кнопка «Наверх»
Закрыть