Разработка → Анализ защиты Sony PlayStation 4

перевод
HotWaterMusic 24 августа 2015 в 00:11 86,8k
Оригинал: CTurt
image

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

В данной статье я затрону некоторые принципы безопасности, касающиеся всех современных систем, а также поделюсь своими находками, сделанными благодаря выполнению ROP-тестов на моей PS4.

Если вы плохо знакомы с применением эксплойтов, вам cледует сначала прочитать мою прошлую статью про взлом игр DS с помощью уязвимости целостности стека (stack smash) в файлах сохранений.

Загрузить всё необходимое для собственных экспериментов можно здесь, на данный момент поддерживается исключительно прошивка 1.76.

Известные факты про PS4


Как вам скорее всего известно, в PS4 используется специальный восьмиядерный x86-64 CPU от AMD, про архитектуру которого опубликовано достаточно много исследований, и даже если эта специфическая версия процессора слегка отличается от общепринятого стандарта, это едва ли будет заметно. Например, PFLA (Page Fault Liberation Army) на 29C3 (29th Chaos Communication Congress) продемонстрировала доказательство proof-of-concept того, что можно реализовать полную по Тюрингу машину, используя лишь страничные ошибки (page fault) и x86 MMU, видео доступно на YouTube. Это будет интересно и тем, кто, запустив код в виртуальной машине, при этом желает выполнять инструкции на CPU хоста.
Новостная статья EurAsia под номером 3251

Причем мы имеем дело не только с хорошо задокументированной архитектурой CPU — использованное в PS4 ПО по большей части относится к open source.

Для нас самым главным является то, что Orbis OS, на которой работает консоль, основана на FreeBSD и использует отдельные части NetBSD, повторяя в этом плане ситуацию с PS3; помимо FreeBSD 9.0, из другого заметного крупного ПО используются Mono VM и WebKit.

Точка входа — WebKit


WebKit — это открытый движок рендеринга веб-страниц в браузерах для iOS, Wii U, 3DS, PS Vita и PS4.

Несмотря на столь широкое применение и зрелость проекта, WebKit не лишен отдельных уязвимостей; о большинстве из них вы можете узнать по записям Pwn2Own.

В частности, браузер в PS4 с прошивкой 1.76 использует версию WebKit, уязвимую к CVE-2012-3748, переполнению буфера в области данных кучи (heap-based buffer overflow) в методе JSArray::sort(...).

В 2014 году nas и Proxima заявили, что им удалось успешно портировать данный эксплойт для использования на браузере PS4, и выложили код PoC в паблик, чем положили начало процессу взлома PS4.

Этот код дает произвольный доступ к чтению и записи всего, что процесс WebKit может читать/записывать, и это в свою очередь может быть использовано для дампа модулей и перезаписывания адресов возврата на стеке, позволив нам установить контроль над cчётчиком команд (для ROP).

С того времени было обнаружено много других уязвимостей в WebKit, которые предположительно позволяют производить дампинг модулей и ROP на новейших прошивках PS4, но в момент написания ни один из этих эксплойтов не был портирован на PS4.

Что такое ROP (return oriented programming)?


В отличие от примитивных устройств вроде DS и PSP, в PS4 используется ядро, контролирующее опции разных областей памяти. Страницы памяти, помеченные выполняемыми, не могут быть перезаписаны; страницы, помеченные записываемыми, не могут быть выполнены; этот принцип известен под названием Data Execution Prevention (DEP).

Для нас это означает невозможность использования простого пути: копирования полезной нагрузки (payload) в память и ее последующего выполнения. Однако, мы можем выполнить код, который уже загружен в память и помечен как выполняемый.

Сама по себе возможность прыжка по одному адресу не несёт особой пользы, если мы не можем записать наш собственный код по этому адресу — вот поэтому мы и прибегнем к ROP.

Возвратно-ориентированное программирование (ROP) — это всего лишь усовершенствованная версия традиционного «stack smashing» (атаки на переполнение буфера), но вместо перезаписывания одного значения, на которое прыгнет PC, мы можем сцепить вместе много различных адресов, известных как «гаджеты»

Обычно, гаджет — это всего лишь единственная желаемая конструкция, за которой следует ret.

В ассемблере x86_64, когда выполнение доходит до инструкции ret, 64-битное значение выталкивается со стека и PC прыгает на него; поскольку мы можем контролировать стек, то можем заставить каждую инструкцию ret прыгать на следующий нужный гаджет.

Например, начиная с 0x80000 могут храниться инструкции:

mov rax, 0
ret

А начиная с 0x90000 хранятся следующие инструкции:

mov rbx, 0
ret

Если мы перезапишем адрес возврата на стеке, чтобы тот хранил 0x80000 и следом 0x90000, то как только выполнение дойдет до первой инструкции ret, оно прыгнет на mov rax, 0, а сразу после этого следующая инструкция ret вытолкнет со стека 0x90000 и прыгнет на mov rbx, 0.

Таким образом, данная цепочка сыграет нам на руку и установит оба регистра rax и rbx в 0, как если бы мы просто написали код в одном месте и выполнили последовательно.

Цепочки ROP не ограничиваются только списком адресов; предположим, что с 0xa0000 идут следующие инструкции:

pop rax
ret

Мы можем установить первый элемент цепочки в 0xa0000 и следующий элемент — в любое желаемое значение для rax.

Гаджеты также не обязаны заканчиваться на инструкции ret; мы можем использовать гаджеты, заканчивающиеся на jmp:

add rax, 8
jmp rcx

Сделав так, что rcx указывает на инструкцию ret, цепочка выполнится обычным образом:

chain.add("pop rcx", "ret");
chain.add("add rax, 8; jmp rcx");

