Перейти к содержанию

Codeinjection howto, или инъекция кода своими руками.


Рекомендуемые сообщения


 
Инъекция кода своими руками на C\C++
 
Когда: 16.03.2015
Кто: keng aka Михаил Ремизов
Инструменты:
- Ассемблер. Я использовал Flat Assembler 1.71.38) - [flatassembler.net]
- Отладчик. Я использовал Olly Debugger 2.01 - [ollydbg.de]
- Компилятор языка C\C++. Я использовал GCC 4.8.1-4
 
ВНИМАНИЕ: Если вы не в курсе, что значит "инъекция кода", то давайте не будем 
тратить ваше время - сходите да почитайте.
 
 
 
 
Настал тот светлый день, когда вы, освоив азы взлома игр, научились заменять 
плохие и не слишком нужные ассемблерные инструкции в коде игры на некоторое 
количество команд NOP (No Operation), а так же менять DEC на INC и обратно. 
Возможно, вы даже пользовались какими-нибудь более продвинутыми программами, 
которые позволяют создавать инъекции кода автоматически, а вам остается лишь 
указать нужный адрес памяти и инструкции, которые следует внедрить по этому 
адресу. Со временем вам стало не хватать возможностей таких программ и\или же 
стало интересно, как такое провернуть самостоятельно. Эта статья написана, чтобы
 пролить толику света на сие таинство. Для начала, нам понадобится испытуемый, 
исходный код которого я приведу прямо сейчас:
 
/*-Исходный код испытуемой программы------------------------------------------*/
 
include 'win32ax.inc'
 
.code
 
start:
    nop
    nop
    nop
    nop
    nop
    nop
    nop
    invoke ExitProcess,0
 
.end start
 
/*-Исходный код испытуемой программы------------------------------------------*/
 
Это маленькое чудо можно успешно скомпилировать при помощи 
ассемблера, получив на выходе исполняемый файл, который запускается (!). К 
большому сожалению, все, что он делает - это некоторое время не делает ничего, а
затем завершается, вызвав WinApi-функцию ExitProcess через макрос invoke. Макрос
 этот нужен, чтобы не писать в столбик push push push call, как это обычно 
делается при вызове процедур. В общем, получили мы бинарник, который запускается
 и ничего не делает. Настало время его отладить! Открываем его в Olly и видим 
картину примерно такую же, как на рис. 0:
 
/*----------------------------------------------------------------------------*/
 
post-6695-0-03481300-1426463704.png
 
/*----------------------------------------------------------------------------*/
 
В моем случае точка входа (то самое место, куда первым делом передастся 
управление сразу после запуска программы) находится по адресу 0x00401000. В 
вашем, скорее всего, тоже. Будем думать, что этот адрес является местом, куда 
будет осуществлена инъекция нашего кода. Берем и аккуратно переписываем его в 
на бумажку.
 
Ассемблер, как вы, возможно, знаете, был придуман затем, чтобы не натыкивать 
программу на перфокарте и не писать ее в 16-ричном редакторе, мучаясь. Грубо 
говоря, это более понятное представление для человека команд компьютера, нежели 
просто цифры. Что делает компилятор? Он берет буквы команд и переводит их в 
цифры с буквами, а все переменные и имена функций заменяет на адреса. Почему? В 
основном, так короче. И работать так будет быстрее, раз места занимает меньше. 
Поэтому для каждой инструкции ассемблера есть так называемый опкод (от 
английского opcode - OPeration CODE). Скажем, для команды NOP он будет равен 
0x90. Это священное знание понадобится нам в процессе написания программы, 
делающей инъекцию. Для того, чтобы впихнуть вместо короткой команды несколько 
длинных, как известно, используется команда безусловного перехода, она же JMP (
от английского to JuMP - прыгать). Она занимает 5 байт - 1 на опкод инструкции и
 еще 4 на адрес, куда же нужно прыгнуть. Все переходы происходят относительно 
того места, откуда была вызвана команда. Почему? Потому что программа уже была 
скомпилирована, а значит - внутри там все максимально компактно сжато, все 
адреса для переходов и адреса функций рассчитываются тогда же и не меняются по 
ходу работы программы. То есть существует строго определенный порядок, с 
точностью вплоть до байта. И его нарушать нельзя, иначе очень велика вероятность
 того, что программа не поймет, что ей делать дальше. Так что если мы заменяем 
инструкцию, длина опкодов которой больше пяти байт, нам нужно заполнить 
оставшееся место чем-то бесполезным. Тут к нам на помощь приходит команда NOP. 
В результате игра послушно перенесет нас по новому адресу, где выполнит наш код.
 А потом что? А потом она завершится с ошибкой, потому что после нашего кода 
