Kaip radau savo pirmą „ZeroDay“ (KPP)

Iki šiol niekada nebandžiau pažeidžiamumo tyrimo klaidų medžioklės dalies. Daugiau nei dešimtmetį tobulinu „Windows“ kenkėjiškas programas ir retkarčiais atlikdavau pataisų analizę, bet niekada nemačiau prasmės ieškoti klaidų pagrindinėje OS. Galų gale, yra pažeidžiamumo tyrinėtojų komandos, turinčios dešimtmečių patirtį, tikrinančios kiekvieną kodo colį, taigi kokia tikimybė, kad kažkas naujo, kaip aš, ką nors ras? Šansai atrodė neįmanomi, todėl niekada nesivarginau bandyti. Tik tada, kai pradėjau keisti BlueKeep pataisą, mano požiūris į klaidų medžioklę pradėjo keistis.

2019 m. gegužės mėn. „Microsoft“ išleido precedento neturintį įspėjimą apie būsimą saugos pataisą. Po 2017 m. „WannaCry“ sukurto chaoso (kuriam buvo naudojamas pataisytas pažeidžiamumas, žinomas kaip „EternalBlue“), „Microsoft“ nerizikavo. Įspėjimas informavo apie būsimą didelio kirminų pažeidžiamumo pataisymą ir paskatino Windows naudotojus įdiegti pataisą, kai tik ji bus prieinama.

Pati pataisyta klaida buvo niūri. Tai buvo išankstinio autentifikavimo RCE pažeidžiamumas, turintis įtakos visoms Windows versijoms iki Windows 8. Tiesą sakant, jis buvo toks blogas, kad Microsoft netgi išleido pataisas nutrauktoms operacinėms sistemoms, pvz., XP ir Server 2003. Nors galėjo būti lengva parašyti šią klaidą. Atsiradus ir išnykus dar vienam epiniam pažeidžiamumui, jis papasakojo apie esminius KPP trūkumus.

Jei nesate susipažinę su „BlueKeep“ veikimu, labai rekomenduoju perskaityti mano rašymą čia.

Greitas atnaujinimas: kad suaktyvintų „BlueKeep“, kenkėjiškas klientas prisijungia prie kanalo „MS_T120“, kuris naudojamas tik serverio viduje. Kanalo protokolas įgyvendina uždarymo užklausą, kurią klientas gali išsiųsti. Kadangi kanalas yra vidinis, serveris tikisi, kad jis bus uždarytas tik serveriui jį uždarius. Prieš laiką uždarydamas kanalą, klientas suaktyvina būseną „Naudoti po nemokamo naudojimo“, kurią galima išnaudoti.

Pataisa ištaiso naudojimo po nemokamo naudojimo (UAF) sąlygą, pagerindama kanalo uždarymo užklausos tvarkymą, tačiau išryškina didesnę ir plačiau paplitusią problemą.

KPP iš tikrųjų nėra vienas protokolas, o daugiau protokolų protokolas. Įdiegta kanalo sąsaja, leidžianti skirtingiems komponentams kalbėtis tarpusavyje taip, kaip jiems patinka. Pavyzdžiui, yra USB kanalas, garso kanalas ir grafikos kanalas. Kanalai paprastai skirstomi į dvi kategorijas, kurias vadinu „vidiniais“ ir „išoriniais“. Vidiniai kanalai skirti ryšiui tarp skirtingų KPP komponentų serveryje. Išoriniai kanalai skirti ryšiui tarp KPP kliento ir KPP serverio.

Vidiniai kanalai yra šiek tiek numanoma pasitikėjimo riba. Tikimasi, kad juos naudos tik serveryje jau veikiantis kodas; todėl mažiau tikėtina, kad jie tinkamai tvarkys nepatikimus duomenis.

„BlueKeep“ pabrėžė problema, kad KPP neskiria vidinių ir išorinių kanalų. Klientas gali tiesiog pereiti į bet kurį kanalą ir pradėti pliaukštelėti. Dėl to, kad pleistras buvo skirtas simptomams (UAF), o ne priežastims (faktas, kad vidiniai kanalai yra prieinami iš išorės), aš supratau, kad tai yra daugiau.

