Innehåll

Hackskydd i OpenBSD 3.3

Introduktion och historik
OpenBSDs skydd
Andra metoder

OpenBSD 3.3 - This you can trust


16/06-03 | IcePic | icepic@64bits.se

Utskriftsvänligare versionUtskriftsvänligare version


OpenBSDs skydd

OpenBSD har alltid letat i sourcekoden efter svagheter och problem. Precis som linuxjournal-artikeln ovan anger så finns det en bunt anrop och funktioner som är osäkra i sin design, som är svåra att ens få felfria utom i väldigt specifika sammanhang. Anledningen till att de kunnat bli kvar så länge är ju att man under lång tid inte såg just buffer overflows som ett problem i sig, utan när en buffer slog över så kraschade applikationen och sen letade man efter buggen och ev. fixade just den. Anropen blev standard och alla kompilatorer och system vill givetvis följa standarden.

Det OpenBSD valde att göra var att när en ny typ av buggar upptäcktes så gick man igenom all kod som kunde tänkas vara känslig för samma typ av attack och skrev om den snutten till en säkrare version. Det var tidigare rätt vanligt i C att folk slog ihop strängar med "sprintf()" vilket är en av de problematiska anropen, och alla dessa byttes ut mot "snprintf()" som har en längdbegränsning. Först tog man nätverksdemonerna och all kod som har setuid-biten satt (dvs körs som en annan user än den som startar programmet) men nu nyligen i all kod man överhuvudtaget hittat dem i, oavsett om det nånsin kommer vara exploaterbart eller inte.

Så för varje typ av problem som upptäcks (ocheckad stränghantering, signed-vs-unsigned-jämförelser, off-by-one, inte droppa privilegier fort nog, race i tempfils-skapande eller race i signalhanterare osv) så har man letat efter liknande instanser i all kod. Ser man ett program som skapar tempfiler på ett osäkert sätt med anropet mktemp() så letar man upp alla ställen där mktemp() anropas och byter ut mot mkstemp() som är bättre på att skapa temporärfiler utan att riskera att den byts ut under fötterna på en.

Tyvärr så visar det sig att få andra distar (om man nu kallar OpenBSD för en dist) gör på det viset. De förlitar sig på att varje programs team ska "laga" allting själva i stor utsträckning eftersom de är så stora att de inte kan hålla ordning på koden själva. Då får man inte den samordningsvinst som OpenBSD lyckats ha på sin kod. Allt från webservern till de terminalbaserade spelen i /usr/games har fått en (många!) översyn vad gäller alla kända typer av hål och buggar.

Detta har länge varit OpenBSD's stora "övertag" på säkerhetsfronten, vid sidan om krypto som amerikabaserade distar inte kunnat ha med p.g.a. USA's idiotregler om att krypto var likställt med ammunition/vapen och därmed inte kunde exporteras.

Men det räcker inte med att ha tittat igenom koden "ett par gånger". Även om det skulle råka fånga upp alla buggar i koden man distribuerar så kommer folk att ladda ner andra program och installera, så det är en klar fördel om man kan skydda systemet i sig och inte bara se till att ha buggfria applikationer. Problem är hur man skyddar systemet utan att göra det oanvändbart.

Ett av skydden var ProPolice, ett tillägg till C-kompilatorn som kontrollerar att returaddressen inte är förstörd innan man returnerar. Det är inte vrålnytt utan har funnits tidigare i olika skepnader, bl.a som StackGuard och speciella gcc-versioner, men de tidigare har ofta krävt att man kör antingen x86 eller linux eller bådadera. Till slut fick man tag på en version som inte var plattformsberoende utan som fungerade överlag och den åkte in direkt.

Det man gör då är lägga "canaries" runt returaddressen, dvs junkdata med känt värde (som slumpas för varje körning) runt returaddressen och när sedan funktionen hoppar ur så kontrollerar den att dessa inte är borta. Skulle de vara det så kraschar applikationen omedelbart och loggar problemet. Eftersom en overflow oftast går ut på att flöda över lite av varje, inklusive returaddressen, så blir det väldigt mycket svårare att genomföra en attack oupptäckt. Ordet "canary" kommer ju ifrån gammaldags gruvdrift när man hade en kanariefågel med sig ner i gruvan, och eftersom de andas så fort så dog de mycket snabbare om det kom giftig gas i gruvan. Dog fågeln var det bara att springa mot utgången. På samma vis sitter "canary"-värdet runtom returaddressen på stacken, och innan man returnerar via returaddressen kontrollerar man att inte ens "canary" är död.

