При поиске "Unicorn Engine" на Хабре, я с удивлением обнаружил, что этот инструмент еще ни разу не попадал в статьи. Я попробую заполнить эту пустоту. Начнем, пожалуй, с азов, и посмотрим на пример использования эмулятора в реальной жизни. Для того, чтобы не изобретать велосипед, я решил просто перевести этот мануал. Перед началом скажу, что все мои комментарии или замечания будут выглядеть так.
Что такое Unicorn Engine?
Сами разработчики пишут о Двигатель Единорога Unicorn Engine так:
Unicorn — это легковесный, мультиплатформенный и мультиархитектурный эмулятор процессора.
Это не стандартный эмулятор. Он не эмулирует работу всей программы или целой ОС. Он не поддерживает системные команды (такие как открытие файла, вывод символа в консоль и т. д.). Вам прийдется самостоятельно заниматься разметкой памяти и загрузкой данных в нее, а дальше вы просто запускаете выполнение с какого-то конкретного адреса.
Так чем же он полезен?
- При анализе вирусов, вы можете вызывать одиночные функции, не создавая вредоносного процесса.
- Для решения CTF.
- Для фаззинга.
- Плагин для gdb для предсказывания будущего состояния, к примеру, будущих прыжков или значений регистров.
- Эмуляции обфурсифицированного кода.
Что вам понадобится?
- Установленный Unicorn Engine с привязкой к Python.
- Дизассемблер.
Пример
Для примера возьмем задачу с hxp CTF 2017 под именем Fibonacci. Бинарник можно загрузить здесь.
Когда вы запускаете программу, она начинает выводить наш флаг в консоль, но очень медленно. Каждый следующий байт флага считается все медленнее и медленнее.
The flag is: hxp{F
Это означает, что для получения флага в разумные сроки, нам необходимо оптимизировать работу данного приложения.
С помощью IDA Pro (я лично использовал radare2 + Cutter) мы декомпилировали код в С-подобный псевдокод. Не смотря на то, что код не декомпилировался должным образом, мы все равно можем получить от него информацию о том, что происходит внутри.
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
void *v3; // rbp@1
int v4; // ebx@1
signed __int64 v5; // r8@2
char v6; // r9@3
__int64 v7; // r8@3
char v8; // cl@3
__int64 v9; // r9@5
int a2a; // [sp+Ch] [bp-1Ch]@3
v3 = &encrypted_flag;
v4 = 0;
setbuf(stdout, 0LL);
printf("The flag is: ", 0LL);
while ( 1 )
{
LODWORD(v5) = 0;
do
{
a2a = 0;
fibonacci(v4 + v5, &a2a);
v8 = v7;
v5 = v7 + 1;
}
while ( v5 != 8 );
v4 += 8;
if ( (unsigned __int8)(a2a << v8) == v6 )
break;
v3 = (char *)v3 + 1;
_IO_putc((char)(v6 ^ ((_BYTE)a2a << v8)), stdout);
v9 = *((char *)v3 - 1);
}
_IO_putc(10, stdout);
return 0LL;
}
unsigned int __fastcall fibonacci(int i, _DWORD *a2)
{
_DWORD *v2; // rbp@1
unsigned int v3; // er12@3
unsigned int result; // eax@3
unsigned int v5; // edx@3
unsigned int v6; // esi@3
unsigned int v7; // edx@4
v2 = a2;
if ( i )
{
if ( i == 1 )
{
result = fibonacci(0, a2);
v5 = result - ((result >> 1) & 0x55555555);
v6 = ((result - ((result >> 1) & 0x55555555)) >> 2) & 0x33333333;
}
else
{
v3 = fibonacci(i - 2, a2);
result = v3 + fibonacci(i - 1, a2);
v5 = result - ((result >> 1) & 0x55555555);
v6 = ((result - ((result >> 1) & 0x55555555)) >> 2) & 0x33333333;
}
v7 = v6 + (v5 & 0x33333333) + ((v6 + (v5 & 0x33333333)) >> 4);
*v2 ^= ((BYTE1(v7) & 0xF) + (v7 & 0xF) + (unsigned __int8)((((v7 >> 8) & 0xF0F0F) + (v7 & 0xF0F0F0F)) >> 16)) & 1;
}
else
{
*a2 ^= 1u;
result = 1;
}
return result;
}
Вот ассемблерный код main и fibonacci функций:
.text:0x4004E0 main proc near ; DATA XREF: start+1Do
.text:0x4004E0
.text:0x4004E0 var_1C = dword ptr -1Ch
.text:0x4004E0
.text:0x4004E0 push rbp
.text:0x4004E1 push rbx
.text:0x4004E2 xor esi, esi ; buf
.text:0x4004E4 mov ebp, offset unk_4007E1
.text:0x4004E9 xor ebx, ebx
.text:0x4004EB sub rsp, 18h
.text:0x4004EF mov rdi, cs:stdout ; stream
.text:0x4004F6 call _setbuf
.text:0x4004FB mov edi, offset format ; "The flag is: "
.text:0x400500 xor eax, eax
.text:0x400502 call _printf
.text:0x400507 mov r9d, 49h
.text:0x40050D nop dword ptr [rax]
.text:0x400510
.text:0x400510 loc_400510: ; CODE XREF: main+8Aj
.text:0x400510 xor r8d, r8d
.text:0x400513 jmp short loc_40051B
.text:0x400513 ; ---------------------------------------------------------------------------
.text:0x400515 align 8
.text:0x400518
.text:0x400518 loc_400518: ; CODE XREF: main+67j
.text:0x400518 mov r9d, edi
.text:0x40051B
.text:0x40051B loc_40051B: ; CODE XREF: main+33j
.text:0x40051B lea edi, [rbx+r8]
.text:0x40051F lea rsi, [rsp+28h+var_1C]
.text:0x400524 mov [rsp+28h+var_1C], 0
.text:0x40052C call fibonacci
.text:0x400531 mov edi, [rsp+28h+var_1C]
.text:0x400535 mov ecx, r8d
.text:0x400538 add r8, 1
.text:0x40053C shl edi, cl
.text:0x40053E mov eax, edi
.text:0x400540 xor edi, r9d
.text:0x400543 cmp r8, 8
.text:0x400547 jnz short loc_400518
.text:0x400549 add ebx, 8
.text:0x40054C cmp al, r9b
.text:0x40054F mov rsi, cs:stdout ; fp
.text:0x400556 jz short loc_400570
.text:0x400558 movsx edi, dil ; c
.text:0x40055C add rbp, 1
.text:0x400560 call __IO_putc
.text:0x400565 movzx r9d, byte ptr [rbp-1]
.text:0x40056A jmp short loc_400510
.text:0x40056A ; ---------------------------------------------------------------------------
.text:0x40056C align 10h
.text:0x400570
.text:0x400570 loc_400570: ; CODE XREF: main+76j
.text:0x400570 mov edi, 0Ah ; c
.text:0x400575 call __IO_putc
.text:0x40057A add rsp, 18h
.text:0x40057E xor eax, eax
.text:0x400580 pop rbx
.text:0x400581 pop rbp
.text:0x400582 retn
.text:0x400582 main endp
.text:0x400670 fibonacci proc near ; CODE XREF: main+4Cp
.text:0x400670 ; fibonacci+19p ...
.text:0x400670 test edi, edi
.text:0x400672 push r12
.text:0x400674 push rbp
.text:0x400675 mov rbp, rsi
.text:0x400678 push rbx
.text:0x400679 jz short loc_4006F8
.text:0x40067B cmp edi, 1
.text:0x40067E mov ebx, edi
.text:0x400680 jz loc_400710
.text:0x400686 lea edi, [rdi-2]
.text:0x400689 call fibonacci
.text:0x40068E lea edi, [rbx-1]
.text:0x400691 mov r12d, eax
.text:0x400694 mov rsi, rbp
.text:0x400697 call fibonacci
.text:0x40069C add eax, r12d
.text:0x40069F mov edx, eax
.text:0x4006A1 mov ebx, eax
.text:0x4006A3 shr edx, 1
.text:0x4006A5 and edx, 55555555h
.text:0x4006AB sub ebx, edx
.text:0x4006AD mov ecx, ebx
.text:0x4006AF mov edx, ebx
.text:0x4006B1 shr ecx, 2
.text:0x4006B4 and ecx, 33333333h
.text:0x4006BA mov esi, ecx
.text:0x4006BC
.text:0x4006BC loc_4006BC: ; CODE XREF: fibonacci+C2j
.text:0x4006BC and edx, 33333333h
.text:0x4006C2 lea ecx, [rsi+rdx]
.text:0x4006C5 mov edx, ecx
.text:0x4006C7 shr edx, 4
.text:0x4006CA add edx, ecx
.text:0x4006CC mov esi, edx
.text:0x4006CE and edx, 0F0F0F0Fh
.text:0x4006D4 shr esi, 8
.text:0x4006D7 and esi, 0F0F0Fh
.text:0x4006DD lea ecx, [rsi+rdx]
.text:0x4006E0 mov edx, ecx
.text:0x4006E2 shr edx, 10h
.text:0x4006E5 add edx, ecx
.text:0x4006E7 and edx, 1
.text:0x4006EA xor [rbp+0], edx
.text:0x4006ED pop rbx
.text:0x4006EE pop rbp
.text:0x4006EF pop r12
.text:0x4006F1 retn
.text:0x4006F1 ; ---------------------------------------------------------------------------
.text:0x4006F2 align 8
.text:0x4006F8
.text:0x4006F8 loc_4006F8: ; CODE XREF: fibonacci+9j
.text:0x4006F8 mov edx, 1
.text:0x4006FD xor [rbp+0], edx
.text:0x400700 mov eax, 1
.text:0x400705 pop rbx
.text:0x400706 pop rbp
.text:0x400707 pop r12
.text:0x400709 retn
.text:0x400709 ; ---------------------------------------------------------------------------
.text:0x40070A align 10h
.text:0x400710
.text:0x400710 loc_400710: ; CODE XREF: fibonacci+10j
.text:0x400710 xor edi, edi
.text:0x400712 call fibonacci
.text:0x400717 mov edx, eax
.text:0x400719 mov edi, eax
.text:0x40071B shr edx, 1
.text:0x40071D and edx, 55555555h
.text:0x400723 sub edi, edx
.text:0x400725 mov esi, edi
.text:0x400727 mov edx, edi
.text:0x400729 shr esi, 2
.text:0x40072C and esi, 33333333h
.text:0x400732 jmp short loc_4006BC
.text:0x400732 fibonacci endp
На данном этапе у нас много возможностей решить эту задачу. К примеру, мы можем восстановить код с использованием одного из языков программирования и применить оптимизацию там, но процесс восстановления кода — это очень сложная задача, во время которой мы можем допустить ошибки. Ну а сравнивать потом код, чтобы найти ошибку, — вообще никуда не годится. Но, если мы используем Unicorn Engine, то мы можем пропустить этап реконструкции кода и избежать проблемы, описанной выше. Мы, конечно, можем избежать этих неприятностей с помощью frida или написанием скриптов для gdb, но речь здесь не о том.
Прежде чем начинать оптимизацию, мы запустим эмуляцию в Unicorn Engine без изменений программы. И уже только после успешного запуска, перейдем к оптимизации.
Шаг 1: Да прийдет виртуализация
Давайте создадим файл fibonacci.py и сохраним рядом с бинарником.
Начнем с импортирования необходимых библиотек:
from unicorn import *
from unicorn.x86_const import *
import struct
Первая строка загружает основной бинарник и базовые Unicorn константы. Вторая строка загружает константы для двух архитектур x86 и x86_64.
Дальше добавим некоторые нужные функции:
def read(name):
with open(name) as f:
return f.read()
def u32(data):
return struct.unpack("I", data)[0]
def p32(num):
return struct.pack("I", num)
Здесь мы объявили функции, которые нам понадобятся позже:
- read просто возвращает контент файла,
- u32 берет 4-байтную строку в LE кодировке и конвертирует в int,
- p32 делает противоположное действие — он берет число и превращает в 4-байтную строку в LE кодировке.
Примечание: Если у вас есть установленный pwntools, то вам не нужно создавать эти функции, вам достаточно импортировать их:
from pwn import *
И так, наконец-то приступим к инициализации нашего Unicorn Engine класса для архитектуры x86_64:
mu = Uc (UC_ARCH_X86, UC_MODE_64)
Тут мы вызываем функции Uc со следующими параметрами:
- первый параметр — это основная архитектура. Константы начинаются с UC_ARCH_;
- второй параметр — это спецификация архитектуры. Константы начинаются с UC_MODE_.
Вы можете найти все константы в Cheatsheet.
Как я писал выше, для использования Unicorn Engine, нам понадобиться инициализировать виртуальную память вручную. Для этого примера нам нужно где-то в памяти разместить код и стек.
Базовый адрес (Base addr) бинарника начинается c 0x400000. Давайте разместим наш стек в 0x0 и выделим для него 1024*1024 памяти. Скорей всего, нам не нужно столько места, но это все равно не повредит.
Мы можем размечать память вызывая метод mem_map.
Добавим эти строки:
BASE = 0x400000
STACK_ADDR = 0x0
STACK_SIZE = 1024*1024
mu.mem_map(BASE, 1024*1024)
mu.mem_map(STACK_ADDR, STACK_SIZE)
Теперь нам нужно загрузить бинарник в его основной адрес точно так же, как это делает загрузчик. После этого нам нужно выставить RSP в конец стека.
mu.mem_write(BASE, read("./fibonacci"))
mu.reg_write(UC_X86_REG_RSP, STACK_ADDR + STACK_SIZE - 1)
Теперь мы можем начать эмуляцию и запускать код, но нам нужно выяснить, с какого адреса начинать работу и когда эмулятору стоит остановиться.
Возьмем адрес первой команды из main(), мы можем начать эмуляцию с 0x004004e0. Концом будет считаться вызов putc("\n"), который находится по адресу 0x00400575, после выведения всего флага.
.text:0x400570 mov edi, 0Ah ; c
.text:0x400575 call __IO_putc
Мы можем начать эмуляцию:
mu.emu_start(0x004004e0,0x00400575)
Теперь запустим скрипт:
a@x:~/Desktop/unicorn_engine_lessons$ python solve.py
Traceback (most recent call last):
File "solve.py", line 32, in <module>
mu.emu_start(0x00000000004004E0, 0x0000000000400575)
File "/usr/local/lib/python2.7/dist-packages/unicorn/unicorn.py", line 288, in emu_start
raise UcError(status)
unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)
Упс, что-то пошло не так, но мы даже не знаем что. Сразу перед вызовом mu.emu_start мы можем добавить:
def hook_code(mu, address, size, user_data):
print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size))
mu.hook_add(UC_HOOK_CODE, hook_code)
Этот код добавляет хук. Мы объявляем собственную функцию hook_code, которая вызывается эмулятором перед каждой командой. Она принимает такие параметры:
- наш экземпляр Uc,
- адрес инструкции,
- размер инструкции,
- данные пользователя (мы можем передать это значение с помощью необязательного аргумента в hook_add()).
Теперь, если мы запустим скрипт, то должны увидеть такой вывод:
a@x:~/Desktop/unicorn_engine_lessons$ python solve.py >>> Tracing instruction at 0x4004e0, instruction size = 0x1 >>> Tracing instruction at 0x4004e1, instruction size = 0x1 >>> Tracing instruction at 0x4004e2, instruction size = 0x2 >>> Tracing instruction at 0x4004e4, instruction size = 0x5 >>> Tracing instruction at 0x4004e9, instruction size = 0x2 >>> Tracing instruction at 0x4004eb, instruction size = 0x4 >>> Tracing instruction at 0x4004ef, instruction size = 0x7 Traceback (most recent call last): File "solve.py", line 41, in <module> mu.emu_start(0x00000000004004E0, 0x0000000000400575) File "/usr/local/lib/python2.7/dist-packages/unicorn/unicorn.py", line 288, in emu_start raise UcError(status) unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)
По адресу, на котором произошла ошибка, мы можем понять, что наш скрипт не может обработать эту команду:
.text:0x4004EF mov rdi, cs:stdout ; stream
Эта инструкция считывает данные с адреса 0x601038 (вы можете увидеть это в IDA Pro). Это .bss секция, которую мы не размечали. Моим решением будет просто пропустить все проблематичные инструкции, если это не влияет на логику программы.
Ниже есть еще одна проблемная инструкция:
.text:0x4004F6 call _setbuf
Мы не можем вызывать любые функции с glibc, так как у нас нет загруженных glibc в памяти. В любом случае, нам эта команда не нужна, так что, можем тоже ее пропустить.
Вот полный список команд, которые нужно пропустить:
.text:0x4004EF mov rdi, cs:stdout ; stream .text:0x4004F6 call _setbuf .text:0x400502 call _printf .text:0x40054F mov rsi, cs:stdout ; fp
Для пропуска команд нам нужно перезаписывать RIP на следующую инструкцию:
mu.reg_write(UC_X86_REG_RIP, address+size)
Теперь hook_code должен выглядеть как-то так:
instructions_skip_list = [0x004004ef,0x004004f6,0x00400502,0x0040054f] def hook_code(mu, address, size, user_data): print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size)) if address in instructions_skip_list: mu.reg_write(UC_X86_REG_RIP, address+size)
Нам также нужно что-то сделать с инструкциями, которые выводят флаг в консоль байт-за-байтом.
.text:0x400558 movsx edi, dil ; c .text:0x40055C add rbp, 1 .text:0x400560 call __IO_putc
__IO_putc принимает байты для вывода в качестве первого аргумента (это регистр RDI).
Мы можем считывать данные непосредственно с регистра, выводить данные в консоль и пропускать этот набор инструкций. Обновленный hook_code представлен ниже:
instructions_skip_list = [0x004004ef,0x004004f6,0x00400502,0x0040054f] def hook_code(mu, address, size, user_data): #print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' %(address, size)) if address in instructions_skip_list: mu.reg_write(UC_X86_REG_RIP, address+size) elif address == 0x400560: # c = mu.reg_read(UC_X86_REG_RDI) print(chr(c),end="") mu.reg_write(UC_X86_REG_RIP, address+size)
Мы можем запустить и это все будет работать, но по прежнему медленно.
Шаг 2: Увеличим скорость!
Давайте подумаем про увеличение скорости работы. Почему эта программа такая медленная?
Если мы посмотрим на декомпилированный код, то увидим, что main() вызывает fibonacci() несколько раз и fibonacci() — это рекурсивная функция. Посмотрим на эту функцию поближе, она принимает и возвращает два аргумента. Первое возвращаемое значение передается через RAX регистр, второе возвращается через ссылку, которую передали через второй аргумент функции. Если мы посмотрим глубже, на связь main() и fibonacci(), то мы увидим, что второй аргумент принимает только два возможных значения: 0 или 1. Если вы этого все еще не видите, то запустите gdb и поставьте точку остановки на начало функции fibonacci().
Для оптимизации работы алгоритма, мы можем воспользоваться динамическим программированием для того, чтобы запомнить возвращаемые значение для входящих параметров. Подумайте сами, второй аргумент может принимать только два возможных значения, так что нам достаточно запомнить лишь пар.
Для тех, кто не понялfibonacci — это рекурсивная функция, которая вычисляет следующее значение как сумму двух предыдущих. На каждом шаге она заходит все глубже. Каждый раз, когда она начинает сначала, она проходит тот же путь, что и раньше, плюс одно новое значение.
Пример:
Допустим, глубина = 6, тогда: 1 1 2 3 5 8.
А теперь глубина = 8, тогда: 1 1 2 3 5 8 13 21.
Мы бы могли просто запомнить, что первые 6 членов это 1 1 2 3 5 8, и когда нас просят посчитать больше, чем мы запомнили, то мы берем то, что запомнили и считаем только то, чего не хватает.
Как только RIP находится в начале fibonacci(), мы можем получить аргументы функции. Мы знаем, что функция возвращает результат, когда выходит из функции. Так как мы не можем оперировать двумя параметрами сразу, нам понадобится стек для возвращения параметров. Во время входа в fibonacci() нам нужно положить аргументы в стек, и забрать их при выходе. Для хранения посчитанных пар мы можем использовать словарь.
Как обработать пару значений?
- В самом начале функции мы можем проверить, нет ли этой пары в уже известных нам результатах:
- если есть, то мы можем возвращать эту пару. Нам всего лишь нужно записать возвращаемые значения в RAX и по адресу ссылки, которая находится во втором аргументе. Также мы присваиваем RIP адрес выхода из функции. Мы не можем использовать RET в fibonacci(), так как эти вызовы находятся под хуком, так что мы возьмем какой-то RET из main();
- если этих значений нет, то мы просто добавляем их в стек.
- Перед выходом из функции мы можем сохранить возвращаемую пару. Мы знаем входящие аргументы, так как мы можем их прочитать с нашего стека.
Этот код представлен здесьFIBONACCI_ENTRY = 0x00400670 FIBONACCI_END = [ 0x004006f1, 0x00400709] instructions_skip_list = [0x004004ef,0x004004f6,0x00400502,0x0040054f] # Стек для хранения аргументов stack = [] # Словарь, в котором мы храним известные пары d = {} def hook_code(mu, address, size, user_data): if address in instructions_skip_list: mu.reg_write(UC_X86_REG_RIP, address+size) # Эта инструкция выводит байт флага elif address == 0x400560: c = mu.reg_read(UC_X86_REG_RDI) print(chr(c),end="") mu.reg_write(UC_X86_REG_RIP, address+size) # Мы находимся в начале функции? elif address == FIBONACCI_ENTRY: # Считать первый аргумент из RDI arg0 = mu.reg_read(UC_X86_REG_RDI) # Считать второй аргумент (ссылка) r_rsi = mu.reg_read(UC_X86_REG_RSI) # Считать второй аргумент, перейдя по ссылке arg1 = u32(mu.mem_read(r_rsi, 4)) # Проверить, сохраненная ли это пара? if (arg0,arg1) in d: (ret_rax, ret_ref) = d[(arg0,arg1)] # Поместить сохраненное значение в RAX mu.reg_write(UC_X86_REG_RAX, ret_rax) # Записать результат через ссылку mu.mem_write(r_rsi, p32(ret_ref)) # Поменять RIP на RET инструкцию, так как мы хотим выйти из fibonacci mu.reg_write(UC_X86_REG_RIP, 0x400582) else: # Если аргументы не были ранее сохранены, то помещаем их в стек stack.append((arg0,arg1,r_rsi)) elif address in FIBONACCI_END: # Получаем аргументы из стека (arg0, arg1, r_rsi) = stack.pop() # Считываем возвращаемое значение из RAX ret_rax = mu.reg_read(UC_X86_REG_RAX) # Считываем значение, которое было передано по ссылке ret_ref = u32(mu.mem_read(r_rsi,4)) # Запоминаем эту пару в словарь d[(arg0, arg1)]=(ret_rax, ret_ref)
Вот весь скрипт#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import print_function from unicorn import * from unicorn.x86_const import * import struct def read(name): with open(name) as f: return f.read() def u32(data): return struct.unpack("I", data)[0] def p32(num): return struct.pack("I", num) FIBONACCI_ENTRY = 0x00400670 FIBONACCI_END = [ 0x004006f1, 0x00400709] instructions_skip_list = [0x004004ef,0x004004f6,0x00400502,0x0040054f] # Стек для хранения аргументов stack = [] # Словарь, в котором мы храним известные пары d = {} def hook_code(mu, address, size, user_data): if address in instructions_skip_list: mu.reg_write(UC_X86_REG_RIP, address+size) # Эта инструкция выводит байт флага elif address == 0x400560: c = mu.reg_read(UC_X86_REG_RDI) print(chr(c),end="") mu.reg_write(UC_X86_REG_RIP, address+size) # Мы находимся в начале функции? elif address == FIBONACCI_ENTRY: # Считать первый аргумент из RDI arg0 = mu.reg_read(UC_X86_REG_RDI) # Считать второй аргумент (ссылка) r_rsi = mu.reg_read(UC_X86_REG_RSI) # Считать второй аргумент, перейдя по ссылке arg1 = u32(mu.mem_read(r_rsi, 4)) # Проверить, сохраненная ли это пара? if (arg0,arg1) in d: (ret_rax, ret_ref) = d[(arg0,arg1)] # Поместить сохраненное значение в RAX mu.reg_write(UC_X86_REG_RAX, ret_rax) # Записать результат через ссылку mu.mem_write(r_rsi, p32(ret_ref)) # Поменять RIP на RET инструкцию. Мы хотим выйти из fibonacci mu.reg_write(UC_X86_REG_RIP, 0x400582) else: # Если аргументы не были ранее сохранены, то помещаем их в стек stack.append((arg0,arg1,r_rsi)) elif address in FIBONACCI_END: # Получаем аргументы из стека (arg0, arg1, r_rsi) = stack.pop() # Считываем возвращаемое значение из RAX ret_rax = mu.reg_read(UC_X86_REG_RAX) # Считываем значение, которое было переданное через ссылку ret_ref = u32(mu.mem_read(r_rsi,4)) # Запоминаем эту пару в словарь d[(arg0, arg1)]=(ret_rax, ret_ref) mu = Uc (UC_ARCH_X86, UC_MODE_64) BASE = 0x400000 STACK_ADDR = 0x0 STACK_SIZE = 1024*1024 mu.mem_map(BASE, 1024*1024) mu.mem_map(STACK_ADDR, STACK_SIZE) mu.mem_write(BASE, read("./fibonacci")) mu.reg_write(UC_X86_REG_RSP, STACK_ADDR + STACK_SIZE - 1) mu.hook_add(UC_HOOK_CODE, hook_code) mu.emu_start(0x004004e0, 0x00400575) print()
Ура, мы наконец-то смогли оптимизировать приложение, используя Unicorn Engine. Хорошая работа!
Заметка
Теперь я решил дать вам небольшое домашнее задание.
Здесь вы можете найти еще три задачи, каждая из которых имеет подсказку и полное решение. Вы можете подглядывать в Cheatsheet во время решение задач.
Одна из самых назойливых проблем — это вспомнить имя нужной константы. С этим легко бороться, если использовать Tab-дополнения в IPython. Когда у вас есть установленный IPython, вы можете написать from unicorn import UC_ARCH_ нажать Tab и вам будет выведены все константы, которые начинаются так же.
- В самом начале функции мы можем проверить, нет ли этой пары в уже известных нам результатах:
Комментарии (4)
loony_dev Автор
27.11.2018 20:55Я точно не знаю, что имел ввиду автор статьи, но я бы пошел почти по такой же схеме как и с эмулятором, только на более высоком уровне. Флов работы был бы такой:
- Создаем словарь для хранения пар
- Ставим хук перед вызовом fibonacci
- Если входящие параметры есть в словаре, то сразу идем на ретурн, если нет, то вызываем оригинальную функцию.
Мы не переписываем алгоритм а просто вызываем исходную функцию.
Если интересно, могу написать более подробно.
А на счет скриптов для gdb, то я с ними ещё не работал и не смогу с этим помочь
loony_dev Автор
27.11.2018 20:56ser-mk Я точно не знаю, что имел ввиду автор статьи, но я бы пошел почти по такой же схеме как и с эмулятором, только на более высоком уровне. Флов работы был бы такой:
— Создаем словарь для хранения пар
— Ставим хук перед вызовом fibonacci
— Если входящие параметры есть в словаре, то сразу идем на ретурн, если нет, то вызываем оригинальную функцию.
Мы не переписываем алгоритм а просто вызываем исходную функцию.
Если интересно, могу написать более подробно.
А на счет скриптов для gdb, то я с ними ещё не работал и не смогу с этим помочьser-mk
27.11.2018 22:34Тогда добавьте хаб перевод и ссылку на статью, без этого трудно догадаться о переводе.
ser-mk
Вы про ошибки при переписывании алгоритма?
Если да, то интересно, чем здесь так поможет frida или скрипты gdb?