Так как тема реверс инжиниринга довольно популярна на хабре, решил поделиться своими наработками по этой теме. Мне, как и многим любителям визуальных новелл, знакома такая программа, как AGTH (Anime-Game-Text-Hooker). Она позволяет извлекать текст из новелл для последующего перевода(большинство игр – японские). Разработка этой программы, судя по всему, была прекращена ещё в 2011м году, исходников найти не удалось, а так как душа хотела дополнительных фич, было принято решение отреверсить эту программу и на основе полученных данных воссоздать альтернативную оболочку со всеми недостающими мне функциями.
Оригинальная программа состоит из двух частей – исполняемого файла и модуля перехвата выполненного в виде динамической библиотеки. Эту библиотеку программа внедряет в процесс игры и с её помощью получает оттуда текст.
Реверсить и переписывать я буду лишь исполняемый файл, а модуль перехвата оставлю оригинальный. На это есть несколько причин. Помимо очевидной сложности модуля и присущей мне лени, необходимо обеспечить совместимость моей разработки с так называемыми H-кодами. H-код — это набор данных нужный перехватчику для корректной установки хука в случае, когда дефолтные хуки неэффективны. Он содержит в себе адреса памяти, номера регистров и прочую информацию о местонахождении текста в игре. Для каждой отдельной игры этот код уникален и найден энтузиастами. Поэтому написать свой модуль так сказать «по мотивам» — не выйдет. Нужно будет обеспечить полную совместимость по этим кодам, а это совсем другой уровень сложности. Да и никаких дополнительных преимуществ это не даст.
Очевидно, что модуль перехвата в игре и AGTH как-то взаимодействуют между собой, и для написания альтернативной оболочки нужно узнать как. Способов передать данные от одной программы к другой довольно много, начиная от оконных сообщений и заканчивая сокетами. Какой же способ использован на самом деле я узнал случайно. Просто зашел в свойства процесса agth.exe через Process Explorer и решил посмотреть, какие строки содержит эта программа.
В глаза сразу бросилась строка "\\.\pipe\agth" — так указывается именованный канал, а значит можно предположить, что AGTH использует пайпы для общения с игрой. Теперь у нас есть направление, в котором можно начинать поиски. Для отладки я буду использовать любимый многими отладчик OllyDbg.
Загрузим AGTH в «Олю» и сразу поставим бряки на CreateNamedPipe* функции внутри модуля kernel32. Один из этих бряков должен сработать как только программа попытается создать именованный канал и из этой точки можно будет добраться до кода который с этими пайпами работает.
Продолжим выполнение и со второго срабатывания бряка попадаем в нужное место. О том, что это место нужное говорит нам наличие строки "\\.\pipe\agth" на стеке.
Теперь перейдём по адресу 0x00AF3A64, который лежит на вершине стека и должен указывать на код сразу за вызовом CreateNamedPipeW.
Тут уже можно разобрать с какими параметрами наш пайп создаётся, а именно:
Воспользуемся документацией и развернём магические числа в именованные константы. Получится так:
Пробежав по коду чуть ниже можно встретить вызов функций ConnectNamedPipe и WaitForMultipleObjects , который ожидает события от созданного пайпа.
Хорошо, теперь нужно узнать, как происходит чтение данных, а точнее каков размер блока данных, передаваемого от игры к приложению. О том, что данные передаются блоками, а не непрерывным потоком байт, говорит наличие флага PIPE_TYPE_MESSAGE используемое при создании канала.
Легко заметить, что после того, как WaitForMultipleObjects вернёт управление, будет создан новый поток, который вероятно и обрабатывает события на свежеподключенном пайпе. Перейдём по адресу 0x00CC5080:
Вот и искомая функция ReadFile, которая вызывается с параметрами:
Их я достал со стека в тот момент, когда сработал бряк, заблаговременно установленный на вызов ReadFile. В общем-то, нас интересует лишь параметр BytesToRead, который равен 8168-ми байтам. Вероятно – это и есть размер структуры с текстом, которую передаёт игра в программу.
В итоге собрано достаточно информации о том, как происходит взаимодействие с игрой: AGTH реализует пайп-сервер, который принимает данные кусками по 8168 байт. Теперь можно переходить к разбору того, что же эти байты означают.
Разбор формата данных я решил сделать уже внутри своей программы. В ней я реализовал собственный сервер, воспользовавшись данными полученными ранее, и с его помощью получал сообщения от игры. Очень удобно – можно завести структуру нужного размера и вычитывать данные прямо в неё. По ходу разбора того, что значит та или иная группа байт, эту структуру можно модифицировать и в конце получить полное описание всех полей.
Вот примерно так выглядит то, что приходит в программу из игры. Сразу бросаются в глаза строки UserHookQ и K.o.t.a.r.o.u. Первая — имя функции, которое отображается в оригинальной программе, второе — текст из игры в кодировке UTF-16. Также замечено число 7 (синее выделение) которое, как оказалось, всегда равно количеству символов строки игрового текста. Перебирая разные наборы данных выяснилось, что имя функции — это null-terminated строка с максимальной длинной в 24 символа. То есть, в случае со скриншотом выше, все байты между зелёным и синим выделением — просто мусор. Осталось ещё 16 байт данных в начале структуры. Первые две переменные определить было легко — это Context и Subcontext, которые также можно видеть в окне оригинальной программы. Третий параметр найти было чуть сложнее — он всегда имел небольшие значения и менялся только при перезапуске игры. Им оказался ProcessID игры. Последний из четвёрки менялся постоянно и имел достаточно большие значения. Единственной зацепкой было то, что это значение всегда увеличивалось со временем и никогда не уменьшалось. Это и было временем, точнее результатом вызова функции GetTickCount.
В итоге получилась такая структура:
С коммуникацией между приложением и игрой разобрались, теперь нужно узнать каким образом модуль перехвата текста попадает в игру и получает информацию о том, куда и как устанавливать хуки.
Запустим игру (или какое угодно другое приложение), подождём окончательной загрузки и прицепимся к ней отладчиком. Далее откроем список модулей, выберем kernel32, и в списке функций поставим брякпоинты на всех функциях, которые начинаются на LoadLibrary*. Это сделано потому, что как не крути, а финальная загрузка dll будет произведена с помощью вызова одной из этих функций и, если перехватить вызов — можно, побродив по стеку, выйти на сам загрузчик.
Продолжим выполнение программы. Затем запустим AGTH и укажем ему процесс игры:
Тут же сработает отладчик. В моём случае бряк сработал на функции LoadLibraryW.
Посмотрим на стек:
второй сверху это аргумент функции, а вот первый — это адрес возврата и ведёт он куда-то в недра kernel32. Странно, я ожидал увидеть там адрес внедрённого в игру кода загрузчика. Что ж, посмотрим, что лежит рядом аргументом LoadLibraryW. Перейдём по адресу 0x7EF80022 и вот оно!
Это и есть искомый загрузчик, кстати, довольно хитрый: всего 4 команды (начиная с адреса 0x7EF80014 идут данные).
Сначала на стек складываются параметры функции SetEnvironmentVariableW('AGTH','0'), потом — адрес функции LoadLibraryW, который служит адресом возврата для функции SetEnvironmentVariableW, так как вызывается она не через CALL, а с помощью безусловного перехода JMP. «Так вот почему LoadLibraryW был вызван откуда-то из недр kernel32, а не загрузчиком!» — так я подумал. Но мысль о том, что же будет после того как отработает LoadLibrary не давала мне покоя. Поэтому я решил глянуть, куда же все-таки вернётся управление после вызова. Идём по адресу 0x754D3677 и видим:
Судя по всему после вызова LoadLibraryW, будет вызван RtlExitUserThread с параметром, который вернёт LoadLibraryW и таким образом удалённый поток успешно завершится. Казалось бы — всё хорошо, но меня не покидала мысль: «А откуда вообще на стеке оказался этот адрес, и где программа достала адрес строки, в которой путь к внедряемой dll лежит? Ведь в коде загрузчика ничего подобного нет!». Выходит кто-то положил эти адреса на стек ещё до того как была вызвана первая инструкция загрузчика. И тут меня осенило: удалённые потоки создаются с помощью функции CreateRemoteThread, а она кроме указателя на функцию принимает ещё и параметр для этой функции. То есть она складывает на стек сначала адрес RtlExitUserThread, чтобы поток, сделав RET, корректно завершился, а потом ещё и переменную — параметр.
Ещё раз вкратце:
Кстати, такая игра со стеком, когда функция после RET-а попадает не в вызвавший её код, а в другую функцию, называется техникой возвратно-ориентированного программирования или просто ROP (Return-Oriented Programming).
Хорошо, с внедрением и передачей параметров в целевой процесс разобрались, все параметры передаются через переменную окружения с именем «AGTH». Получается, что в случае написания собственного загрузчика достаточно установить переменную окружения и загрузить dll.
Теперь нужно разобраться с параметрами, точнее с тем как командная строка программы, через которую задаётся H-код, превращается в значение той самой переменной окружения.
Чтобы постоянно не ковыряться в отладчике была написана библиотека-заглушка единственной функцией которой является чтение и вывод переменной «AGTH» для дальнейшего изучения.
Код заглушки:
Далее, подменив оригинальную dll, я начал перебирать все возможные ключи командной строки и смотреть как они отображаются на переменную окружения. Это оказалось несложно.
Список всех команд можно посмотреть в справке, встроенной в оригинальную программу. Из этих команд меня интересовали только Hook options.
Дальше просто вводим случайные параметры командной строки и смотрим, как они влияют на финальный результат.
Например, набор ключей '/HQN54@48693e /NH /Slocalhost' превращается в '20S0:localhostUQN54@48693e' и сразу видно, что значения ключей /H и /S передаются как есть. Также было выяснено, что префиксы U и S0: не меняются никогда и исчезают совсем лишь при отсутствии соответствующих ключей /H и /S. Все остальные ключи влияют только на первые два шестнадцатеричных числа. Поиграв с ключами ещё немного выяснилось, что это битовые флаги, где каждый ключ отвечает за установку отдельного бита в байте, который представляют эти два числа.
Получилась табличка:
Таким образом формат параметров для библиотеки удалось разобрать.
Вот и всё. Дело осталось за малым – реализовать собственный интерфейс и добавить нужных фич. Что и было сделано:
Написание остального кода достаточно тривиально поэтому здесь я приводить его не буду, просто оставлю ссылку на Github.
Оригинальная программа состоит из двух частей – исполняемого файла и модуля перехвата выполненного в виде динамической библиотеки. Эту библиотеку программа внедряет в процесс игры и с её помощью получает оттуда текст.
Реверсить и переписывать я буду лишь исполняемый файл, а модуль перехвата оставлю оригинальный. На это есть несколько причин. Помимо очевидной сложности модуля и присущей мне лени, необходимо обеспечить совместимость моей разработки с так называемыми H-кодами. H-код — это набор данных нужный перехватчику для корректной установки хука в случае, когда дефолтные хуки неэффективны. Он содержит в себе адреса памяти, номера регистров и прочую информацию о местонахождении текста в игре. Для каждой отдельной игры этот код уникален и найден энтузиастами. Поэтому написать свой модуль так сказать «по мотивам» — не выйдет. Нужно будет обеспечить полную совместимость по этим кодам, а это совсем другой уровень сложности. Да и никаких дополнительных преимуществ это не даст.
Разбор протокола общения модуля перехвата и AGTH
Очевидно, что модуль перехвата в игре и AGTH как-то взаимодействуют между собой, и для написания альтернативной оболочки нужно узнать как. Способов передать данные от одной программы к другой довольно много, начиная от оконных сообщений и заканчивая сокетами. Какой же способ использован на самом деле я узнал случайно. Просто зашел в свойства процесса agth.exe через Process Explorer и решил посмотреть, какие строки содержит эта программа.
В глаза сразу бросилась строка "\\.\pipe\agth" — так указывается именованный канал, а значит можно предположить, что AGTH использует пайпы для общения с игрой. Теперь у нас есть направление, в котором можно начинать поиски. Для отладки я буду использовать любимый многими отладчик OllyDbg.
Загрузим AGTH в «Олю» и сразу поставим бряки на CreateNamedPipe* функции внутри модуля kernel32. Один из этих бряков должен сработать как только программа попытается создать именованный канал и из этой точки можно будет добраться до кода который с этими пайпами работает.
Продолжим выполнение и со второго срабатывания бряка попадаем в нужное место. О том, что это место нужное говорит нам наличие строки "\\.\pipe\agth" на стеке.
Теперь перейдём по адресу 0x00AF3A64, который лежит на вершине стека и должен указывать на код сразу за вызовом CreateNamedPipeW.
001B3A43 > 56 PUSH ESI ; 0x0
00AF3A44 . 6A 00 PUSH 0
00AF3A46 . 68 00000200 PUSH 20000
00AF3A4B . 6A 00 PUSH 0
00AF3A4D . 68 FF000000 PUSH 0FF
00AF3A52 . 6A 06 PUSH 6
00AF3A54 . 68 01000840 PUSH 40080001
00AF3A59 . 68 A026AF00 PUSH agth.00AF26A0 ; UNICODE "\\.\pipe\agth"
00AF3A5E . FF15 4010AF00 CALL DWORD PTR DS:[<&KERNEL32.CreateName>; kernel32.CreateNamedPipeW
00AF3A64 . 8BF8 MOV EDI,EAX
00AF3A66 . EB 03 JMP SHORT agth.00AF3A6B
Тут уже можно разобрать с какими параметрами наш пайп создаётся, а именно:
CreateNamedPipeW("\\.\pipe\agth", 40080001, 6, 0xFF, 0, 0x20000, 0, NULL);
Воспользуемся документацией и развернём магические числа в именованные константы. Получится так:
CreateNamedPipeW("\\.\pipe\agth", PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED | FILE_FLAG_FIRST_PIPE_INSTANCE, PIPE_WAIT | PIPE_READMODE_MESSAGE | PIPE_TYPE_MESSAGE, 0xFF, 0, 0x20000, 0, NULL);
Пробежав по коду чуть ниже можно встретить вызов функций ConnectNamedPipe и WaitForMultipleObjects , который ожидает события от созданного пайпа.
Хорошо, теперь нужно узнать, как происходит чтение данных, а точнее каков размер блока данных, передаваемого от игры к приложению. О том, что данные передаются блоками, а не непрерывным потоком байт, говорит наличие флага PIPE_TYPE_MESSAGE используемое при создании канала.
Легко заметить, что после того, как WaitForMultipleObjects вернёт управление, будет создан новый поток, который вероятно и обрабатывает события на свежеподключенном пайпе. Перейдём по адресу 0x00CC5080:
Вот и искомая функция ReadFile, которая вызывается с параметрами:
0291D9B4 00000104 |hFile = 00000104 (window)
0291D9B8 0291DA78 |Buffer = 0291DA78
0291D9BC 00001FE8 |BytesToRead = 1FE8 (8168.)
0291D9C0 0291DA14 |pBytesRead = 0291DA14
0291D9C4 004C4168 \pOverlapped = 004C4168
Их я достал со стека в тот момент, когда сработал бряк, заблаговременно установленный на вызов ReadFile. В общем-то, нас интересует лишь параметр BytesToRead, который равен 8168-ми байтам. Вероятно – это и есть размер структуры с текстом, которую передаёт игра в программу.
В итоге собрано достаточно информации о том, как происходит взаимодействие с игрой: AGTH реализует пайп-сервер, который принимает данные кусками по 8168 байт. Теперь можно переходить к разбору того, что же эти байты означают.
Разбор формата данных я решил сделать уже внутри своей программы. В ней я реализовал собственный сервер, воспользовавшись данными полученными ранее, и с его помощью получал сообщения от игры. Очень удобно – можно завести структуру нужного размера и вычитывать данные прямо в неё. По ходу разбора того, что значит та или иная группа байт, эту структуру можно модифицировать и в конце получить полное описание всех полей.
Вот примерно так выглядит то, что приходит в программу из игры. Сразу бросаются в глаза строки UserHookQ и K.o.t.a.r.o.u. Первая — имя функции, которое отображается в оригинальной программе, второе — текст из игры в кодировке UTF-16. Также замечено число 7 (синее выделение) которое, как оказалось, всегда равно количеству символов строки игрового текста. Перебирая разные наборы данных выяснилось, что имя функции — это null-terminated строка с максимальной длинной в 24 символа. То есть, в случае со скриншотом выше, все байты между зелёным и синим выделением — просто мусор. Осталось ещё 16 байт данных в начале структуры. Первые две переменные определить было легко — это Context и Subcontext, которые также можно видеть в окне оригинальной программы. Третий параметр найти было чуть сложнее — он всегда имел небольшие значения и менялся только при перезапуске игры. Им оказался ProcessID игры. Последний из четвёрки менялся постоянно и имел достаточно большие значения. Единственной зацепкой было то, что это значение всегда увеличивалось со временем и никогда не уменьшалось. Это и было временем, точнее результатом вызова функции GetTickCount.
В итоге получилась такая структура:
TAGTHRcPckt = packed record // SizeOf = 8168 bytes
Context: Cardinal;
Subcontext: Cardinal;
ProcessID: Cardinal;
UpTime: Cardinal;
TextLength: Cardinal;
HookName: array [0 .. 23] of ansichar;
Text: array [0 .. 4061] of widechar;
end;
С коммуникацией между приложением и игрой разобрались, теперь нужно узнать каким образом модуль перехвата текста попадает в игру и получает информацию о том, куда и как устанавливать хуки.
Исследование загрузчика
Запустим игру (или какое угодно другое приложение), подождём окончательной загрузки и прицепимся к ней отладчиком. Далее откроем список модулей, выберем kernel32, и в списке функций поставим брякпоинты на всех функциях, которые начинаются на LoadLibrary*. Это сделано потому, что как не крути, а финальная загрузка dll будет произведена с помощью вызова одной из этих функций и, если перехватить вызов — можно, побродив по стеку, выйти на сам загрузчик.
Продолжим выполнение программы. Затем запустим AGTH и укажем ему процесс игры:
agth /PNИмя_процесса.exe
Тут же сработает отладчик. В моём случае бряк сработал на функции LoadLibraryW.
Посмотрим на стек:
второй сверху это аргумент функции, а вот первый — это адрес возврата и ведёт он куда-то в недра kernel32. Странно, я ожидал увидеть там адрес внедрённого в игру кода загрузчика. Что ж, посмотрим, что лежит рядом аргументом LoadLibraryW. Перейдём по адресу 0x7EF80022 и вот оно!
Это и есть искомый загрузчик, кстати, довольно хитрый: всего 4 команды (начиная с адреса 0x7EF80014 идут данные).
7EF80000 68 1E00F87E PUSH 7EF8001E ; UNICODE "0"
7EF80005 68 1400F87E PUSH 7EF80014 ; UNICODE "AGTH"
7EF8000A 68 121E4D75 PUSH kernel32.LoadLibraryW
7EF8000F -E9 CE9755F6 JMP kernel32.SetEnvironmentVariableW
Сначала на стек складываются параметры функции SetEnvironmentVariableW('AGTH','0'), потом — адрес функции LoadLibraryW, который служит адресом возврата для функции SetEnvironmentVariableW, так как вызывается она не через CALL, а с помощью безусловного перехода JMP. «Так вот почему LoadLibraryW был вызван откуда-то из недр kernel32, а не загрузчиком!» — так я подумал. Но мысль о том, что же будет после того как отработает LoadLibrary не давала мне покоя. Поэтому я решил глянуть, куда же все-таки вернётся управление после вызова. Идём по адресу 0x754D3677 и видим:
754D3677 50 PUSH EAX
754D3678 FF15 F0064D75 CALL DWORD PTR DS:[<&ntdll.RtlExitUserThread>] ; ntdll.RtlExitUserThread
Судя по всему после вызова LoadLibraryW, будет вызван RtlExitUserThread с параметром, который вернёт LoadLibraryW и таким образом удалённый поток успешно завершится. Казалось бы — всё хорошо, но меня не покидала мысль: «А откуда вообще на стеке оказался этот адрес, и где программа достала адрес строки, в которой путь к внедряемой dll лежит? Ведь в коде загрузчика ничего подобного нет!». Выходит кто-то положил эти адреса на стек ещё до того как была вызвана первая инструкция загрузчика. И тут меня осенило: удалённые потоки создаются с помощью функции CreateRemoteThread, а она кроме указателя на функцию принимает ещё и параметр для этой функции. То есть она складывает на стек сначала адрес RtlExitUserThread, чтобы поток, сделав RET, корректно завершился, а потом ещё и переменную — параметр.
Ещё раз вкратце:
- CreateRemoteThread складывает на стек адрес RtlExitUserThread, путь к dll и запускает загрузчик
- загрузчик складывает на стек аргументы для SetEnvironmentVariableW, адрес LoadLibraryW и делает безусловный переход на SetEnvironmentVariableW
- SetEnvironmentVariableW забирает свои аргументы со стека и при возврате из неё поток оказывается в начале LoadLibraryW
- LoadLibraryW забирает со стека путь к dll и при возврате из неё поток попадает на RtlExitUserThread
- RtlExitUserThread завершает поток
Кстати, такая игра со стеком, когда функция после RET-а попадает не в вызвавший её код, а в другую функцию, называется техникой возвратно-ориентированного программирования или просто ROP (Return-Oriented Programming).
Хорошо, с внедрением и передачей параметров в целевой процесс разобрались, все параметры передаются через переменную окружения с именем «AGTH». Получается, что в случае написания собственного загрузчика достаточно установить переменную окружения и загрузить dll.
Загрузчик:
// Структура представляющая собой будущий машинный код
TInject = packed record
// code
cmd0: BYTE;
cmd1: BYTE;
cmd1arg: DWORD;
cmd2: BYTE;
cmd2arg: DWORD;
cmd3: WORD;
cmd3arg: DWORD;
cmd4: BYTE;
cmd4arg: DWORD;
cmd5: WORD;
cmd5arg: DWORD;
cmd6: BYTE;
cmd6arg: DWORD;
cmd7: WORD;
cmd7arg: DWORD;
// data
pLoadLibrary: Pointer;
pExitThread: Pointer;
pSetEnvironmentVariableW: Pointer;
ENVName: array [0 .. 4] of WideChar;
ENVValue: array [0 .. MAX_PATH] of WideChar;
LibraryPath: array [0 .. MAX_PATH] of WideChar;
end;
const // бинарное представление ассемблерных команд
PUSH: BYTE = $68;
CALL_DWORD_PTR: WORD = $15FF;
INT3: BYTE = $CC;
NOP: BYTE = $90;
{ Внедрение Dll в процесс }
class function THooker.InjectDll(Process: DWORD;
ModulePath, HCode: WideString): boolean;
var
Memory: Pointer;
CodeBase: DWORD;
BytesWritten: SIZE_T;
ThreadId: DWORD;
hThread: DWORD;
hKernel32: DWORD;
Inject: TInject;
function RebasePtr(ptr: Pointer): DWORD;
// перебазируем локальные указатели на адреса
// в целевом процессе
begin
Result := CodeBase + DWORD(ptr) - DWORD(@Inject);
end;
begin
Result := false;
// выделяем память в целевом процессе
// с атрибутами на чтение запись и выполнение
Memory := VirtualAllocEx(Process, nil, sizeof(Inject), MEM_TOP_DOWN or
MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if Memory = nil then
Exit;
CodeBase := DWORD(Memory);
hKernel32 := GetModuleHandle('kernel32.dll');
// инициализация внедряемого кода:
// структура Inject представляет собой машинный код нашего загрузчика
FillChar(Inject, sizeof(Inject), 0);
with Inject do
begin
// code
cmd0 := NOP;
cmd1 := PUSH;
cmd1arg := RebasePtr(@ENVValue);
cmd2 := PUSH;
cmd2arg := RebasePtr(@ENVName);
cmd3 := CALL_DWORD_PTR;
cmd3arg := RebasePtr(@pSetEnvironmentVariableW);
cmd4 := PUSH;
cmd4arg := RebasePtr(@LibraryPath);
cmd5 := CALL_DWORD_PTR;
cmd5arg := RebasePtr(@pLoadLibrary);
cmd6 := PUSH;
cmd6arg := 0;
cmd7 := CALL_DWORD_PTR;
cmd7arg := RebasePtr(@pExitThread);
// data
// тут происходит магия основанная на том,
// что ImageBase kernel32.dll во всех процессах одинаков
// поэтому не требуется пересчитывать указатели на его функции
// они такие-же как и в нашем процессе
// это справедливо лишь для kernel32.dll только
// и вообще недокументированная особенность
// не делайте так в серьёзных проектах
pLoadLibrary := GetProcAddress(hKernel32, 'LoadLibraryW');
pExitThread := GetProcAddress(hKernel32, 'ExitThread');
pSetEnvironmentVariableW := GetProcAddress(hKernel32,
'SetEnvironmentVariableW');
lstrcpy(@LibraryPath, PWideChar(ModulePath));
lstrcpy(@ENVName, PWideChar('AGTH'));
lstrcpy(@ENVValue, PWideChar(HCode));
end;
// записать машинный код по зарезервированному адресу
WriteProcessMemory(Process, Memory, @Inject, SIZE_T(sizeof(Inject)),
BytesWritten);
// выполнить машинный код
hThread := CreateRemoteThread(Process, nil, 0, Memory, nil, 0, ThreadId);
if hThread = 0 then
Exit;
// подождём пока отработает наш загрузчик
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
VirtualFreeEx(Process, Memory, 0, MEM_RELEASE);
// надо-надо умываться по утрам и вечерам
Result := true;
end;
Теперь нужно разобраться с параметрами, точнее с тем как командная строка программы, через которую задаётся H-код, превращается в значение той самой переменной окружения.
Чтобы постоянно не ковыряться в отладчике была написана библиотека-заглушка единственной функцией которой является чтение и вывод переменной «AGTH» для дальнейшего изучения.
Код заглушки:
library AGTH;
uses windows;
var
buffer: array [0 .. 255] of widechar;
begin
GetEnvironmentVariableW('AGTH', buffer, 256);
MessageBoxW(0, buffer, buffer, 0);
end.
Далее, подменив оригинальную dll, я начал перебирать все возможные ключи командной строки и смотреть как они отображаются на переменную окружения. Это оказалось несложно.
Список всех команд можно посмотреть в справке, встроенной в оригинальную программу. Из этих команд меня интересовали только Hook options.
Hook options:
/H[X]{A|B|W|S|Q}[N][data_offset[*drdo]][:sub_offset[*drso]]@addr[:module[:{name|#ordinal}]] - select OK for more help
/NC - don't hook child processes
/NH - no default hooks
/NJ - use thread code page instead of Shift-JIS for non-unicode text (should be specified for capturing non-japanese text)
/NS - don't use subcontexts
/S[IP_address] - send text to custom computer (default parameter: local computer)
/V - process text threads from system contexts
/X[sets_mask] - extended sets of hooked functions (default parameter: 1; number of available sets: 2)
Дальше просто вводим случайные параметры командной строки и смотрим, как они влияют на финальный результат.
Например, набор ключей '/HQN54@48693e /NH /Slocalhost' превращается в '20S0:localhostUQN54@48693e' и сразу видно, что значения ключей /H и /S передаются как есть. Также было выяснено, что префиксы U и S0: не меняются никогда и исчезают совсем лишь при отсутствии соответствующих ключей /H и /S. Все остальные ключи влияют только на первые два шестнадцатеричных числа. Поиграв с ключами ещё немного выяснилось, что это битовые флаги, где каждый ключ отвечает за установку отдельного бита в байте, который представляют эти два числа.
Получилась табличка:
/nh - 20 - 10 0000
/nc - 10 - 01 0000
/nj - 08 - 00 1000
/x3 - 06 - 00 0110 // комбинация /x2 и /x
/x2 - 04 - 00 0100
/x - 02 - 00 0010
/V - 01 - 00 0001
Функция преобразования командной строки в H-код
const
PROCESS_SYSTEM_CONTEXT = $01;
HOOK_SET_1 = $02;
HOOK_SET_2 = $04;
USE_THREAD_CODEPAGE = $08;
NO_HOOK_CHILD = $10;
NO_DEF_HOOKS = $20;
class function THooker.GenerateHCode(AGTHcmd: string): string;
var
i: Integer;
lcmd, uFlag, sFlag: string;
flags: BYTE;
begin
lcmd := lowercase(AGTHcmd);
flags := 0;
if pos('/nh', lcmd) > 0 then
flags := flags or NO_DEF_HOOKS;
if pos('/nc', lcmd) > 0 then
flags := flags or NO_HOOK_CHILD;
if pos('/nj', lcmd) > 0 then
flags := flags or USE_THREAD_CODEPAGE;
if pos('/v', lcmd) > 0 then
flags := flags or PROCESS_SYSTEM_CONTEXT;
if pos('/x3', lcmd) > 0 then
flags := flags or (HOOK_SET_1 or HOOK_SET_2)
else if pos('/x2', lcmd) > 0 then
flags := flags or HOOK_SET_2
else if pos('/x', lcmd) > 0 then
flags := flags or HOOK_SET_1;
// выгребаем все между /h и пробелом и в начало ставим символ U
i := pos('/h', lcmd);
if i > 0 then
begin
uFlag := copy(AGTHcmd, i, length(AGTHcmd) - (i - 1)); // /h -> endstr
delete(uFlag, 1, 2); // del /h
i := pos(' ', uFlag);
if i > 0 then
delete(uFlag, i, length(uFlag) - (i - 1));
uFlag := 'U' + uFlag;
end
else
uFlag := '';
// выгребаем все между /s и пробелом и в начало ставим символы S0:
i := pos('/s', lcmd);
if i > 0 then
begin
sFlag := copy(AGTHcmd, i, length(AGTHcmd) - (i - 1));
delete(sFlag, 1, 2); // del /s
i := pos(' ', sFlag);
if i > 0 then
delete(sFlag, i, length(sFlag) - (i - 1));
sFlag := 'S0:' + sFlag;
end
else
sFlag := '';
Result := IntToHex(flags, 1) + sFlag + uFlag;
end;
Таким образом формат параметров для библиотеки удалось разобрать.
Конец
Вот и всё. Дело осталось за малым – реализовать собственный интерфейс и добавить нужных фич. Что и было сделано:
- с помощью слоистых окон был реализован вывод субтитров поверх игры
- добавлена интеграция с гуглопереводчиком
- юзерскрипты на JS для препроцессинга текста перед переводом
Написание остального кода достаточно тривиально поэтому здесь я приводить его не буду, просто оставлю ссылку на Github.
GreyCat
Всё хорошо, непонятно только удивление от завершения функции JMPом вместо RET. Это совершенно типовая конструкция, которую уже лет 20 делают фактически все более-менее популярные оптимизирующие компиляторы. Она, кстати, даже от архитектуры обычно не зависит, тот же clang ее чуть ли не в intermediate делает.
Если чуть приблизиться к контексту — к тому, что там идет загрузка библиотеки и нахождение точки в ней — то тем более, это совершенно типичная конструкция PLT / GOT. Чудесная книга, которая Linkers and Loaders слегка устарела, но в целом очень хорошо и подробно объясняет, как вообще можно и почему делают именно так в конкретном случае.