Иногда у вас не получится найти именно тот гаджет, который вам нужен, сам по себе — только с другими инструкциями после него. Например, если вы хотите установить r8 в какое-либо значение, но у вас есть только это гаджет, то вам придется установить r9 в какое-нибудь фиктивное значение:

pop r8
pop r9
ret

Хотя вам время от времени и придется проявить свои способности к творчеству при написании ROP-цепочек, тем не менее, принято считать, что при использовании достаточно большого дампа кода полученных гаджетов будет достаточно для полной по Тьюрингу функциональности; это и делает ROP жизнеспособным методом обхода DEP.

Поиск гаджетов


Понять ROP вам поможет следующая метафора.

Представьте, что вы пишете новую главу в книге, при этом пользуясь исключительно теми словами, которые стояли в концах предложений предыдущих глав. Очевидно, что в силу принципов конструкции фраз вы едва ли встретите слова «and» или «or» в конце одного из предложений — но нам потребуются эти соединительные элементы, если мы хотим написать что-нибудь осмысленное.

Вполне возможно, однако, что одно из предложений завершилось на слове «sand». И, хоть по авторскому замыслу мы и должны прочитать это слово целиком начиная с буквы «s», если мы начнем свое чтение с «a», то по чистой случайности получим совсем другое слово — «and», что нам и требовалось.

Эти принципы также применимы и к ROP.

Поскольку структура практически всех функций выглядит примерно так:
; Сохранение регистров
push    rbp
mov     rbp, rsp
push    r15
push    r14
push    r13
push    r12
push    rbx
sub     rsp, 18h

; Тело функции

; Восстановление регистров
add     rsp, 18h
pop     rbx
pop     r12
pop     r13
pop     r14
pop     r15
pop     rbp
ret

Поэтому стоит ожидать обнаружения только гаджетов pop, или, что бывает реже, xor rax, rax, которые устанавливают значение в 0 перед возвратом.

Сравнение вроде
cmp [rax], r12
ret

не имеет никакого смысла, поскольку результат сравнения не используется функцией. Однако, вероятность обнаружить подобные гаджеты все ещё остается.

Инструкции x86_64 похожи на слова тем, что обладают переменной длиной, и могут значить совершенно разные вещи в зависимости от того, откуда начинается декодирование.

Архитектура x86_64 — это набор инструкций CISC переменной длины. Возвратно-ориентированное программирование на x86_64 пользуется тем фактом, что набор инструкций является очень «плотным» — в том плане, что любая произвольная последовательность байт чаще всего может быть интерпретирована как валидный набор инструкций x86_64.

— Wikipedia

Для демонстрации этого, взгляните на конце этой функции из модуля WebKit:

000000000052BE0D                 mov     eax, [rdx+8]
000000000052BE10                 mov     [rsi+10h], eax
000000000052BE13                 or      byte ptr [rsi+39h], 20h
000000000052BE17                 ret

Теперь взгляните, как будет выглядеть код, если мы начнем декодирование с 0x52be14:
000000000052BE14                 cmp     [rax], r12
000000000052BE17                 ret

Пусть этот код никогда и не предназначался для выполнения, он находится в области памяти, которая была помечена «выполняемой», что делает её весьма привлекательной для использования в качестве гаджета.

Конечно, было бы невероятно затратно тратить время на поиск всех возможных способов интерпретации кода перед каждой инструкцией ret вручную; за нас это умеют делать существующие утилиты. Для поиска ROP-гаджетов я предпочитаю использовать rp++; чтобы сгенерировать текстовый файл, заполненный гаджетами, просто введите команду:

rp-win-x64 -f mod14.bin --raw=x64 --rop=1 --unique > mod14.txt


Ошибки сегментации


Если мы попытаемся выполнить неисполняемую страницу памяти, или попытаемся записать в незаписываемую страницу памяти, произойдет ошибка сегментации.

К примеру, так выглядит попытка выполнить код на стеке, который «замаппен» только на чтение и запись (rw):

setU8to(chain.data + 0, 0xeb);
setU8to(chain.data + 1, 0xfe);

chain.add(chain.data);

А вот так — попытка записать код, который «замаппен» только на чтение и выполнение (rx):

setU8to(moduleBases[webkit], 0);

Если происходит ошибка сегментации, то на экране появляется сообщение «Недостаточно свободной системной памяти», и загрузки страницы не произойдет:

image

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

ASLR


Address Space Layout Randomization (ASLR) — технология безопасности, применяемая в операционных системах, при использовании которой случайным образом изменяется расположение в адресном пространстве процесса важных структур, а именно: образа исполняемого файла, подгружаемых библиотек, кучи и стека. Из-за нее базовые адреса модулей изменяются каждый раз, когда вы запускаете свою PS4.

Мне поступали свидетельства, что в самых старых версиях прошивки (1.05) ASLR была отключена, но она появилась где-то в районе 1.70. Заметьте, что ASLR для ядра отключена, по крайней мере для прошивок версии 1.76 и ниже, и это будет доказано дальше.

Для большинства эксплойтов ASLR станет проблемой, поскольку если вы не знаете адреса гаджетов в памяти, то и не догадаетесь, что нужно записать в стек.

К счастью для нас, мы не ограничены написанием статичных ROP-цепочек. Мы можем использовать JavaScript для чтения таблицы модулей, что поможет нам получить базовые адреса загруженных модулей. Используя эти адреса, мы сможем посчитать адреса всех наших гаджетов до выполнения цепочки ROP, обойдя ASLR.

Таблица модулей также включает имена файлов модулей:
  • WebProcess.self
  • libkernel.sprx
  • libSceLibcInternal.sprx
  • libSceSysmodule.sprx
  • libSceNet.sprx
  • libSceNetCtl.sprx
  • libSceIpmi.sprx
  • libSceMbus.sprx
  • libSceRegMgr.sprx
  • libSceRtc.sprx
  • libScePad.sprx
  • libSceVideoOut.sprx
  • libScePigletv2VSH.sprx
  • libSceOrbisCompat.sprx
  • libSceWebKit2.sprx
  • libSceSysCore.sprx
  • libSceSsl.sprx
  • libSceVideoCoreServerInterface.sprx
  • libSceSystemService.sprx
  • libSceCompositeExt.sprx

