

Microsoft Security Response Centre (MSRC) har undersøgt hver eneste sårbarhed i Microsofts programmer siden 2004.
Konklusionen er klar: Langt de fleste fejl handler, som man nok ville forvente, om usikker håndtering af hukommelse i programmerne.
Men hvad hvis man helt kunne slippe for denne type fejl?
Det spørgsmål stiller Gavin Thomas, som er ledende sikkerhedschef i MSRC, i et blogindlæg.
- emailE-mail
- linkKopier link

Fortsæt din læsning
Nu kan du afprøve nye muligheder i C# 11
C#26. april 2022Første .Net 6-kandidat på gaden
.Net22. september 2021Her er første kig på .Net 6 fra Microsoft
C#22. februar 2021
- Sortér efter chevron_right
- Trådet debat
...hvilket vel egentligt er en større fejl end Tony Hoares "Billion Dollar Mistake", hvis man ser på hvor meget exploitable kode det har resulteret i :-)Det kan også være et bevidst valg at ikke specificere opførslen.
Det gav nok OK mening back then, hvor der var noget mere flippede arkitketurer i dag, og maskinerne var væsentligt langsommere. Men det har godt nok kostet os en del... og der er så meget ID/US/UB i C/C++ at jeg er lidt mistænksom overfor personer der påstår de kan lave fejlfri kode.
Det kan også være et bevidst valg at ikke specificere opførslen. C specificerer ikke hvad der sker når en signed integer laver overløb, fordi sproget tillader at hardwaren repræsenterer signed integers som 2-complement, one's-complement og sign-magnitude. Man kunne have valgt at der skulle ske en "trap", men flere arkitekturer har ingen effektiv måde at gøre det på, og det skulle checkes for hver addition/subtraktion/multiplikation. Så man valgte at ikke specificerer hvad der sker. Det samme gør sig gældende for de fleste andre steder i C/C++ hvor opførslen ikke er specificeret.Helt principielt vil jeg også give Troels ret i at forekomsten af undefined behavior kun kan forekomme som resultat af (mangelfuldt) sprogdesign. Standard ML har så vidt jeg ved ingen udefineret opførsel. Det er primært C og C++ der har den slags problemer,
Det er ret besværligt at fremprovokere undefined behavior i Rust. Manish Goregaokar (som er en del af rusts "core" developers) har skrevet en fin artikel om det her: Undefined vs Unsafe in Rust
Pointen er at man skal gøre sig umage hvis man skal producere undefined behaviors i (ikke-unsafe) rust, og det samme gør sig gældende for implementation defined og unspecified behaviors. Langt de fleste udviklere vil aldrig have brug for at skrive mere end en håndfuld meget overskuelige unsafe blokke, som man nemt kan sikre sig rent faktisk er sikre.
Helt principielt vil jeg også give Troels ret i at forekomsten af undefined behavior kun kan forekomme som resultat af (mangelfuldt) sprogdesign. Standard ML har så vidt jeg ved ingen udefineret opførsel. Det er primært C og C++ der har den slags problemer, og det korte og det lange er at Rust undgår/løser dem tilfredsstillende, lige som så mange andre programmeringssprog gør det.
Derudover er GCC sammen med LLVM de oversættere der udnytter udefineret opførsel allermest aggressivt (eksempler. Det er også mit indtryk at de fleste C-programmører ikke har forståelse for hvad der egentlig er udefineret opførsel - f.eks. regner de fleste med at signed heltal bare løber over og bliver negativ ved overflow, men det passer ikke, og det har ikke noget med maskinen at gøre.
Før C11 var det også udefineret opførsel hvis ens C-fil (eller "compilation unit" i spec-sprog) ikke sluttede med et linjeskift. Jeg har dog aldrig hørt om en oversætter der udnyttede det til noget!
Tværtimod, det er en fordel at teste flere compilere samt flere arkitekturer. ID/US/UB er fejl i din kode, ikke i compileren – hvis du vil forbedre dine chancer for at finde fejl før din nuværende compiler introducerer en ny optimering, der genererer fejlbehæftet kode på grund af din UB, har du bedre chancer hvis du tester flere compilere allerede nu.Der er det så måske en fordel at bruge den samme compiler.
Der er det så måske en fordel at bruge den samme compiler. Jeg må sige at det er længe siden jeg har døjet med compiler problemer (7-9-13) da jeg bruger GCC og Linux. Ja principielt er der tale om en ny platform når der kommer en ny CPU, men da antallet af arkitekturer er begrænset, så kører det fint.Mit gæt ville være kodebaser hvor der er en bunke ID/US/UB, hvor det ikke har bidt én i røven endnu, fordi man kun har brugt én compiler. Ny platform, ny compiler, bang.
Når programmøren flere gange bliver nævnt som en almindelig kilde til problemer, så synes jeg det er værd at bemærke, at der er gode grunde til at det er således og at de altså ikke altid skal læses som et angreb på programmørstanden:
Skriver man alene, vil man have svært ved at se dårlige mønstre i sin programmering, og deri kan der skjule sig uhensigtsmæssig opførsel.
Skriver man i grupper, bliver en del af det første problem afhjulpet af flere øjne der kigger, men til gengæld opstår et nyt, hvor een udvikler ikke nødvendigvis er i samme tankesæt som en anden, og dermed misforstår konstruktioner og bagvedliggende motivation. Det kan minimeres med god dokumentation og kommunikation, men det koster penge og er et af de første gode adfærdsmønstre der ofres på deadline-alteret.
Har Rust macroer???
Ja.
De tillader vilkårlige transformationer og kodegenerering, ganske som Lisp-makroer, og med nogenlunde de samme fordele og ulemper. Beregningsmæssigt er de ikke stærkere end C++ templates (begge er Turing-komplette), men de er langt mere udtryksfulde, da deres definitioner udtrykkes via ordinær Rust-kode, i stedet for C++'s sære implicitte template-metaprogrammeringssprog.
Hvad typesikre måleenheder angår har jeg fundet dette bibliotek der påstår at være zero-overhead. Det foregår dog ikke via makroer, men via almindelig parametrisk polymorfi. Det er for så vidt også godt nok, da Rust's nuværende implementering af parametrisk polymorfi gør brug af monomorfisering, hvilket har samme kodemæssige effekt som C++'s template-instantiering (nemlig en kopi af den polymorfe kode for hver type den anvendes med).
Har Rust macroer???
Jeg kan se type generiske metoder som i ML og andre funktionelle sprog. Og generiske typer. De minder end del om templates, men er umiddelbart ikke så kraftfulde: man kan så vidt jeg kan se ikke angive andet end typer som argument. Jeg kan derfor ikke se, hvordan man kan lave noget, som minder om boost::unit - fysiske enheder indlejret i typerne og checket compile-time med zero runtime overhead. Man får f.eks. en compile fejl, hvis man ligger en længde til en længde med en tid, fås en hastighed. Enhedsalgebra compile-time, kort og godt.
Det jeg kunne finde på nettet bar rundt på enheder runtime, ligesom forsøget på at lave det i Java. Det giver jo selv sagt et overhead.
Det som man mangler er en generisk type, som kan regne på potenser i enhederne, f.eks. m*m / s / s = m^2 s^-2 compile time.
Det er ikke lige til i Rust, da man ikke kan angive potenserne ovenfor som "template" argumenter og regne på dem compile time.
Mit gæt ville være kodebaser hvor der er en bunke ID/US/UB, hvor det ikke har bidt én i røven endnu, fordi man kun har brugt én compiler. Ny platform, ny compiler, bang.Hvad er det for en kobling du ser mellem krydskompilering og undefined behaviour? Sidstnævnte er et koncept fra sprogspecifikationen, og har ikke noget at gøre med en konkret platform, endsige krydskompilering.
Det er en fin diskussion :-)
Men er der ikke nogen der bruger Rust/emb Rust og vil dele deres erfaringer ?
En anden er, at meget kode i dag genbruges på flere platforme, og krydskompileres, med deraf risiko for manglende styr på "undefined behavior".
Hvad er det for en kobling du ser mellem krydskompilering og undefined behaviour? Sidstnævnte er et koncept fra sprogspecifikationen, og har ikke noget at gøre med en konkret platform, endsige krydskompilering.
Jo, det er en af de nærliggende årsager. En anden er, at meget kode i dag genbruges på flere platforme, og krydskompileres, med deraf risiko for manglende styr på "undefined behavior".Er dette ikke primært et problem i C og C++, hvor der er så kæmpe mængder af implementation-defined, unspecified og undefined behavior at det er forbandet svært at skrive korrekt kode?
Der er en del forskel på macroer og templates: de sidste er et Turing-komplet, funktionelt
Rust har makroer, ikke templates som du kender dem fra c++. Men du kan meget mere end C makroer og de er sandsynligvis turingkomplet.
Den eneste grund til at C makroer ikke er turingkomplet er at de ikke kan rekursere. Det kan Rust makroer.
Tak for uddybning. Det betyder så at sproget skal kende til tråde. Jeg går ud fra at det et eller andet sted giver en tradeoff.Du kan kun lave sikre operationer. Eksempelvis kan du give ejerskab over noget data til en anden tråd. Det er så den tråd der har ansvaret for at frigive hukommelse. Den oprindelige tråd kan ikke tilgå data mere, end ikke read only, så der er ikke risiko for at data tilgås efter hukommelse er frigivet.
10 PAGE 20 PRINT "COMAL 80 vil genopstå!!!" 30 Goto 10
Der er en del forskel på macroer og templates: de sidste er et Turing-komplet, funktionelt programmeringssprog, som kører i compileren og genererer ny kode og typer undervejs. Med optimering slået til, vil man ofte få fjernet meget af koden igen, og lokkeren kan måske også fjerne ubrugte symboler; men generelt bliver alle "mellemregningerne" hængende i det endelige program.
Jeg foretrækker selv templates frem for macroer, da disse bliver meget værere, hvis de skal skulle bare en smule af, hvad templates kan. Se blot på Linux kernens list implementation: det kan gøres meget kønnere med templates.
Der er en del forskel på macroer og templates: de sidste er et Turing-komplet, funktionelt programmeringssprog, som kører i compileren og genererer ny kode og typer undervejs. Med optimering slået til, vil man ofte få fjernet meget af koden igen, og lokkeren kan måske også fjerne ubrugte symboler; men generelt bliver alle "mellemregningerne" hængende i det endelige program.
Jeg foretrækker selv templates frem for macroer, da disse bliver meget værere, hvis de skal skulle bare en smule af, hvad templates kan. Se blot på Linux kernens list implementation: det kan gøres meget kønnere med templates.
Er dette ikke primært et problem i C og C++, hvor der er så kæmpe mængder af implementation-defined, unspecified og undefined behavior at det er forbandet svært at skrive korrekt kode?Han dekomponerer maskinkoden og kigger på hvordan han i givet fald kan udnytte sårbarheder. De opstår naturligvis som beskrevet hvis den logiske sammenhæng i programmeringssproget har huller, men så sandelig også hvis compileren generere dårlig maskinkode.
Jeg kan ikke huske at have set nogle eksempler på et compiler-fejl har genereret exploitable kode. Eksempler på at selv dygtige udviklere bliver bidt af ID/US/UB er derimod en anden sag.
Og hvis man skriver C-kode som om det var en "high-level assembly" kan man også få sig nogle overraskelser, ikke mindst på grund af hvilke optimeringer compilere (ganske tilladt af standarden) kan lave. Det har mere end én gang fjernet runtime null-checks i Linux kernen, og har ledt til privesc bugs.
Det, som laver megen ekstra, ukontrolleret kode i C++, er templates. Dem har man så vidt jeg ved også i Rust.
Og C har #define. Du bestemmer selv hvor meget du benytter makroer i din kode.
Men hvordan det så hjælper i forbindelse med f.eks. multithreading bliver lidt mere tåget for mig. Specielt når der ikke sker checks på target.
Du kan kun lave sikre operationer. Eksempelvis kan du give ejerskab over noget data til en anden tråd. Det er så den tråd der har ansvaret for at frigive hukommelse. Den oprindelige tråd kan ikke tilgå data mere, end ikke read only, så der er ikke risiko for at data tilgås efter hukommelse er frigivet.
Alt det er sikret og tjekket inden maskinkoden bliver genereret. Derfor behøver maskinkoden ikke at indeholde tjeks for det samme.
Det lyder jo godt. Men hvordan det så hjælper i forbindelse med f.eks. multithreading bliver lidt mere tåget for mig. Specielt når der ikke sker checks på target.Mantraet indenfor Rust er zero cost abstractions. Det vil sige at alt det med ejerskab med videre er noget der tjekkes af oversætteren og herefter smides det væk. Der er ingen tjeks i den generede maskinkode.
Inden jeg ville hoppe på en ting som Rust, vil jeg sætte mig ind i hvad jeg smider på mit target.
Hvordan holder sproget f.eks. styr på ejerskabet. Hvad er overhead ved det og hvilke dumheder kan man lave med det.
Mantraet indenfor Rust er zero cost abstractions. Det vil sige at alt det med ejerskab med videre er noget der tjekkes af oversætteren og herefter smides det væk. Der er ingen tjeks i den generede maskinkode.
Et andet eksempel er at der ingen "null" i sproget, men det er der i maskinkoden. Fordi det er effektivt og oversætteren laver ikke fejl, så den kan godt bruge konstruktioner der er farlige for os mennesker.
Og der er ikke nødvendigvis en 100% garanti for, at selv om et programmeringssprog er sikret i forhold til sprogets logik og brug af memory, at maskinkoden ikke kan rumme (masser af) uhensigtsmæssigheder.
Der er bestemt mulighed for at en oversætter kan generere forkert kode, og at denne kode kan udnyttes af en angriber. Uagtet gamle travere som Ken Thompson's Trusting Trust, er der så praktiske eksempler på at dette er sket? Hvor ofte sker det ift. så mange andre sikkerhedsbrister? Jeg tror ikke det i praksis er noget det er værd at bekymre sig om. Faktisk er jeg villig til at postulere en (empirisk u-underbygget) stærkere hypotese: Det er værd at lave sproget og oversætteren mere kompliceret (som Rust) for at undgå hukommelsesfejl i den programmørskrevne kode, selv hvis øger risikoen for fejl i oversætteren der potentielt kan udnyttes. Jeg mener at gevinsten er større end risikoen.
Der er massevis eksempler på at oversættere genererer forkert kode (især med mange optimeringer slået til), men det er sjældent de er blevet udnyttet af angribere, for som regel får de programmet til at crashe før det kommer i brug. Og der er blevet udviklet beviseligt korrekte oversættere (desværre til C) som garanterer at de slet ikke har sådanne fejl i den genererede kode (under en række antagelser; læs altid hvad der står med småt).
Jeg synes et eller andet sted, at der mangler et aspekt i diskussionen. Et er, at forbedre og modernisere programmeringssprog, så risikoen for fejl i koden minimeres. Noget helt andet er at se tingene fra en hackers synspunkt.
Vi taler her om complierede sprog, som generere masser af maskinkode, og det er klart, at der ikke må være oplagte uhensigtsmæssigheder i f.eks. memory allokering, frigivelse, gensidig beskyttelse, raeltidsparametre osv. men typisk er det ikke kun det en hacker går efter.
Han dekomponerer maskinkoden og kigger på hvordan han i givet fald kan udnytte sårbarheder. De opstår naturligvis som beskrevet hvis den logiske sammenhæng i programmeringssproget har huller, men så sandelig også hvis compileren generere dårlig maskinkode. Og der er ikke nødvendigvis en 100% garanti for, at selv om et programmeringssprog er sikret i forhold til sprogets logik og brug af memory, at maskinkoden ikke kan rumme (masser af) uhensigtsmæssigheder.
Så uanset hvor meget man holder af et givet sprog, og uanset hvor godt sproget er til at beskytte brugeren imod fejl, så ændrer det ikke på, at man fortsat har behov for at se på den konkrete implementering af selve compileren. Er (maskin)koden sikret. Hvad sker der ved evt. krydskompilering. Hvad sker der i forskellige hardware konstellationer osv. osv. Jeg har gennem årene set skrækkelige compilere, som generere den rene skod-kode, selv om alting ser helt vandtæt og smukt ud i kildeteksten.
Så skal sproget være sikkert, så omfatter det også at selve compileringsprocessen gøres til genstand for sikkerhedsanalyse.
Udefineret opførsel er ikke problemet. Problemet er hvor mange ukendte operationer sproget benytter for at opnå den definerede opførsel. Esben Nielsens oplevelse ovenfor et et godt eksempel. Jeg er sikker på at hans program gjorde som det skulle, hvis der var ressourcer til det.Det kan du godt hvis du aktivt tilvælger det (ved at bruge "unsafe"-blokke), men uden for disse garanterer Rust typesikkerhed og fraværet af udefineret opførsel.
I øvrigt er C blevet mere typesafe. Men man kan stadigt typecaste sig hele vejen til helvede.
Du gøer op ad det forkerte træ!Man må finde en stærkere differentiator hvis man mener at Pascal/Delphi er værd at bruge ift. f.eks. Rust.
Jeg har ikke sagt at Pascal skulle være bedre end Rust, men kommenteret at FreePascal godt kan køre flertrådet, og at Pascal generelt har en rigtig god string håndtering.
Og så er jeg i øvrigt ret så ligeglad med om man har en eller anden up-nose holdning til at Pascal skulle være 70'er agtig, hvis det kan løse den opgave jeg vil have det til at løse, på en brøkdel af tiden med et hurtigt og stabilt resultat til følge - og det er så stadig lysår foran f.eks. MUMPS som man i region Sjælland har inficeret sygehusvæsenet med.
de bedste programmer er lavet i ASM :)
Jeg er godt klar over at det er lidt OT i forhold til programmering af embeddede systemer, men hvis man laver forretningsorienterede programmer, og/eller webapplikationer, så er der tit en farlig masse stings man skal rode med, hvorfor string håndtering så afgjort er vigtig
Det var heller ikke min pointe - strings er skam vigtige, det tror jeg alle ved. Min pointe var snarere at de er et løst problem. C er det eneste sprog hvor de stadigvæk er elendige[1]. Man må finde en stærkere differentiator hvis man mener at Pascal/Delphi er værd at bruge ift. f.eks. Rust.
[1]: Afhængigt af hvor kræsen man er har de fleste sprog dårlige strings, men så er det fordi man går op i hvorvidt de repræsenterer bytes, Unicode code points, grafemklynger, eller hvad det nu måtte være. Tekst er i almindelighed svært. Men ingen andre udbredte sprog end C har problemer med at tekstbehandling giver hukommelseskorruption.
Det, som laver megen ekstra, ukontrolleret kode i C++, er templates. Dem har man så vidt jeg ved også i Rust.
Jeg har selv brændt nallerne med at lave pæn C++ kode, som miksede templates og virtuelle funktioner: det gav et hav af klasser med virtuelle tabeller og runtime type-information, som fyldte alt for meget på et 16 MB stort system.
Nu koder jeg på PC størrelse systemer, og har derfor ikke den slags problemer. Jeg har gennemtvunget, at vi kun lavet een-trådede programmer, men flere af dem på Linux og på den måde bruger MMU'en til at gennemtvinge data-adskillelse. På den måde er C++ mindst lige så nemt som Java - men det oversætter meget langsommere, og værktøjerne er dårligere.
Det samme gør man iøvrigt ofte i mere embeddede systemer uden MMU, her bruger man blot besked-køer frem for pipes eller TCP forbindelser, som vi gør.
Med Rust ville jeg nok turde begynde at parellelliserer noget af koden direkte inde i hvert program, men med C++, holder jeg mig til at parellisere ved at starte flere processor op og samle igen. Det kan også bedre spredes ud på mange maskiner.
Nede i kernen, hvor parellisering er uundgåelig, er Rust nok helt klart at foretrække - men man skal nok ikke drømme om mindre hukommelsesforbrug end C++, da Rust, så vidt jeg kan se, bygger på meget af det samme.
Det var en mærkelig hovski-snovski holdning at udvise.Det er altså noget af en 70'er/80'er-problemstilling at gå op i strings.
Jeg er godt klar over at det er lidt OT i forhold til programmering af embeddede systemer, men hvis man laver forretningsorienterede programmer, og/eller webapplikationer, så er der tit en farlig masse stings man skal rode med, hvorfor string håndtering så afgjort er vigtig, og det kan korte udviklingstiden voldsomt meget ned, hvis man ikke skal bøvle med trivialproblemer.
Jeg kan ikke se, at du ikke skulle kunne lave samme dumheder i Rust, som du kan i eks c/c++.
Det kan du godt hvis du aktivt tilvælger det (ved at bruge "unsafe"-blokke), men uden for disse garanterer Rust typesikkerhed og fraværet af udefineret opførsel. Det er altså ikke umuligt at kvaje sig i Rust, men det er mere vanskeligt, mens man samtidigt har mulighed for (bevidst) at slå sikkerhedsmekanismerne fra, hvis man har behov for at lave noget man er rimeligt sikker på er korrekt, men som bryder sprogets (konservative) regler. Man kan også bygge abstraktioner i C++ og C, men man er ikke på samme måde beskyttet mod at bryde reglerne.
Til gengæld så har Pascal rigtig god strenghåndtering, og man behøver ikke rode sig ud i et pointerhelvede.
Det er altså noget af en 70'er/80'er-problemstilling at gå op i strings. Ja, de er elendige i C. Ja, Pascal gør og gjorde det bedre. Men det har ikke noget at gøre med Rust, eller endda C++. Der er nye kampe at kæmpe! Hvis Delphi stadigvæk kæmper kampen mod C, og ikke de mange nyere sprog der har erstattet det meste C-programmering (uagtet at C stadigvæk er udbredt), så undrer det ikke at Delphi står med en fod i graven...
Hvad er overhead ved det og hvilke dumheder kan man lave med det.
Jeg kan ikke se, at du ikke skulle kunne lave samme dumheder i Rust, som du kan i eks c/c++.
Siden alt fra udgangspunktet er immutable (write once / read many) så er det klart nemmere at holde styr på sikkerhed og samtidighed.
Til gengæld er tradeoff jo ganske givet et højere hukommelses forbrug og mere memory allokerings overhead.
Det kan man så let lave med C++.der er mange ting i TP/Delphi som jeg savner i C, incl. ShortString
Men når vi taler embedded, så er der mange som ikke ønsker C++. Forlaringen er den simple, at C++ i modsætning til C kan generere kode som laver ting bagom ryggen på udvikleren.
Inden jeg ville hoppe på en ting som Rust, vil jeg sætte mig ind i hvad jeg smider på mit target. Hvordan holder sproget f.eks. styr på ejerskabet. Hvad er overhead ved det og hvilke dumheder kan man lave med det.
Det er muligt at lave flertrådede programmer i FreePascal - men jeg vil blankt indrømme at jeg aldrig har haft behov for det, da programafvikling som regel går så hurtigt at det ikke er nødvendigt med flere tråde (men ikke dermed sagt at andre ikke kan have behovet).Rust's ownership-model handler især om at gøre det rimeligt at skrive højtydende og korrekte flertrådede programmer. Der er mig bekendt ikke nogen lignende løsning i Delphi.
Til gengæld så har Pascal rigtig god strenghåndtering, og man behøver ikke rode sig ud i et pointerhelvede.
Jeg har ikke selv erfaringer emd embedded Rust udover lidt hobby-projekter på en STM32F3DISCOVERY, men der er en hel embedded Rust bog, og det er et af Rust-projektets fokus-punkter at sproget skal være en gyldig erstatning til C også på det område.
For mere info:https://rust-embedded.github.io/book/https://www.rust-lang.org/what/embeddedhttps://github.com/rust-embedded/wg
Rust's ownership-model handler især om at gøre det rimeligt at skrive højtydende og korrekte flertrådede programmer. Der er mig bekendt ikke nogen lignende løsning i Delphi.
Helt enig, der er mange ting i TP/Delphi som jeg savner i C, incl. ShortString.
Hej Der/vi er mange der bruger C og eks freeRTOS til embeddede systemer typisk på noget ARM.
Er der nogen der har prøvet at bruge Rust istedet (evt sammen med freeRTOS eller anden preemptive kerne) og vil dele deres erfaringer med os andre der går og overvejer det ?
Så vil jeg blive glad :-)
mvh
JEns
Kan vel løse det gammel kendte problem i det vilde C