Ytterligare en detalj var det man kallar "Stack gap", nämligen att inte låta stacken (och indirekt returaddressen) ligga på samma plats/offset varje gång man kör, utan att även slumpa en offset för varje gång som ett program exekveras som läggs till på stacken innan alla operationer görs, vilket ytterligare försvårar för den potentielle hackern. På OpenBSD 3.3-releasen tror jag den slumpar mellan 1-4095 bytes offset, så det kostar ju runt 4k per startat program i RAMminne, men det är en rätt blygsam kostnad. Maxvärdet för slumpen går numera att ställa ännu högre för den som vill ha större variation, men 4k är ett ganska rimligt värde troligen.

Den som läst min tidigare artikel om virtuellt minne vet ju att ett program som körs i ett operativsystem med virtuellt minne alltid ser "likadant" ut ur sitt eget perspektiv, det tycker att det ligger på addressen 0x2000 och framåt eller liknande, även om det är mappat till en helt annan plats i det fysiska RAM:et. Även stacken ligger på samma plats, och ens allokerade data hamnar på samma virtuella plats om man allokerar minne i samma ordning varje gång man kör. Filer som mappas in i minnet ser ut att vara på samma plats och delade bibliotek likaså.

När man då skapar en buffer overflow så har man enorm hjälp av detta, eftersom man vet att den buffer som ligger på 0x435F46A på min maskin gör det även på din, om du kör samma OS/cpu/program. Det går alltså att labba fram alla addresser man behöver veta hemma i lugn och ro (eller scripta och sova medans datorn provar) och när man väl fått fram rätt addresser och offsets så skjuter man över nätet på alla maskiner av samma typ (vanligtvis redhat på x86 ;-) och vinner varje gång. Av den anledningen kan det vara skoj att ha en icke-linux på en icke-x86, eftersom det försvårar enormt för den som attackerar.

Det är ju inte bara negativt att det är på det viset med addresseringen, för det hjälper för supportfolk att om ditt program kraschar på address 0x123456 så kan jag se på min dator att det motsvarar rad 2345 i programmet och leta efter felen där. Men allt som är bra för "folk som sysslar med debuggning" är ju samtidigt bra för "elaka hackers som letar buggar" eftersom buffer overflows går ut på att bugga programmen och få dem att krascha på ett kontrollerat sätt.

Ett annat av skydden som kommer med OpenBSD 3.3 är Write-xor-Execute (W^X), vilket går ut på att man ser till att alla minnessidor som har fått Write-permission satt på sig blir av med Execute och tvärsom. Det låter enkelt, men visade sig vara beroende av cpu-typ och ställde till det för en del programspråk som faktiskt ville dumpa kod lite här och där och sedan exekvera den. Dessa måste man fixa genom att lägga till lite kod som kör anropet mprotect() och anger att "här ska man nu få exekvera" innan de hoppar till den nyskapade koden. Givetvis försvinner Write-rättigheten där då.

Den som inte hunnit tänka sig för kanske tycker att anropet skulle ju givetvis busarna också kunna använda, men se så enkelt är det ju inte. mprotect() körs ju (givetvis) från redan exekverande kod, medans busens kod ligger ju i en buffer nånstans i inkommande-arean hos programmet, kanske en nätverksbuffert eller så. Denna har givetvis Write-permission på sig, och följdaktligen inte Execute. När buffern sedan flödar över och en returaddress pekar in till den så sitter ju fortfarande Write/Read men inte Execute på det minnet, varpå det inte går att exekvera. I Teorin(TM) kan man ju försöka hitta kod som kör mprotect() i binären som samtidigt måste kunna peka ut just överflödesbufferten och som just råkar sätta den till Execute och sedan peka hoppaddressen dit. Sannolikheten för att det finns exakt sån kod är en på ziljonen i princip. Alltså, kod som utan någon som helst annan förberedelse tar just dina 4/8k (av alla tänkbara 4G) och sätter Execute på den. Troligen större chans att Bingolotto ringer fel till mig och ger mig alla bilar och miljoner som inte lottades ut under 2002 istället för att återföra dem till detta årets lotterier.