Несмотря на то, что PS4 по большей части использует формат [Signed] PPU Relocatable Executable ([S]PRX) для модулей, в дампе libSceSysmodule.sprx замечены строки, ссылающиеся на объектные файлы [Signed] Executable and Linking Format ([S]ELF) — bdj.elf, web_core.elf и orbis-jsc-compiler.self.

Данная комбинация модулей и объектов напоминает ту, что использовалась в PSP и PS3.

Полный список всех доступных модулей (а не только тех, что загружены браузером) можно в libSceSysmodule.sprx. Мы можем загрузить и сдампить некоторые из них благодаря нескольким специальным системным вызовам за авторством Sony, о чем и пойдет речь дальше.

JuSt-ROP


Использование JavaScript для написания и выполнения динамических ROP-цепочек дает нам огромное преимущество над обычной атакой переполнения буфера.

Кроме обхода ASLR, мы можем прочитать user agent браузера, и подставлять другую ROP-цепочку для другой версии браузера, давая нашему эксплойту высшую степень возможной совместимости.

Мы можем использовать JavaScript даже для чтения памяти по нашим адресам гаджетов для того, чтобы убедиться в их корректности, что дает нам практически идеальную надежность.

Динамическое написание ROP-цепочек приобретает смысл по сравнению с их предварительной генерацией скриптом.

По этим причинам я и создал собственный фрейморк на JavaScript для написания ROP-цепочек, JuSt-ROP.

Подводные камни JavaScript


JavaScript использует представление чисел в формате двойной точности (64 бита) IEEE-754. Это дает нам 53 бита точности (мантисса VT_R8 имеет только 53 бита), что означает невозможность отобразить каждое 64-битное значение — для некоторых из них придется применить аппроксимацию.

Если вам просто нужно установить 64-битное число в какое-нибудь небольшое значение, вроде 256, то setU64to справится с задачей. Но для случаев, когда вам нужно записать буфер или структуру данных, есть вероятность что отдельные байты будут записаны некорректно, если они были записаны в блоках по 64 бита. Вместо этого, вы должны писать данные блоками по 32 бита (помня о том, что PS4 использует порядок little-endian), чтобы убедиться в том, что каждый байт идентичен.

Системные вызовы


Интересно, что PS4 использует тот же формат вызовов, что и Linux и MS-DOS для системных вызовов, с аргументами хранимыми в регистрах, а не традиционным UNIX-способом (который FreeBSD использует по умолчанию), когда аргументы хранятся в стеке:
Регистр Значение
rax Номер системного вызова
rdi Аргумент 1
rsi Аргумент 2
rdx Аргумент 3
r10 Аргумент 4
r8 Аргумент 5
r9 Аргумент 6

Мы можем попробовать выполнить любой системный вызов с помощью метода JuSt-ROP:

this.syscall = function(name, systemCallNumber, arg1, arg2, arg3, arg4, arg5, arg6) {
	console.log("syscall " + name);
	
	this.add("pop rax", systemCallNumber);
	if(typeof(arg1) !== "undefined") this.add("pop rdi", arg1);
	if(typeof(arg2) !== "undefined") this.add("pop rsi", arg2);
	if(typeof(arg3) !== "undefined") this.add("pop rdx", arg3);
	if(typeof(arg4) !== "undefined") this.add("pop rcx", arg4);
	if(typeof(arg5) !== "undefined") this.add("pop r8", arg5);
	if(typeof(arg6) !== "undefined") this.add("pop r9", arg6);
	this.add("pop rbp", stackBase + returnAddress - (chainLength + 8) + 0x1480);
	this.add("mov r10, rcx; syscall");
}


Использование системных вызовов может многое нам поведать о ядре PS4. Более того, использование системных вызовов — единственный способ, которым мы можем взаимодействовать с ядром, и потенциально может выполнить эксплойт ядра.

Если провести реверс-инжениринг модулей для идентификации некоторых из специальных системных вызовов Sony, то можно обнаружить альтернативный формат вызовов:

Регистр Значение
rax 0
rdi Номер системного вызова
rsi Аргумент 1
rdx Аргумент 2
r10 Аргумент 3
r8 Аргумент 4
r9 Аргумент 5


По всей видимости, Sony поступила так для простой совместимости с соглашением о вызове функций, например:

unsigned long syscall(unsigned long n, ...) {
	register unsigned long rax asm("rax");
	
	asm("mov r10, rcx");
	rax = 0;
	asm("syscall");
	return rax;
}

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

// Обе команды возвращают ID текущего процесса:
chain.syscall("getpid", 20);
chain.syscall("getpid", 0, 20);


Об этом полезно помнить на случай возможности выбора самого удобного из доступных гаджетов.

getpid


Один-единственный системный вызов под номером 20, getpid(void), уже способен многое рассказать нам о ядре.

Сам тот факт, что этот системный вызов работает, говорит нам о том, что Sony даже не удосужилась перемешать номера системных вызовов, как того требует техника "безопасность через неясность" (а под лицензией BSD они могли сделать это без публикации в Интернете новых номеров системных вызовов).

Таким образом, мы автоматически заполучили в свои руки список системных вызовов, которые можно попробовать сделать в ядре PS4.

Во-вторых, вызвав getpid(), перезапустив браузер, а затем вызвав его снова, мы получим значение возврата на 1 большее, чем предыдущее. Хоть FreeBSD и поддерживают рандомизацию PID со времен версии 4.0, последовательное выделение PID — это поведение по умолчанию. По всей видимости, Sony и здесь не удосужилась усилить защиту вроде того, как это сделали в проектах по типу HardenedBSD.

