C++ Fragen zu asio::buffer

T_55

Lieutenant
Registriert
Feb. 2013
Beiträge
638
Hallo,

ich bin dabei mich in Boost Asio einzuarbeiten. Die Verbindung zwischen Serverprogramm und Clientprogramm aufzubauen und Daten austauschen klappt alles, aber zwei Dinge sind für mich noch nicht ganz klar:

1. Zum Thema Datentyp: Nach einigen Compilerfehlern habe ich durch ausprobieren herausgefunden, dass unterschiedliche Datentypen auch unterschiedlich an den buffer weitergegeben werden müssen.

Bei einem std::array geht es nur so:
Code:
std::array<int,5> arr {8,8,8,8,8};
async_write(the_socket, boost::asio::buffer(arr), write_handler);

Und bei einem integer geht es nur so:
Code:
int test = 5;
async_write(the_socket, boost::asio::buffer(&test, sizeof(test)), write_handler);

Zum Verständnis, wieso muss bei std::array einfach der Variablenname eingefügt werden während beim integer die Sache per Referenz übergeben wird und dazu die Angabe von sizeof?

2. Zum Thema Sicherheit: Wenn ich aus Spaß das versendete Array größer mache als das vom Empfangsbuffer dann führt dies zu Dingen die denke ich lieber nicht passieren sollten:

Im Client:
Code:
std::array<int,5> arr {8,8,8,8,8};
async_write(the_socket, boost::asio::buffer(arr), write_handler);
int test = 5;
async_write(the_socket, boost::asio::buffer(&test, sizeof(test)), write_handler);

Im Server:
Code:
std::array<int,2> arr {0};
the_socket.read_some(boost::asio::buffer(arr));
int test = 0;
the_socket.read_some(boost::asio::buffer(&test, sizeof(test)));

Wenn ich nun die Daten vom Client zum Server übertrage und die Werte ausgebe, erhält die integer-Variable test im Server den Wert 8 obwohl dies ein Wert des arrays ist. Das heißt, nur durch Vergrößern des Arrays im Client, kann es im Server schon zu nicht gewollten Dingen kommen. Wohin werden die verbleibenden Daten geschrieben für die der buffer im Server kein Platz freihält? Ist kenne mich mit dem Thema noch nicht wirklich aus aber wäre das nicht ein Stapelüberlauf und damit eine klassische Sicherheitslücke? Was tun dagegen?

Grüße
 
Danke genau das hatte ich auch schon probiert aber es führt leider zum gleichen Problem. Es wird dabei wohl nur geschaut ob der Puffer voll ist aber nicht ob er überfüllt wird.

Ich hab jetzt diese drei Möglichkeiten zum Empfang ausprobiert und bei jeder Variante ist das Problem gleich.

Code:
the_socket.read_some(boost::asio::buffer(arr));

Code:
read(the_socket, boost::asio::buffer(arr));

Code:
the_socket.async_read_some(boost::asio::buffer(arr),read_handler);

Wenn der Sender entgegen des Empfängers, zB dieses Array vergrößert, kann er damit alle danach kommenden Variablen beim Empfänger überschreiben (Datentyp egal). Und wer weiß in welche Speicherbereiche noch geschrieben wird :confused_alt:

Das Grundlegende Problem ist, dass im Empfängerprogramm in den Speicherbereich fremder Variablen geschrieben wird, sobald das Senderprogramm zu viel Daten verschickt, zB Arrays vergrößert. Am Ende könnte ein möglicher Angreifer sogar Speicherbereiche ausserhalb des Programms manipulieren?! Dachte immer Asio bringt eine gewisse Grundsicherheit mit...

Das in Variablen enthaltene Werte manipuliert werden könnten ist ja nie zu vermeiden sofern der Sender dies so will.
Aber das heftige ist doch, dass der Sender durch einfaches Vergrößern der Daten beim Empfänger in irgendwelche Variablen-fremde Speicherbereiche scheiben kann.

Wie kann man sicherstellen, dass nur der dafür vorgesehene Speicherbereich (die Zielvariable) beim Empfänger beschrieben wird?
 
Dir scheint es ja vor allem um die Sicherheit zu gehen, wie du auch hier nochmal betonst:
Wie kann man sicherstellen, dass nur der dafür vorgesehene Speicherbereich (die Zielvariable) beim Empfänger beschrieben wird?
Bisher habe ich ehrlich gesagt noch nie gesehen, dass jemand einen nicht-dynamischen Speicher als Zielvariable verwendet für empfange Daten und das kam mir vorhin auch schon komisch vor bei deiner Lösung: Wieso schreibst du in einen int und nicht in etwas dynamisches wie std::vector?

