Inhalt
- Motivation
- OWASP und CWE
- Bekannte Probleme und Lösungen mit der STL
- Was kann man von der STL lernen
sven-johannsen.de
sven@sven-johannsen.de
heise devSec()
25.10.2017
From OWASP: Buffer Overflows
#include <string.h>
void f(char* s) {
char buffer[10];
strcpy(buffer, s);
// ...
}
int main() {
f("01234567890123456789");
}
Kein Problem für C++?
#include <string>
void f(const std::string& s) {
std::string buffer;
buffer = s;
// ...
}
int main() {
f("01234567890123456789");
}
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.
The Open Web Application Security Project
https://www.owasp.org/index.php/Main_Page
OWASP Top 10 Application Security Risks - 2013 (2017rc)
Die OWASP C++ Beispiele sind alles C Probleme, die so auch in C++ auftreten könnten:
https://cwe.mitre.org/index.html
Common Weakness Enumeration: CWE™ is a community-developed list of common software security weaknesses.
Liste von "Software Schwachstellen"
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);
...
}
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
.
Strings: Felder von "char
" oder Zeiger auf "char
".
Plus: abschließendes 0
Zeichens.
Länge eines Strings und Größe des Buffers sind 2 unabhängige Dinge
strcpy()
bekommt nur die Zeiger auf den Buffer-Anfang übergeben.
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 =
?
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
std::string
Basiert intern auf einem 3 Zeiger Model:
Anfang <= Ende <= Kapazität
Länge = Ende - Anfang (ohne abschließendes 0
)
Größe des Buffers = Kapazität - Anfang
std::string
3 Zeiger-Modell als private Member.
Modifikation und Zugriff nur über Member-Funktionen:
resize()
insert()
, append()
,clear()
,operator=()
)#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:
Die Klasse std::string
löst viele Probleme, aber bringt aber auch neue:
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++ 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) {
// ...
}
[]
Zugriff,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.
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);
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
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()
Rewrite of boost::range (2.0) from Eric Niebler
Proposal für C++20? (D4128: Ranges for the Standard Library)
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.
Der Stack und die Destruktoren sind die Freunde des
C++-Programmierers.
MyClass* pMyClass;
// ...
if (!pMyClass {
pMyClass = new MyClass(param1, param2);
}
pMyClass->memberFunction(); // crash???
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:
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:
CComPtr<T>
, Qt: QPointer<T>
, QSharedPointer<T>
, ATL, ...Achtung: auto_ptr<T>
macht unerwartete Dinge
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
}
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
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()
)
// RAW pointer
MyObject* getMyObject(const string&);
MyObject* ptr = getMyObject("Test");
// 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.
C++-11:
unique_ptr<T>
Zeiger auf einen Wertunique_ptr<T[]>
Zeiger auf ein dynamisches Feldshared_ptr<T>
Zeiger auf einen WertC++-17
shared_ptr<T[]>
Zeiger auf ein dynamisches FeldSmart 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<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)
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.
std::string
,std::vector<T>
und alle anderen Containerstd::ifstream
, std::ofstream
std::mutex g_mtx;
void foo() {
std::lock_guard(g_mtx); // g_mtx.lock();
// do something.
// g_mtx.unlock();
}
(C++ Standard Bibliothek, die inzwischen weit über die ursprüngliche STL Container/Iteratoren und Algorithmen hinausgeht.)
Klassen bändigen unsichere Datentypen, wie z.B. Zeiger.
Beispiel:
Keine Memory leaks
Gesicherte Initialisierung (korrekte Konstruktoren vorausgesetzt)
Kontrollierte Kopien (flache Kopie, tiefe Kopie, Kopien verbieten)
Kontrollierte Konvertierung
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")
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 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))
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.
https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md
by Bjarne Stroustrup & Herb Sutter + other 173 Contributors
https://www.heise.de/developer/artikel/C-Core-Guidelines-Die-Philosophie-3760777.html
CppCon 2017: Kate Gregory “10 Core Guidelines You Need to Start Using Now” (https://www.youtube.com/watch?v=XkDEzfpdcSg)
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.