Сколько системных вызовов здесь есть?


Последним системным вызовом во FreeBSD 9 является wait6 за номером 523; всё, что имеет номер выше — специальные системные вызовы Sony.

Попытка вызвать любой из специльных системных вызовов Sony без корректных аргументов вернет ошибку 0x16, "Invalid argument"; однако, любые совместимые системные вызовы, или же нереализованные системные вызовы приведут к ошибке "There is not enough free system memory".

Путем проб и ошибок, я выяснил, что системный вызов под номером 617 — это последний вызов Sony, все вызовы дальше не реализованы.

Исходя из этого, мы можем сделать логичное заключение, что в ядре PS4 есть 85 специальных системных вызовов (617 — 532) за авторством Sony.

Это значительно меньше, чем было в PS3, в которой было почти 1000 системных вызовов в целом. Что ж, пусть это и указывает меньший простор для потенциальных векторов атак, но зато нам будет проще задокументировать все вызовы.

Едем дальше. 9 из этих 85 системных вызовов всегда возвращают 0x4e, ENOSYS, что означает простую вещь — эти вызовы работают только на тестовых устройствах для разработчиков, оставляя нас всего с 76 полезными вызовами.

Из этих 76, libkernel.sprx ссылается только на 45 (все приложения, не являющиеся частью ядра, для выполнения системных вызовов используют этот модуль). Итого, у разработчика остается всего 45 доступных специальных системных вызовов.

Интересно, что хотя для использования предназначались только 45 вызовов (поскольку в libkernel.sprx есть обертки для них), некоторые из оставшихся 31 все равно доступны из процесса браузера. Вполне возможно, что в этих ненамеренно оставленных вызовах вероятность найти уязвимость гораздо выше, поскольку на их тестирование времени явно ушло меньше всего.

libkernel.sprx


Для того, чтобы разобраться, как специальные системные вызовы используются ядром, главное — обязательно запомнить, что это всего лишь модификация стандартных библиотек FreeBSD 9.0.

Вот выдержка кода _libpthread_init из файла thr_init.c:

/*
 * Check for the special case of this process running as
 * or in place of init as pid = 1:
 */
if ((_thr_pid = getpid()) == 1) {
	/*
	 * Setup a new session for this process which is
	 * assumed to be running as root.
	 */
	if (setsid() == -1)
		PANIC("Can't set session ID");
	if (revoke(_PATH_CONSOLE) != 0)
		PANIC("Can't revoke console");
	if ((fd = __sys_open(_PATH_CONSOLE, O_RDWR)) < 0)
		PANIC("Can't open console");
	if (setlogin("root") == -1)
		PANIC("Can't set login to root");
	if (_ioctl(fd, TIOCSCTTY, (char *) NULL) == -1)
		PANIC("Can't set controlling terminal");
}

Эта же функция может быть найдена на оффсете 0x215F0 из libkernel.sprx. Вот как приведенный выше код выглядит в дампе libkernel:

call    getpid
mov     cs:dword_5B638, eax
cmp     eax, 1
jnz     short loc_2169F

call    setsid
cmp     eax, 0FFFFFFFFh
jz      loc_21A0C

lea     rdi, aDevConsole ; "/dev/console"
call    revoke
test    eax, eax
jnz     loc_21A24

lea     rdi, aDevConsole ; "/dev/console"
mov     esi, 2
xor     al, al
call    open

mov     r14d, eax
test    r14d, r14d
js      loc_21A3C
lea     rdi, aRoot       ; "root"
call    setlogin
cmp     eax, 0FFFFFFFFh
jz      loc_21A54

mov     edi, r14d
mov     esi, 20007461h
xor     edx, edx
xor     al, al
call    ioctl
cmp     eax, 0FFFFFFFFh
jz      loc_21A6C


Реверсинг дампов модулей для анализа системных вызовов


libkernel открыт не полностью: в его состав входит большое количество собственного кода Sony, который мог бы раскрыть их системные вызовы.

Хотя процесс анализа будет отличаться в зависимости от выбранного системного вызова, для некоторых из них довольно просто выяснить состав аргументов, которые передаются в вызов.

Обертка системного вызова будет объявлена где-нибудь в libkernel.sprx и почти всегда будет следовать следующему шаблону:
000000000000DB70 syscall_601     proc near
000000000000DB70                 mov     rax, 259h
000000000000DB77                 mov     r10, rcx
000000000000DB7A                 syscall
000000000000DB7C                 jb      short error
000000000000DB7E                 retn
000000000000DB7F
000000000000DB7F error:
000000000000DB7F                 lea     rcx, sub_DF60
000000000000DB86                 jmp     rcx
000000000000DB86 syscall_601     endp

Заметим, что инструкция mov r10, rcx не обязательно означает то, что системный вызов принимает как минимум 4 аргумента; эта инструкция есть у всех оберток системных вызовов, и даже у тех, что не принимают никаких аргументов — например, getpid.

Как только вы нашли обертку, можете посмотреть xrefs к ней:

0000000000011D50                 mov     edi, 10h
0000000000011D55                 xor     esi, esi
0000000000011D57                 mov     edx, 1
0000000000011D5C                 call    syscall_601
0000000000011D61                 test    eax, eax
0000000000011D63                 jz      short loc_11D6A


Неплохой мыслью будет поискать еще несколько штук, просто чтобы убедиться, что регистры не были изменены для чего-нибудь несвязанного:
0000000000011A28                 mov     edi, 9
0000000000011A2D                 xor     esi, esi
0000000000011A2F                 xor     edx, edx
0000000000011A31                 call    syscall_601
0000000000011A36                 test    eax, eax
0000000000011A38                 jz      short loc_11A3F

