Sichere Softwareentwicklung mit C++

sven-johannsen.de
sven@sven-johannsen.de

heise devSec()
25.10.2017

Sven Johannsen

  • Software Entwickler bei Schlumberger
  • 20 Jahre C++
  • C++ User Group Aachen und Düsseldorf

Motivation 1/3

From OWASP: Buffer Overflows

#include <string.h>

void f(char* s) {
    char buffer[10];
    strcpy(buffer, s);
    // ...
}

int main() {
    f("01234567890123456789");
}

Motivation 2/3

Kein Problem für C++?

#include <string>

void f(const std::string& s) {
    std::string buffer;
    buffer = s;
    // ...
}

int main() {
    f("01234567890123456789");
}

Motivation 3/3

C & C++ haben den Ruf unsichere Programmiersprachen zu sein.
Für C++ muss das nicht sein:

These:
C++ besteht aus "C" und "++".
Die konsequente Verwendung des "++"-Teil von C++ ("Modern C++") macht es einfacher, sichere Programme zu schreiben.

Inhalt

  • Motivation
  • OWASP und CWE
  • Bekannte Probleme und Lösungen mit der STL
  • Was kann man von der STL lernen

OWASP

The Open Web Application Security Project

https://www.owasp.org/index.php/Main_Page

OWASP Top 10 Application Security Risks - 2013 (2017rc)

OWASP

Die OWASP C++ Beispiele sind alles C Probleme, die so auch in C++ auftreten könnten:

Mitre CWE

https://cwe.mitre.org/index.html

Common Weakness Enumeration: CWE™ is a community-developed list of common software security weaknesses.

Liste von "Software Schwachstellen"

CWE-120: Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')

https://cwe.mitre.org/data/definitions/120.html

Description Summary:
The program copies an input buffer to an output buffer without verifying that the size of the input buffer is less than the size of the output buffer, leading to a buffer overflow.

Applicable Platforms: ...
Common Consequences: ...
Likelihood of Exploit: ...
Demonstrative Examples:

void manipulate_string(char* string){
    char buf[24];
    strcpy(buf, string);
    ...
}

Kopieren eines Strings in C

CWE-120: Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')
Buffer Overflow https://www.owasp.org/index.php/Buffer_Overflow
char *strcpy(char *dest, const char *src);

void f(char* s) {
    char buffer[10];
    strcpy(buffer, s); // buffer overflow
    // ...
}

 f("01234567890123456789");

strcpy() kann die Länge des Strings s ermitteln, aber nicht die Größe der Buffer buffer und s.

Zeichenketten in C

Strings: Felder von "char" oder Zeiger auf "char".
Plus: abschließendes 0 Zeichens.

C string model

Länge eines Strings und Größe des Buffers sind 2 unabhängige Dinge

strcpy() bekommt nur die Zeiger auf den Buffer-Anfang übergeben.

Lösung strncpy?

char *strncpy(char *dest, const char *src, size_t n);

void f(char* s) {
    char buffer[10];
    strncpy(buffer, s, 10);
    int len = strlen(buffer); // len == ?
    // ...
}

f("01234567890123456789");

strncpy bekommt zusätzlich die Länge des Ziel-Buffers übergeben.

int len = ?

man 3 strncpy

CWE-170: Improper Null Termination
String Termination Error https://www.owasp.org/index.php/String_Termination_Error

Aus der Linux Hilfe: man 3 strcpy

The strncpy() function is similar, except that at most n bytes of src are copied. Warning: If there is no null byte among the first n bytes of src, the string placed in dest will not be null-terminated.

char buf[4] = "";          // (1) "\0???"
strncpy(buf, "ABC", 4);    // (2) "ABC\0"
strncpy(buf, "ABCDEF", 4); // (3) "ABCD"
int l = strlen(buf);       // (4) Improper Null Termination

Zeichenketten in C++:

std::string

Basiert intern auf einem 3 Zeiger Model:
Anfang <= Ende <= Kapazität

C++ string model

Länge = Ende - Anfang (ohne abschließendes 0)
Größe des Buffers = Kapazität - Anfang

std::string

Information-Hiding (=Datenkapselung)