Ich nutzte eh immer Qt statt Boost wo es möglich ist und dort würde man auf das Signal readyRead reagieren und im dazugehörigen Slot dann zB readAll am Socket aufrufen. Der return ist also automatisch ein dynamisch großer Kontainer. Diesen kann man ja immernoch im folgenden zu einem int umwandeln aber eben auch nur, wenn die Anzahl der bytes Sinn macht. Sonst verwirft man die Daten halt einfach bzw verrät dem Sender, dass man das Kommando nicht versteht.

Ich würde dir generell empfehlen irgend eine Art von 'Message' Klasse zu verwenden, um Datenübertragung zu wrappen.
Dh du schickst also alles zB so:
uint32_t commandId;
uint32_t numOfBytes;
std::string data; // oder std::vector<uint8_t> oder QByteArray oder usw..
Dh der Empfänger weiß dann, dass MINDESTENS 8 byte (die beiden uints bei leerem string) ankommen müssen bevor es irgendwie Sinn macht die Daten zu interpretieren.
Sollte der Sender jetzt absichtlich 'lügen' und data länger machen als vereinbart, würde automatisch der restliche Datenblock natürlich auch als nächstes interpretiert werden aber du schreibst nicht unkontrollierbar im Speicher umher.

Wenn du dich weiter auch vor so etwas schützen willst musst du natürlich den Input in deinen Server Filtern und zB ein Kommando wie 'setParameterX auf 1000' Filtern und prüfen, ob 1000 überhaupt ein gültiger Wert ist der gesetzt werden darf.

Bei RS232 und PC zu µC Kommunikation habe ich früher mal folgendes gemacht, um mich vor EMV Störung zu schützen:
uint8_t startByte;
uint32_t cmdId;
uint32_t dataLength;
std::string data;
uint32_t crc;
uint8_t endByte;

startByte und endByte waren unterschiedlich und wurden exklusiv verwendet. Dh weder cmdId, dataLength, data oder crc durften diese bytes benutzen.
Um das zu erreichen habe ich einfach ALLE bytes (außer start und end natürlich) in 2 hex-chars umgewandelt. Dh Dezimal 200 (= 0xc8) wurde geschickt als 2 bytes 'c' und '8' (mit den Ascii Werten 99 und 56). Dadurch bläst man die Menge an übertragenen Daten zwar fast um Faktor 2 auf aber dafür ist es deppensicher und Wasserdicht. Es werden dann im Grunde nur noch '0'-'9' und 'a'-'f' benutzt für die Übertragung.
Außerdem können 'verlorene' oder zu viel gesendete Bytes das Protokoll nicht dauerhaft aus dem Tritt bringen, weil man in deinem Fall nie wieder weiß, welches Bytes als cmdId oder dataLength zu interpretieren sind. Ich wusste ja durch endByte und CRC Prüfung ob eine Message komplett und Fehlerfrei ist und im Falle von Problemen durch startByte auch, dass jetzt wieder eine neue Message kommt die hoffentlich wieder Fehlerfrei ist. Dh der worst-case war, dass eine Message komplett verschluckt wurde. Um das zu verhindern braucht man mehr als Fire&Forget und müsste jede Nachricht erst durch ein ACK/NACK (zB plus CRC) bestätigen. Der Sender wartet dann mit dem nächsten Senden, bevor er diese Bestätigung erhalten hat.
 
Zuletzt bearbeitet:
Im Prinzip war das mit dem integer nur ein Test, in der Praxis sind dynamische Größen vermutlich wirklich gängiger. Diese Strukturierung in Head und Body oder wie man das nennt bietet sich an für sowas, find ich gut.
Ich hab jetzt hier was gefunden https://stackoverflow.com/questions...he-buffer-size-and-preventing-buffer-overflow
Per socket::available() kann man die bytes die noch anstehen auslesen. So kann ich mit dem Vergleich zu den bytes der Zielvariablen prüfen ob zuviel oder zuwenig gesendet wird. Damit hätte ich sozusagen einen "Sicherheitscheck" um das Verlassen des Zielspeicherraumes zu verhindern.
 
Das hat doch nichts mit Pufferüberläufen oder ähnlichem zu tun: Der Server sendet insgesamt 5+1 integer (24 Byte). Da es ein streaming socket ist macht es im Prinzip keinen Unterschied, ob du die 6 Byte auf einmal sendest oder jeden integer einzeln oder eben erst 5 und dann einen. Wie auch immer die daten gesendet werden, der Client liest nur die ersten 2+1 Integer ein. D.h. in der einzelnen Variable test landet der dritte Wert aus dem Array. Wenn du jetzt noch mal read_some aufrufst kannst du die restlichen 3 Integer in einen weiteren Puffer einlesen.
 
Zurück
Oben