Мы видим, как с завидным постоянством первых три регистра из соглашения о системных вызовов (rdi, rsi, и rdx), так что мы вполне уверенно можем заявить, что вызов принимает три аргумента.

Для понимания, вот как мы воспроизведем эти вызовы с помощью JuSt-ROP:

chain.syscall("unknown", 601, 0x10, 0, 1);
chain.syscall("unknown", 601, 9, 0, 0);

Как и большинство системных вызовов, эти вызовы вернут 0 в случае успеха, что видно по коду выше, где jz выполняет переход после testа возвращаемого значения.

Выяснение чего-то более сложного, чем количество аргументов, потребует гораздо более глубокого анализа кода до и после вызова для понимания контекста, но рассказанного должно хватить вам для старта.

Брутфорс системных вызовов


Несмотря на то, что реверс-инжениринг дампов модулей — самый надежный способ идентифицировать системные вызовы, некоторые из них не упоминаются в дампах, поэтому мы вынуждены анализировать их «вслепую».

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

Мы также можем передавать нули для всех аргументов, и брутфорсить все системные вызовы, которые возвращают полезные ошибки вроде 0xe, "Bad address", которые указывают на то, что вызовы принимают как минимум один указатель.

Во-первых, нам нужно выполнить ROP-цепочку как только страница загрузится. Мы можем сделать это при помощи навешивания нашей функции на onload элемента body:
<body onload="exploit()">

Следом нам нужно выполнить специальный системный вызов в зависимости от значения из HTTP GET. Хоть это и может быть сделано при помощи JavaScript, для простоты я использую PHP:
var Sony = 533;
chain.syscall("Sony system call", Sony + <?php print($_GET["b"]); ?>, 0, 0, 0, 0, 0, 0);
chain.write_rax_ToVariable(0);

Как только системный вызов выполнится, мы можем проверить значение возврата, и если он не даст нам ничего интересного, сделать редирект на следующий системный вызов:
if(chain.getVariable(0) == 0x16) window.location.assign("index.php?b=" + (<?php print($_GET["b"]); ?> + 1).toString());

Запуск страницы с ?b=0 в конце запустит брутфорс с первого системного вызова Sony.

Хотя этот метод требует немалого количества экспериментов, можно уверенно сказать, что он позволит найти несколько системных вызовов, которых вам удастся частично идентифицировать.

Системный вызов 538


В качестве примера, давайте рассмотрим системный вызов 538, не полагаясь на дампы каких-либо модулей.

Вот значения возврата в зависимости от того, что передается в качестве первого аргумента:
  • 0 — 0x16, «Invalid argument»
  • 1 — 0xe, «Bad address»
  • Указатель на 0 — первоначально 0x64, но с каждым обновлением страницы значение увеличивается на 1.

Другими потенциальными аргументами, которые можно попытаться подставить, будут PID, ID потока и файловый дескриптор.

Несмотря на то, что большинство системных вызовов возвращают 0 при успешном выполнении, часть вызовов возвращает увеличивающееся при каждом новом вызове значение — видимо, эти вызовы аллоцируют какой-либо ресурс, вроде файлового дескриптора.

Следующим шагом будет наблюдение за данными до и после выполнения системного вызова для того, чтобы понять, было ли что-нибудь записано в них.

Поскольку изменений в данных не наблюдается, можем с чистой совестью считать, что это ввод.

Затем попытаемся скормить методу длинную строку в качестве первого аргумента. Вы должны пробовать это с каждым вводом, который вам удастся обнаружить, поскольку существует вероятность обнаружения переполнения буфера.