3 Zeiger-Modell als private Member.

Modifikation und Zugriff nur über Member-Funktionen:

  • resize()
  • insert(), append(),
  • clear(),
  • Zuweisungs-Operator (operator=())
  • ...

Kopieren eines Strings in C++

#include <string>

void f(const std::string& s) {
    std::string buffer;
    buffer = s;
    // ...
}

f("01234567890123456789");

Die Größe von buffer wird bei der Zuweisung angepasst:

  • kein Buffer Overflow!
  • kein "String Termination Error"

Probleme des C++ Strings

Die Klasse std::string löst viele Probleme, aber bringt aber auch neue:

  • Speichert immer auf dem Heap (Performance)
  • Neues Fehlerkonzept (Exceptions)
  • (Zu viele Member-Funktionen)

std::string (C++11/C++17)

C++11

  • Small String Optimization (SSO)
  • Move Semantic

C++17

  • std::string_view

Der Datentyp ist entscheidend!

Gefährlicher Legacy Code

char text[10];
gets(text); // Buffer overflow

Austausch der Funktion, gleicher Datentyp

char text[10];
std::cin >> text; // Buffer overflow

Austausch des Datentyps

std::string text;
std::cin >> text;
std::string text;
std::getline(std::cin, text);

C++ von und für C-Programmierer

C++ wurde von und für C-Programmierer entwickelt.

Alte Probleme wurden nicht direkt angegangen und alte Schreibweisen wurden übernommen:

Beispiele:

std::vector<int> v = { 1, 2, 3};
int n1 = v[3];    // CWE-129: Improper Validation of Array Index
int n2 = v.at(3); // Exception: std::out_of_range

for (vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
    // ...
}
  • Keine Bereichsüberprüfung beim [] Zugriff,
  • Iteratoren sind auch nur "Pointer" mit Semantik-Sugar

Kontrollstrukturen outsourcen

Z.B. durch die Verwendung von Algorithmen:

vector<int> v = { 1, 2, 3};
list<int>   l = { 1, 2, 3};

bool isEqual = std::equal(v.begin(), v.end(), l.begin(), l.end());

Schleife über alle Werte von v und l und die einzelnen Werte vergleichen.

Effizient für verschiedene Container, ohne dass der Algorithmus den Container kennen muss.

boost range

Vereinfachte Syntax mit boost::range

vector<int> v = { 1, 2, 3};
list<int>   l = { 1, 2, 3};

//bool isEqual = std::equal(v.begin(), v.end(), l.begin(), l.end());
bool isEqual = boost::range::equal(v, l);

boost range

Was ist ein range?

vector<int> v = { 1, 2, 3};

// v is a range, if boost::begin(v) and boost::end(v) are valid (1)
auto it_begin = boost::begin(v); // calls intern v.begin()
auto it_end   = boost::end(v);   // calls intern v.end()

int sum = boost::range::accumulate(v, 0);

1) Ruft intern .begin() und .end() auf

Algorithmen und Ranges

Eine ungerade Parameteranzahl ist für Input Ranges eine schlechte Idee:

// C++ 98 
template< class InputIt1, class InputIt2 >
bool equal( InputIt1 first1, InputIt1 last1,
            InputIt2 first2 );

// since C++14
template< class InputIt1, class InputIt2 >
bool equal( InputIt1 first1, InputIt1 last1,
            InputIt2 first2, InputIt2 last2 );

bool isEqual = std::equal(v.begin(), v.end(), l.begin());
bool isEqual = std::equal(v.begin(), v.end(), l.begin(), l.end()); // C++14

boost::range::equal hatte nie dieses Problem!

Gilt im Prinzip auch für Output Ranges.
Lösungsansätze: z.B. boost::range::overwrite(), boost::range::push_back()

range v3

Rewrite of boost::range (2.0) from Eric Niebler

Proposal für C++20? (D4128: Ranges for the Standard Library)

https://github.com/ericniebler/range-v3

Range based for loop

C++11

vector<int> v = { 1, 2, 3 };

for(int val : v) {
    cout << val << ", ";
}

Ruft intern .begin() und .end() auf.
Gleiche Anforderungen wie boost range.

Stack & Destruktoren

