Control-flow integrity ( CFI ) er en generel betegnelse for computersikkerhedsteknikker, der har til formål at begrænse de mulige stier til programafvikling inden for en forudsagt kontrolflowgraf for at øge dens sikkerhed [1] . CFI gør det sværere for en hacker at tage kontrol over udførelsen af et program ved at gøre det umuligt for nogle måder at genbruge allerede eksisterende dele af maskinkoden. Lignende teknikker omfatter code-pointer separation (CPS) og code-pointer integrity (CPI) [2] [3] .
CFI-understøttelse er til stede i Clang [4] og GCC [5] kompilatorerne samt Control Flow Guard [6] og Return Flow Guard [7] fra Microsoft og Genbrug Attack Protector [8] fra PaX Team.
Opfindelsen af måder at beskytte mod eksekvering af vilkårlig kode, såsom Data Execution Prevention og NX-bit , har ført til fremkomsten af nye metoder, der giver dig mulighed for at få kontrol over programmet (for eksempel returorienteret programmering ) [ 8] . I 2003 udgav PaX Team et dokument, der beskrev mulige situationer, der fører til hacking af programmet, og ideer til beskyttelse mod dem [8] [9] . I 2005 formaliserede en gruppe Microsoft-forskere disse ideer og opfandt udtrykket Control-flow Integrity for at henvise til metoder til beskyttelse mod ændringer i et programs oprindelige kontrolflow. Ud over dette foreslog forfatterne en metode til instrumentering af allerede kompileret maskinkode [1] .
Efterfølgende foreslog forskere, baseret på ideen om CFI, mange forskellige måder at øge programmets modstand mod angreb. De beskrevne tilgange er ikke blevet brugt i vid udstrækning af årsager, herunder store programopbremsninger eller behovet for yderligere information (f.eks. opnået gennem profilering ) [10] .
I 2014 offentliggjorde et team af forskere fra Google et papir, der så på implementeringen af CFI for industrielle compilere GCC og LLVM til instrumentering af C++-programmer. Officiel CFI-støtte blev tilføjet i 2014 i GCC 4.9.0 [5] [11] og i 2015 i Clang 3.7 [12] [13] . Microsoft udgav Control Flow Guard i 2014 til Windows 8.1 og tilføjede understøttelse fra operativsystemet til Visual Studio 2015 [6] .
Hvis der er indirekte hop i programkoden , er det potentielt muligt at overføre styringen til en hvilken som helst adresse , hvor kommandoen kan findes (f.eks. på x86 vil det være enhver mulig adresse, da den mindste kommandolængde er en byte [14] ). Hvis en angriber på en eller anden måde kan ændre den værdi, som kontrol overføres med, når han udfører en springinstruktion, så kan han genbruge den eksisterende programkode til sine egne behov.
I rigtige programmer fører ikke-lokale spring normalt til begyndelsen af funktioner (f.eks. hvis en procedurekaldsinstruktion bruges) eller til instruktionen efter den kaldende instruktion (procedureretur). Den første type overgange er en direkte (engelsk fremadkant ) overgang, da den vil blive angivet med en direkte bue på kontrolflowgrafen. Den anden type kaldes tilbage (eng. back-edge ) overgang, analogt med den første - buen svarende til overgangen vil være omvendt [15] .
Ved direkte hop vil antallet af mulige adresser, som kontrol kan overføres til, svare til antallet af funktioner i programmet. Når der tages højde for typesystemet og semantikken for det programmeringssprog , som kildekoden er skrevet i, er yderligere begrænsninger mulige [16] . For eksempel, i C++ , i et korrekt program , skal en funktionsmarkør, der bruges i et indirekte kald, indeholde adressen på en funktion med samme type som selve pointeren [ 17] .
En måde at implementere kontrol-flow-integritet for direkte spring er, at du kan analysere programmet og bestemme sættet af juridiske adresser for forskellige greninstruktioner [1] . For at bygge et sådant sæt bruges statisk kodeanalyse sædvanligvis på et eller andet abstraktionsniveau (på niveau med kildekode , intern repræsentation af analysatoren eller maskinkode [1] [10] ). Derefter, ved hjælp af den modtagne information, indsættes koden ved siden af instruktionerne fra den indirekte gren for at kontrollere, om den adresse, der modtages ved kørsel, matcher den statisk beregnede. Ved divergens går programmet normalt ned, selvom implementeringer giver dig mulighed for at tilpasse adfærden i tilfælde af en overtrædelse af det forudsagte kontrolflow [18] [19] . Således er kontrolflowgrafen begrænset til kun de kanter (funktionskald) og toppunkter (funktionsindgangspunkter) [1] [16] [20] , der evalueres under statisk analyse, så når man forsøger at ændre den pointer, der bruges til indirekte spring , vil angriberen mislykkes.
Denne metode giver dig mulighed for at forhindre springorienteret programmering [21] og opkaldsorienteret programmering [22] , da sidstnævnte aktivt anvender direkte indirekte spring.
For tilbagegående overgange er flere tilgange til implementering af CFI mulige [8] .
Den første tilgang er baseret på de samme antagelser som CFI for direkte spring, det vil sige evnen til at beregne returadresser fra en funktion [23] .
Den anden tilgang er at behandle returadressen specifikt. Udover blot at gemme det på stakken , gemmes det også, muligvis med nogle ændringer, til et sted, der er specielt tildelt det (for eksempel til et af processorregistrene). Før returinstruktionen tilføjes også kode, der gendanner returadressen og kontrollerer den mod den på stakken [8] .
Den tredje tilgang kræver yderligere support fra hardwaren. Sammen med CFI anvendes en skyggestak - et særligt hukommelsesområde, der er utilgængeligt for en angriber, hvori der gemmes returadresser, når funktioner kaldes [24] .
Ved implementering af CFI-skemaer for tilbagespring er det muligt at forhindre et retur -til-bibliotek-angreb og retur - orienteret programmering baseret på ændring af returadressen på stakken [ 23] .
I dette afsnit vil eksempler på kontrol-flow-integritetsimplementeringer blive overvejet.
Indirect Function Call Checking (IFCC) omfatter kontrol for indirekte hop i et program, med undtagelse af nogle "særlige" hop, såsom virtuelle funktionskald. Når der konstrueres et sæt adresser, hvortil der kan ske en overgang, tages der hensyn til funktionens type. Takket være dette er det muligt at forhindre ikke kun brugen af forkerte værdier, der ikke peger på begyndelsen af funktionen, men også forkert type casting i kildekoden. For at aktivere kontrol i compileren er der en mulighed -fsanitize=cfi-icall[4] .
// clang-ifcc.c #include <stdio.h> int sum ( int x , int y ) { returner x + y _ } int dbl ( int x ) { returnere x + x ; } void call_fn ( int ( * fn )( int )) { printf ( "Resultatværdi: %d \n " , ( * fn )( 42 )); } void erase_type ( void * fn ) { // Opførsel er udefineret, hvis den dynamiske type af fn ikke er den samme som int (*)(int). call_fn ( fn ); } int main () { // Når du kalder erase_type, går statisk typeinformation tabt. slette_type ( sum ); returnere 0 ; }Et program uden kontrol kompilerer uden nogen fejlmeddelelser og udføres med et udefineret resultat, der varierer fra kørsel til kørsel:
$ clang -Wall -Wextra clang-ifcc.c $ ./a.ud Resultatværdi: 1388327490Sammensat med følgende muligheder får du et program, der afbryder, når call_fn kaldes.
$ clang -flto -fvisibility=skjult -fsanitize=cfi -fno-sanitize-trap=alle clang-ifcc.c $ ./a.ud clang-ifcc.c:12:32: runtime error: kontrol flow integritetskontrol for typen 'int (int)' mislykkedes under indirekte funktionskald (./a.out+0x427a20): bemærk: (ukendt) defineret herDenne metode er rettet mod at kontrollere integriteten af virtuelle opkald i C++-sproget. For hvert klassehierarki, der indeholder virtuelle funktioner , er der bygget bitmaps, der viser, hvilke funktioner der kan kaldes for hver statisk type. Hvis tabellen over virtuelle funktioner for ethvert objekt er beskadiget under udførelse i programmet (f.eks. forkert type, der kaster hierarkiet ned eller blot hukommelseskorruption af en angriber), vil den dynamiske type af objektet ikke matche nogen af de forudsagte statisk [10] [25] .
// virtual-calls.cpp #include <cstdio> struktur B { virtual void foo () = 0 ; virtuel ~ B () {} }; struct D : public B { void foo () tilsidesætte { printf ( "Højre funktion \n " ); } }; struct Bad : public B { void foo () tilsidesætte { printf ( "Forkert funktion \n " ); } }; int main () { Dårlig dårlig ; // C++-standarden tillader casting som denne: B & b = static_cast < B &> ( bad ); // Afledt1 -> Base -> Afledt2. D & normal = statisk_kast < D &> ( b ); // Som et resultat er objektets dynamiske type normal normal . foo (); // vil være dårlig, og den forkerte funktion vil blive kaldt. returnere 0 ; }Efter kompilering uden kontrol aktiveret:
$ clang++ -std=c++11 virtual-calls.cpp $ ./a.ud Forkert funktionI programmet kaldes fooklasseimplementeringen fra . Dette problem vil blive fanget, hvis du kompilerer programmet med : DfooBad-fsanitize=cfi-vcall
$ clang++ -std=c++11 -Væg -flto -fvisibility=skjult -fsanitize=cfi-vcall -fno-sanitize-trap=alle virtuelle-opkald.cpp $ ./a.ud virtual-calls.cpp:24:3: runtime error: kontrolflowintegritetskontrol for type 'D' mislykkedes under virtuelt opkald (vtable-adresse 0x000000431ce0) 0x000000431ce0: bemærk: vtable er af typen 'Dårlig' 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 ^