Tyvärr var just x86 mindre bra designad så W^X fick vänta lite på den (och PPC), medans alpha, sparc64, och hp-pa (och AMDs hammer i 64bits-mod även om den inte är release:ad än för OpenBSD) har W^X redan nu. För x86 lyckades man göra ett ganska intressant trick. Den kunde nämligen inte sätta Execute på en per-sida-basis, dvs på 4/8k's noggrannhet, utan hade nåt skumt segmentregister som hade upplösningen 1Gig men som ändå gick att använda till just det här. Det man gjorde då var att man trixade med programladdaren och kompilatorn så att det data som är read-only i programmet och koden ligger 1G ifrån datat som man avser skriva till och allt minne som man allokerar under drift. Eftersom man har virtuellt minne så kan man göra sådana tricks, utan att programmet påverkas nämnvärt, men det gör en enorm skillnad för säkerheten.

Allt data man hämtat in från okända källor hamnar på fel sida om 1Gigs-staketet och all känd data (konstanter och kod från programmet) ligger på den "säkra" sidan. För PPC var det inte ens så enkelt, där hade man någon suspekt 256M's limit, men eftersom det inte alls är orimligt med kod och data som är större än 256M så kunde man inte bara dela mitt på, som man gjorde med x86, utan kommer sätta varannan 256M:are till W och de andra till X, och sen se till att dela in allt data på rätt plats allteftersom. Ett programs kod kan som det är nu redan delas in i olika segment i binären, så då får man fylla på tills en 256M-sektion är full och sen hoppa över en och fylla vidare på nästa osv. tills programmet är helt inlagt i minnet. Likaså när programmen allokerar minne eller mappar in filer. Det blir mer att hålla reda på, men det får det vara värt.

Men det är inte nog där, utan man jobbar även på andra saker. Jag nämnde ju att den som inte får med nog med kod i sin overflow kan leta efter kod i delade bibliotek där man kanske kan finna lite kod värd att peka den flödade returaddressen till. Detta låter sig göras om man vet att delade bibliotek alltid ligger på samma plats. Precis som med binärer på system med virtuellt minne så är det samma "tradition" att delade bibliotek ser ut att ligga på samma plats med. Men här jobbar OpenBSD på att slumpa i vilken ordning som de delade biblioteken ska mappas in till binären, så att ett program som har X stycken delade bibliotek som mappas in vid programladdningen då ger den potentielle busen (100/X) % chans att koden han vill "låna" ligger på rätt plats. Har slumpen placerat biblioteken i annan ordning så kommer han hoppa rätt in i gud-vet-vad och med enormt stor sannolikhet bara krascha programmet istället för att göra sitt bus. Tyvärr exponerar den här typen av tricks problem med diverse program så första försöket gick inte igenom 100%-igt och det fick tas bort, men jag förutsätter att de jobbar på att åter igen införa slumpad länkning när de rett ut problemen.

Många av de metoder som används är ju inte 100%-iga eller ofelbara, de är snarare till för att göra det osannolikt svårt att den som angriper lyckas på försök #1. Dessa "canaries" t.ex, som man har lagt in via ProPolice, kan ju anta ett par miljarder olika värden så genomsnittsattacken måste köras hälften så många gånger i snitt för att lyckas, och hälften av ett par miljarder blir "ett fåtal miljarder" gånger. Risken för att man blir upptäckt om man kraschar ett program "ett fåtal miljarder" gånger är rätt stor jämfört med att lyckas på första försöket som det var innan.


« Föregående Nästa sida »


16/06-03 | IcePic | icepic@64bits.se

Utskriftsvänligare versionUtskriftsvänligare version

Diskutera denna artikeln i vårt forum!