Der Stack und die Destruktoren sind die Freunde des
C++-Programmierers.

Fehlende Initialisierung

CWE-456: Missing Initialization of a Variable
MyClass* pMyClass;
// ...
if (!pMyClass {
    pMyClass = new MyClass(param1, param2);
}
pMyClass->memberFunction(); // crash???

Smart Pointer als Universal-Werkzeug für Pointer

unique_ptr<MyClass> pMyClass;   // calls default constructor
// ...
if (!pMyClass) {
    pMyClass.reset(new MyClass(param1, param2));
}
pMyClass->memberFunction(); // no Crash!!!

Zeiger sollten in Smart-Pointer gespeichert werden:

  • Immer initialisiert
  • Keine Memory-Leaks (incl. Exception Safety)
  • Semantische Zeiger (Ownership, Single value vs. Field)

Smart Pointer: nicht erst seit C++11

Pre-C++11 Compiler: (boost)

  • boost::scoped_ptr<T>
  • boost::shared_ptr<T>

C++11:

  • std::unique_ptr<T>
  • std::shared_ptr<T> (TR1)

andere:

  • COM-Pointer: CComPtr<T>, Qt: QPointer<T>, QSharedPointer<T>, ATL, ...

Achtung: auto_ptr<T> macht unerwartete Dinge

Keine Leaks

CWE-401: Improper Release of Memory Before Removing Last Reference ('Memory Leak')
MyObject* getMyObject(const string& name)
{
    MyObject* pResult = new MyObject;
    // ...
    if (...) return nullptr; // leak 1
    // ...
    checkObject(); // may throw an exception => leak 2
    // ...
    return pResult;
}

void foo()
{
    getMyObject("Test"); // leak 3
}

Keine Leaks

unique_ptr<T>

// MyObject*         getMyObject(const string& name)
unique_ptr<MyObject> getMyObject(const string& name)
{
    //MyObject* pResult        = new MyObject;
    unique_ptr<MyObject> pResult(new MyObject); // fix syntax later
    // ...
    if (...) return nullptr; // return {};
    // ...
    checkObject(); // may throw an exception => no leak
    // ...
    return pResult;
}

void foo()
{
    getMyObject("Test");
}

unique_ptr::~unique_ptr() gibt den Speicher frei

Kontrollierte Freigabe von Ressourcen

CWE-415: Double Free

unique_ptr<MyObject> ptr1 = make_unique<MyObject>();
unique_ptr<MyObject> ptr2 = ptr1; // Compiler error

// ptr2.reset(ptr1.get()); .get() only for legacy code

shared_ptr<MyObject> ptr3 = make_shared<MyObject>();
shared_ptr<MyObject> ptr4 = ptr3; // ok: ref counting
// last shared_ptr will free memory

shared_ptr<MyObject> ptr5(ptr1.get()); // C++ Double Free
//shared_ptr<MyObject> ptr5(ptr1.release()); No Double Free

shared_ptr haben Probleme mit Zyklischen Abhängigkeiten

Speicher darf nicht 2 Smart Pointer übergeben werden. (.get())

Semantik für Pointer

// RAW pointer
MyObject* getMyObject(const string&);

MyObject* ptr = getMyObject("Test");
  • Wer gibt ptr frei? (Ownership)
  • Ist ptr ein Array?
// Smart Pointer
unique_ptr<MyObject> getMyObject(const string&);

unique_ptr<MyObject> ptr = getMyObject("Test");

ptr ist der "owner" für den Speicher und ptr zeigt auf ein einzelnes Element.

shared_ptr für Felder (C++17)

C++-11:

  • unique_ptr<T> Zeiger auf einen Wert
  • unique_ptr<T[]> Zeiger auf ein dynamisches Feld
  • shared_ptr<T> Zeiger auf einen Wert

C++-17

  • shared_ptr<T[]> Zeiger auf ein dynamisches Feld

Smart Pointer für Felder haben keine Informationen über Feldgröße:

Für Felder: string, vector<T>, array<T,N> !
Für Legacy Code: c_str() und data()

make_unique (C++14)

make_unique<T> ermöglicht eine schönere Syntax und sicheren Code.

unique_ptr<MyClass> pMyClass;
// ...
if (...) {
    // C++11, boost::smart_pointer
    pMyClass.reset(new MyClass(param1, param2));

    // C++14 for unique_ptr, C++11 for shared_ptr
    pMyClass = make_unique<MyClass>(param1, param2);
}

make_shared<T> (C++11)

RAII "Resource Acquisition Is Initialization"

Smart-Pointer ist der Archetyp der RAII Typen:

{
    FILE* f = fopen("test.txt","r");
    // ...
    fclose(f);
}
{
    ifstream ifs("test.txt"); // implicit ifs.open("test.txt")
    // ...
}   // ~ifstream close file handle

Der Destruktor gibt die Ressourcen frei.

RAII funktioniert auf dem Stack (lokale Variablen) und mit Member-Variablen.

RAII Beispiele

  • std::string,
  • std::vector<T> und alle anderen Container
  • std::ifstream, std::ofstream
  • Mutex guards:
std::mutex g_mtx;

void foo() {
    std::lock_guard(g_mtx); // g_mtx.lock();

    // do something.

    // g_mtx.unlock();
}
  • ...

Was kann man von der STL lernen?

(C++ Standard Bibliothek, die inzwischen weit über die ursprüngliche STL Container/Iteratoren und Algorithmen hinausgeht.)

Klassen

Klassen bändigen unsichere Datentypen, wie z.B. Zeiger.

Beispiel:

  • std::string
  • Smart Pointer

Keine Memory leaks
Gesicherte Initialisierung (korrekte Konstruktoren vorausgesetzt)
Kontrollierte Kopien (flache Kopie, tiefe Kopie, Kopien verbieten)
Kontrollierte Konvertierung

Es geht auch ohne Polymorphie

Die STL kommt weitgehend ohne Laufzeit Polymorphie aus. (Vererbung)

Stattdessen wird Compile time Polymorphism verwendet (templates, Duck-Typing)

Dies ermöglich eine hohe "composability". ("Lego Stein Prinzip")

Zusammengesetzte Klassen

class MyClass
{
public:
    // MyClass() {}

private:
    std::string mText;
    std::unique_ptr<OtherObj> mOther;
}

Wenn alle Member-Variablen richtig programmiert sind, können Kopier- und Move-Konstruktoren und Zuweisungs- und Move-Zuweisungs-Operatoren entfallen.

Exceptions safety

"Exceptions safe" code, nicht nur im Fall von Exceptions.

void foo()
{
    unique_ptr<Bar> ptr = make_unique<Bar>(...):
    // ...
    baz(); // may throw an exception
    // ...
    if (...) return;
    // ...
}

Gilt genauso für Mutex, file handles, Wait cursor (Sanduhr), ...

Basic exception safety (== No Resource leaks (vereinfacht))

Get- und Set-Funktionen

Die STL kennt 2 Typen von Klassen:

Klassen (class) ohne set-Zugriff. Der innere Zustand wird durch Member-Funktionen geschützt.

Get-Funktionen für Legacy Funktionen: .c_str(), .data() und smart_ptr<>::get().

Strukturen (struct) mit get- und set-Zugriff. Z.B. std::pair oder std::tuple.

Die einzelnen Teile sind unabhängig.

Cpp Core Guidelines

https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md

by Bjarne Stroustrup & Herb Sutter + other 173 Contributors

Deutsche Blog über Core Guidelines (Rainer Grimm)

https://www.heise.de/developer/artikel/C-Core-Guidelines-Die-Philosophie-3760777.html

Kate Gregory: 10 Core Guidelines ...

CppCon 2017: Kate Gregory “10 Core Guidelines You Need to Start Using Now” (https://www.youtube.com/watch?v=XkDEzfpdcSg)

Zusammenfassung

Ein gemischter C- und C++-Programmierstil sorgt für vielfältige Probleme.

Die Stärke von C++ liegt in den Datentypen, während die Kompatibilität zu C einen einfachen Zugriff auf das Betriebssystem (und andere Bibliotheken) ermöglicht.

C++ ist eine moderne Sprache, die ein sichere Programmierung ermöglicht, aber nicht forciert.

Fragen