„Windows 8“ KPP kodų bazė buvo visiškai pakeista, o tai reiškia, kad „BlueKeep“ trūkumas egzistavo tik „Windows 7“ ir senesnėse versijose. Maniau, kad nors „BlueKeep“ niekada neegzistavo, negalėjo pakenkti ištirti, ar naujasis KPP paketas patyrė tą patį esminį trūkumą kaip ir jo pirmtakas. Nusprendžiau sutelkti savo tyrimus į „Widows 10“ ir išplėtus visą „Windows“ versiją, naujesnę nei 8.

Derindamas atvirkštinę inžineriją, dinaminę analizę ir „Google“ sudarau tvirtą žinomų KPP kanalų sąrašą. Tada aš peržiūrėjau protokolo dokumentus ir bandžiau išsiaiškinti, kurie kanalai buvo išoriniai arba po autentifikavimo, ir juos neįtraukiau. Galiausiai su sąrašu pradėjau dirbti tikrindamas kiekvieno kanalo atitinkamą tvarkyklę.

Akį patraukė kanalas „rdpinpt“, nes tvarkymo kodas atrodo taip.

Rdpinpt pranešimų tvarkymo kodas.

Tik iš pirmo žvilgsnio jau labai aišku, kad mes čia neturime būti. Pirmoji eilutė nuskaito žymeklį į struktūrą iš „channel_msg + 12“, kuri yra vartotojo valdomi duomenys. Jokiomis aplinkybėmis klientas neturėtų turėti serverio atminties adresų (nes tai sugadintų ASLR), todėl tai yra raudona vėliavėlė. Negana to, gautas adresas nėra tikrinamas. Šis kodas tiesiog bando nuskaityti duomenis iš bet kurio jam perduoto adreso. Tai turėtų būti smagu.

Kad išbandyčiau savo teoriją, turėjau užkoduoti RDP klientą, galintį prisijungti prie savavališkų kanalų ir siųsti savavališkus duomenis. Klientui rašyti prireiktų savaičių dėl KPP protokolo sudėtingumo, bet buvau pasiryžęs įrodyti savo teoriją. Ironiška, kad kodo parašymas užtruko daug ilgiau, nei man prireikė rasti pažeidžiamumą.

Kodas, kurį parašiau norėdamas patikrinti klaidą.

Klaidos suaktyvinimas turėtų būti gana paprastas. Man tereikia nusiųsti 20 baitų užklausą. Pirmieji du DWORD yra ignoruojami. 3-asis DWORD turi būti 12 (kad išlaikytumėte mouse_input_len patikrą). Tada paskutiniai 8 baitai yra adresas, kurį funkcija bandys perskaityti (aš nustačiau kaip 0x1337133713371337, kad būtų galima naudoti papildomai).

Pirmoji kūdikio DoS

Puiku! Atradimo metu „Microsoft“ mokėjo iki 10 000 USD už nuotolinį paslaugų atsisakymo išnaudojimą. Nusprendžiau nepranešti apie klaidą, nes norėjau sužinoti, ar galėčiau ją paversti kažkuo geresniu.

Rdpinpt pranešimų tvarkymo kodas.

Dar pažvelgęs į kodą pastebėjau, kad pranešimo ilgio laukas taip pat nebuvo patvirtintas. Nurodęs ilgesnį nei tikrasis pranešimo ilgis, galėčiau suaktyvinti ribų nuskaitymą (OOB). Problema buvo ta, kad adreso rodyklė yra 12 poslinkyje, o krūvos antraštė yra 16 baitų. Krūvos antraštėje nėra jokių adresų, todėl nebuvo jokio būdo į rodyklės lauką įvesti galiojantį adresą.

Dar daugiau kasinėjęs radau tokį kodą:

Vėliava, nurodanti, kaip tvarkoma užklausa

Jei DWORD kanale_msg+1 nustatytas, vartotojo nurodyti adresai ir ilgis perduodami CTiMouse::SendInput. Ši funkcija iš esmės atlieka tą patį, kaip ir ankstesnis kodo fragmentas, bet iš branduolio. Dėl to, kad branduolys patvirtina vartotojo režimo adresus, funkcija sugenda, o ne sugrius RDP, jei adreso nėra. Jei adresas vis dėlto egzistuoja, branduolys bandys interpretuoti jo turinį kaip pelės įvesties paketą.

Pelės įvesties paketo formatas

