Решение одного таска с помощью pin
В этой статье я попробую описать не стандартный ход решения одной проблемы с помощью pintool.
Задача
Нам необходимо 10000 раз выиграть в игре сапер. Поле игры - 24х24 с 99 минами. Все мины хранятся в константом массиве размера 99 * 10000 * 2 * 4 байт. После прохождения 10000 игр, нам выдается победный флаг.
Декомпилированный код игры:
void play(void)
{
char board[25];
char board_[25];
int magic = 0;
for (int i = 0; i <= 9999; ++i )
{
printf("New game_%d: \n", i);
int win = 0;
int empty_cells = SIDE * SIDE - MINES;
initialise(board, board_);
placemines(MinesAll[99 * i], board);
int is_first_move = 0;
while ( !win )
{
clear();
printf("Current game_%d: \n", i);
printboard(board_);
make_move(&row, &col);
if ( !is_first_move && ismine(row, col, board) )
replacemine(row, col, board);
magic += row + col;
++is_first_move;
win = playminesuntil(board_, board, MinesAll[99 * i], row, col, &empty_cells);
if ( win )
return;
if ( !empty_cells )
{
printf("\nVictory, but still to win_%d !\n", 10000 - i);
win = 1;
}
}
}
printf("VKACTF{");
magic = 58 * (1337 * magic % 0xFFFFFF);
int v7 = (magic + 7) % 49 + 28;
for (int j = 0; j < v7; ++j )
{
for (int k = 0; k < v7 * v7 / 2 / v7; ++k )
{
char v6 = inter[j * (v7 * v7 / 2 / v7) + k];
int v5 = (magic >> k);
v5 ^= v6;
std::cout << (char)v5;
}
}
puts("}");
}
Проблема
Можно заметить, что флаг, который расшифровывается после победы, зависит от значения переменной magic
, которая, в свою очередь равна magic += row + col
, где row - строка, col - столбец введенной нами клетки.
Конечно, мы можем статично посчитать чему равна эта переменная, сложив все строки и столбцы свободных клеток на поле. Но из-за того, что после выбора клетки, прилежащие к ней клетки тоже открываются и их уже вводить не надо, задача становится нетривиальной.
PIN
Pin - это фреймворк для динамической инструментации прогаммного кода для x86 и x86_x64 архитектур. Пин предоставляет обширный набор апи для любого типа инструментации, единственное ограничение - наше воображение.
Большинство апи можно разделить на следующие категории:
Модуль | Описание |
---|---|
IMG | Для работы с образами программ в памяти |
RTN | Для работы с функциями |
SEC | Для работы с секциями |
INS | Для работы с инструкциями |
REG | Для работы с регистрами |
Подбронее про пин можно прочесть тут.
Идея
Нужно сделать так, чтобы программа сама решала какие ячейки открывать без ввода значений от пользователя.
Для этого неотходимо изменить функцию make_move
в коде программы.
void make_move(int *row, int *col)
{
printf("\nYour move, (row, column) >> ");
scanf("%d %d", row, col);
}
Так же нам нужно знать, в какой момент мы выиграли и в какой момент началась новая игра. Для этого будем использовать функцию printf
как наш oracle. Мы создадим вектор всех мин во всех полях и при начале новой игры (нового поля) будем генерировать вектор свободных ячеек для этого поля.
Решение
Для начала, поскольку массив мин константен и лежит в памяти, сдампим его на диск для дальнейшего использования в нашем pintool.
Для этого в Иде выполним следующий код:
import idaapi
mines = idaapi.get_many_bytes(0x203020, 99 * 2 * 4 * 10000)
open('mines.bin','wb').write(mines)
Напишем функцию, которая загружает мины из файла и сохраняет их в вектор в виде пары строка столбец:
vector<pair<int, int>> v_mines;
void load_mines()
{
char *memblock;
int size;
// open file from disk and read it
ifstream file("mines.bin", ios::in|ios::binary|ios::ate);
if (!file.is_open())
{
logg << "[-] unable to read mines.bin" << endl;
exit(-1);
}
size = file.tellg();
memblock = new char [size];
file.seekg(0, ios::beg);
file.read(memblock, size);
file.close();
logg << "[+] mines are in memory." << endl;
// memblock -> arr of ints.
auto mines = (int*)memblock;
// basic check just to be sure it was loaded correctly
assert(mines[0] == 20);
// fill v_mines vector
for (int i = 0; i < m_size; i += 2)
v_mines.push_back(make_pair(mines[i], mines[i+1]));
logg << "[+] " << v_mines.size() << " mines are loaded." << std::endl;
delete[] memblock;
}
Теперь напишем алгоритм, который будет находить все свободные ячейки для текущего поля.
const int field_size = 24;
vector<pair<int, int>> solution;
// k - field (from 0 up to 9999)
int k_field = 0;
// field to make operations on
char field[field_size][field_size];
void fill()
{
auto st = v_mines.begin() + (k_field * 99);
auto cur_mines = vector<pair<int, int>>(st, st + 99);
for (auto &i : cur_mines)
field[i.first][i.second] = 1;
}
void clear_field()
{
for (int i = 0; i < field_size; i++)
for (int j = 0; j < field_size; j++)
field[i][j] = 0;
}
void generate_solution()
{
solution.clear();
clear_field();
fill();
for (int i = 0; i < field_size; i++)
for (int j = 0; j < field_size; j++)
if (!field[i][j])
solution.push_back(make_pair(i, j));
}
Все что осталось сделать - заменить функцию make_move
и хукнуть функцию printf
.
Хуки будем делать в коллбэке загрузки новых модулей. Пин позволяет регистрировать свой коллбэк, который будет вызывать при загрузки новых модулей в процесс.
Регистрируем коллбэк:
IMG_AddInstrumentFunction(ImageLoad, 0);
/*
Called on every image that was loaded.
*/
VOID ImageLoad(IMG img, VOID *v)
{
logg << "loaded:\t" << IMG_Name(img) << std::endl;
// we replace make_move function in main executable
if (IMG_IsMainExecutable(img))
{
auto imageBase = IMG_LowAddress(img);
// address of make_move in memory
auto funcAddr = imageBase + 0xb67;
auto rtn = RTN_FindByAddress(funcAddr);
if (RTN_Valid(rtn))
{
// replace
RTN_Replace(rtn, AFUNPTR(make_move));
logg << "[+] make_move hijacked." << std::endl;
MineSweeper::load_mines();
}
}
// we will use printf as an oracle, so we hook it as well
// notice that we are not replacing it, but just hooking before the call.
auto rtn = RTN_FindByName(img, "printf");
if (RTN_Valid(rtn))
{
RTN_Open(rtn);
RTN_InsertCall(rtn, IPOINT_BEFORE,
AFUNPTR(PrintfHander),
IARG_FUNCARG_ENTRYPOINT_VALUE, 0,
IARG_END);
RTN_Close(rtn);
}
}
Наш printf
oracle и новый make_move
:
void PrintfHander(char * fmt)
{
string str(fmt);
// if we start a new game
if (str.find("New game") != std::string::npos)
{
MineSweeper::pos = 0;
MineSweeper::generate_solution();
}
// if we win
if (str.find("Victory") != std::string::npos)
MineSweeper::k_field++;
}
void make_move(unsigned int * row, unsigned int * col)
{
PIN_LockClient();
// get current empty position
auto p = MineSweeper::solution[MineSweeper::pos++];
*row = p.first;
*col = p.second;
PIN_UnlockClient();
}
После чего запускаем нашу программу под пином и через ~50 минут получаем флаг :)
Весь код доступен здесь.
Конец.