должен быть переход обратно в игровой код, которого там нет. Поэтому перехода у 
нас два - сперва из игрового кода, заменив инструкцию игры, а затем - из нашей 
инъекции обратно в игровой код.
 
Настало время нам написать уже хотя бы пару строк кода. Открываем редактор и 
пишем:
 
/*-Исходный код нашего инжектора----------------------------------------------*/
 
#include <stdio.h> /* Подключаем заголовочные файлы нужных функций. */
#include <windows.h>
 
int main() /* Точка входа. */
{
/* Объявляем переменную, в которую попадает адрес выделенного участка 
  памяти. */
LPVOID addr_allocated_memory = VirtualAlloc(0, 10, MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
if (addr_allocated_memory != 0) { /* Если память выделилась успешно, */
/* то выводим на экран адрес участка.*/
printf("allocated %08X\n", addr_allocated_memory);
/* Вызываем функцию для очистки памяти. */
   int r = VirtualFree(addr_allocated_memory, 0, MEM_RELEASE);
if (r != 0) /* Если успешно очистили память, выводим адрес еще раз. */
printf("released %08X\n", addr_allocated_memory);
}
return 0; /* Выход. */
}
 
/*-Исходный код нашего инжектора----------------------------------------------*/
 
Делаем мы, как видно из комментариев, простую вещь - выделяем 10 байт памяти, а 
затем сразу же их очищаем, выведя на экран адрес выделенного участка. Функции 
для работы с памятью не имеют суффикса "Ex", это значит, что память выделяется в
адресном пространстве нашего же процесса. Ну, это пока что.
 
Выделять и очищать память мы научились, теперь будем записывать туда наш код. 
Первый же вопрос, который возникает - что же записывать. Открываем отладчик, 
выбираем какой-нибудь адрес выше точки входа или ниже команды выхода, а затем 
думаем. Допустим, что в нашей инъекции будет одна-единственная команда NOP. Она 
занимает 1 байт. Помимо этого нам нужна одна команда JMP, которая займет 5 байт 
(1 байт на команду и еще 4 на адрес). Итого - 6. Примерно так:
 
/*----------------------------------------------------------------------------*/
 
post-6695-0-20425000-1426463710.png
 
/*----------------------------------------------------------------------------*/
 
Обе команды JMP пока никуда не ведут, потому что мы не вычислили, куда нам 
прыгать. Так как все прыжки совершаются относительно, то вычислять мы будем так:
 
Для начала, прыгнем из игрового кода в нашу инъекцию. Берем адрес откуда, адрес 
куда, еще 5 - это длина jmp и адреса. И считаем:
 
offset = addr_to - addr_from - 0x5 = 0x401011 - 0x401000 - 0x5 = 0xC
 
Записывать в опкодах это смещение нам нужно задом наперед. Почему? Можно 
почитать в википедии (статья "Порядок байтов", или же big endian little endian в
 поисковике). В результате опкоды примут вид:
 
E90C000000
 
Как видим, ровно 5 байт. И прыгнет эта штука на 12 байт вперед, то есть на адрес
 0x401011, что нам и требовалось. Теперь нам осталась задачка посложнее - это 
подсчитать второй оффсет, для выхода. У нас есть адрес выделенной памяти и длина
 кода нашей инъекции. Считаем:
 
offset = addr_to + 0x5 - addr_allocated + 0x1 - 0x5 = 0x401000 + 0x5 - 0x401011 
+ 0x1 - 0x5 = 0x401005 - 0x401012 - 0x5 = 0xFFFFFFEE
 
Если кого-то испугал результат, то пугаться не стоит - это просто представление 
отрицательного числа в 16-ричной системе счисления. Как и в прошлый раз, нам 
необходимо развернуть адрес задом наперед. Получаем опкоды команды:
 
E9EEFFFFFF
 
Если запишем вручную это в отладчике (ставим курсор на нужную строчку, нажимаем 
Ctrl + E, вводим байты), то получим такую картину:
 
/*----------------------------------------------------------------------------*/
 
post-6695-0-89816800-1426463712.png
 
/*----------------------------------------------------------------------------*/
 
Теперь мы вплотную подошли к тому, чтобы наша программа записывала это все не в 
собственную память, а в чужое адресное пространство. Беда в том, что тестовая 
программа на ассемблере слишком быстро завершается. Исправим ее так, чтобы она 
завершалась по нажатию клавиши, а мы за это время могли бы изменить ее память:
 

/*-Исходный код модификации тестовой программы--------------------------------*/
 

include 'win32ax.inc'
 
.code
 
start:
    nop
    nop
    nop
    nop
    nop
    nop
    nop
    invoke MessageBox,0,0,0,0
    invoke ExitProcess,0
 
.end start

 
/*-Исходный код модификации тестовой программы--------------------------------*/
 

Мы вызываем функцию MessageBox перед выходом, так что пока мы не нажмем кнопку 
"ОК", программа не завершится. Настал черед нашей инъекции. Пишем:

 
/*-Исходный код готового инжектора--------------------------------------------*/
 

#include <windows.h>
#include <tlhelp32.h>
#include <string.h>
 
#define PROCNAME "example1.EXE" /* Имя исполняемого файла. */
#define ADDRINJ 0x401000 /* Адрес инъекции. */
 
void get_phandle(); /* Прототипы функций. */
void inject(DWORD addr);
 
HANDLE hProcess; /* Глобальная переменная под хэндл процесса. */
 
int main()
{
    get_phandle(); /* Получаем хэндл. */
    if (hProcess == 0) return -1;
    LPVOID addr_allocated_memory = VirtualAllocEx(hProcess, 0, 6, MEM_RESERVE |
        MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (addr_allocated_memory != 0) {
        /* Делаем инъекцию, передав в функцию адрес выделенной памяти. */
        inject((DWORD)addr_allocated_memory);
        /* Записываем обратно оригинальный код. */
        char original_code[5] = { 0x90, 0x90, 0x90, 0x90, 0x90 };
        WriteProcessMemory(hProcess, (LPVOID)ADDRINJ, &original_code, 5, 0);
        /* Очищаем выделенную память. */
        VirtualFreeEx(hProcess, addr_allocated_memory, 0, MEM_RELEASE);
    }
    return 0; /* И выходим. */
}
 
void get_phandle()
{
    PROCESSENTRY32 p;
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    Process32First(hSnap, &p);
    while (Process32Next(hSnap, &p))
        if (strstr(p.szExeFile, PROCNAME) != 0)
            hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, p.th32ProcessID);
    CloseHandle(hSnap);
}
 
void inject(DWORD addr)
{
    char injection_code[1] = { 0x90 }; /* Байткод нашей инъекции пишем первым */
    WriteProcessMemory(hProcess, (LPVOID)addr, &injection_code, 1, 0);
 
    char jmp[5] = { 0xE9 }; /* Байткод перехода из инъекции обратно в 
    оригинальный код. */
    DWORD offset = ADDRINJ + 0x5 - addr + 0x1 - 0x5; /* Считаем смещение. */
    memcpy(jmp + 1, &offset, sizeof(DWORD)); /* Копируем в байткод.
    Функция memcpy за нас развернет адрес - вручную этого делать не нужно.
    Записываем. К адресу прибавили 1, потому что 1 - длина кода инъекции. */
    WriteProcessMemory(hProcess, (LPVOID)(addr + 1), &jmp, 5, 0); 
    /* Теперь запишем переход в нашу выделенную память из оригинального кода. 
    Для удобства используем уже объявленные переменные. */
    offset = addr - ADDRINJ - 0x5;
    memcpy(jmp + 1, &offset, sizeof(DWORD));
    WriteProcessMemory(hProcess, (LPVOID)ADDRINJ, &jmp, 5, 0);
    /* Готово! */
}

 
/*-Исходный код готового инжектора--------------------------------------------*/
 

Я не стал комментировать функцию поиска хэндла - эта функция встречается всюду и
 всем хорошо знакома. Остальное прокомментировано достаточно полно, на мой 
взгляд. Лишний вывод в консоль я тоже убрал. Результат можно посмотреть на 
картинках (если кому-то интересно отлаживать свой код):
 
/*----------------------------------------------------------------------------*/
 
post-6695-0-60694900-1426463716.png
 
/*----------------------------------------------------------------------------*/
 
/*----------------------------------------------------------------------------*/
 
post-6695-0-88439100-1426463722.png
 
/*----------------------------------------------------------------------------*/
 
Исходный код прикладываю к статье - это бонус! :D
 
На этом, думаю, можно закругляться. Главное - внимательность и усердие. Второе 
главное - закрывать хэндлы и проверять возвращаемые значения функций. И 
обязательно очищать всю выделенную память.
 
Напоследок передам пару приветов - проекту gamehacklab.ru, его администратору 
Xipho, а так же свои приветы получают MasterGH и Coder.
 
 
 
  • Плюс 1
Ссылка на комментарий
Поделиться на другие сайты

×
×
  • Создать...

Важная информация

Находясь на нашем сайте, Вы автоматически соглашаетесь соблюдать наши Условия использования.