writeString(chain.data, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
chain.syscall("unknown", 538, chain.data, 0, 0, 0, 0, 0); 

Мы получим в качестве значения возврата 0x3f, ENAMETOOLONG. Увы, но мы видим, что системный вызов корректно ограничивает имя (32 байта включая ограничитель NULL), однако теперь мы знаем, что метод ожидает строку, а не структуру.

Что ж, теперь у нас есть несколько идей относительно того, что может делать этот вызов. Самый очевидный вариант — какое-либо действие, связанное с файловой системой (например, специальная версии mkdir или open), однако эта версия вряд ли нам подойдет — ведь ресурс аллоцируется еще до того, как мы записали любые данные в указатель.

Попробуем проверить, является ли первый параметр путём. Разобьем его несколькими символами / и посмотрим, позволит ли это передать в метод длинную строку:

writeString(chain.data, "aaaaaaaaaa/aaaaaaaaaa/aaaaaaaaaa");
chain.syscall("unknown", 538, chain.data, 0, 0, 0, 0, 0); 

Поскольку этот вызов также вернет 0x3f, мы можем предположить, что первый аргумент — это не путь; это имя для чего-то, что будет размещено в памяти и получит последовательный идентификатор (sequential identifier).

После анализа других системных вызовов, мне удалось обнаружить, что все перечисленные ниже обладают одинаковым поведением:
  • 533
  • 538
  • 557
  • 574
  • 580

При помощи полученной информации практически невозможно догадаться, что именно делают эти системные вызовы, но если вы проведете другие тесты, то понемногу раскроете тайну. Сэкономлю вам немного времени — системный вызов 538 выделяет память под флаг события (и принимает в качестве параметра не только имя).

При помощи базовых знаний о том, как работает ядро, вы можете предположить, а затем проверить, под что выделяется память системными вызовами — семафоры, мьютексты и так далее.

Дамп дополнительных модулей


Мы можем дампить дополнительные модули следующим образом:
  • Загружаем модуль
  • Получаем базовый адрес модуля
  • Дампим модуль.

Я взял на себя утомительный труд загрузки и дампа каждого возможного модуля из браузера руками и опубликовал результаты на psdevwiki. Все модули с маркером «Yes» могут быть сдампены этим методом.

Для загрузки модуля нам потребуется использовать функцию sceSysmoduleLoadModule из libSceSysmodule.sprx + 0x1850. Первым параметром идет идентификатор загружаемого модуля, в остальных трех просто передается 0.

Приведенный ниже метод JuSt-ROP пригодится для выполнения этого вызова:
this.call = function(name, module, address, arg1, arg2, arg3, arg4, arg5, arg6) {
	console.log("call " + name);
	
	if(typeof(arg1) !== "undefined") this.add("pop rdi", arg1);
	if(typeof(arg2) !== "undefined") this.add("pop rsi", arg2);
	if(typeof(arg3) !== "undefined") this.add("pop rdx", arg3);
	if(typeof(arg4) !== "undefined") this.add("pop rcx", arg4);
	if(typeof(arg5) !== "undefined") this.add("pop r8", arg5);
	if(typeof(arg6) !== "undefined") this.add("pop r9", arg6);
	this.add("pop rbp", stack_base + return_va - (chainLength + 8) + 0x1480);
	this.add(module_bases[module] + address);
}

Итак, для загрузки libSceAvSetting.sprx (0xb) используем:

chain.call("sceSysmoduleLoadModule", libSysmodule, 0x1850, 0xb, 0, 0, 0);

Как и большинство системных вызовов, этот должен вернуть 0 при успехе. Чтобы увидеть идентификатор аллоцированного в памяти модуля, мы можем использовать один из системных вызовов Sony под номером 592 для получения списка загруженных модулей:
var countAddress = chain.data;
var modulesAddress = chain.data + 8;

// Системный вызов 592, getLoadedModules(int *destinationModuleIDs, int max, int *count);
chain.syscall("getLoadedModules", 592, modulesAddress, 256, countAddress);

chain.execute(function() {
	var count = getU64from(countAddress);
	for(var index = 0; index < count; index++) {
		logAdd("Module: 0x" + getU32from(modulesAddress + index * 4).toString(16));
	}
});

Выполнение этого кода без загрузки других дополнительных модулей отобразит следующий список:
0x0, 0x1, 0x2, 0xc, 0xe, 0xf, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1e, 0x37, 0x59

Однако, если мы запустим её после загрузки модуля 0xb, то увидим дополнительный элемент, 0x65. Запомните — идентификатор модуля это не то же самое, что и идентификатор загруженного модуля.

Теперь мы можем использовать другой системный вызов Sony под номером 593, который принимает идентификатор загруженного модуля и буфер, и заполняет буфер информацией о загруженном модуле, включая его базовый адрес. Поскольку идентификатор загруженного модуля всегда 0x65, мы можем «захардкодить» его в нашу цепочку, вместо того чтобы хранить результат из списка модулей.

Буфер должен начинаться с размера структуры, которая должна быть возвращена, в противном случае вернется ошибка 0x16, "Invalid argument":
setU64to(moduleInfoAddress, 0x160);
chain.syscall("getModuleInfo", 593, 0x65, moduleInfoAddress);

chain.execute(function() {
	logAdd(hexDump(moduleInfoAddress, 0x160));
});

В случае успеха вернется 0, а буфер будет заполнен структурой, которую можно прочитать так:

var name = readString(moduleInfoAddress + 0x8);
var codeBase = getU64from(moduleInfoAddress + 0x108);
var codeSize = getU32from(moduleInfoAddress + 0x110);
var dataBase = getU64from(moduleInfoAddress + 0x118);
var dataSize = getU32from(moduleInfoAddress + 0x120);

Теперь у нас есть все необходимое для дампа модуля!

dump(codeBase, codeSize + dataSize);

Существует другой системный вызов Sony, под номером 608, который работает схожим с 593 образом, но предоставляет немного другую информацию о загруженном модуле:

setU64to(moduleInfoAddress, 0x1a8);
chain.syscall("getDifferentModuleInfo", 608, 0x65, 0, moduleInfoAddress);
logAdd(hexDump(moduleInfoAddress, 0x1a8));

Неизвестно, что может означать эта информация.

Исследуем файловую систему


Для чтения файлов и директорий PS4 использует стандартные системные вызовы FreeBSD 9.0.

Однако, несмотря на то что чтение отдельных директорий вроде /dev/ сработает, чтение других — например, / — нет.
Не знаю, почему так происходит, но если использовать gendents вместо read для директорий, то работать всё будет более надежно:

writeString(chain.data, "/dev/");
chain.syscall("open", 5, chain.data, 0, 0);
chain.write_rax_ToVariable(0);

chain.read_rdi_FromVariable(0);
chain.syscall("getdents", 272, undefined, chain.data + 0x10, 1028);

Вот результирующая память:
0000010: 0700 0000 1000 0205 6469 7073 7700 0000  ........dipsw...
0000020: 0800 0000 1000 0204 6e75 6c6c 0000 0000  ........null....
0000030: 0900 0000 1000 0204 7a65 726f 0000 0000  ........zero....
0000040: 0301 0000 0c00 0402 6664 0000 0b00 0000  ........fd......
0000050: 1000 0a05 7374 6469 6e00 0000 0d00 0000  ....stdin.......
0000060: 1000 0a06 7374 646f 7574 0000 0f00 0000  ....stdout......
0000070: 1000 0a06 7374 6465 7272 0000 1000 0000  ....stderr......
0000080: 1000 0205 646d 656d 3000 0000 1100 0000  ....dmem0.......
0000090: 1000 0205 646d 656d 3100 0000 1300 0000  ....dmem1.......
00000a0: 1000 0206 7261 6e64 6f6d 0000 1400 0000  ....random......
00000b0: 1000 0a07 7572 616e 646f 6d00 1600 0000  ....urandom.....
00000c0: 1400 020b 6465 6369 5f73 7464 6f75 7400  ....deci_stdout.
00000d0: 1700 0000 1400 020b 6465 6369 5f73 7464  ........deci_std
00000e0: 6572 7200 1800 0000 1400 0209 6465 6369  err.........deci
00000f0: 5f74 7479 3200 0000 1900 0000 1400 0209  _tty2...........
0000100: 6465 6369 5f74 7479 3300 0000 1a00 0000  deci_tty3.......
0000110: 1400 0209 6465 6369 5f74 7479 3400 0000  ....deci_tty4...
0000120: 1b00 0000 1400 0209 6465 6369 5f74 7479  ........deci_tty
0000130: 3500 0000 1c00 0000 1400 0209 6465 6369  5...........deci
0000140: 5f74 7479 3600 0000 1d00 0000 1400 0209  _tty6...........
0000150: 6465 6369 5f74 7479 3700 0000 1e00 0000  deci_tty7.......
0000160: 1400 020a 6465 6369 5f74 7479 6130 0000  ....deci_ttya0..
0000170: 1f00 0000 1400 020a 6465 6369 5f74 7479  ........deci_tty
0000180: 6230 0000 2000 0000 1400 020a 6465 6369  b0.. .......deci
0000190: 5f74 7479 6330 0000 2200 0000 1400 020a  _ttyc0..".......
00001a0: 6465 6369 5f73 7464 696e 0000 2300 0000  deci_stdin..#...
00001b0: 0c00 0203 6270 6600 2400 0000 1000 0a04  ....bpf.$.......
00001c0: 6270 6630 0000 0000 2900 0000 0c00 0203  bpf0....).......
00001d0: 6869 6400 2c00 0000 1400 0208 7363 655f  hid.,.......sce_
00001e0: 7a6c 6962 0000 0000 2e00 0000 1000 0204  zlib............
00001f0: 6374 7479 0000 0000 3400 0000 0c00 0202  ctty....4.......
0000200: 6763 0000 3900 0000 0c00 0203 6463 6500  gc..9.......dce.
0000210: 3a00 0000 1000 0205 6462 6767 6300 0000  :.......dbggc...
0000220: 3e00 0000 0c00 0203 616a 6d00 4100 0000  >.......ajm.A...
0000230: 0c00 0203 7576 6400 4200 0000 0c00 0203  ....uvd.B.......
0000240: 7663 6500 4500 0000 1800 020d 6e6f 7469  vce.E.......noti
0000250: 6669 6361 7469 6f6e 3000 0000 4600 0000  fication0...F...
0000260: 1800 020d 6e6f 7469 6669 6361 7469 6f6e  ....notification
0000270: 3100 0000 5000 0000 1000 0206 7573 6263  1...P.......usbc
0000280: 746c 0000 5600 0000 1000 0206 6361 6d65  tl..V.......came
0000290: 7261 0000 8500 0000 0c00 0203 726e 6700  ra..........rng.
00002a0: 0701 0000 0c00 0403 7573 6200 c900 0000  ........usb.....
00002b0: 1000 0a07 7567 656e 302e 3400 0000 0000  ....ugen0.4.....
00002c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