Kol adreso duomenys laisvai atitinka šią supaprastintą pelės įvesties duomenų struktūrą, pelės būsena bus atitinkamai nustatyta. Iš esmės dabar turėtume susilpninti ASLR, nes galėsime saugiai patikrinti, ar adresas yra paskirtas, ar ne. Be to, mes taip pat galime nuotoliniu būdu nuskaityti parašus atmintyje, jei jis atitinka paketo specifikaciją. Galėtume sugadinti ASLR, išpurškę galiojančius pelės įvesties paketus, o tada naudodami klaidą, kad šiurkščiai atšauktume vieno atminties adresą.

Nors tai nėra ypač naudinga, yra trečiasis naudojimas, kuris, mano nuomone, buvo visiškai linksmas. Jei adreso duomenys prasideda 0x00000000 arba 0x0001000, sistema perkels pelės žymeklį į bet kurią kitą reikšmę. Žymeklio padėtis rodoma per KPP, todėl iš tikrųjų galime nutekėti nedidelius atminties fragmentus per žymeklio padėtį, pavyzdžiui, kokią nors RDP Ouija plokštę.

Šiuo metu turime savavališką skaitymą, OOB nuskaitymą ir informacijos nutekėjimą – visa tai atliekama iš tos pačios funkcijos. Kai kuriais atvejais mačiau, kaip žmonės buvo apdovanoti keliais CVE, nepaisant to, kad visi jie kilo dėl tos pačios pagrindinės problemos. Jei taip būtų šiuo atveju, premija galėtų siekti iki 30 000 USD. Deja, praėjus 4 mėnesiams po to, kai radau klaidas, DoS ir Info Leak klaidų dovana buvo sumažinta nuo 10 000 USD iki 1 000 USD. Kliento kodavimas užtruko mėnesį, o atbulinės eigos keitimas užtruko dar mėnesį, be to, turėčiau užtrukti kelias valandas, kol parašysiu pranešimą apie riktą, todėl nusprendžiau praeiti.

Vietoj to, aš bendradarbiavau su kuo nors, kad padėtų man išsiaiškinti, ar galėtume suaktyvinti branduolio klaidą arba sujungti vieną iš klaidų į visą RCE.

Pirmąją klaidą radau 2019 m. gruodžio mėn., praėjus 6 mėnesiams po „BlueKeep“ pataisos, ir nusprendžiau prie jos atsisėsti. 2020 m. rugsėjo mėn. apie klaidas pranešė kitas tyrėjas, todėl jos buvo pataisytos 2020 m. spalio mėn. saugos naujinime (arba taip manė Microsoft). Manau, kad priskirti CVE buvo CVE-2020-16927 ir CVE-2020-16896.

Pleistras įdomus tuo, kad iš tikrųjų (kaip) sprendžia pagrindinę priežastį, o ne simptomus.

Bandymas pataisyti

Užuot tiesiog pataisęs OOB arba savavališkai nuskaitęs, kodas neleidžia klientui iš viso prisijungti prie vidinių kanalų. Deja, pleistras neveikia.

CRdpDynVCMgr::IsFakeChannel() patikra iškviečiama iš CRdpDynVCMgr::CreateChannelInternal(), kuri iškviečiama „drdynvc“ kanalo inicijavimo metu. Tiesiog neprisijungę prie drdynvc kanalo, praleidžiame inicijavimą, todėl galime laisvai prisijungti prie uždraustų kanalų.

Tik šio mėnesio saugos pataisa (2020 m. gruodžio mėn.) klaida buvo ištaisyta visam laikui. Funkcija IsFakeChannel() buvo perkelta į pagrindinį KPP inicijavimą, visiškai uždarant prieigą prie kanalų. Dėl to, kad galutinis pataisymas nebuvo įtrauktas į pataisos pastabas, reikėjo daug laužyti galvą, kad išsiaiškinčiau, kas tiksliai nutiko mano klaidai.

Apskritai aš nesigailiu, kad nepranešiau apie klaidą, kai pirmą kartą ją radau. Nors šiek tiek pinigų būtų buvę gerai, man buvo daug smagiau pirmą kartą pasinerti į tikrą vabzdžių medžioklę. Nustebau, kad švaistydamas dienas atšaukdamas tokį viešą pažeidžiamumo pataisą, radau savo nulius. Tikiuosi, kad ateityje rasiu ką nors geresnio ir turėsiu daugiau laiko dirbti.

Kriptografinis įrodymas (SHA 256):

Sumaišyti duomenys: https://pastebin.com/ZazpTTbU