Kontrollflödesintegritet ( CFI ) är ett allmänt namn för datorsäkerhetstekniker som syftar till att begränsa de möjliga vägarna för programexekvering inom en förutspådd kontrollflödesgraf för att öka dess säkerhet [1] . CFI gör det svårare för en angripare att ta kontroll över exekveringen av ett program genom att göra det omöjligt för vissa sätt att återanvända redan befintliga delar av maskinkoden. Liknande tekniker inkluderar kodpekarseparation (CPS) och kodpekarintegritet (CPI) [2] [3] .
CFI-stöd finns i Clang [4] och GCC [5] kompilatorer , såväl som Control Flow Guard [6] och Return Flow Guard [7] från Microsoft och Reuse Attack Protector [8] från PaX Team.
Uppfinningen av sätt att skydda mot exekvering av godtycklig kod, såsom Data Execution Prevention och NX-bit , har lett till uppkomsten av nya metoder som låter dig få kontroll över programmet (till exempel returorienterad programmering ) [ 8] . 2003 publicerade PaX Team ett dokument som beskrev möjliga situationer som leder till hacking av programmet, och idéer för att skydda mot dem [8] [9] . 2005 formaliserade en grupp Microsoft-forskare dessa idéer och myntade termen Control-flow Integrity för att hänvisa till metoder för att skydda mot ändringar i ett programs ursprungliga kontrollflöde. Utöver detta föreslog författarna en metod för instrumentering av redan kompilerad maskinkod [1] .
Därefter föreslog forskare, baserat på idén om CFI, många olika sätt att öka programmets motståndskraft mot attacker. De beskrivna tillvägagångssätten har inte använts i stor utsträckning av skäl, inklusive stora programnedgångar eller behovet av ytterligare information (till exempel erhållen genom profilering ) [10] .
2014 publicerade ett team av forskare från Google en artikel som tittade på implementeringen av CFI för industriella kompilatorer GCC och LLVM för instrumentering av C++-program. Officiellt CFI-stöd lades till 2014 i GCC 4.9.0 [5] [11] och 2015 i Clang 3.7 [12] [13] . Microsoft släppte Control Flow Guard 2014 för Windows 8.1 och lade till stöd från operativsystemet till Visual Studio 2015 [6] .
Om det finns indirekta hopp i programkoden är det potentiellt möjligt att överföra kontrollen till vilken adress som helst där kommandot kan finnas (till exempel på x86 kommer det att vara vilken adress som helst, eftersom den minsta kommandolängden är en byte [14] ). Om en angripare på något sätt kan ändra värdet med vilket kontroll överförs när en hoppinstruktion utförs, kan han återanvända den befintliga programkoden för sina egna behov.
I verkliga program leder icke-lokala hopp vanligtvis till början av funktioner (till exempel om en proceduranropsinstruktion används) eller till instruktionen efter den anropande instruktionen (procedurretur). Den första typen av övergångar är en direkt (engelska framåtkant ) övergång, eftersom den kommer att betecknas med en direktbåge på kontrollflödesgrafen. Den andra typen kallas back (eng. back-edge ) transition, analogt med den första - den båge som motsvarar övergången kommer att vara omvänd [15] .
För direkthopp kommer antalet möjliga adresser till vilka kontroll kan överföras att motsvara antalet funktioner i programmet. Dessutom, när man tar hänsyn till typsystemet och semantiken för det programmeringsspråk som källkoden är skriven på, är ytterligare begränsningar möjliga [16] . Till exempel, i C++ , i ett korrekt program , måste en funktionspekare som används i ett indirekt anrop innehålla adressen till en funktion med samma typ som själva pekaren [ 17] .
Ett sätt att implementera kontrollflödesintegritet för direkta hopp är att du kan analysera programmet och bestämma uppsättningen lagliga adresser för olika greninstruktioner [1] . För att bygga en sådan uppsättning används vanligen statisk kodanalys på någon abstraktionsnivå (på nivån för källkod , intern representation av analysatorn eller maskinkod [1] [10] ). Sedan, med hjälp av den mottagna informationen, infogas koden bredvid instruktionerna för den indirekta grenen för att kontrollera om adressen som tas emot vid körning matchar den statiskt beräknade. Vid divergens kraschar programmet vanligtvis, även om implementeringar tillåter dig att anpassa beteendet i händelse av en överträdelse av det förutsagda kontrollflödet [18] [19] . Sålunda är kontrollflödesgrafen begränsad till endast de kanter (funktionsanrop) och hörn (funktionsingångspunkter) [1] [16] [20] som utvärderas under statisk analys, så när man försöker modifiera pekaren som används för indirekt hopp , kommer angriparen att misslyckas.
Denna metod låter dig förhindra hopporienterad programmering [21] och anropsorienterad programmering [22] eftersom de senare aktivt använder direkta indirekta hopp.
För bakåtövergångar är flera metoder för implementering av CFI möjliga [8] .
Det första tillvägagångssättet bygger på samma antaganden som CFI för direkta hopp, det vill säga möjligheten att beräkna returadresser från en funktion [23] .
Det andra tillvägagångssättet är att behandla returadressen specifikt. Förutom att helt enkelt spara den i stacken , sparas den också, eventuellt med vissa modifieringar, på en plats som är speciellt tilldelad för den (till exempel till ett av processorregistren). Före returinstruktionen läggs också kod till som återställer returadressen och kontrollerar den mot den på stacken [8] .
Det tredje tillvägagångssättet kräver ytterligare stöd från hårdvaran. Tillsammans med CFI används en skuggstack - ett speciellt minnesområde som är otillgängligt för en angripare, i vilket returadresser lagras vid anrop av funktioner [24] .
När man implementerar CFI-scheman för bakåthopp är det möjligt att förhindra en retur -till-biblioteksattack och returorienterad programmering baserat på att ändra returadressen på stacken [ 23] .
I det här avsnittet kommer exempel på implementeringar av kontrollflödesintegritet att övervägas.
Indirekt funktionsanropskontroll (IFCC) inkluderar kontroller av indirekta hopp i ett program, med undantag för vissa "speciella" hopp, såsom virtuella funktionsanrop. När man konstruerar en uppsättning adresser till vilka en övergång kan ske, beaktas typen av funktion. Tack vare detta är det möjligt att förhindra inte bara användningen av felaktiga värden som inte pekar på början av funktionen, utan också felaktig typ av casting i källkoden. För att aktivera kontroller i kompilatorn finns ett alternativ -fsanitize=cfi-icall[4] .
// clang-ifcc.c #include <stdio.h> int summa ( int x , int y ) { returnera x + y _ } int dbl ( int x ) { returnera x + x ; } void call_fn ( int ( * fn )( int )) { printf ( "Resultatvärde: %d \n " , ( * fn )( 42 )); } void erase_type ( void * fn ) { // Beteende är odefinierat om den dynamiska typen av fn inte är samma som int (*)(int). call_fn ( fn ); } int main () { // När erase_type anropas går information om statisk typ förlorad. radera_typ ( summa ); returnera 0 ; }Ett program utan kontroller kompilerar utan några felmeddelanden och körs med ett odefinierat resultat som varierar från körning till körning:
$ clang -Vägg -Wextra clang-ifcc.c $ ./a.ut Resultatvärde: 1388327490Sammanställt med följande alternativ får du ett program som avbryter när call_fn anropas.
$ clang -flto -fvisibility=dolda -fsanitize=cfi -fno-sanitize-trap=alla clang-ifcc.c $ ./a.ut clang-ifcc.c:12:32: runtime error: kontrollflödesintegritetskontroll för typen 'int (int)' misslyckades under indirekt funktionsanrop (./a.out+0x427a20): not: (okänd) definieras härDenna metod syftar till att kontrollera integriteten för virtuella samtal i C++-språket. För varje klasshierarki som innehåller virtuella funktioner byggs bitmappar som visar vilka funktioner som kan anropas för varje statisk typ. Om tabellen över virtuella funktioner för något objekt är skadad under körning i programmet (till exempel felaktig typ som kastar ner hierarkin eller helt enkelt minneskorruption av en angripare), kommer den dynamiska typen av objektet inte att matcha någon av de statiskt förutsagda [10] [25] .
// virtual-calls.cpp #include <cstdio> struktur B { virtuell void foo () = 0 ; virtuell ~ B () {} }; struct D : public B { void foo () åsidosätta { printf ( "Höger funktion \n " ); } }; struct Bad : public B { void foo () åsidosätta { printf ( "Fel funktion \n " ); } }; int main () { Dåligt dåligt ; // C++-standarden tillåter casting så här: B & b = static_cast < B &> ( bad ); // Härledd1 -> Bas -> Härledd2. D & normal = static_cast < D &> ( b ); // Som ett resultat är den dynamiska typen av objektet normal normal . foo (); // kommer att vara dåligt och fel funktion kommer att anropas. returnera 0 ; }Efter kompilering utan kontroller aktiverade:
$ clang++ -std=c++11 virtual-calls.cpp $ ./a.ut Fel funktionI programmet, istället för att fooklassimplementeringen Danropas foofrån Bad. Detta problem kommer att fångas om du kompilerar programmet med -fsanitize=cfi-vcall:
$ clang++ -std=c++11 -Vägg -flto -fvisibility=dold -fsanitize=cfi-vcall -fno-sanitize-trap=alla virtuella-samtal.cpp $ ./a.ut virtual-calls.cpp:24:3: runtime error: kontrollflödesintegritetskontroll för typ 'D' misslyckades under virtuellt samtal (vtable-adress 0x000000431ce0) 0x000000431ce0: notera: vtable är av typen "Bad" 00 00 00 00 30 a2 42 00 00 00 00 00 e0 a1 42 00 00 00 00 00 60 a2 42 00 00 00 00 00 00 00 00 00 ^