Некоторые из этих устройств можно читать, например чтение /dev/urandom заполнит память случайными данными.
Также можно пропарсить эту память и получить список сущностей; взгляните на browser.html из репозитория, который выполняет роль файлового менеджера:

image

Увы, из-за песочницы мы не имеем полного доступа к файловой системе. Попытка считать файлы или директории, которые существуют, но доступ к ним ограничен, вернет вам ошибку 2, ENOENT, "No such file or directory". Правда, мы все равно можем получить доступ к разным интересностям — зашифрованным файлам сохранений, трофеям и информации об аккаунте — подробнее о них я расскажу в своих следующих статьях.

Песочница


Проблема с работой системных вызовов не ограничивается отдельными путями — есть и другие причины, по которым их не удается выполнить.

Чаще всего, запрещенный системный вызов просто вернет ошибку 1, EPERM, "Operation not permitted"; это утверждение справедливо для вызовов вроде ptrace, поскольку другие системные вызовы не будут работать по самым различным причинам.

Совместимые системные вызовы отключены. К примеру, если вы хотите вызвать mmap, то должны использовать системный вызов номер 477, а не 71 или 197; в противном случае, вы получите сегфолт.

Другие системные вызовы, вроде exit, также вызовут ошибку сегментации (segmentation fault):

chain.syscall("exit", 1, 0);


Попытка создать SCTP-сокет вернет ошибку 0x2b, EPROTONOSUPPORT, указывающую на то, что SCTP-сокеты были выключены в ядре PS4:

//int socket(int domain, int type, int protocol);
//socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
chain.syscall("socket", 97, 2, 1, 132);

И, хотя вызов mmap с PROT_READ | PROT_WRITE | PROT_EXEC вернет валидный указатель, флаг PROT_EXEC будет проигнорирован. Чтение его защиты вернет 3 (RW), и любая попытка выполнить память приведет к сегфолту:

chain.syscall("mmap", 477, 0, 4096, 1 | 2 | 4, 4096, -1, 0);
chain.write_rax_ToVariable(0);
chain.read_rdi_FromVariable(0);
chain.add("pop rax", 0xfeeb);
chain.add("mov [rdi], rax");
chain.add("mov rax, rdi");
chain.add("jmp rax");

Список open source ПО, используемого в PS4, не содержит в себе специализированного ПО для песочниц вроде Capsicum, так что PS4 либо использует «чистые» jails из FreeBSD, или же полагается на собственную проприетарную систему изолирования окружений (что вряд ли).

Jail


Мы можем доказать существование активного использования jail-ов из FreeBSD в ядре PS4 с помощью системного вызова auditon, который невозможно выполнить в изолированном jailed-окружении:

chain.syscall("auditon", 446, 0, 0, 0);


Первое, что делает системный вызов audition — это проверка jailed здесь, и если это так, возвращает ENOSYS:

if (jailed(td->td_ucred))
	return (ENOSYS);


В противном случае случае, системный вызов скорее всего вернет EPERM из mac_system_check_auditon здесь:

error = mac_system_check_auditon(td->td_ucred, uap->cmd);
if (error)
	return (error);


Или из priv_check здесь:

error = priv_check(td, PRIV_AUDIT_CONTROL);
if (error)
	return (error);


Самое дальнее, куда может дойти системный вызов, будет сразу после priv_check, здесь, до возврата EINVAL из-за длины аргумента, равной 0:

if ((uap->length <= 0) || (uap->length > sizeof(union auditon_udata)))
	return (EINVAL);


Поскольку mac_system_check_auditon и priv_check никогда не возвращают ENOSYS, попадание на проверку jailed — единственный вариант, когда возвращается ENOSYS.

При выполнении цепочки, возвращается ENOSYS (0x48).

Это говорит нам о том, что система песочницы, используемая PS4, как минимум основана на jail-ах, поскольку использует проверки jailed.

Эксплойты ядра FreeBSD 9.0


Нет особого смысла в поиске новых уязвимостей в исходниках ядра FreeBSD 9.0, поскольку с момента релиза в 2012 было найдено несколько эксплойтов ядра, к которым PS4 может быть потенциально уязвима.

Некоторые из них мы можем отбросить сходу:

FreeBSD 9.0-9.1 mmap/ptrace — Privilege Escalation Exploit — не сработает, потому что у нас нет доступа к системному вызову ptrace.
FreeBSD 9.0 — Intel SYSRET Kernel Privilege Escalation Exploit — не сработает, поскольку в PS4 используется процессор AMD.
FreeBSD Kernel — Multiple Vulnerabilities — возможно, первая уязвимость из этого комплекта и сработает, но две другие полагаются на сокеты SCTP, которые в ядре PS4 отключены, как уже говорилось ранее.

К счастью, есть несколько уязвимостей поменьше, которые могут привести нас к чему-нибудь интересному.

getlogin


Одна из уязвимостей, которую несложно попробовать — использование системного вызова getlogin для утечки небольшого количества памяти ядра.

Системный вызов getlogin предназначен для копирования имени пользователя текущей сессии в пользовательскую память, однако, из-за бага, всегда копируется буфер полностью, а не только размер строки с именем. Это означает, что мы можем прочитать некоторые неинициализированные данные из ядра, что может нам пригодиться.

Заметим, что системный вызов (49) на самом деле int getlogin_r(char *name, int len);, а не char *getlogin(void);.

Итак, давайте попробуем скопировать немного памяти ядра в неиспользуемую часть пользовательской памяти:
chain.syscall("getlogin", 49, chain.data, 17);


Увы, больше 17 байт нам никак не вытащить, поскольку:

Длина имени пользователя ограничена MAXLOGNAME (из <sys/param.h>) символов, в данный момент это 17 символов, включая пустые.
— FreeBSD Man Pages

После выполнения цепочки, значение возврата равно 0, из чего следует, что системный вызов отработал! Отличное начало. Теперь взглянем на память, на которую мы указывали:

До выполнения цепочки:

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00

После выполнения цепочки:

72 6f 6f 74 00 fe ff ff 08 62 61 82 ff ff ff ff
00

После декодирования первых четырех байт в ASCII:

root

Оказывается, браузер выполняется от рута! Вот это неожиданность.

Но что еще интереснее, это то что утекшая память напоминает указатель на что-то в ядре, что при каждом запуске цепочки остается одним и тем же; это свидетельство подтверждает теорию Yifanlu о том, что в PS4 нет защиты ASLR (рандомизации адресного пространства) на уровне ядра!

Итого


Судя по собранным сведениям, ядро PS4 очень сильно напоминает стоковое ядро FreeBSD 9.0. Важно отметить, что найденные изменения относятся скорее к изменениям стандартной конфигурации ядра, чем к модификации кода. Хотя Sony и добавила несколько собственных специальных системных вызовов в ядро, вся остальная часть ядра, судя по всему, осталась практически не тронутой.

В силу этих причин я склонен считать, что у PS4 есть всё те же «сочные» уязвимости, что и в ядре FreeBSD 9.0!

К сожалению, большая часть эксплойтов для ядра не может быть выполнена из входной точки WebKit в силу ограничений песочницы (которые скорее всего управляются стандартным механизмом jails из FreeBSD). Увы, на публикацию приватных эксплойтов для FreeBSD 9 надеяться не приходится, так что пока вдруг внезапно не выйдет новый, мы вынуждены работать с тем, что есть. Я допускаю, что существует возможность эксплойта ядра PS4 с использованием некоторых из существующих уязвимостей ошибок повреждения памяти, но это будет определенно непросто.

Лучшим подходом здесь будет реверс-инжиниринг всех модулей, которые получится сдампить, с целью документирования максимально возможного количества специальных системных вызовов от Sony; интуиция подсказывает мне, что с ними шанс достичь удачи будет выше, чем со стандартными системными вызовами FreeBSD.

Недавно Jaicrab обнаружил на PS4 два порта УАПП (UART), что говорит нам о потенциальной заинтересованности hardware-хакеров в консоли. Хотя роль hardware-хакеров обычно была в дампинге RAM системы (как это было с DSi), на этот раз с этой задачей мы уже справились сами благодаря эксплойту WebKit — однако, существует возможность обнаружения уязвимости ядра, которая будет «включаться» железом, как это было с исходным хаком гипервизора в PS3 за авторством geohot. Впрочем, это не отменяет того, что скорее всего эксплойт ядра PS4 будет сделан благодаря уязвимости системного вызова.

Спасибо за прочтение!
Если вы заметили ошибку, или хотите предложить уточнение/исправление, то не стесняйтесь написать мне в ЛС.
Проголосовать:
+117
Сохранить: